From da4c7e7ed675c3bf405668739c3012d140856109 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:34:42 +0200 Subject: Adding upstream version 126.0. Signed-off-by: Daniel Baumann --- .../components/browser/domains/README.md | 67 + .../components/browser/domains/build.gradle | 40 + .../components/browser/domains/proguard-rules.pro | 21 + .../browser/domains/src/main/AndroidManifest.xml | 4 + .../browser/domains/src/main/assets/domains/br | 50 + .../browser/domains/src/main/assets/domains/ca | 49 + .../browser/domains/src/main/assets/domains/de | 50 + .../browser/domains/src/main/assets/domains/fr | 50 + .../browser/domains/src/main/assets/domains/gb | 49 + .../browser/domains/src/main/assets/domains/global | 444 + .../browser/domains/src/main/assets/domains/hk | 50 + .../browser/domains/src/main/assets/domains/id | 50 + .../browser/domains/src/main/assets/domains/pl | 50 + .../browser/domains/src/main/assets/domains/ru | 50 + .../browser/domains/src/main/assets/domains/sg | 49 + .../browser/domains/src/main/assets/domains/tw | 50 + .../browser/domains/src/main/assets/domains/us | 49 + .../components/browser/domains/CustomDomains.kt | 68 + .../mozilla/components/browser/domains/Domain.kt | 39 + .../browser/domains/DomainAutoCompleteProvider.kt | 151 + .../mozilla/components/browser/domains/Domains.kt | 84 + .../browser/domains/autocomplete/Providers.kt | 110 + .../domains/BaseDomainAutocompleteProviderTest.kt | 140 + .../browser/domains/CustomDomainsTest.kt | 81 + .../domains/DomainAutoCompleteProviderTest.kt | 116 + .../components/browser/domains/DomainTest.kt | 39 + .../components/browser/domains/DomainsTest.kt | 30 + .../components/browser/domains/ProvidersTest.kt | 85 + .../src/test/resources/robolectric.properties | 1 + .../components/browser/engine-gecko/README.md | 41 + .../components/browser/engine-gecko/build.gradle | 193 + .../browser/engine-gecko/docs/metrics.md | 8 + .../browser/engine-gecko/geckoview.fml.yaml | 24 + .../components/browser/engine-gecko/metrics.yaml | 30 + .../browser/engine-gecko/proguard-rules.pro | 21 + .../fetch/geckoview/GeckoViewFetchTestCases.kt | 126 + .../engine-gecko/src/main/AndroidManifest.xml | 4 + .../components/browser/engine/gecko/GeckoEngine.kt | 1502 +++ .../browser/engine/gecko/GeckoEngineSession.kt | 1880 +++ .../engine/gecko/GeckoEngineSessionState.kt | 73 + .../browser/engine/gecko/GeckoEngineView.kt | 249 + .../components/browser/engine/gecko/GeckoResult.kt | 76 + .../GeckoTrackingProtectionExceptionStorage.kt | 146 + .../browser/engine/gecko/NestedGeckoView.kt | 258 + .../engine/gecko/activity/GeckoActivityDelegate.kt | 45 + .../activity/GeckoScreenOrientationDelegate.kt | 35 + .../activity/GeckoViewActivityContextDelegate.kt | 43 + .../autofill/GeckoAutocompleteStorageDelegate.kt | 110 + .../blocking/GeckoTrackingProtectionException.kt | 20 + .../cookiebanners/GeckoCookieBannersStorage.kt | 128 + .../cookiebanners/ReportSiteDomainsRepository.kt | 75 + .../components/browser/engine/gecko/ext/Address.kt | 42 + .../browser/engine/gecko/ext/CreditCard.kt | 32 + .../browser/engine/gecko/ext/GeckoChoice.kt | 31 + .../engine/gecko/ext/GeckoContentPermissions.kt | 25 + .../components/browser/engine/gecko/ext/Login.kt | 43 + .../engine/gecko/ext/TrackingProtectionPolicy.kt | 82 + .../engine/gecko/fetch/GeckoViewFetchClient.kt | 139 + .../gecko/integration/LocaleSettingUpdater.kt | 56 + .../engine/gecko/integration/SettingUpdater.kt | 45 + .../engine/gecko/media/GeckoMediaDelegate.kt | 53 + .../gecko/mediaquery/PreferredColorScheme.kt | 22 + .../mediasession/GeckoMediaSessionController.kt | 56 + .../mediasession/GeckoMediaSessionDelegate.kt | 125 + .../gecko/permission/GeckoPermissionRequest.kt | 185 + .../permission/GeckoSitePermissionsStorage.kt | 461 + .../browser/engine/gecko/profiler/Profiler.kt | 84 + .../engine/gecko/prompt/ChoicePromptDelegate.kt | 66 + .../engine/gecko/prompt/GeckoPromptDelegate.kt | 911 ++ .../gecko/prompt/PromptInstanceDismissDelegate.kt | 21 + .../selection/GeckoSelectionActionDelegate.kt | 70 + .../serviceworker/GeckoServiceWorkerDelegate.kt | 35 + .../translate/GeckoTranslateSessionDelegate.kt | 79 + .../gecko/translate/GeckoTranslationUtils.kt | 63 + .../engine/gecko/util/SpeculativeSessionFactory.kt | 154 + .../engine/gecko/webextension/GeckoWebExtension.kt | 449 + .../webextension/GeckoWebExtensionException.kt | 72 + .../GeckoWebNotificationDelegate.kt | 39 + .../engine/gecko/webpush/GeckoWebPushDelegate.kt | 73 + .../engine/gecko/webpush/GeckoWebPushHandler.kt | 31 + .../engine/gecko/window/GeckoWindowRequest.kt | 23 + .../experiment/NimbusExperimentDelegate.kt | 93 + .../engine/gecko/GeckoEngineSessionStateTest.kt | 61 + .../browser/engine/gecko/GeckoEngineSessionTest.kt | 4874 ++++++++ .../browser/engine/gecko/GeckoEngineTest.kt | 3673 ++++++ .../browser/engine/gecko/GeckoEngineViewTest.kt | 299 + .../browser/engine/gecko/GeckoResultTest.kt | 86 + .../GeckoTrackingProtectionExceptionStorageTest.kt | 274 + .../engine/gecko/GeckoWebExtensionExceptionTest.kt | 116 + .../browser/engine/gecko/NestedGeckoViewTest.kt | 580 + .../gecko/activity/GeckoActivityDelegateTest.kt | 75 + .../activity/GeckoScreenOrientationDelegateTest.kt | 62 + .../GeckoViewActivityContextDelegateTest.kt | 28 + .../cookiebanners/GeckoCookieBannersStorageTest.kt | 161 + .../ReportSiteDomainsRepositoryTest.kt | 74 + .../gecko/ext/TrackingProtectionPolicyKtTest.kt | 81 + .../gecko/fetch/GeckoViewFetchUnitTestCases.kt | 351 + .../engine/gecko/integration/SettingUpdaterTest.kt | 69 + .../engine/gecko/media/GeckoMediaDelegateTest.kt | 116 + .../GeckoMediaSessionControllerTest.kt | 49 + .../mediasession/GeckoMediaSessionDelegateTest.kt | 219 + .../gecko/permission/GeckoPermissionRequestTest.kt | 242 + .../permission/GeckoSitePermissionsStorageTest.kt | 740 ++ .../gecko/prompt/ChoicePromptDelegateTest.kt | 81 + .../engine/gecko/prompt/GeckoPromptDelegateTest.kt | 2136 ++++ .../prompt/PromptInstanceDismissDelegateTest.kt | 40 + .../selection/GeckoSelectionActionDelegateTest.kt | 92 + .../GeckoServiceWorkerDelegateTest.kt | 40 + .../translate/GeckoTranslateSessionDelegateTest.kt | 103 + .../gecko/util/SpeculativeSessionFactoryTest.kt | 129 + .../gecko/webextension/GeckoWebExtensionTest.kt | 644 + .../engine/gecko/webextension/MockWebExtension.kt | 115 + .../GeckoWebNotificationDelegateTest.kt | 117 + .../gecko/webnotifications/MockWebNotification.kt | 37 + .../gecko/webpush/GeckoWebPushDelegateTest.kt | 154 + .../gecko/webpush/GeckoWebPushHandlerTest.kt | 40 + .../engine/gecko/window/GeckoWindowRequestTest.kt | 20 + .../experiment/NimbusExperimentDelegateTest.kt | 68 + .../mozilla/components/test/ReflectionUtils.kt | 26 + .../org.mockito.plugins.MockMaker | 2 + .../src/test/resources/robolectric.properties | 1 + .../components/browser/engine-system/README.md | 58 + .../components/browser/engine-system/build.gradle | 54 + .../browser/engine-system/proguard-rules.pro | 21 + .../browser/engine/system/VersionTest.kt | 28 + .../org.mockito.plugins.MockMaker | 2 + .../engine-system/src/main/AndroidManifest.xml | 4 + .../browser/engine/system/NestedWebView.kt | 170 + .../browser/engine/system/SystemEngine.kt | 152 + .../browser/engine/system/SystemEngineSession.kt | 636 + .../engine/system/SystemEngineSessionState.kt | 95 + .../browser/engine/system/SystemEngineView.kt | 835 ++ .../engine/system/matcher/ReversibleString.kt | 98 + .../browser/engine/system/matcher/Safelist.kt | 176 + .../browser/engine/system/matcher/Trie.kt | 114 + .../browser/engine/system/matcher/UrlMatcher.kt | 323 + .../system/permission/SystemPermissionRequest.kt | 41 + .../engine/system/window/SystemWindowRequest.kt | 52 + .../src/main/res/raw/domain_blocklist.json | 11046 +++++++++++++++++ .../src/main/res/raw/domain_safelist.json | 12347 +++++++++++++++++++ .../src/main/res/values-am/strings.xml | 10 + .../src/main/res/values-an/strings.xml | 10 + .../src/main/res/values-ann/strings.xml | 10 + .../src/main/res/values-ar/strings.xml | 10 + .../src/main/res/values-ast/strings.xml | 10 + .../src/main/res/values-az/strings.xml | 10 + .../src/main/res/values-azb/strings.xml | 10 + .../src/main/res/values-ban/strings.xml | 10 + .../src/main/res/values-be/strings.xml | 10 + .../src/main/res/values-bg/strings.xml | 10 + .../src/main/res/values-bn/strings.xml | 10 + .../src/main/res/values-br/strings.xml | 10 + .../src/main/res/values-bs/strings.xml | 10 + .../src/main/res/values-ca/strings.xml | 10 + .../src/main/res/values-cak/strings.xml | 10 + .../src/main/res/values-ceb/strings.xml | 10 + .../src/main/res/values-ckb/strings.xml | 10 + .../src/main/res/values-co/strings.xml | 10 + .../src/main/res/values-cs/strings.xml | 10 + .../src/main/res/values-cy/strings.xml | 10 + .../src/main/res/values-da/strings.xml | 10 + .../src/main/res/values-de/strings.xml | 10 + .../src/main/res/values-dsb/strings.xml | 10 + .../src/main/res/values-el/strings.xml | 10 + .../src/main/res/values-en-rCA/strings.xml | 10 + .../src/main/res/values-en-rGB/strings.xml | 10 + .../src/main/res/values-eo/strings.xml | 10 + .../src/main/res/values-es-rAR/strings.xml | 10 + .../src/main/res/values-es-rCL/strings.xml | 10 + .../src/main/res/values-es-rES/strings.xml | 10 + .../src/main/res/values-es-rMX/strings.xml | 10 + .../src/main/res/values-es/strings.xml | 10 + .../src/main/res/values-et/strings.xml | 10 + .../src/main/res/values-eu/strings.xml | 10 + .../src/main/res/values-fa/strings.xml | 10 + .../src/main/res/values-ff/strings.xml | 10 + .../src/main/res/values-fi/strings.xml | 10 + .../src/main/res/values-fr/strings.xml | 10 + .../src/main/res/values-fur/strings.xml | 10 + .../src/main/res/values-fy-rNL/strings.xml | 10 + .../src/main/res/values-ga-rIE/strings.xml | 10 + .../src/main/res/values-gd/strings.xml | 10 + .../src/main/res/values-gl/strings.xml | 10 + .../src/main/res/values-gn/strings.xml | 10 + .../src/main/res/values-gu-rIN/strings.xml | 10 + .../src/main/res/values-hi-rIN/strings.xml | 10 + .../src/main/res/values-hil/strings.xml | 5 + .../src/main/res/values-hr/strings.xml | 10 + .../src/main/res/values-hsb/strings.xml | 10 + .../src/main/res/values-hu/strings.xml | 10 + .../src/main/res/values-hy-rAM/strings.xml | 10 + .../src/main/res/values-ia/strings.xml | 10 + .../src/main/res/values-in/strings.xml | 10 + .../src/main/res/values-is/strings.xml | 10 + .../src/main/res/values-it/strings.xml | 10 + .../src/main/res/values-iw/strings.xml | 10 + .../src/main/res/values-ja/strings.xml | 10 + .../src/main/res/values-ka/strings.xml | 10 + .../src/main/res/values-kaa/strings.xml | 10 + .../src/main/res/values-kab/strings.xml | 10 + .../src/main/res/values-kk/strings.xml | 10 + .../src/main/res/values-kmr/strings.xml | 10 + .../src/main/res/values-kn/strings.xml | 10 + .../src/main/res/values-ko/strings.xml | 10 + .../src/main/res/values-kw/strings.xml | 10 + .../src/main/res/values-lij/strings.xml | 10 + .../src/main/res/values-lo/strings.xml | 10 + .../src/main/res/values-lt/strings.xml | 10 + .../src/main/res/values-mix/strings.xml | 10 + .../src/main/res/values-ml/strings.xml | 10 + .../src/main/res/values-mr/strings.xml | 10 + .../src/main/res/values-my/strings.xml | 10 + .../src/main/res/values-nb-rNO/strings.xml | 10 + .../src/main/res/values-ne-rNP/strings.xml | 10 + .../src/main/res/values-nl/strings.xml | 10 + .../src/main/res/values-nn-rNO/strings.xml | 10 + .../src/main/res/values-nv/strings.xml | 5 + .../src/main/res/values-oc/strings.xml | 10 + .../src/main/res/values-or/strings.xml | 5 + .../src/main/res/values-pa-rIN/strings.xml | 10 + .../src/main/res/values-pa-rPK/strings.xml | 10 + .../src/main/res/values-pl/strings.xml | 10 + .../src/main/res/values-ppl/strings.xml | 10 + .../src/main/res/values-pt-rBR/strings.xml | 10 + .../src/main/res/values-pt-rPT/strings.xml | 10 + .../src/main/res/values-rm/strings.xml | 10 + .../src/main/res/values-ro/strings.xml | 10 + .../src/main/res/values-ru/strings.xml | 10 + .../src/main/res/values-sat/strings.xml | 10 + .../src/main/res/values-sc/strings.xml | 10 + .../src/main/res/values-si/strings.xml | 10 + .../src/main/res/values-sk/strings.xml | 10 + .../src/main/res/values-skr/strings.xml | 10 + .../src/main/res/values-sl/strings.xml | 10 + .../src/main/res/values-sq/strings.xml | 10 + .../src/main/res/values-sr/strings.xml | 10 + .../src/main/res/values-su/strings.xml | 10 + .../src/main/res/values-sv-rSE/strings.xml | 10 + .../src/main/res/values-szl/strings.xml | 10 + .../src/main/res/values-ta/strings.xml | 10 + .../src/main/res/values-te/strings.xml | 10 + .../src/main/res/values-tg/strings.xml | 10 + .../src/main/res/values-th/strings.xml | 10 + .../src/main/res/values-tl/strings.xml | 10 + .../src/main/res/values-tok/strings.xml | 10 + .../src/main/res/values-tr/strings.xml | 10 + .../src/main/res/values-trs/strings.xml | 10 + .../src/main/res/values-tt/strings.xml | 10 + .../src/main/res/values-tzm/strings.xml | 10 + .../src/main/res/values-ug/strings.xml | 10 + .../src/main/res/values-uk/strings.xml | 10 + .../src/main/res/values-ur/strings.xml | 10 + .../src/main/res/values-uz/strings.xml | 10 + .../src/main/res/values-vec/strings.xml | 10 + .../src/main/res/values-vi/strings.xml | 10 + .../src/main/res/values-yo/strings.xml | 10 + .../src/main/res/values-zh-rCN/strings.xml | 10 + .../src/main/res/values-zh-rTW/strings.xml | 10 + .../engine-system/src/main/res/values/strings.xml | 13 + .../browser/engine/system/NestedWebViewTest.kt | 165 + .../engine/system/SystemEngineSessionStateTest.kt | 138 + .../engine/system/SystemEngineSessionTest.kt | 1238 ++ .../browser/engine/system/SystemEngineTest.kt | 105 + .../browser/engine/system/SystemEngineViewTest.kt | 1654 +++ .../engine/system/matcher/ReversibleStringTest.kt | 122 + .../browser/engine/system/matcher/SafelistTest.kt | 126 + .../browser/engine/system/matcher/TrieTest.kt | 46 + .../engine/system/matcher/UrlMatcherTest.kt | 296 + .../permission/SystemPermissionRequestTest.kt | 99 + .../system/window/SystemWindowRequestTest.kt | 76 + .../org.mockito.plugins.MockMaker | 2 + .../src/test/resources/robolectric.properties | 1 + .../components/browser/errorpages/README.md | 91 + .../components/browser/errorpages/build.gradle | 42 + .../browser/errorpages/proguard-rules.pro | 21 + .../errorpages/src/main/AndroidManifest.xml | 4 + .../errorpages/src/main/assets/errorPageScripts.js | 122 + .../errorpages/src/main/assets/error_page_js.html | 58 + .../errorpages/src/main/assets/error_style.css | 165 + .../components/browser/errorpages/ErrorPages.kt | 249 + .../errorpages/src/main/res/values-am/strings.xml | 327 + .../errorpages/src/main/res/values-an/strings.xml | 247 + .../errorpages/src/main/res/values-ann/strings.xml | 95 + .../errorpages/src/main/res/values-ar/strings.xml | 223 + .../errorpages/src/main/res/values-ast/strings.xml | 269 + .../errorpages/src/main/res/values-az/strings.xml | 121 + .../errorpages/src/main/res/values-azb/strings.xml | 98 + .../errorpages/src/main/res/values-ban/strings.xml | 65 + .../errorpages/src/main/res/values-be/strings.xml | 262 + .../errorpages/src/main/res/values-bg/strings.xml | 206 + .../errorpages/src/main/res/values-bn/strings.xml | 135 + .../errorpages/src/main/res/values-br/strings.xml | 324 + .../errorpages/src/main/res/values-bs/strings.xml | 255 + .../errorpages/src/main/res/values-ca/strings.xml | 311 + .../errorpages/src/main/res/values-cak/strings.xml | 274 + .../errorpages/src/main/res/values-ceb/strings.xml | 293 + .../errorpages/src/main/res/values-ckb/strings.xml | 142 + .../errorpages/src/main/res/values-co/strings.xml | 304 + .../errorpages/src/main/res/values-cs/strings.xml | 298 + .../errorpages/src/main/res/values-cy/strings.xml | 301 + .../errorpages/src/main/res/values-da/strings.xml | 309 + .../errorpages/src/main/res/values-de/strings.xml | 299 + .../errorpages/src/main/res/values-dsb/strings.xml | 303 + .../errorpages/src/main/res/values-el/strings.xml | 319 + .../src/main/res/values-en-rCA/strings.xml | 298 + .../src/main/res/values-en-rGB/strings.xml | 298 + .../errorpages/src/main/res/values-eo/strings.xml | 260 + .../src/main/res/values-es-rAR/strings.xml | 290 + .../src/main/res/values-es-rCL/strings.xml | 322 + .../src/main/res/values-es-rES/strings.xml | 303 + .../src/main/res/values-es-rMX/strings.xml | 252 + .../errorpages/src/main/res/values-es/strings.xml | 303 + .../errorpages/src/main/res/values-et/strings.xml | 259 + .../errorpages/src/main/res/values-eu/strings.xml | 308 + .../errorpages/src/main/res/values-fa/strings.xml | 260 + .../errorpages/src/main/res/values-ff/strings.xml | 179 + .../errorpages/src/main/res/values-fi/strings.xml | 293 + .../errorpages/src/main/res/values-fr/strings.xml | 291 + .../errorpages/src/main/res/values-fur/strings.xml | 313 + .../src/main/res/values-fy-rNL/strings.xml | 462 + .../src/main/res/values-ga-rIE/strings.xml | 284 + .../errorpages/src/main/res/values-gd/strings.xml | 263 + .../errorpages/src/main/res/values-gl/strings.xml | 308 + .../errorpages/src/main/res/values-gn/strings.xml | 276 + .../src/main/res/values-gu-rIN/strings.xml | 163 + .../src/main/res/values-hi-rIN/strings.xml | 221 + .../errorpages/src/main/res/values-hil/strings.xml | 87 + .../errorpages/src/main/res/values-hr/strings.xml | 267 + .../errorpages/src/main/res/values-hsb/strings.xml | 296 + .../errorpages/src/main/res/values-hu/strings.xml | 274 + .../src/main/res/values-hy-rAM/strings.xml | 269 + .../errorpages/src/main/res/values-ia/strings.xml | 330 + .../errorpages/src/main/res/values-in/strings.xml | 328 + .../errorpages/src/main/res/values-is/strings.xml | 284 + .../errorpages/src/main/res/values-it/strings.xml | 290 + .../errorpages/src/main/res/values-iw/strings.xml | 296 + .../errorpages/src/main/res/values-ja/strings.xml | 302 + .../errorpages/src/main/res/values-ka/strings.xml | 271 + .../errorpages/src/main/res/values-kaa/strings.xml | 319 + .../errorpages/src/main/res/values-kab/strings.xml | 322 + .../errorpages/src/main/res/values-kk/strings.xml | 264 + .../errorpages/src/main/res/values-kmr/strings.xml | 330 + .../errorpages/src/main/res/values-kn/strings.xml | 215 + .../errorpages/src/main/res/values-ko/strings.xml | 290 + .../errorpages/src/main/res/values-kw/strings.xml | 124 + .../errorpages/src/main/res/values-lij/strings.xml | 250 + .../errorpages/src/main/res/values-lo/strings.xml | 321 + .../errorpages/src/main/res/values-lt/strings.xml | 275 + .../errorpages/src/main/res/values-mix/strings.xml | 138 + .../errorpages/src/main/res/values-ml/strings.xml | 218 + .../errorpages/src/main/res/values-mr/strings.xml | 265 + .../errorpages/src/main/res/values-my/strings.xml | 254 + .../src/main/res/values-nb-rNO/strings.xml | 326 + .../src/main/res/values-ne-rNP/strings.xml | 304 + .../errorpages/src/main/res/values-nl/strings.xml | 396 + .../src/main/res/values-nn-rNO/strings.xml | 332 + .../errorpages/src/main/res/values-nv/strings.xml | 12 + .../errorpages/src/main/res/values-oc/strings.xml | 272 + .../errorpages/src/main/res/values-or/strings.xml | 133 + .../src/main/res/values-pa-rIN/strings.xml | 330 + .../src/main/res/values-pa-rPK/strings.xml | 50 + .../errorpages/src/main/res/values-pl/strings.xml | 309 + .../errorpages/src/main/res/values-ppl/strings.xml | 115 + .../src/main/res/values-pt-rBR/strings.xml | 327 + .../src/main/res/values-pt-rPT/strings.xml | 280 + .../errorpages/src/main/res/values-rm/strings.xml | 263 + .../errorpages/src/main/res/values-ro/strings.xml | 241 + .../errorpages/src/main/res/values-ru/strings.xml | 304 + .../errorpages/src/main/res/values-sat/strings.xml | 304 + .../errorpages/src/main/res/values-sc/strings.xml | 164 + .../errorpages/src/main/res/values-si/strings.xml | 327 + .../errorpages/src/main/res/values-sk/strings.xml | 323 + .../errorpages/src/main/res/values-skr/strings.xml | 312 + .../errorpages/src/main/res/values-sl/strings.xml | 324 + .../errorpages/src/main/res/values-sq/strings.xml | 289 + .../errorpages/src/main/res/values-sr/strings.xml | 275 + .../errorpages/src/main/res/values-su/strings.xml | 279 + .../src/main/res/values-sv-rSE/strings.xml | 314 + .../errorpages/src/main/res/values-ta/strings.xml | 282 + .../errorpages/src/main/res/values-te/strings.xml | 258 + .../errorpages/src/main/res/values-tg/strings.xml | 330 + .../errorpages/src/main/res/values-th/strings.xml | 259 + .../errorpages/src/main/res/values-tl/strings.xml | 297 + .../errorpages/src/main/res/values-tok/strings.xml | 83 + .../errorpages/src/main/res/values-tr/strings.xml | 243 + .../errorpages/src/main/res/values-trs/strings.xml | 264 + .../errorpages/src/main/res/values-tt/strings.xml | 232 + .../errorpages/src/main/res/values-tzm/strings.xml | 25 + .../errorpages/src/main/res/values-ug/strings.xml | 330 + .../errorpages/src/main/res/values-uk/strings.xml | 310 + .../errorpages/src/main/res/values-ur/strings.xml | 204 + .../errorpages/src/main/res/values-uz/strings.xml | 289 + .../errorpages/src/main/res/values-vec/strings.xml | 288 + .../errorpages/src/main/res/values-vi/strings.xml | 310 + .../errorpages/src/main/res/values-yo/strings.xml | 316 + .../src/main/res/values-zh-rCN/strings.xml | 307 + .../src/main/res/values-zh-rTW/strings.xml | 284 + .../errorpages/src/main/res/values/strings.xml | 307 + .../browser/errorpages/ErrorPagesTest.kt | 107 + .../src/test/resources/robolectric.properties | 1 + .../components/browser/icons/.gitignore | 1 + .../components/browser/icons/README.md | 19 + .../components/browser/icons/build.gradle | 92 + .../components/browser/icons/proguard-rules.pro | 21 + .../browser/icons/OnDeviceBrowserIconsTest.kt | 70 + .../browser/icons/decoder/ICOIconDecoderTest.kt | 129 + .../browser/icons/src/main/AndroidManifest.xml | 4 + .../main/assets/extensions/browser-icons/icons.js | 81 + .../browser-icons/manifest.template.json | 22 + .../assets/mozac.browser.icons/icons-top200.json | 1056 ++ .../components/browser/icons/BrowserIcons.kt | 476 + .../java/mozilla/components/browser/icons/Icon.kt | 52 + .../components/browser/icons/IconRequest.kt | 140 + .../browser/icons/compose/IconLoaderScope.kt | 59 + .../browser/icons/compose/IconLoaderState.kt | 35 + .../components/browser/icons/compose/Loader.kt | 50 + .../browser/icons/decoder/ICOIconDecoder.kt | 42 + .../browser/icons/decoder/SvgIconDecoder.kt | 108 + .../icons/decoder/ico/IconDirectoryEntry.kt | 315 + .../browser/icons/extension/IconMessage.kt | 118 + .../browser/icons/extension/IconMessageHandler.kt | 53 + .../browser/icons/extension/WebAppManifest.kt | 49 + .../icons/generator/DefaultIconGenerator.kt | 106 + .../browser/icons/generator/IconGenerator.kt | 18 + .../browser/icons/loader/DataUriIconLoader.kt | 36 + .../browser/icons/loader/DiskIconLoader.kt | 26 + .../browser/icons/loader/HttpIconLoader.kt | 116 + .../components/browser/icons/loader/IconLoader.kt | 34 + .../browser/icons/loader/MemoryIconLoader.kt | 27 + .../icons/loader/NonBlockingHttpIconLoader.kt | 43 + .../icons/pipeline/IconResourceComparator.kt | 75 + .../browser/icons/preparer/DiskIconPreparer.kt | 32 + .../browser/icons/preparer/IconPreprarer.kt | 16 + .../browser/icons/preparer/MemoryIconPreparer.kt | 29 + .../browser/icons/preparer/TippyTopIconPreparer.kt | 89 + .../icons/processor/AdaptiveIconProcessor.kt | 76 + .../browser/icons/processor/ColorProcessor.kt | 37 + .../browser/icons/processor/DiskIconProcessor.kt | 52 + .../browser/icons/processor/IconProcessor.kt | 24 + .../browser/icons/processor/MemoryIconProcessor.kt | 42 + .../browser/icons/processor/ResizingProcessor.kt | 67 + .../browser/icons/utils/IconDiskCache.kt | 185 + .../browser/icons/utils/IconMemoryCache.kt | 62 + .../components/browser/icons/utils/Utils.kt | 64 + .../browser/icons/src/main/res/values/colors.xml | 18 + .../browser/icons/src/main/res/values/dimens.xml | 13 + .../browser/icons/src/main/res/values/tags.xml | 7 + .../components/browser/icons/BrowserIconsTest.kt | 339 + .../browser/icons/decoder/ICOIconDecoderTest.kt | 238 + .../browser/icons/decoder/SvgIconDecoderTest.kt | 156 + .../icons/extension/IconMessageHandlerTest.kt | 223 + .../browser/icons/extension/IconMessageKtTest.kt | 62 + .../icons/generator/DefaultIconGeneratorTest.kt | 66 + .../browser/icons/loader/DataUriIconLoaderTest.kt | 93 + .../browser/icons/loader/DiskIconLoaderTest.kt | 60 + .../browser/icons/loader/FailureCacheTest.kt | 51 + .../browser/icons/loader/HttpIconLoaderTest.kt | 273 + .../browser/icons/loader/MemoryIconLoaderTest.kt | 62 + .../icons/loader/NonBlockingHttpIconLoaderTest.kt | 318 + .../icons/pipeline/IconResourceComparatorTest.kt | 354 + .../browser/icons/preparer/DiskIconPreparerTest.kt | 69 + .../icons/preparer/MemoryIconPreparerTest.kt | 69 + .../icons/preparer/TippyTopIconPreparerTest.kt | 110 + .../icons/processor/AdaptiveIconProcessorTest.kt | 89 + .../browser/icons/processor/ColorProcessorTest.kt | 53 + .../icons/processor/DiskIconProcessorTest.kt | 117 + .../icons/processor/MemoryIconProcessorTest.kt | 85 + .../icons/processor/ResizingProcessorTest.kt | 124 + .../browser/icons/utils/IconDiskCacheTest.kt | 106 + .../browser/icons/utils/IconMemoryCacheTest.kt | 62 + .../browser/icons/src/test/resources/bmp/test.bmp | Bin 0 -> 30122 bytes .../browser/icons/src/test/resources/gif/cat.gif | Bin 0 -> 844849 bytes .../icons/src/test/resources/ico/golem_favicon.ico | Bin 0 -> 40648 bytes .../src/test/resources/ico/microsoft_favicon.ico | Bin 0 -> 17174 bytes .../src/test/resources/ico/nvidia_favicon.ico | Bin 0 -> 25214 bytes .../browser/icons/src/test/resources/jpg/tonys.jpg | Bin 0 -> 83782 bytes .../browser/icons/src/test/resources/misc/test.txt | 1 + .../org.mockito.plugins.MockMaker | 3 + .../browser/icons/src/test/resources/png/mozac.png | Bin 0 -> 406 bytes .../src/test/resources/robolectric.properties | 1 + .../icons/src/test/resources/webp/test.webp | Bin 0 -> 2010 bytes .../components/browser/menu/README.md | 152 + .../components/browser/menu/build.gradle | 57 + .../components/browser/menu/lint.xml | 7 + .../components/browser/menu/proguard-rules.pro | 21 + .../browser/menu/src/main/AndroidManifest.xml | 6 + .../mozilla/components/browser/menu/BrowserMenu.kt | 320 + .../components/browser/menu/BrowserMenuAdapter.kt | 68 + .../components/browser/menu/BrowserMenuBuilder.kt | 29 + .../browser/menu/BrowserMenuHighlight.kt | 109 + .../components/browser/menu/BrowserMenuItem.kt | 64 + .../browser/menu/BrowserMenuPlacement.kt | 64 + .../browser/menu/BrowserMenuPositioning.kt | 143 + .../browser/menu/WebExtensionBrowserMenu.kt | 141 + .../browser/menu/WebExtensionBrowserMenuBuilder.kt | 166 + .../components/browser/menu/ext/BrowserMenuItem.kt | 35 + .../mozilla/components/browser/menu/ext/View.kt | 16 + .../browser/menu/facts/BrowserMenuFacts.kt | 45 + .../menu/item/AbstractParentBrowserMenuItem.kt | 66 + .../browser/menu/item/BackPressMenuItem.kt | 72 + .../browser/menu/item/BrowserMenuCategory.kt | 81 + .../browser/menu/item/BrowserMenuCheckbox.kt | 33 + .../browser/menu/item/BrowserMenuCompoundButton.kt | 72 + .../browser/menu/item/BrowserMenuDivider.kt | 30 + .../menu/item/BrowserMenuHighlightableItem.kt | 212 + .../menu/item/BrowserMenuHighlightableSwitch.kt | 99 + .../browser/menu/item/BrowserMenuImageSwitch.kt | 59 + .../browser/menu/item/BrowserMenuImageText.kt | 111 + .../item/BrowserMenuImageTextCheckboxButton.kt | 111 + .../browser/menu/item/BrowserMenuItemToolbar.kt | 212 + .../browser/menu/item/BrowserMenuSwitch.kt | 33 + .../components/browser/menu/item/CustomTooltip.kt | 154 + .../browser/menu/item/ParentBrowserMenuItem.kt | 105 + .../item/SimpleBrowserMenuHighlightableItem.kt | 109 + .../browser/menu/item/SimpleBrowserMenuItem.kt | 84 + .../menu/item/TwoStateBrowserMenuImageText.kt | 133 + .../menu/item/WebExtensionBrowserMenuItem.kt | 168 + .../menu/item/WebExtensionPlaceholderMenuItem.kt | 36 + .../browser/menu/view/DynamicWidthRecyclerView.kt | 86 + .../browser/menu/view/ExpandableLayout.kt | 436 + .../components/browser/menu/view/MenuButton.kt | 211 + .../menu/view/StickyFooterLinearLayoutManager.kt | 74 + .../menu/view/StickyHeaderLinearLayoutManager.kt | 73 + .../browser/menu/view/StickyItemLayoutManager.kt | 483 + .../src/main/res/anim-ldrtl/menu_enter_bottom.xml | 22 + .../src/main/res/anim-ldrtl/menu_enter_top.xml | 22 + .../menu/src/main/res/anim/menu_enter_bottom.xml | 22 + .../menu/src/main/res/anim/menu_enter_top.xml | 22 + .../browser/menu/src/main/res/anim/menu_exit.xml | 10 + .../mozac_browser_menu_notification_icon.xml | 15 + .../src/main/res/drawable/mozac_menu_indicator.xml | 29 + .../main/res/drawable/mozac_menu_notification.xml | 21 + .../menu/src/main/res/drawable/rounded_corner.xml | 9 + .../src/main/res/layout/mozac_browser_menu.xml | 25 + .../main/res/layout/mozac_browser_menu_button.xml | 39 + .../res/layout/mozac_browser_menu_category.xml | 11 + .../mozac_browser_menu_highlightable_item.xml | 92 + .../mozac_browser_menu_highlightable_switch.xml | 60 + .../layout/mozac_browser_menu_item_checkbox.xml | 16 + .../res/layout/mozac_browser_menu_item_divider.xml | 8 + .../mozac_browser_menu_item_image_switch.xml | 15 + .../layout/mozac_browser_menu_item_image_text.xml | 32 + ...rowser_menu_item_image_text_checkbox_button.xml | 69 + .../layout/mozac_browser_menu_item_parent_menu.xml | 48 + .../res/layout/mozac_browser_menu_item_simple.xml | 15 + .../res/layout/mozac_browser_menu_item_switch.xml | 10 + .../res/layout/mozac_browser_menu_item_toolbar.xml | 10 + .../layout/mozac_browser_menu_web_extension.xml | 57 + .../res/layout/mozac_browser_tooltip_layout.xml | 27 + .../menu/src/main/res/values-am/strings.xml | 21 + .../menu/src/main/res/values-an/strings.xml | 11 + .../menu/src/main/res/values-ar/strings.xml | 13 + .../menu/src/main/res/values-ast/strings.xml | 11 + .../menu/src/main/res/values-az/strings.xml | 11 + .../menu/src/main/res/values-azb/strings.xml | 21 + .../menu/src/main/res/values-ban/strings.xml | 11 + .../menu/src/main/res/values-be/strings.xml | 15 + .../menu/src/main/res/values-bg/strings.xml | 21 + .../menu/src/main/res/values-bn/strings.xml | 11 + .../menu/src/main/res/values-br/strings.xml | 17 + .../menu/src/main/res/values-bs/strings.xml | 21 + .../menu/src/main/res/values-ca/strings.xml | 13 + .../menu/src/main/res/values-cak/strings.xml | 15 + .../menu/src/main/res/values-ceb/strings.xml | 11 + .../menu/src/main/res/values-ckb/strings.xml | 11 + .../menu/src/main/res/values-co/strings.xml | 21 + .../menu/src/main/res/values-cs/strings.xml | 21 + .../menu/src/main/res/values-cy/strings.xml | 21 + .../menu/src/main/res/values-da/strings.xml | 21 + .../menu/src/main/res/values-de/strings.xml | 21 + .../menu/src/main/res/values-dsb/strings.xml | 21 + .../menu/src/main/res/values-el/strings.xml | 21 + .../menu/src/main/res/values-en-rCA/strings.xml | 21 + .../menu/src/main/res/values-en-rGB/strings.xml | 21 + .../menu/src/main/res/values-eo/strings.xml | 15 + .../menu/src/main/res/values-es-rAR/strings.xml | 21 + .../menu/src/main/res/values-es-rCL/strings.xml | 21 + .../menu/src/main/res/values-es-rES/strings.xml | 21 + .../menu/src/main/res/values-es-rMX/strings.xml | 13 + .../menu/src/main/res/values-es/strings.xml | 21 + .../menu/src/main/res/values-et/strings.xml | 13 + .../menu/src/main/res/values-eu/strings.xml | 15 + .../menu/src/main/res/values-fa/strings.xml | 13 + .../menu/src/main/res/values-ff/strings.xml | 7 + .../menu/src/main/res/values-fi/strings.xml | 21 + .../menu/src/main/res/values-fr/strings.xml | 21 + .../menu/src/main/res/values-fur/strings.xml | 21 + .../menu/src/main/res/values-fy-rNL/strings.xml | 21 + .../menu/src/main/res/values-gd/strings.xml | 11 + .../menu/src/main/res/values-gl/strings.xml | 21 + .../menu/src/main/res/values-gn/strings.xml | 21 + .../menu/src/main/res/values-gu-rIN/strings.xml | 11 + .../menu/src/main/res/values-hi-rIN/strings.xml | 11 + .../menu/src/main/res/values-hil/strings.xml | 11 + .../menu/src/main/res/values-hr/strings.xml | 13 + .../menu/src/main/res/values-hsb/strings.xml | 21 + .../menu/src/main/res/values-hu/strings.xml | 21 + .../menu/src/main/res/values-hy-rAM/strings.xml | 21 + .../menu/src/main/res/values-ia/strings.xml | 21 + .../menu/src/main/res/values-in/strings.xml | 21 + .../menu/src/main/res/values-is/strings.xml | 21 + .../menu/src/main/res/values-it/strings.xml | 21 + .../menu/src/main/res/values-iw/strings.xml | 21 + .../menu/src/main/res/values-ja/strings.xml | 21 + .../menu/src/main/res/values-ka/strings.xml | 13 + .../menu/src/main/res/values-kaa/strings.xml | 11 + .../menu/src/main/res/values-kab/strings.xml | 15 + .../menu/src/main/res/values-kk/strings.xml | 21 + .../menu/src/main/res/values-kmr/strings.xml | 15 + .../menu/src/main/res/values-kn/strings.xml | 11 + .../menu/src/main/res/values-ko/strings.xml | 21 + .../menu/src/main/res/values-kw/strings.xml | 13 + .../menu/src/main/res/values-ldrtl/dimens.xml | 7 + .../menu/src/main/res/values-lij/strings.xml | 7 + .../menu/src/main/res/values-lo/strings.xml | 13 + .../menu/src/main/res/values-lt/strings.xml | 11 + .../menu/src/main/res/values-mix/strings.xml | 15 + .../menu/src/main/res/values-mr/strings.xml | 11 + .../menu/src/main/res/values-my/strings.xml | 11 + .../menu/src/main/res/values-nb-rNO/strings.xml | 21 + .../menu/src/main/res/values-ne-rNP/strings.xml | 11 + .../menu/src/main/res/values-nl/strings.xml | 21 + .../menu/src/main/res/values-nn-rNO/strings.xml | 21 + .../menu/src/main/res/values-oc/strings.xml | 21 + .../menu/src/main/res/values-or/strings.xml | 11 + .../menu/src/main/res/values-pa-rIN/strings.xml | 21 + .../menu/src/main/res/values-pa-rPK/strings.xml | 13 + .../menu/src/main/res/values-pl/strings.xml | 21 + .../menu/src/main/res/values-pt-rBR/strings.xml | 21 + .../menu/src/main/res/values-pt-rPT/strings.xml | 21 + .../menu/src/main/res/values-rm/strings.xml | 21 + .../menu/src/main/res/values-ro/strings.xml | 13 + .../menu/src/main/res/values-ru/strings.xml | 21 + .../menu/src/main/res/values-sat/strings.xml | 21 + .../menu/src/main/res/values-sc/strings.xml | 13 + .../menu/src/main/res/values-si/strings.xml | 17 + .../menu/src/main/res/values-sk/strings.xml | 21 + .../menu/src/main/res/values-skr/strings.xml | 21 + .../menu/src/main/res/values-sl/strings.xml | 21 + .../menu/src/main/res/values-sq/strings.xml | 21 + .../menu/src/main/res/values-sr/strings.xml | 13 + .../menu/src/main/res/values-su/strings.xml | 21 + .../menu/src/main/res/values-sv-rSE/strings.xml | 21 + .../menu/src/main/res/values-szl/strings.xml | 13 + .../menu/src/main/res/values-ta/strings.xml | 11 + .../menu/src/main/res/values-te/strings.xml | 11 + .../menu/src/main/res/values-tg/strings.xml | 21 + .../menu/src/main/res/values-th/strings.xml | 21 + .../menu/src/main/res/values-tl/strings.xml | 11 + .../menu/src/main/res/values-tok/strings.xml | 7 + .../menu/src/main/res/values-tr/strings.xml | 21 + .../menu/src/main/res/values-trs/strings.xml | 13 + .../menu/src/main/res/values-tt/strings.xml | 11 + .../menu/src/main/res/values-tzm/strings.xml | 5 + .../menu/src/main/res/values-ug/strings.xml | 21 + .../menu/src/main/res/values-uk/strings.xml | 21 + .../menu/src/main/res/values-ur/strings.xml | 11 + .../menu/src/main/res/values-uz/strings.xml | 11 + .../menu/src/main/res/values-vec/strings.xml | 11 + .../menu/src/main/res/values-vi/strings.xml | 21 + .../menu/src/main/res/values-yo/strings.xml | 11 + .../menu/src/main/res/values-zh-rCN/strings.xml | 21 + .../menu/src/main/res/values-zh-rTW/strings.xml | 21 + .../browser/menu/src/main/res/values/colors.xml | 8 + .../browser/menu/src/main/res/values/dimens.xml | 80 + .../browser/menu/src/main/res/values/strings.xml | 24 + .../browser/menu/src/main/res/values/style.xml | 106 + .../browser/menu/BrowserMenuAdapterTest.kt | 210 + .../browser/menu/BrowserMenuBuilderTest.kt | 44 + .../browser/menu/BrowserMenuHighlightTest.kt | 49 + .../browser/menu/BrowserMenuPositioningTest.kt | 163 + .../components/browser/menu/BrowserMenuTest.kt | 496 + .../menu/WebExtensionBrowserMenuBuilderTest.kt | 510 + .../browser/menu/WebExtensionBrowserMenuTest.kt | 525 + .../browser/menu/ext/BrowserMenuItemTest.kt | 113 + .../menu/item/AbstractParentBrowserMenuItemTest.kt | 90 + .../browser/menu/item/BrowserMenuCategoryTest.kt | 183 + .../browser/menu/item/BrowserMenuCheckboxTest.kt | 38 + .../menu/item/BrowserMenuCompoundButtonTest.kt | 190 + .../browser/menu/item/BrowserMenuDividerTest.kt | 47 + .../menu/item/BrowserMenuHighlightableItemTest.kt | 334 + .../item/BrowserMenuHighlightableSwitchTest.kt | 124 + .../menu/item/BrowserMenuImageSwitchTest.kt | 60 + .../item/BrowserMenuImageTextCheckboxButtonTest.kt | 204 + .../browser/menu/item/BrowserMenuImageTextTest.kt | 131 + .../menu/item/BrowserMenuItemToolbarTest.kt | 439 + .../browser/menu/item/BrowserMenuSwitchTest.kt | 38 + .../browser/menu/item/ParentBrowserMenuItemTest.kt | 150 + .../item/SimpleBrowserMenuHighlightableItemTest.kt | 152 + .../browser/menu/item/SimpleBrowserMenuItemTest.kt | 122 + .../menu/item/TwoStateBrowserMenuImageTextTest.kt | 87 + .../menu/item/WebExtensionBrowserMenuItemTest.kt | 355 + .../menu/view/DynamicWidthRecyclerViewTest.kt | 226 + .../browser/menu/view/ExpandableLayoutTest.kt | 822 ++ .../menu/view/FakeStickyItemLayoutManager.kt | 32 + .../browser/menu/view/FakeStickyItemsAdapter.kt | 23 + .../components/browser/menu/view/MenuButtonTest.kt | 172 + .../view/StickyFooterLinearLayoutManagerTest.kt | 189 + .../view/StickyHeaderLinearLayoutManagerTest.kt | 155 + .../view/StickyItemsLinearLayoutManagerTest.kt | 631 + .../org.mockito.plugins.MockMaker | 3 + .../mockito-extensions/robolectric.properties | 1 + .../menu/src/test/resources/robolectric.properties | 1 + .../components/browser/menu2/README.md | 51 + .../components/browser/menu2/build.gradle | 50 + .../components/browser/menu2/lint.xml | 7 + .../components/browser/menu2/proguard-rules.pro | 21 + .../browser/menu2/src/main/AndroidManifest.xml | 7 + .../browser/menu2/BrowserMenuController.kt | 182 + .../adapter/CompoundMenuCandidateViewHolders.kt | 81 + .../DecorativeTextMenuCandidateViewHolder.kt | 47 + .../adapter/DividerMenuCandidateViewHolder.kt | 25 + .../browser/menu2/adapter/LastItemViewHolder.kt | 33 + .../menu2/adapter/MenuCandidateListAdapter.kt | 80 + .../menu2/adapter/MenuCandidateViewHolder.kt | 22 + .../menu2/adapter/NestedMenuCandidateViewHolder.kt | 53 + .../menu2/adapter/RowMenuCandidateViewHolder.kt | 55 + .../menu2/adapter/SmallMenuCandidateViewHolder.kt | 60 + .../menu2/adapter/TextMenuCandidateViewHolder.kt | 55 + .../adapter/icons/DrawableMenuIconViewHolders.kt | 224 + .../browser/menu2/adapter/icons/MenuIconAdapter.kt | 51 + .../menu2/adapter/icons/MenuIconViewHolder.kt | 71 + .../menu2/adapter/icons/TextMenuIconViewHolder.kt | 62 + .../browser/menu2/ext/BrowserMenuPositioning.kt | 261 + .../mozilla/components/browser/menu2/ext/View.kt | 95 + .../components/browser/menu2/view/MenuButton2.kt | 118 + .../components/browser/menu2/view/MenuView.kt | 95 + .../menu2/src/main/res/anim/menu_enter_left.xml | 22 + .../src/main/res/anim/menu_enter_left_bottom.xml | 22 + .../src/main/res/anim/menu_enter_left_top.xml | 22 + .../menu2/src/main/res/anim/menu_enter_right.xml | 22 + .../src/main/res/anim/menu_enter_right_bottom.xml | 22 + .../src/main/res/anim/menu_enter_right_top.xml | 22 + .../browser/menu2/src/main/res/anim/menu_exit.xml | 10 + .../res/drawable/mozac_browser_menu2_indicator.xml | 29 + .../drawable/mozac_browser_menu2_notification.xml | 21 + .../mozac_browser_menu2_notification_icon.xml | 15 + .../main/res/layout/mozac_browser_menu2_button.xml | 39 + ...c_browser_menu2_candidate_compound_checkbox.xml | 36 + ...zac_browser_menu2_candidate_compound_switch.xml | 29 + ...zac_browser_menu2_candidate_decorative_text.xml | 18 + .../mozac_browser_menu2_candidate_divider.xml | 8 + .../mozac_browser_menu2_candidate_nested.xml | 29 + .../layout/mozac_browser_menu2_candidate_row.xml | 10 + ...ozac_browser_menu2_candidate_row_small_icon.xml | 15 + .../layout/mozac_browser_menu2_candidate_text.xml | 29 + .../res/layout/mozac_browser_menu2_icon_button.xml | 10 + .../layout/mozac_browser_menu2_icon_drawable.xml | 13 + .../mozac_browser_menu2_icon_notification_dot.xml | 15 + .../res/layout/mozac_browser_menu2_icon_text.xml | 13 + .../main/res/layout/mozac_browser_menu2_view.xml | 31 + .../menu2/src/main/res/values-am/strings.xml | 7 + .../menu2/src/main/res/values-an/strings.xml | 7 + .../menu2/src/main/res/values-ar/strings.xml | 7 + .../menu2/src/main/res/values-ast/strings.xml | 7 + .../menu2/src/main/res/values-az/strings.xml | 7 + .../menu2/src/main/res/values-azb/strings.xml | 7 + .../menu2/src/main/res/values-ban/strings.xml | 7 + .../menu2/src/main/res/values-be/strings.xml | 7 + .../menu2/src/main/res/values-bg/strings.xml | 7 + .../menu2/src/main/res/values-bn/strings.xml | 7 + .../menu2/src/main/res/values-br/strings.xml | 7 + .../menu2/src/main/res/values-bs/strings.xml | 7 + .../menu2/src/main/res/values-ca/strings.xml | 7 + .../menu2/src/main/res/values-cak/strings.xml | 7 + .../menu2/src/main/res/values-ceb/strings.xml | 7 + .../menu2/src/main/res/values-ckb/strings.xml | 7 + .../menu2/src/main/res/values-co/strings.xml | 7 + .../menu2/src/main/res/values-cs/strings.xml | 7 + .../menu2/src/main/res/values-cy/strings.xml | 7 + .../menu2/src/main/res/values-da/strings.xml | 7 + .../menu2/src/main/res/values-de/strings.xml | 7 + .../menu2/src/main/res/values-dsb/strings.xml | 7 + .../menu2/src/main/res/values-el/strings.xml | 7 + .../menu2/src/main/res/values-en-rCA/strings.xml | 7 + .../menu2/src/main/res/values-en-rGB/strings.xml | 7 + .../menu2/src/main/res/values-eo/strings.xml | 7 + .../menu2/src/main/res/values-es-rAR/strings.xml | 7 + .../menu2/src/main/res/values-es-rCL/strings.xml | 7 + .../menu2/src/main/res/values-es-rES/strings.xml | 7 + .../menu2/src/main/res/values-es-rMX/strings.xml | 7 + .../menu2/src/main/res/values-es/strings.xml | 7 + .../menu2/src/main/res/values-et/strings.xml | 7 + .../menu2/src/main/res/values-eu/strings.xml | 7 + .../menu2/src/main/res/values-fa/strings.xml | 7 + .../menu2/src/main/res/values-fi/strings.xml | 7 + .../menu2/src/main/res/values-fr/strings.xml | 7 + .../menu2/src/main/res/values-fur/strings.xml | 7 + .../menu2/src/main/res/values-fy-rNL/strings.xml | 7 + .../menu2/src/main/res/values-gd/strings.xml | 7 + .../menu2/src/main/res/values-gl/strings.xml | 7 + .../menu2/src/main/res/values-gn/strings.xml | 7 + .../menu2/src/main/res/values-gu-rIN/strings.xml | 7 + .../menu2/src/main/res/values-hi-rIN/strings.xml | 7 + .../menu2/src/main/res/values-hil/strings.xml | 7 + .../menu2/src/main/res/values-hr/strings.xml | 7 + .../menu2/src/main/res/values-hsb/strings.xml | 7 + .../menu2/src/main/res/values-hu/strings.xml | 7 + .../menu2/src/main/res/values-hy-rAM/strings.xml | 7 + .../menu2/src/main/res/values-ia/strings.xml | 7 + .../menu2/src/main/res/values-in/strings.xml | 7 + .../menu2/src/main/res/values-is/strings.xml | 7 + .../menu2/src/main/res/values-it/strings.xml | 7 + .../menu2/src/main/res/values-iw/strings.xml | 7 + .../menu2/src/main/res/values-ja/strings.xml | 7 + .../menu2/src/main/res/values-ka/strings.xml | 7 + .../menu2/src/main/res/values-kaa/strings.xml | 7 + .../menu2/src/main/res/values-kab/strings.xml | 7 + .../menu2/src/main/res/values-kk/strings.xml | 7 + .../menu2/src/main/res/values-kmr/strings.xml | 7 + .../menu2/src/main/res/values-kn/strings.xml | 7 + .../menu2/src/main/res/values-ko/strings.xml | 7 + .../menu2/src/main/res/values-kw/strings.xml | 7 + .../menu2/src/main/res/values-ldrtl/dimens.xml | 7 + .../menu2/src/main/res/values-lij/strings.xml | 7 + .../menu2/src/main/res/values-lo/strings.xml | 7 + .../menu2/src/main/res/values-lt/strings.xml | 7 + .../menu2/src/main/res/values-mix/strings.xml | 7 + .../menu2/src/main/res/values-mr/strings.xml | 7 + .../menu2/src/main/res/values-my/strings.xml | 7 + .../menu2/src/main/res/values-nb-rNO/strings.xml | 7 + .../menu2/src/main/res/values-ne-rNP/strings.xml | 7 + .../menu2/src/main/res/values-nl/strings.xml | 7 + .../menu2/src/main/res/values-nn-rNO/strings.xml | 7 + .../menu2/src/main/res/values-oc/strings.xml | 7 + .../menu2/src/main/res/values-or/strings.xml | 7 + .../menu2/src/main/res/values-pa-rIN/strings.xml | 7 + .../menu2/src/main/res/values-pa-rPK/strings.xml | 7 + .../menu2/src/main/res/values-pl/strings.xml | 7 + .../menu2/src/main/res/values-pt-rBR/strings.xml | 7 + .../menu2/src/main/res/values-pt-rPT/strings.xml | 7 + .../menu2/src/main/res/values-rm/strings.xml | 7 + .../menu2/src/main/res/values-ro/strings.xml | 7 + .../menu2/src/main/res/values-ru/strings.xml | 7 + .../menu2/src/main/res/values-sat/strings.xml | 7 + .../menu2/src/main/res/values-sc/strings.xml | 7 + .../menu2/src/main/res/values-si/strings.xml | 7 + .../menu2/src/main/res/values-sk/strings.xml | 7 + .../menu2/src/main/res/values-skr/strings.xml | 7 + .../menu2/src/main/res/values-sl/strings.xml | 7 + .../menu2/src/main/res/values-sq/strings.xml | 7 + .../menu2/src/main/res/values-sr/strings.xml | 7 + .../menu2/src/main/res/values-su/strings.xml | 7 + .../menu2/src/main/res/values-sv-rSE/strings.xml | 7 + .../menu2/src/main/res/values-szl/strings.xml | 7 + .../menu2/src/main/res/values-ta/strings.xml | 7 + .../menu2/src/main/res/values-te/strings.xml | 7 + .../menu2/src/main/res/values-tg/strings.xml | 7 + .../menu2/src/main/res/values-th/strings.xml | 7 + .../menu2/src/main/res/values-tl/strings.xml | 7 + .../menu2/src/main/res/values-tr/strings.xml | 7 + .../menu2/src/main/res/values-trs/strings.xml | 7 + .../menu2/src/main/res/values-tt/strings.xml | 7 + .../menu2/src/main/res/values-tzm/strings.xml | 5 + .../menu2/src/main/res/values-ug/strings.xml | 7 + .../menu2/src/main/res/values-uk/strings.xml | 7 + .../menu2/src/main/res/values-ur/strings.xml | 7 + .../menu2/src/main/res/values-uz/strings.xml | 7 + .../menu2/src/main/res/values-vec/strings.xml | 7 + .../menu2/src/main/res/values-vi/strings.xml | 7 + .../menu2/src/main/res/values-yo/strings.xml | 7 + .../menu2/src/main/res/values-zh-rCN/strings.xml | 7 + .../menu2/src/main/res/values-zh-rTW/strings.xml | 7 + .../browser/menu2/src/main/res/values/colors.xml | 8 + .../browser/menu2/src/main/res/values/dimens.xml | 48 + .../browser/menu2/src/main/res/values/strings.xml | 10 + .../browser/menu2/src/main/res/values/style.xml | 88 + .../browser/menu2/BrowserMenuControllerTest.kt | 132 + .../adapter/CompoundMenuCandidateViewHolderTest.kt | 99 + .../DecorativeTextMenuCandidateViewHolderTest.kt | 61 + .../adapter/DividerMenuCandidateViewHolderTest.kt | 24 + .../menu2/adapter/MenuCandidateListAdapterTest.kt | 75 + .../adapter/RowMenuCandidateViewHolderTest.kt | 103 + .../adapter/SmallMenuCandidateViewHolderTest.kt | 130 + .../adapter/TextMenuCandidateViewHolderTest.kt | 117 + .../icons/DrawableMenuIconViewHoldersTest.kt | 177 + .../menu2/adapter/icons/MenuIconAdapterTest.kt | 86 + .../adapter/icons/TextMenuIconViewHolderTest.kt | 68 + .../menu2/ext/BrowserMenuPositioningTest.kt | 856 ++ .../components/browser/menu2/ext/ViewTest.kt | 143 + .../browser/menu2/view/MenuButton2Test.kt | 120 + .../components/browser/menu2/view/MenuViewTest.kt | 92 + .../org.mockito.plugins.MockMaker | 3 + .../src/test/resources/robolectric.properties | 1 + .../components/browser/session-storage/README.md | 19 + .../browser/session-storage/build.gradle | 74 + .../browser/session-storage/proguard-rules.pro | 21 + .../src/androidTest/assets/index.html | 8 + .../browser/session/storage/FullRestoreTest.kt | 113 + .../storage/RestoringBrowsingSessionsTest.kt | 310 + .../session-storage/src/main/AndroidManifest.xml | 4 + .../components/browser/session/storage/AutoSave.kt | 247 + .../storage/FileEngineSessionStateStorage.kt | 66 + .../session/storage/RecoverableBrowserState.kt | 18 + .../browser/session/storage/SessionStorage.kt | 128 + .../storage/serialize/BrowserStateReader.kt | 258 + .../storage/serialize/BrowserStateWriter.kt | 164 + .../browser/session/storage/serialize/Keys.kt | 47 + .../browser/session/storage/AutoSaveTest.kt | 384 + .../storage/FileEngineSessionStateStorageTest.kt | 84 + .../browser/session/storage/SessionStorageTest.kt | 405 + .../serialize/BrowserStateWriterReaderTest.kt | 441 + .../org.mockito.plugins.MockMaker | 2 + .../components/browser/state/README.md | 23 + .../components/browser/state/build.gradle | 82 + .../components/browser/state/proguard-rules.pro | 21 + .../browser/state/helper/OnDeviceTargetTest.kt | 177 + .../browser/state/src/main/AndroidManifest.xml | 4 + .../browser/state/action/ActionWithTab.kt | 32 + .../browser/state/action/AwesomeBarAction.kt | 30 + .../browser/state/action/BrowserAction.kt | 1838 +++ .../browser/state/engine/EngineMiddleware.kt | 61 + .../browser/state/engine/EngineObserver.kt | 491 + .../state/engine/middleware/CrashMiddleware.kt | 47 + .../middleware/CreateEngineSessionMiddleware.kt | 134 + .../engine/middleware/EngineDelegateMiddleware.kt | 224 + .../middleware/ExtensionsProcessMiddleware.kt | 45 + .../state/engine/middleware/LinkingMiddleware.kt | 137 + .../middleware/SessionPrioritizationMiddleware.kt | 145 + .../state/engine/middleware/SuspendMiddleware.kt | 60 + .../engine/middleware/TabsRemovedMiddleware.kt | 75 + .../engine/middleware/TranslationsMiddleware.kt | 845 ++ .../engine/middleware/TrimMemoryMiddleware.kt | 92 + .../engine/middleware/WebExtensionMiddleware.kt | 78 + .../browser/state/ext/CustomTabSessionState.kt | 19 + .../browser/state/ext/PermissionRequest.kt | 15 + .../components/browser/state/helper/Target.kt | 96 + .../state/reducer/AwesomeBarStateReducer.kt | 30 + .../browser/state/reducer/BrowserStateReducer.kt | 161 + .../browser/state/reducer/ContainerReducer.kt | 38 + .../browser/state/reducer/ContentStateReducer.kt | 385 + .../state/reducer/CookieBannerStateReducer.kt | 17 + .../reducer/CopyInternetResourceStateReducer.kt | 28 + .../browser/state/reducer/CrashReducer.kt | 32 + .../browser/state/reducer/CustomTabListReducer.kt | 42 + .../browser/state/reducer/DebugReducer.kt | 24 + .../browser/state/reducer/DownloadStateReducer.kt | 43 + .../browser/state/reducer/EngineStateReducer.kt | 110 + .../state/reducer/ExtensionsProcessStateReducer.kt | 17 + .../state/reducer/HistoryMetadataReducer.kt | 50 + .../state/reducer/InternetResourceReducerUtils.kt | 18 + .../browser/state/reducer/LastAccessReducer.kt | 45 + .../browser/state/reducer/LocaleStateReducer.kt | 21 + .../browser/state/reducer/MediaSessionReducer.kt | 142 + .../browser/state/reducer/ReaderStateReducer.kt | 59 + .../browser/state/reducer/RecentlyClosedReducer.kt | 35 + .../browser/state/reducer/SearchReducer.kt | 206 + .../reducer/ShareInternetResourceStateReducer.kt | 21 + .../browser/state/reducer/SystemReducer.kt | 23 + .../browser/state/reducer/TabGroupReducer.kt | 169 + .../browser/state/reducer/TabListReducer.kt | 358 + .../reducer/TrackingProtectionStateReducer.kt | 45 + .../state/reducer/TranslationsStateReducer.kt | 446 + .../browser/state/reducer/UndoReducer.kt | 46 + .../browser/state/reducer/WebExtensionReducer.kt | 117 + .../components/browser/state/search/RegionState.kt | 25 + .../browser/state/search/SearchEngine.kt | 75 + .../components/browser/state/selector/Selectors.kt | 187 + .../browser/state/state/AppIntentState.kt | 18 + .../browser/state/state/AwesomeBarState.kt | 19 + .../components/browser/state/state/BrowserState.kt | 59 + .../browser/state/state/ContainerState.kt | 56 + .../components/browser/state/state/ContentState.kt | 109 + .../browser/state/state/CustomTabConfig.kt | 107 + .../browser/state/state/CustomTabSessionState.kt | 98 + .../components/browser/state/state/EngineState.kt | 39 + .../browser/state/state/LastMediaAccessState.kt | 29 + .../browser/state/state/LoadRequestState.kt | 18 + .../browser/state/state/MediaSessionState.kt | 40 + .../components/browser/state/state/ReaderState.kt | 30 + .../components/browser/state/state/SearchState.kt | 94 + .../browser/state/state/SecurityInfoState.kt | 19 + .../components/browser/state/state/SessionState.kt | 201 + .../components/browser/state/state/TabPartition.kt | 70 + .../browser/state/state/TabSessionState.kt | 150 + .../browser/state/state/TrackingProtectionState.kt | 24 + .../state/state/TranslationsBrowserState.kt | 29 + .../browser/state/state/TranslationsState.kt | 43 + .../browser/state/state/UndoHistoryState.kt | 24 + .../browser/state/state/WebExtensionState.kt | 37 + .../browser/state/state/content/DownloadState.kt | 97 + .../browser/state/state/content/FindResultState.kt | 14 + .../browser/state/state/content/HistoryState.kt | 20 + .../state/content/PermissionHighlightsState.kt | 52 + .../state/content/ShareInternetResourceState.kt | 25 + .../state/extension/WebExtensionPromptRequest.kt | 80 + .../browser/state/state/recover/RecoverableTab.kt | 119 + .../components/browser/state/store/BrowserStore.kt | 49 + .../browser/state/action/AwesomeBarActionTest.kt | 111 + .../browser/state/action/ContainerActionTest.kt | 113 + .../browser/state/action/ContentActionTest.kt | 894 ++ .../browser/state/action/CookieBannerActionTest.kt | 49 + .../state/action/CustomTabListActionTest.kt | 92 + .../browser/state/action/DebugActionTest.kt | 34 + .../browser/state/action/DownloadActionTest.kt | 132 + .../browser/state/action/EngineActionTest.kt | 140 + .../state/action/HistoryMetadataActionTest.kt | 67 + .../browser/state/action/LastAccessActionTest.kt | 32 + .../browser/state/action/LocaleActionTest.kt | 32 + .../browser/state/action/MediaSessionActionTest.kt | 304 + .../browser/state/action/ReaderActionTest.kt | 154 + .../browser/state/action/SearchActionTest.kt | 409 + .../browser/state/action/TabGroupActionTest.kt | 319 + .../browser/state/action/TabListActionTest.kt | 1477 +++ .../state/action/TrackingProtectionActionTest.kt | 171 + .../browser/state/action/TranslationsActionTest.kt | 903 ++ .../action/UpdateProductUrlStateActionTest.kt | 77 + .../browser/state/action/WebExtensionActionTest.kt | 420 + .../browser/state/engine/EngineMiddlewareTest.kt | 76 + .../browser/state/engine/EngineObserverTest.kt | 1832 +++ .../state/engine/middleware/CrashMiddlewareTest.kt | 148 + .../CreateEngineSessionMiddlewareTest.kt | 213 + .../middleware/EngineDelegateMiddlewareTest.kt | 813 ++ .../middleware/ExtensionsProcessMiddlewareTest.kt | 44 + .../engine/middleware/LinkingMiddlewareTest.kt | 246 + .../SessionPrioritizationMiddlewareTest.kt | 155 + .../engine/middleware/SuspendMiddlewareTest.kt | 155 + .../engine/middleware/TabsRemovedMiddlewareTest.kt | 246 + .../middleware/TranslationsMiddlewareTest.kt | 945 ++ .../engine/middleware/TrimMemoryMiddlewareTest.kt | 257 + .../middleware/WebExtensionMiddlewareTest.kt | 135 + .../components/browser/state/helper/TargetTest.kt | 138 + .../state/reducer/BrowserStateReducerKtTest.kt | 242 + .../CopyInternetResourceStateReducerTest.kt | 59 + .../browser/state/reducer/DebugReducerTest.kt | 33 + .../reducer/ExtensionsProcessStateReducerTest.kt | 41 + .../reducer/InternetResourceReducerUtilsTest.kt | 26 + .../browser/state/reducer/LastAccessReducerTest.kt | 128 + .../state/reducer/LocaleStateReducerTest.kt | 41 + .../ShareInternetResourceStateReducerTest.kt | 60 + .../browser/state/selector/SelectorsKtTest.kt | 306 + .../browser/state/state/LanguageSettingTest.kt | 85 + .../browser/state/state/SearchStateTest.kt | 248 + .../browser/state/state/TabPartitionTest.kt | 65 + .../state/state/TranslationEngineStateTest.kt | 123 + .../browser/state/state/TranslationSupportTest.kt | 141 + .../state/content/PermissionHighlightsStateTest.kt | 78 + .../state/store/BrowserStoreExceptionTest.kt | 201 + .../browser/state/store/BrowserStoreTest.kt | 95 + .../org.mockito.plugins.MockMaker | 2 + .../components/browser/storage-sync/README.md | 22 + .../components/browser/storage-sync/build.gradle | 64 + .../storage-sync/src/main/AndroidManifest.xml | 4 + .../components/browser/storage/sync/Connection.kt | 127 + .../storage/sync/GlobalPlacesDependencyProvider.kt | 36 + .../browser/storage/sync/PlacesBookmarksStorage.kt | 299 + .../browser/storage/sync/PlacesHistoryStorage.kt | 409 + .../storage/sync/PlacesHistoryStorageWorker.kt | 41 + .../browser/storage/sync/PlacesStorage.kt | 228 + .../browser/storage/sync/RemoteTabsStorage.kt | 154 + .../browser/storage/sync/StorageExtensions.kt | 44 + .../storage/sync/StorageMaintenanceWorker.kt | 48 + .../components/browser/storage/sync/Types.kt | 241 + .../sync/GlobalPlacesDependencyProviderTest.kt | 32 + .../storage/sync/PlacesBookmarksStorageTest.kt | 239 + .../storage/sync/PlacesHistoryStorageTest.kt | 1312 ++ .../storage/sync/PlacesHistoryStorageWorkerTest.kt | 93 + .../browser/storage/sync/PlacesStorageTest.kt | 109 + .../browser/storage/sync/RemoteTabsStorageTest.kt | 174 + .../src/test/resources/databases/bookmarks-v23.db | Bin 0 -> 270336 bytes .../src/test/resources/databases/empty-v0.db | Bin 0 -> 4096 bytes .../src/test/resources/databases/history-v34.db | Bin 0 -> 360448 bytes .../test/resources/databases/pinnedSites-v39.db | Bin 0 -> 253952 bytes .../src/test/resources/databases/populated-v38.db | Bin 0 -> 335873 bytes .../src/test/resources/databases/populated-v39.db | Bin 0 -> 598016 bytes .../test/resources/databases/populated-v39.db-shm | Bin 0 -> 32768 bytes .../test/resources/databases/populated-v39.db-wal | 0 .../test/resources/databases/withHistory-v39.db | Bin 0 -> 303104 bytes .../org.mockito.plugins.MockMaker | 1 + .../components/browser/tabstray/README.md | 19 + .../components/browser/tabstray/build.gradle | 55 + .../components/browser/tabstray/proguard-rules.pro | 21 + .../browser/tabstray/src/main/AndroidManifest.xml | 7 + .../browser/tabstray/SelectableTabViewHolder.kt | 17 + .../browser/tabstray/TabTouchCallback.kt | 53 + .../components/browser/tabstray/TabViewHolder.kt | 152 + .../components/browser/tabstray/TabsAdapter.kt | 105 + .../components/browser/tabstray/TabsTray.kt | 35 + .../components/browser/tabstray/TabsTrayStyling.kt | 31 + .../browser/tabstray/thumbnail/TabThumbnailView.kt | 33 + .../res/layout/mozac_browser_tabstray_item.xml | 68 + .../browser/tabstray/src/main/res/values/ids.xml | 8 + .../res/values/mozac_browser_tabstray_strings.xml | 10 + .../browser/tabstray/DefaultTabViewHolderTest.kt | 223 + .../browser/tabstray/TabTouchCallbackTest.kt | 73 + .../browser/tabstray/TabViewHolderTest.kt | 52 + .../components/browser/tabstray/TabsAdapterTest.kt | 161 + .../tabstray/thumbnail/TabThumbnailViewTest.kt | 81 + .../org.mockito.plugins.MockMaker | 2 + .../components/browser/thumbnails/README.md | 81 + .../components/browser/thumbnails/build.gradle | 63 + .../browser/thumbnails/proguard-rules.pro | 21 + .../thumbnails/src/main/AndroidManifest.xml | 4 + .../browser/thumbnails/BrowserThumbnails.kt | 86 + .../browser/thumbnails/ThumbnailsMiddleware.kt | 72 + .../browser/thumbnails/loader/ThumbnailLoader.kt | 71 + .../browser/thumbnails/storage/ThumbnailStorage.kt | 133 + .../browser/thumbnails/utils/ThumbnailDiskCache.kt | 130 + .../thumbnails/src/main/res/values/dimens.xml | 8 + .../thumbnails/src/main/res/values/tags.xml | 8 + .../browser/thumbnails/BrowserThumbnailsTest.kt | 139 + .../browser/thumbnails/ThumbnailsMiddlewareTest.kt | 199 + .../thumbnails/loader/ThumbnailLoaderTest.kt | 93 + .../thumbnails/storage/ThumbnailStorageTest.kt | 142 + .../thumbnails/utils/ThumbnailDiskCacheTest.kt | 148 + .../org.mockito.plugins.MockMaker | 2 + .../components/browser/toolbar/README.md | 37 + .../components/browser/toolbar/build.gradle | 56 + .../components/browser/toolbar/proguard-rules.pro | 21 + .../browser/toolbar/src/main/AndroidManifest.xml | 4 + .../components/browser/toolbar/BrowserToolbar.kt | 691 ++ .../browser/toolbar/display/DisplayToolbar.kt | 711 ++ .../browser/toolbar/display/DisplayToolbarView.kt | 51 + .../browser/toolbar/display/HighlightView.kt | 91 + .../browser/toolbar/display/MenuButton.kt | 106 + .../browser/toolbar/display/OriginView.kt | 198 + .../toolbar/display/SiteSecurityIconView.kt | 48 + .../toolbar/display/TrackingProtectionIconView.kt | 135 + .../components/browser/toolbar/edit/EditToolbar.kt | 415 + .../browser/toolbar/facts/ToolbarFacts.kt | 60 + .../browser/toolbar/internal/ActionContainer.kt | 134 + .../browser/toolbar/internal/ActionWrapper.kt | 16 + ...ac_browser_toolbar_icons_vertical_separator.xml | 9 + .../main/res/drawable/mozac_dot_notification.xml | 16 + .../main/res/drawable/mozac_ic_site_security.xml | 11 + ...mozac_ic_tracking_protection_off_for_a_site.xml | 12 + ..._tracking_protection_on_no_trackers_blocked.xml | 12 + ..._ic_tracking_protection_on_trackers_blocked.xml | 12 + .../mozac_browser_toolbar_displaytoolbar.xml | 175 + .../layout/mozac_browser_toolbar_edittoolbar.xml | 116 + .../toolbar/src/main/res/values-am/strings.xml | 18 + .../toolbar/src/main/res/values-an/strings.xml | 16 + .../toolbar/src/main/res/values-ar/strings.xml | 18 + .../toolbar/src/main/res/values-ast/strings.xml | 18 + .../toolbar/src/main/res/values-az/strings.xml | 16 + .../toolbar/src/main/res/values-azb/strings.xml | 18 + .../toolbar/src/main/res/values-ban/strings.xml | 8 + .../toolbar/src/main/res/values-be/strings.xml | 18 + .../toolbar/src/main/res/values-bg/strings.xml | 18 + .../toolbar/src/main/res/values-bn/strings.xml | 18 + .../toolbar/src/main/res/values-br/strings.xml | 18 + .../toolbar/src/main/res/values-bs/strings.xml | 18 + .../toolbar/src/main/res/values-ca/strings.xml | 18 + .../toolbar/src/main/res/values-cak/strings.xml | 18 + .../toolbar/src/main/res/values-ceb/strings.xml | 18 + .../toolbar/src/main/res/values-ckb/strings.xml | 18 + .../toolbar/src/main/res/values-co/strings.xml | 19 + .../toolbar/src/main/res/values-cs/strings.xml | 18 + .../toolbar/src/main/res/values-cy/strings.xml | 18 + .../toolbar/src/main/res/values-da/strings.xml | 18 + .../toolbar/src/main/res/values-de/strings.xml | 18 + .../toolbar/src/main/res/values-dsb/strings.xml | 18 + .../toolbar/src/main/res/values-el/strings.xml | 18 + .../toolbar/src/main/res/values-en-rCA/strings.xml | 18 + .../toolbar/src/main/res/values-en-rGB/strings.xml | 18 + .../toolbar/src/main/res/values-eo/strings.xml | 18 + .../toolbar/src/main/res/values-es-rAR/strings.xml | 18 + .../toolbar/src/main/res/values-es-rCL/strings.xml | 18 + .../toolbar/src/main/res/values-es-rES/strings.xml | 18 + .../toolbar/src/main/res/values-es-rMX/strings.xml | 18 + .../toolbar/src/main/res/values-es/strings.xml | 18 + .../toolbar/src/main/res/values-et/strings.xml | 18 + .../toolbar/src/main/res/values-eu/strings.xml | 18 + .../toolbar/src/main/res/values-fa/strings.xml | 18 + .../toolbar/src/main/res/values-ff/strings.xml | 16 + .../toolbar/src/main/res/values-fi/strings.xml | 18 + .../toolbar/src/main/res/values-fr/strings.xml | 18 + .../toolbar/src/main/res/values-fur/strings.xml | 18 + .../toolbar/src/main/res/values-fy-rNL/strings.xml | 18 + .../toolbar/src/main/res/values-ga-rIE/strings.xml | 16 + .../toolbar/src/main/res/values-gd/strings.xml | 18 + .../toolbar/src/main/res/values-gl/strings.xml | 18 + .../toolbar/src/main/res/values-gn/strings.xml | 18 + .../toolbar/src/main/res/values-gu-rIN/strings.xml | 16 + .../toolbar/src/main/res/values-hi-rIN/strings.xml | 16 + .../toolbar/src/main/res/values-hil/strings.xml | 8 + .../toolbar/src/main/res/values-hr/strings.xml | 18 + .../toolbar/src/main/res/values-hsb/strings.xml | 18 + .../toolbar/src/main/res/values-hu/strings.xml | 18 + .../toolbar/src/main/res/values-hy-rAM/strings.xml | 18 + .../toolbar/src/main/res/values-ia/strings.xml | 18 + .../toolbar/src/main/res/values-in/strings.xml | 18 + .../toolbar/src/main/res/values-is/strings.xml | 18 + .../toolbar/src/main/res/values-it/strings.xml | 18 + .../toolbar/src/main/res/values-iw/strings.xml | 18 + .../toolbar/src/main/res/values-ja/strings.xml | 18 + .../toolbar/src/main/res/values-ka/strings.xml | 18 + .../toolbar/src/main/res/values-kaa/strings.xml | 18 + .../toolbar/src/main/res/values-kab/strings.xml | 18 + .../toolbar/src/main/res/values-kk/strings.xml | 18 + .../toolbar/src/main/res/values-kmr/strings.xml | 18 + .../toolbar/src/main/res/values-kn/strings.xml | 16 + .../toolbar/src/main/res/values-ko/strings.xml | 19 + .../toolbar/src/main/res/values-ldrtl/dimens.xml | 8 + .../toolbar/src/main/res/values-lij/strings.xml | 16 + .../toolbar/src/main/res/values-lo/strings.xml | 18 + .../toolbar/src/main/res/values-lt/strings.xml | 18 + .../toolbar/src/main/res/values-mix/strings.xml | 18 + .../toolbar/src/main/res/values-ml/strings.xml | 16 + .../toolbar/src/main/res/values-mr/strings.xml | 16 + .../toolbar/src/main/res/values-my/strings.xml | 18 + .../toolbar/src/main/res/values-nb-rNO/strings.xml | 18 + .../toolbar/src/main/res/values-ne-rNP/strings.xml | 18 + .../toolbar/src/main/res/values-nl/strings.xml | 18 + .../toolbar/src/main/res/values-nn-rNO/strings.xml | 19 + .../toolbar/src/main/res/values-oc/strings.xml | 18 + .../toolbar/src/main/res/values-or/strings.xml | 16 + .../toolbar/src/main/res/values-pa-rIN/strings.xml | 18 + .../toolbar/src/main/res/values-pa-rPK/strings.xml | 18 + .../toolbar/src/main/res/values-pl/strings.xml | 18 + .../toolbar/src/main/res/values-pt-rBR/strings.xml | 18 + .../toolbar/src/main/res/values-pt-rPT/strings.xml | 18 + .../toolbar/src/main/res/values-rm/strings.xml | 18 + .../toolbar/src/main/res/values-ro/strings.xml | 16 + .../toolbar/src/main/res/values-ru/strings.xml | 18 + .../toolbar/src/main/res/values-sat/strings.xml | 18 + .../toolbar/src/main/res/values-sc/strings.xml | 16 + .../toolbar/src/main/res/values-si/strings.xml | 19 + .../toolbar/src/main/res/values-sk/strings.xml | 18 + .../toolbar/src/main/res/values-skr/strings.xml | 18 + .../toolbar/src/main/res/values-sl/strings.xml | 18 + .../toolbar/src/main/res/values-sq/strings.xml | 18 + .../toolbar/src/main/res/values-sr/strings.xml | 18 + .../toolbar/src/main/res/values-su/strings.xml | 18 + .../toolbar/src/main/res/values-sv-rSE/strings.xml | 18 + .../toolbar/src/main/res/values-szl/strings.xml | 18 + .../toolbar/src/main/res/values-ta/strings.xml | 18 + .../toolbar/src/main/res/values-te/strings.xml | 18 + .../toolbar/src/main/res/values-tg/strings.xml | 18 + .../toolbar/src/main/res/values-th/strings.xml | 18 + .../toolbar/src/main/res/values-tl/strings.xml | 18 + .../toolbar/src/main/res/values-tok/strings.xml | 14 + .../toolbar/src/main/res/values-tr/strings.xml | 18 + .../toolbar/src/main/res/values-trs/strings.xml | 18 + .../toolbar/src/main/res/values-tt/strings.xml | 18 + .../toolbar/src/main/res/values-tzm/strings.xml | 10 + .../toolbar/src/main/res/values-ug/strings.xml | 18 + .../toolbar/src/main/res/values-uk/strings.xml | 18 + .../toolbar/src/main/res/values-ur/strings.xml | 18 + .../toolbar/src/main/res/values-uz/strings.xml | 18 + .../toolbar/src/main/res/values-vec/strings.xml | 16 + .../toolbar/src/main/res/values-vi/strings.xml | 18 + .../toolbar/src/main/res/values-yo/strings.xml | 18 + .../toolbar/src/main/res/values-zh-rCN/strings.xml | 18 + .../toolbar/src/main/res/values-zh-rTW/strings.xml | 18 + .../src/main/res/values/attrs_browser_toolbar.xml | 33 + .../browser/toolbar/src/main/res/values/dimens.xml | 31 + .../browser/toolbar/src/main/res/values/ids.xml | 8 + .../toolbar/src/main/res/values/strings.xml | 21 + .../browser/toolbar/AsyncFilterListenerTest.kt | 350 + .../browser/toolbar/BrowserToolbarTest.kt | 1044 ++ .../browser/toolbar/display/DisplayToolbarTest.kt | 824 ++ .../browser/toolbar/display/HighlightViewTest.kt | 67 + .../browser/toolbar/display/MenuButtonTest.kt | 156 + .../display/TrackingProtectionIconViewTest.kt | 43 + .../browser/toolbar/edit/EditToolbarTest.kt | 290 + .../toolbar/internal/ActionContainerTest.kt | 99 + .../org.mockito.plugins.MockMaker | 2 + 1258 files changed, 158043 insertions(+) create mode 100644 mobile/android/android-components/components/browser/domains/README.md create mode 100644 mobile/android/android-components/components/browser/domains/build.gradle create mode 100644 mobile/android/android-components/components/browser/domains/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/domains/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/br create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/ca create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/de create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/fr create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/gb create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/global create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/hk create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/id create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/pl create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/ru create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/sg create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/tw create mode 100644 mobile/android/android-components/components/browser/domains/src/main/assets/domains/us create mode 100644 mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/CustomDomains.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domain.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/DomainAutoCompleteProvider.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/Domains.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/main/java/mozilla/components/browser/domains/autocomplete/Providers.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/BaseDomainAutocompleteProviderTest.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/CustomDomainsTest.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainAutoCompleteProviderTest.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainTest.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/DomainsTest.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/test/java/mozilla/components/browser/domains/ProvidersTest.kt create mode 100644 mobile/android/android-components/components/browser/domains/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/browser/engine-gecko/README.md create mode 100644 mobile/android/android-components/components/browser/engine-gecko/build.gradle create mode 100644 mobile/android/android-components/components/browser/engine-gecko/docs/metrics.md create mode 100644 mobile/android/android-components/components/browser/engine-gecko/geckoview.fml.yaml create mode 100644 mobile/android/android-components/components/browser/engine-gecko/metrics.yaml create mode 100644 mobile/android/android-components/components/browser/engine-gecko/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/androidTest/java/mozilla/components/browser/engine/gecko/fetch/geckoview/GeckoViewFetchTestCases.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngine.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionState.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineView.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoResult.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorage.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/NestedGeckoView.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/autofill/GeckoAutocompleteStorageDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/content/blocking/GeckoTrackingProtectionException.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorage.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepository.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Address.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/CreditCard.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoChoice.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/GeckoContentPermissions.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/Login.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicy.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchClient.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/LocaleSettingUpdater.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/integration/SettingUpdater.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediaquery/PreferredColorScheme.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionController.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/profiler/Profiler.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslationUtils.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactory.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtension.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionException.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandler.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/experiment/NimbusExperimentDelegate.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionStateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineSessionTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoEngineViewTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoResultTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoTrackingProtectionExceptionStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/GeckoWebExtensionExceptionTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/NestedGeckoViewTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoActivityDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoScreenOrientationDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/activity/GeckoViewActivityContextDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/GeckoCookieBannersStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/cookiebanners/ReportSiteDomainsRepositoryTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/ext/TrackingProtectionPolicyKtTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/fetch/GeckoViewFetchUnitTestCases.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/integration/SettingUpdaterTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/media/GeckoMediaDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionControllerTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/mediasession/GeckoMediaSessionDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequestTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/ChoicePromptDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/prompt/PromptInstanceDismissDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/selection/GeckoSelectionActionDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/serviceworker/GeckoServiceWorkerDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/translate/GeckoTranslateSessionDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/util/SpeculativeSessionFactoryTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/GeckoWebExtensionTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webextension/MockWebExtension.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/GeckoWebNotificationDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webnotifications/MockWebNotification.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/webpush/GeckoWebPushHandlerTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/window/GeckoWindowRequestTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/experiment/NimbusExperimentDelegateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/test/ReflectionUtils.kt create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/engine-gecko/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/browser/engine-system/README.md create mode 100644 mobile/android/android-components/components/browser/engine-system/build.gradle create mode 100644 mobile/android/android-components/components/browser/engine-system/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/engine-system/src/androidTest/java/mozilla/components/browser/engine/system/VersionTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/androidTest/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/NestedWebView.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngine.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSession.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineSessionState.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/SystemEngineView.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/ReversibleString.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Safelist.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/Trie.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/matcher/UrlMatcher.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/window/SystemWindowRequest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_blocklist.json create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/raw/domain_safelist.json create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-am/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-an/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ann/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ar/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ast/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-az/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-azb/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ban/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-be/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-bg/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-bn/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-br/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-bs/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ca/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-cak/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ceb/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ckb/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-co/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-cs/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-cy/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-da/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-de/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-dsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-el/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rCA/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-en-rGB/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-eo/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rAR/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rCL/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rES/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-es-rMX/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-es/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-et/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-eu/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-fa/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ff/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-fi/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-fr/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-fur/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-fy-rNL/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ga-rIE/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-gd/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-gl/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-gn/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-gu-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-hi-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-hil/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-hr/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-hsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-hu/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-hy-rAM/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ia/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-in/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-is/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-it/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-iw/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ja/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ka/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-kaa/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-kab/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-kk/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-kmr/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-kn/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ko/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-kw/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-lij/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-lo/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-lt/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-mix/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ml/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-mr/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-my/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-nb-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ne-rNP/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-nl/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-nn-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-nv/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-oc/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-or/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-pa-rPK/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-pl/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ppl/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rBR/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-pt-rPT/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-rm/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ro/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ru/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-sat/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-sc/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-si/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-sk/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-skr/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-sl/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-sq/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-sr/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-su/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-sv-rSE/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-szl/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ta/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-te/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-tg/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-th/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-tl/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-tok/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-tr/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-trs/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-tt/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-tzm/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ug/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-uk/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-ur/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-uz/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-vec/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-vi/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-yo/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rCN/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values-zh-rTW/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/NestedWebViewTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionStateTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineSessionTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/SystemEngineViewTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/ReversibleStringTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/SafelistTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/TrieTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/matcher/UrlMatcherTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequestTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/java/mozilla/components/browser/engine/system/window/SystemWindowRequestTest.kt create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/engine-system/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/browser/errorpages/README.md create mode 100644 mobile/android/android-components/components/browser/errorpages/build.gradle create mode 100644 mobile/android/android-components/components/browser/errorpages/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/assets/errorPageScripts.js create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/assets/error_page_js.html create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/assets/error_style.css create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/java/mozilla/components/browser/errorpages/ErrorPages.kt create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-am/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-an/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ann/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ar/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ast/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-az/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-azb/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ban/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-be/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-bg/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-bn/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-br/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-bs/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ca/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-cak/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ceb/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ckb/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-co/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-cs/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-cy/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-da/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-de/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-dsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-el/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rCA/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-en-rGB/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-eo/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rAR/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rCL/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rES/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-es-rMX/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-es/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-et/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-eu/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-fa/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ff/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-fi/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-fr/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-fur/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-fy-rNL/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ga-rIE/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-gd/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-gl/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-gn/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-gu-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-hi-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-hil/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-hr/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-hsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-hu/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-hy-rAM/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ia/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-in/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-is/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-it/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-iw/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ja/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ka/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-kaa/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-kab/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-kk/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-kmr/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-kn/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ko/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-kw/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-lij/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-lo/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-lt/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-mix/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ml/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-mr/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-my/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-nb-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ne-rNP/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-nl/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-nn-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-nv/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-oc/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-or/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-pa-rPK/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-pl/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ppl/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rBR/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-pt-rPT/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-rm/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ro/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ru/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-sat/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-sc/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-si/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-sk/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-skr/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-sl/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-sq/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-sr/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-su/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-sv-rSE/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ta/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-te/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-tg/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-th/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-tl/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-tok/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-tr/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-trs/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-tt/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-tzm/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ug/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-uk/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-ur/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-uz/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-vec/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-vi/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-yo/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rCN/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values-zh-rTW/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/components/browser/errorpages/src/test/java/mozilla/components/browser/errorpages/ErrorPagesTest.kt create mode 100644 mobile/android/android-components/components/browser/errorpages/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/browser/icons/.gitignore create mode 100644 mobile/android/android-components/components/browser/icons/README.md create mode 100644 mobile/android/android-components/components/browser/icons/build.gradle create mode 100644 mobile/android/android-components/components/browser/icons/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/OnDeviceBrowserIconsTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/androidTest/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/icons.js create mode 100644 mobile/android/android-components/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.template.json create mode 100644 mobile/android/android-components/components/browser/icons/src/main/assets/mozac.browser.icons/icons-top200.json create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/Icon.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/IconRequest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderScope.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/IconLoaderState.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/compose/Loader.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ICOIconDecoder.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/SvgIconDecoder.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/decoder/ico/IconDirectoryEntry.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessage.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/WebAppManifest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/DefaultIconGenerator.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/generator/IconGenerator.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DataUriIconLoader.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/DiskIconLoader.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/HttpIconLoader.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/IconLoader.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/MemoryIconLoader.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoader.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/pipeline/IconResourceComparator.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/DiskIconPreparer.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/IconPreprarer.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/MemoryIconPreparer.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparer.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessor.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ColorProcessor.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/DiskIconProcessor.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/IconProcessor.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/MemoryIconProcessor.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/processor/ResizingProcessor.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconDiskCache.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/IconMemoryCache.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/java/mozilla/components/browser/icons/utils/Utils.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/main/res/values/colors.xml create mode 100644 mobile/android/android-components/components/browser/icons/src/main/res/values/dimens.xml create mode 100644 mobile/android/android-components/components/browser/icons/src/main/res/values/tags.xml create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/BrowserIconsTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/ICOIconDecoderTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/decoder/SvgIconDecoderTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageKtTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/generator/DefaultIconGeneratorTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DataUriIconLoaderTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/DiskIconLoaderTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/FailureCacheTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/HttpIconLoaderTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/MemoryIconLoaderTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/loader/NonBlockingHttpIconLoaderTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/pipeline/IconResourceComparatorTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/DiskIconPreparerTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/MemoryIconPreparerTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/preparer/TippyTopIconPreparerTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/AdaptiveIconProcessorTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ColorProcessorTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/DiskIconProcessorTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/MemoryIconProcessorTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/processor/ResizingProcessorTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconDiskCacheTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/java/mozilla/components/browser/icons/utils/IconMemoryCacheTest.kt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/bmp/test.bmp create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/gif/cat.gif create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/ico/golem_favicon.ico create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/ico/microsoft_favicon.ico create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/ico/nvidia_favicon.ico create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/jpg/tonys.jpg create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/misc/test.txt create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/png/mozac.png create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/browser/icons/src/test/resources/webp/test.webp create mode 100644 mobile/android/android-components/components/browser/menu/README.md create mode 100644 mobile/android/android-components/components/browser/menu/build.gradle create mode 100644 mobile/android/android-components/components/browser/menu/lint.xml create mode 100644 mobile/android/android-components/components/browser/menu/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/menu/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenu.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuAdapter.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuBuilder.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuHighlight.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPlacement.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/BrowserMenuPositioning.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenu.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilder.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/BrowserMenuItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/ext/View.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/facts/BrowserMenuFacts.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BackPressMenuItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCategory.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCheckbox.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButton.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuDivider.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitch.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitch.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageText.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButton.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbar.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/BrowserMenuSwitch.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/CustomTooltip.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/ParentBrowserMenuItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageText.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/item/WebExtensionPlaceholderMenuItem.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerView.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/ExpandableLayout.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/MenuButton.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManager.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManager.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/java/mozilla/components/browser/menu/view/StickyItemLayoutManager.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_bottom.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/anim-ldrtl/menu_enter_top.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_bottom.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_enter_top.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/anim/menu_exit.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_browser_menu_notification_icon.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_indicator.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/drawable/mozac_menu_notification.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/drawable/rounded_corner.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_button.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_category.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_item.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_highlightable_switch.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_checkbox.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_divider.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_switch.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_image_text_checkbox_button.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_parent_menu.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_simple.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_switch.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_item_toolbar.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_menu_web_extension.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/layout/mozac_browser_tooltip_layout.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-am/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-an/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ar/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ast/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-az/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-azb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ban/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-be/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-bg/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-bn/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-br/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-bs/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ca/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-cak/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ceb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ckb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-co/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-cs/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-cy/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-da/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-de/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-dsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-el/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-en-rCA/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-en-rGB/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-eo/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-es-rAR/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-es-rCL/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-es-rES/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-es-rMX/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-es/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-et/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-eu/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-fa/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ff/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-fi/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-fr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-fur/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-fy-rNL/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-gd/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-gl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-gn/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-gu-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-hi-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-hil/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-hr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-hsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-hu/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-hy-rAM/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ia/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-in/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-is/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-it/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-iw/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ja/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ka/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-kaa/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-kab/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-kk/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-kmr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-kn/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ko/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-kw/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ldrtl/dimens.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-lij/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-lo/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-lt/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-mix/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-mr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-my/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-nb-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ne-rNP/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-nl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-nn-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-oc/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-or/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-pa-rPK/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-pl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rBR/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-pt-rPT/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-rm/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ro/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ru/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-sat/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-sc/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-si/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-sk/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-skr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-sl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-sq/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-sr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-su/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-sv-rSE/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-szl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ta/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-te/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-tg/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-th/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-tl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-tok/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-tr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-trs/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-tt/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-tzm/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ug/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-uk/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-ur/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-uz/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-vec/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-vi/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-yo/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rCN/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values-zh-rTW/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values/colors.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values/dimens.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/main/res/values/style.xml create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuAdapterTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuBuilderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuHighlightTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuPositioningTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/BrowserMenuTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuBuilderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/WebExtensionBrowserMenuTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/ext/BrowserMenuItemTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/AbstractParentBrowserMenuItemTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCategoryTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCheckboxTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuCompoundButtonTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuDividerTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableItemTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuHighlightableSwitchTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageSwitchTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextCheckboxButtonTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuImageTextTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuItemToolbarTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/BrowserMenuSwitchTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/ParentBrowserMenuItemTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuHighlightableItemTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/SimpleBrowserMenuItemTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/TwoStateBrowserMenuImageTextTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/item/WebExtensionBrowserMenuItemTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/DynamicWidthRecyclerViewTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/ExpandableLayoutTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemLayoutManager.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/FakeStickyItemsAdapter.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/MenuButtonTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyFooterLinearLayoutManagerTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyHeaderLinearLayoutManagerTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/java/mozilla/components/browser/menu/view/StickyItemsLinearLayoutManagerTest.kt create mode 100644 mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/menu/src/test/resources/mockito-extensions/robolectric.properties create mode 100644 mobile/android/android-components/components/browser/menu/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/browser/menu2/README.md create mode 100644 mobile/android/android-components/components/browser/menu2/build.gradle create mode 100644 mobile/android/android-components/components/browser/menu2/lint.xml create mode 100644 mobile/android/android-components/components/browser/menu2/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/BrowserMenuController.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolders.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/LastItemViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapter.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/MenuCandidateViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/NestedMenuCandidateViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHolders.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapter.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/MenuIconViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioning.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/ext/View.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuButton2.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/java/mozilla/components/browser/menu2/view/MenuView.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_bottom.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_left_top.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_bottom.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_enter_right_top.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/anim/menu_exit.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_indicator.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/drawable/mozac_browser_menu2_notification_icon.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_button.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_checkbox.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_compound_switch.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_decorative_text.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_divider.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_nested.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_row_small_icon.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_candidate_text.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_button.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_drawable.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_notification_dot.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_icon_text.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/layout/mozac_browser_menu2_view.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-am/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-an/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ar/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ast/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-az/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-azb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ban/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-be/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-bg/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-bn/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-br/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-bs/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ca/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-cak/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ceb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ckb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-co/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-cs/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-cy/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-da/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-de/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-dsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-el/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rCA/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-en-rGB/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-eo/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rAR/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rCL/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rES/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-es-rMX/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-es/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-et/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-eu/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-fa/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-fi/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-fr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-fur/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-fy-rNL/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-gd/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-gl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-gn/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-gu-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-hi-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-hil/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-hr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-hsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-hu/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-hy-rAM/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ia/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-in/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-is/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-it/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-iw/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ja/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ka/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-kaa/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-kab/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-kk/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-kmr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-kn/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ko/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-kw/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ldrtl/dimens.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-lij/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-lo/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-lt/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-mix/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-mr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-my/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-nb-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ne-rNP/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-nl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-nn-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-oc/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-or/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-pa-rPK/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-pl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rBR/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-pt-rPT/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-rm/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ro/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ru/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-sat/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-sc/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-si/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-sk/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-skr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-sl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-sq/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-sr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-su/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-sv-rSE/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-szl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ta/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-te/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-tg/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-th/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-tl/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-tr/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-trs/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-tt/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-tzm/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ug/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-uk/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-ur/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-uz/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-vec/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-vi/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-yo/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rCN/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values-zh-rTW/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values/colors.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values/dimens.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/main/res/values/style.xml create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/BrowserMenuControllerTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/CompoundMenuCandidateViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DecorativeTextMenuCandidateViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/DividerMenuCandidateViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/MenuCandidateListAdapterTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/RowMenuCandidateViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/SmallMenuCandidateViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/TextMenuCandidateViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/DrawableMenuIconViewHoldersTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/MenuIconAdapterTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/adapter/icons/TextMenuIconViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/BrowserMenuPositioningTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/ext/ViewTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuButton2Test.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/java/mozilla/components/browser/menu2/view/MenuViewTest.kt create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/menu2/src/test/resources/robolectric.properties create mode 100644 mobile/android/android-components/components/browser/session-storage/README.md create mode 100644 mobile/android/android-components/components/browser/session-storage/build.gradle create mode 100644 mobile/android/android-components/components/browser/session-storage/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/session-storage/src/androidTest/assets/index.html create mode 100644 mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/FullRestoreTest.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/androidTest/java/mozilla/components/browser/session/storage/RestoringBrowsingSessionsTest.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorage.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/FileEngineSessionStateStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt create mode 100644 mobile/android/android-components/components/browser/session-storage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/state/README.md create mode 100644 mobile/android/android-components/components/browser/state/build.gradle create mode 100644 mobile/android/android-components/components/browser/state/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/state/src/androidTest/java/mozilla/components/browser/state/helper/OnDeviceTargetTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/ActionWithTab.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/AwesomeBarAction.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CrashMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/LinkingMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SuspendMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/helper/Target.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/AwesomeBarStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContainerReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CookieBannerStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DebugReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/DownloadStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/HistoryMetadataReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtils.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LastAccessReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/LocaleStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/MediaSessionReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ReaderStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/RecentlyClosedReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SearchReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabGroupReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TrackingProtectionStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TranslationsStateReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/UndoReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/WebExtensionReducer.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/RegionState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/search/SearchEngine.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/selector/Selectors.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AppIntentState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/AwesomeBarState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContainerState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabConfig.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LastMediaAccessState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/LoadRequestState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/MediaSessionState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/ReaderState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SearchState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SecurityInfoState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabPartition.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TrackingProtectionState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsBrowserState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TranslationsState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/WebExtensionState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/FindResultState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/HistoryState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/ShareInternetResourceState.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/extension/WebExtensionPromptRequest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/recover/RecoverableTab.kt create mode 100644 mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/store/BrowserStore.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/AwesomeBarActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContainerActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CookieBannerActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/CustomTabListActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DebugActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/DownloadActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/HistoryMetadataActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LastAccessActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/LocaleActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/MediaSessionActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/ReaderActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/SearchActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TrackingProtectionActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TranslationsActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/UpdateProductUrlStateActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/WebExtensionActionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CrashMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/CreateEngineSessionMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/EngineDelegateMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/ExtensionsProcessMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SuspendMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TabsRemovedMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TranslationsMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/TrimMemoryMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/WebExtensionMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/helper/TargetTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/BrowserStateReducerKtTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/CopyInternetResourceStateReducerTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/DebugReducerTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ExtensionsProcessStateReducerTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/InternetResourceReducerUtilsTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LastAccessReducerTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/LocaleStateReducerTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/reducer/ShareInternetResourceStateReducerTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/selector/SelectorsKtTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/LanguageSettingTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/SearchStateTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TabPartitionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationEngineStateTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TranslationSupportTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/content/PermissionHighlightsStateTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreTest.kt create mode 100644 mobile/android/android-components/components/browser/state/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/storage-sync/README.md create mode 100644 mobile/android/android-components/components/browser/storage-sync/build.gradle create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Connection.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProvider.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorage.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorage.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorker.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/PlacesStorage.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/RemoteTabsStorage.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageExtensions.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/StorageMaintenanceWorker.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/main/java/mozilla/components/browser/storage/sync/Types.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/GlobalPlacesDependencyProviderTest.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesBookmarksStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesHistoryStorageWorkerTest.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/PlacesStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/java/mozilla/components/browser/storage/sync/RemoteTabsStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/bookmarks-v23.db create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/empty-v0.db create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/history-v34.db create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/pinnedSites-v39.db create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v38.db create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-shm create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/populated-v39.db-wal create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/databases/withHistory-v39.db create mode 100644 mobile/android/android-components/components/browser/storage-sync/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/tabstray/README.md create mode 100644 mobile/android/android-components/components/browser/tabstray/build.gradle create mode 100644 mobile/android/android-components/components/browser/tabstray/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/SelectableTabViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabTouchCallback.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabViewHolder.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTrayStyling.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailView.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/res/layout/mozac_browser_tabstray_item.xml create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/res/values/ids.xml create mode 100644 mobile/android/android-components/components/browser/tabstray/src/main/res/values/mozac_browser_tabstray_strings.xml create mode 100644 mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/DefaultTabViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabTouchCallbackTest.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabViewHolderTest.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/thumbnail/TabThumbnailViewTest.kt create mode 100644 mobile/android/android-components/components/browser/tabstray/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/thumbnails/README.md create mode 100644 mobile/android/android-components/components/browser/thumbnails/build.gradle create mode 100644 mobile/android/android-components/components/browser/thumbnails/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/BrowserThumbnails.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsMiddleware.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoader.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorage.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/main/res/values/dimens.xml create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/main/res/values/tags.xml create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/BrowserThumbnailsTest.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsMiddlewareTest.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/loader/ThumbnailLoaderTest.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCacheTest.kt create mode 100644 mobile/android/android-components/components/browser/thumbnails/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/android-components/components/browser/toolbar/README.md create mode 100644 mobile/android/android-components/components/browser/toolbar/build.gradle create mode 100644 mobile/android/android-components/components/browser/toolbar/proguard-rules.pro create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/AndroidManifest.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbarView.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/HighlightView.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/MenuButton.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/OriginView.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/SiteSecurityIconView.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/edit/EditToolbar.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/facts/ToolbarFacts.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionContainer.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/internal/ActionWrapper.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_browser_toolbar_icons_vertical_separator.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_dot_notification.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_site_security.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_off_for_a_site.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_no_trackers_blocked.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/drawable/mozac_ic_tracking_protection_on_trackers_blocked.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_edittoolbar.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-am/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-an/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ar/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ast/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-az/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-azb/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ban/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-be/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-bg/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-bn/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-br/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-bs/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ca/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-cak/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ceb/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ckb/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-co/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-cs/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-cy/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-da/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-de/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-dsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-el/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rCA/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-en-rGB/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-eo/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rAR/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rCL/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rES/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-es-rMX/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-es/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-et/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-eu/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-fa/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ff/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-fi/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-fr/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-fur/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-fy-rNL/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ga-rIE/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-gd/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-gl/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-gn/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-gu-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-hi-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-hil/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-hr/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-hsb/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-hu/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-hy-rAM/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ia/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-in/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-is/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-it/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-iw/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ja/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ka/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-kaa/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-kab/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-kk/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-kmr/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-kn/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ko/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ldrtl/dimens.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-lij/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-lo/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-lt/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-mix/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ml/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-mr/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-my/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-nb-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ne-rNP/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-nl/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-nn-rNO/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-oc/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-or/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rIN/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-pa-rPK/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-pl/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rBR/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-pt-rPT/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-rm/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ro/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ru/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-sat/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-sc/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-si/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-sk/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-skr/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-sl/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-sq/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-sr/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-su/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-sv-rSE/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-szl/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ta/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-te/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-tg/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-th/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-tl/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-tok/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-tr/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-trs/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-tt/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-tzm/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ug/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-uk/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-ur/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-uz/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-vec/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-vi/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-yo/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rCN/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values-zh-rTW/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values/attrs_browser_toolbar.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values/dimens.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values/ids.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/main/res/values/strings.xml create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/AsyncFilterListenerTest.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/BrowserToolbarTest.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/HighlightViewTest.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/MenuButtonTest.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconViewTest.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/edit/EditToolbarTest.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/internal/ActionContainerTest.kt create mode 100644 mobile/android/android-components/components/browser/toolbar/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker (limited to 'mobile/android/android-components/components/browser') 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 @@ + + 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 = + 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) { + 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() + 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) { + 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.into(): List { + 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() + + @Volatile + internal var shippedDomains = emptyList() + 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, 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 { + return load(context, getCountriesInDefaultLocaleList()) + } + + internal fun load(context: Context, countries: Set): List { + val domains = LinkedHashSet() + 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 { + val availableDomains = LinkedHashSet() + val assetManager = context.assets + val domains = try { + assetManager.list("domains") ?: emptyArray() + } catch (e: IOException) { + emptyArray() + } + availableDomains.addAll(domains) + return availableDomains + } + + private fun loadDomainsForLanguage(context: Context, domains: MutableSet, country: String) { + val assetManager = context.assets + val languageDomains = try { + assetManager.open("domains/$country").bufferedReader().readLines() + } catch (e: IOException) { + emptyList() + } + domains.addAll(languageDomains) + } + + private fun getCountriesInDefaultLocaleList(): Set { + val countries = java.util.LinkedHashSet() + 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 + +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 = 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 @@ + + 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) -> 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() + }, + { throwable -> + onError(throwable) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(GeckoWebExtensionException.createWebExtensionException(throwable)) + GeckoResult() + }, + ) + } + 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() + }, + { throwable -> + onError(GeckoWebExtensionException.createWebExtensionException(throwable)) + GeckoResult() + }, + ) + } + 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() + }, + { throwable -> + onError(ext.id, throwable) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(extension.id, GeckoWebExtensionException(throwable)) + GeckoResult() + }, + ) + } + + /** + * 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 { + val extension = GeckoWebExtension(ext, runtime) + val result = GeckoResult() + + 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, + newOrigins: Array, + ): GeckoResult? { + val result = GeckoResult() + 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, + origins: Array, + ): GeckoResult? { + val result = GeckoResult() + 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) -> 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() + }, + { throwable -> + onError(throwable) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(throwable) + GeckoResult() + }, + ) + } + + /** + * See [Engine.addOptionalPermissions]. + */ + override fun addOptionalPermissions( + extensionId: String, + permissions: List, + origins: List, + 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() + }, + { throwable -> + onError(throwable) + GeckoResult() + }, + ) + } + + /** + * See [Engine.removeOptionalPermissions]. + */ + override fun removeOptionalPermissions( + extensionId: String, + permissions: List, + origins: List, + 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() + }, + { throwable -> + onError(throwable) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(throwable) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(throwable) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { + throwable -> + onError(throwable) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * See [Engine.getTranslationsModelDownloadStates]. + */ + override fun getTranslationsModelDownloadStates( + onSuccess: (List) -> Unit, + onError: (Throwable) -> Unit, + ) { + TranslationsController.RuntimeTranslation.listModelDownloadStates().then( + { + if (it != null) { + var listOfModels = mutableListOf() + 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() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * See [Engine.getSupportedTranslationLanguages]. + */ + override fun getSupportedTranslationLanguages( + onSuccess: (TranslationSupport) -> Unit, + onError: (Throwable) -> Unit, + ) { + TranslationsController.RuntimeTranslation.listSupportedLanguages().then( + { + if (it != null) { + val listOfFromLanguages = mutableListOf() + val listOfToLanguages = mutableListOf() + + 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() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * See [Engine.getUserPreferredLanguages]. + */ + override fun getUserPreferredLanguages( + onSuccess: (List) -> Unit, + onError: (Throwable) -> Unit, + ) { + TranslationsController.RuntimeTranslation.preferredLanguages().then( + { + if (it != null) { + onSuccess(it) + } else { + onError(TranslationError.UnexpectedNull()) + } + + GeckoResult() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * See [Engine.setLanguageSetting]. + */ + override fun setLanguageSetting( + languageCode: String, + languageSetting: LanguageSetting, + onSuccess: () -> Unit, + onError: (Throwable) -> Unit, + ) { + TranslationsController.RuntimeTranslation.setLanguageSettings(languageCode, languageSetting.toString()).then( + { + onSuccess() + GeckoResult() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * See [Engine.getLanguageSettings]. + */ + override fun getLanguageSettings( + onSuccess: (Map) -> Unit, + onError: (Throwable) -> Unit, + ) { + TranslationsController.RuntimeTranslation.getLanguageSettings().then( + { + if (it != null) { + try { + val result = mutableMapOf() + it.forEach { item -> + result[item.key] = LanguageSetting.fromValue(item.value) + } + onSuccess(result) + } catch (e: IllegalArgumentException) { + onError(e.intoTranslationError()) + } + } else { + onError(TranslationError.UnexpectedNull()) + } + GeckoResult() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * See [Engine.getNeverTranslateSiteList]. + */ + override fun getNeverTranslateSiteList( + onSuccess: (List) -> 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() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * See [Engine.setNeverTranslateSpecifiedSite]. + */ + override fun setNeverTranslateSpecifiedSite( + origin: String, + setting: Boolean, + onSuccess: () -> Unit, + onError: (Throwable) -> Unit, + ) { + TranslationsController.RuntimeTranslation.setNeverTranslateSpecifiedSite(setting, origin).then( + { + onSuccess() + GeckoResult() + }, + { throwable -> + onError(throwable.intoTranslationError()) + GeckoResult() + }, + ) + } + + /** + * 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 = + 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 = 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?, + ) + + @VisibleForTesting + internal var initialLoadRequest: LoadRequest? = null + + /** + * See [EngineSession.loadUrl] + */ + override fun loadUrl( + url: String, + parent: EngineSession?, + flags: LoadUrlFlags, + additionalHeaders: Map?, + ) { + 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() + } + + 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() + }, + { 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() + }, + { 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() + } + } + + /** + * 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() + } + } + + /** + * 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() + } + notifyObservers { onCheckForFormData(result) } + GeckoResult() + }, + { throwable -> + notifyObservers { + onCheckForFormDataException(throwable) + } + GeckoResult() + }, + ) + } + + /** + * 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() + }, + { throwable -> + logger.error("Checking for PDF viewer failed.", throwable) + onException(throwable) + GeckoResult() + }, + ) + } + + /** + * See [EngineSession.requestProductRecommendations] + */ + override fun requestProductRecommendations( + url: String, + onResult: (List) -> Unit, + onException: (Throwable) -> Unit, + ) { + geckoSession.requestRecommendations(url).then( + { response: List? -> + 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() + }, + { 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() + }, + { 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() + }, + { 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() + }, + { 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() + }, + { 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() + }, + { 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() + }, + { 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() + }, + { 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() + }, { + 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() + }, { + 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() + }, { + 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() + }, { + 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 { + 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, + 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 { + // 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 { + 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 { + 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 { + 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? { + // 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, + ): GeckoResult? { + 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() + + 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 { + val cookiesPolicies = mutableListOf() + + 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 { + val geckoResult = GeckoResult() + 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?, + audio: Array?, + 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?, + 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() + }, + { + onFinish(null) + GeckoResult() + }, + ) + } 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 GeckoResult.await() = suspendCoroutine { continuation -> + then( + { + continuation.resume(it) + GeckoResult() + }, + { + continuation.resumeWithException(it) + GeckoResult() + }, + ) +} + +/** + * Converts a [GeckoResult] to a [CancellableOperation]. + */ +fun GeckoResult.asCancellableOperation(): CancellableOperation { + val geckoResult = this + return object : CancellableOperation { + override fun cancel(): Deferred { + val result = CompletableDeferred() + geckoResult.cancel().then( + { + result.complete(it ?: false) + GeckoResult() + }, + { throwable -> + result.completeExceptionally(throwable) + GeckoResult() + }, + ) + return result + } + } +} + +/** + * Create a GeckoResult from a co-routine. + */ +@Suppress("TooGenericExceptionCaught") +fun CoroutineScope.launchGeckoResult( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> T, +) = GeckoResult().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) -> Unit) { + runtime.storageController.allPermissions.accept { permissions -> + val trackingExceptions = permissions.filterTrackingProtectionExceptions() + onResult(trackingExceptions.map { exceptions -> exceptions.toTrackingProtectionException() }) + } + } + + private fun List?.filterTrackingProtectionExceptions() = + this.orEmpty().filter { it.isExcludedForTrackingProtection } + + private fun List?.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) -> 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?, 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 = + 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, +) : GeckoRuntime.ActivityDelegate { + + private val logger = Logger(GeckoActivityDelegate::javaClass.name) + + override fun onStartActivityForResult(intent: PendingIntent): GeckoResult { + val result: GeckoResult = 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 { + val result = GeckoResult() + + 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, +) : 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>? { + val result = GeckoResult>() + + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(IO) { + val addresses = creditCardsAddressesStorageDelegate.onAddressesFetch() + .map { it.toAutocompleteAddress() } + .toTypedArray() + + result.complete(addresses) + } + + return result + } + + override fun onCreditCardFetch(): GeckoResult> { + val result = GeckoResult>() + + @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> { + val result = GeckoResult>() + + @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, +) { + + 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() + 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, +): Array = 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 = 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 = 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>() { + + override var value: Array = 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 { + val localeList = LocaleList.getAdjustedDefault() + return arrayOfNulls(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 { + /** + * 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, + ) { + 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, + 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, + ) : 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) { + 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, + 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, + private val audioSources: List, + private val callback: PermissionDelegate.MediaCallback, + ) : GeckoPermissionRequest( + videoSources.map { mapPermission(it) } + audioSources.map { mapPermission(it) }, + ) { + override fun grant(permissions: List) { + 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) { + 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() + + 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 { + 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 { + val onDiskPermissions: List = 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? { + 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>, + ): 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? { + 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> { + return if (this is GeckoPermissionRequest.Content) { + mapOf(geckoPermission.permission to listOf(geckoPermission)) + } else { + findGeckoContentPermissionBy(origin, includeTemporary = true, private).groupByType() + } + } +} + +@VisibleForTesting +internal fun List?.groupByDomain(): Map> { + return this?.groupBy { + it.uri.tryGetHostFromUrl() + }.orEmpty() +} + +@VisibleForTesting +internal fun List?.groupByType(): Map> { + return this?.groupBy { it.permission }.orEmpty() +} + +@VisibleForTesting +internal fun List?.filterNotTemporaryPermissions( + temporaryPermissions: List, +): List? { + 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, features: Array) { + runtime.profilerController.startProfiler(filters, features) + } + + override fun stopProfiler(onSuccess: (ByteArray?) -> Unit, onError: (Throwable) -> Unit) { + runtime.profilerController.stopProfiler().then( + { profileResult -> + onSuccess(profileResult) + GeckoResult() + }, + { throwable -> + onError(throwable) + GeckoResult() + }, + ) + } +} 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 { + val geckoResult = GeckoResult() + + 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 { + val geckoResult = GeckoResult() + + 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 { + val geckoResult = GeckoResult() + + 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, + ): GeckoResult { + val geckoResult = GeckoResult() + + 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, + ): GeckoResult? { + val geckoResult = GeckoResult() + + 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, + ): GeckoResult? { + val geckoResult = GeckoResult() + 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, + ): GeckoResult? { + 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() + 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? { + val geckoResult = GeckoResult() + 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) -> 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, + ): GeckoResult { + val geckoResult = GeckoResult() + + 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 { + val geckoResult = GeckoResult() + 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? { + val geckoResult = GeckoResult() + 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) -> 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? { + val geckoResult = GeckoResult() + 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? { + val geckoResult = GeckoResult() + 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? { + val geckoResult = GeckoResult() + 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? { + val geckoResult = GeckoResult() + 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 { + val geckoResult = GeckoResult() + 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? { + val geckoResult = GeckoResult() + 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 { + val geckoResult = GeckoResult() + 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? { + val geckoResult = GeckoResult() + 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? { + val geckoResult = GeckoResult() + + 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.toIdsArray(): Array { + 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) { + 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 { + 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 { + 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 = 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? { + 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? { + 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? { + 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? { + 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 { + 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 { + 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 { + 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? { + val result: GeckoResult = GeckoResult() + + delegate.onGetSubscription(scope) { subscription -> + result.complete(subscription?.toGeckoSubscription()) + } + + return result + } + + /** + * See [GeckoViewWebPushDelegate.onSubscribe]. + */ + override fun onSubscribe(scope: String, appServerKey: ByteArray?): GeckoResult? { + val result: GeckoResult = GeckoResult() + + delegate.onSubscribe(scope, appServerKey) { subscription -> + result.complete(subscription?.toGeckoSubscription()) + } + + return result + } + + /** + * See [GeckoViewWebPushDelegate.onUnsubscribe]. + */ + override fun onUnsubscribe(scope: String): GeckoResult? { + val result: GeckoResult = 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 { + val result = GeckoResult() + 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 { + 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 { + 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 { + 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 { + val result = GeckoResult() + 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("").`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("", 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 + private lateinit var progressDelegate: ArgumentCaptor + private lateinit var mediaDelegate: ArgumentCaptor + private lateinit var contentDelegate: ArgumentCaptor + private lateinit var permissionDelegate: ArgumentCaptor + private lateinit var scrollDelegate: ArgumentCaptor + private lateinit var contentBlockingDelegate: ArgumentCaptor + private lateinit var historyDelegate: ArgumentCaptor + + @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() + 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() + 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() + 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() + 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 = mutableListOf() + val observedAppPermissionRequests: MutableList = 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> = 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("Hello!") + verify(geckoSession).load( + GeckoSession.Loader().data("Hello!", "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() + + 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, + private val currentIndex: Int, + ) : ArrayList(items), GeckoSession.HistoryDelegate.HistoryList { + override fun getCurrentIndex() = currentIndex + } + + fun mockHistoryItem(title: String?, uri: String): GeckoSession.HistoryDelegate.HistoryItem { + val item = mock() + 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() + + 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.register(observer) + + captureDelegates() + + class MockHistoryList( + items: List, + private val currentIndex: Int, + ) : ArrayList(items), GeckoSession.HistoryDelegate.HistoryList { + override fun getCurrentIndex() = currentIndex + } + + fun mockHistoryItem(title: String?, uri: String): GeckoSession.HistoryDelegate.HistoryItem { + val item = mock() + 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() + val geckoSession = mock() + + 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() + val geckoSession = mock() + + 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() + val geckoSession = mock() + + 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() + 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("

Hello World

") + } + } + + 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("

Hello World

", "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() + 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() + 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() + 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() + 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() + 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() + 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() + 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>() + 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>() + 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>() + 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() + 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() + + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + + 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() + + 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() + + 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() + + 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() + val sessionFinder = mock() + 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() + val sessionFinder = mock() + 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() + 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("") + 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() + val geckoSession = mock() + + 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() + 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() + val runtimeSettings = mock() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + val runtimeSettings = mock() + 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() + 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() + + 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() + val extIdCaptor = argumentCaptor() + 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() + 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() + + 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() + 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() + 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() + + 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() + 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() + 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() + + 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() + 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() + 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() + + 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() + 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() + + 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() + 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() + + whenever(extensionController.install(any(), any())).thenReturn(result) + + engine.installWebExtension( + extUrl, + InstallationMethod.MANAGER, + ) + + result.complete(mockNativeWebExtension(extId, extUrl)) + + shadowOf(getMainLooper()).idle() + + val methodCaptor = argumentCaptor() + + 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() + 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() + + whenever(extensionController.install(any(), any())).thenReturn(result) + + engine.installWebExtension( + extUrl, + InstallationMethod.FROM_FILE, + ) + + result.complete(mockNativeWebExtension(extId, extUrl)) + + shadowOf(getMainLooper()).idle() + + val methodCaptor = argumentCaptor() + + 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() + 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() + + whenever(extensionController.install(any(), any())).thenReturn(result) + + engine.installWebExtension( + extUrl, + null, + ) + + result.complete(mockNativeWebExtension(extId, extUrl)) + + shadowOf(getMainLooper()).idle() + + val methodCaptor = argumentCaptor() + + 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() + 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() + + whenever(extensionController.uninstall(any())).thenReturn(result) + engine.uninstallWebExtension( + ext, + onSuccess = { onSuccessCalled = true }, + onError = { _, _ -> onErrorCalled = true }, + ) + result.complete(null) + + shadowOf(getMainLooper()).idle() + + val extCaptor = argumentCaptor() + verify(extensionController).uninstall(extCaptor.capture()) + assertSame(nativeExtension, extCaptor.value) + assertTrue(onSuccessCalled) + assertFalse(onErrorCalled) + } + + @Test + fun `uninstall web extension failure`() { + val runtime = mock() + 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() + + 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() + whenever(webExtensionController.ensureBuiltIn(extUrl, extId)).thenReturn(result) + engine.installBuiltInWebExtension(extId, extUrl) + result.complete(mockNativeWebExtension(extId, extUrl)) + + shadowOf(getMainLooper()).idle() + + val extCaptor = argumentCaptor() + 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() + whenever(webExtensionController.install(any(), any())).thenReturn(result) + engine.installWebExtension(extUrl) + result.complete(mockNativeWebExtension(extId, extUrl)) + + shadowOf(getMainLooper()).idle() + + val extCaptor = argumentCaptor() + 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() + verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture() + + val result = geckoDelegateCaptor.value.onInstallPrompt(extension) + + val extensionCaptor = argumentCaptor() + 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() + verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture() + + val result = geckoDelegateCaptor.value.onInstallPrompt(extension) + + val extensionCaptor = argumentCaptor() + 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() + verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture() + + val result = geckoDelegateCaptor.value.onUpdatePrompt( + currentExtension, + updatedExtension, + updatedPermissions, + hostPermissions, + ) + assertNotNull(result) + + val currentExtensionCaptor = argumentCaptor() + val updatedExtensionCaptor = argumentCaptor() + 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() + verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture() + + val result = geckoDelegateCaptor.value.onUpdatePrompt( + currentExtension, + updatedExtension, + updatedPermissions, + emptyArray(), + ) + assertNotNull(result) + + val currentExtensionCaptor = argumentCaptor() + val updatedExtensionCaptor = argumentCaptor() + 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() + verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture() + + val result = geckoDelegateCaptor.value.onOptionalPrompt(extension, permissions, origins) + assertNotNull(result) + + val extensionCaptor = argumentCaptor() + 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() + val webExtensionsDelegate: WebExtensionDelegate = mock() + val engine = GeckoEngine(context, runtime = runtime) + engine.registerWebExtensionDelegate(webExtensionsDelegate) + + val geckoDelegateCaptor = argumentCaptor() + verify(webExtensionController).promptDelegate = geckoDelegateCaptor.capture() + + val result = geckoDelegateCaptor.value.onOptionalPrompt(extension, permissions, origins) + assertNotNull(result) + + val extensionCaptor = argumentCaptor() + 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() + 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() + 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() + verify(extension).setActionDelegate(actionDelegateCaptor.capture()) + + val browserAction: org.mozilla.geckoview.WebExtension.Action = mock() + actionDelegateCaptor.value.onBrowserAction(extension, null, browserAction) + + val extensionCaptor = argumentCaptor() + val actionCaptor = argumentCaptor() + 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() + 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() + 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() + verify(extension).setActionDelegate(actionDelegateCaptor.capture()) + + val pageAction: org.mozilla.geckoview.WebExtension.Action = mock() + actionDelegateCaptor.value.onPageAction(extension, null, pageAction) + + val extensionCaptor = argumentCaptor() + val actionCaptor = argumentCaptor() + 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() + 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() + 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() + verify(extension).tabDelegate = tabDelegateCaptor.capture() + + val createTabDetails: org.mozilla.geckoview.WebExtension.CreateTabDetails = mock() + tabDelegateCaptor.value.onNewTab(extension, createTabDetails) + + val extensionCaptor = argumentCaptor() + 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() + 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() + whenever(extensionController.install(any(), any())).thenReturn(result) + engine.installWebExtension(extUrl) + val extension = mockNativeWebExtension(extId, extUrl) + result.complete(extension) + + shadowOf(getMainLooper()).idle() + + val actionDelegateCaptor = argumentCaptor() + verify(extension).setActionDelegate(actionDelegateCaptor.capture()) + + val browserAction: org.mozilla.geckoview.WebExtension.Action = mock() + actionDelegateCaptor.value.onBrowserAction(extension, null, browserAction) + + val extensionCaptor = argumentCaptor() + val actionCaptor = argumentCaptor() + 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() + 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() + whenever(extensionController.install(any(), any())).thenReturn(result) + engine.installWebExtension(extUrl) + val extension = mockNativeWebExtension(extId, extUrl) + result.complete(extension) + + shadowOf(getMainLooper()).idle() + + val actionDelegateCaptor = argumentCaptor() + verify(extension).setActionDelegate(actionDelegateCaptor.capture()) + + val pageAction: org.mozilla.geckoview.WebExtension.Action = mock() + actionDelegateCaptor.value.onPageAction(extension, null, pageAction) + + val extensionCaptor = argumentCaptor() + val actionCaptor = argumentCaptor() + 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() + 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() + whenever(extensionController.install(any(), any())).thenReturn(result) + engine.installWebExtension(extUrl) + val extension = mockNativeWebExtension(extId, extUrl) + result.complete(extension) + + shadowOf(getMainLooper()).idle() + + val tabDelegateCaptor = argumentCaptor() + verify(extension).tabDelegate = tabDelegateCaptor.capture() + + val createTabDetails: org.mozilla.geckoview.WebExtension.CreateTabDetails = mock() + tabDelegateCaptor.value.onNewTab(extension, createTabDetails) + + val extensionCaptor = argumentCaptor() + 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() + 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() + verify(webExtensionController).setExtensionProcessDelegate(extensionProcessDelegate.capture()) + + extensionProcessDelegate.value.onDisabledProcessSpawning() + verify(webExtensionDelegate).onDisabledExtensionProcessSpawning() + } + + @Test + fun `update web extension successfully`() { + val runtime = mock() + val extensionController: WebExtensionController = mock() + + val updatedExtension = mockNativeWebExtension() + val updateExtensionResult = GeckoResult() + 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() + val extensionController: WebExtensionController = mock() + + val updateExtensionResult = GeckoResult() + 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() + val extensionController: WebExtensionController = mock() + + val updateExtensionResult = GeckoResult() + 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() + 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() + 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>() + + val runtime = mock() + val extensionController: WebExtensionController = mock() + whenever(extensionController.list()).thenReturn(installedExtensionResult) + whenever(runtime.webExtensionController).thenReturn(extensionController) + + val engine = GeckoEngine(testContext, runtime = runtime) + var extensions: List? = 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>() + + val runtime = mock() + val extensionController: WebExtensionController = mock() + whenever(extensionController.list()).thenReturn(installedExtensionResult) + whenever(runtime.webExtensionController).thenReturn(extensionController) + + val engine = GeckoEngine(context, runtime = runtime) + var extensions: List? = 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() + val extensionController: WebExtensionController = mock() + + val enabledExtension = mockNativeWebExtension(id = "id", location = "uri") + val enableExtensionResult = GeckoResult() + 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() + val extensionController: WebExtensionController = mock() + + val enableExtensionResult = GeckoResult() + 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() + val extensionController: WebExtensionController = mock() + + val disabledExtension = mockNativeWebExtension(id = "id", location = "uri") + val disableExtensionResult = GeckoResult() + 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() + val extensionController: WebExtensionController = mock() + + val disableExtensionResult = GeckoResult() + 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() + val extensionController: WebExtensionController = mock() + + val allowedInPrivateBrowsing = mockNativeWebExtension(id = "id", location = "uri") + val allowedInPrivateBrowsingExtensionResult = GeckoResult() + 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() + val extensionController: WebExtensionController = mock() + + val allowedInPrivateBrowsingExtensionResult = GeckoResult() + 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() + val extensionController: WebExtensionController = mock() + + val allowedInPrivateBrowsingExtensionResult = GeckoResult() + 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() + 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() + 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() + 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() + 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() + 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() + val engine = GeckoEngine(context, runtime = runtime) + var onSuccessCalled = false + var onErrorCalled = false + val mockSession = mock() + val mockGeckoSetting = mock() + val mockGeckoContentBlockingSetting = mock() + var trackersLog: List? = null + + val mockContentBlockingController = mock() + var logEntriesResult = GeckoResult>() + + 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() + val engine = spy(GeckoEngine(context, runtime = runtime)) + val mockSession = mock() + val mockGeckoSetting = mock() + val mockGeckoContentBlockingSetting = mock() + var trackersLog: List? = null + + val mockContentBlockingController = mock() + val logEntriesResult = GeckoResult>() + + 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() + val engine = GeckoEngine(context, runtime = runtime) + val mockSession = mock() + val mockGeckoSetting = mock() + val mockGeckoContentBlockingSetting = mock() + var trackersLog: List? = null + + val mockContentBlockingController = mock() + var logEntriesResult = GeckoResult>() + + 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() + val engine = GeckoEngine(context, runtime = runtime) + val mockSession = mock() + val mockGeckoSetting = mock() + val mockGeckoContentBlockingSetting = mock() + var trackersLog: List? = null + + val mockContentBlockingController = mock() + var logEntriesResult = GeckoResult>() + + 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() + 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() + 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() + val engine = GeckoEngine(context, runtime = runtime) + + engine.registerActivityDelegate(mock()) + + verify(runtime).activityDelegate = any() + } + + @Test + fun `unregisterActivityDelegate sets delegate to null`() { + val runtime = mock() + 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() + val runtime = mock() + 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() + val runtime = mock() + 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() + 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() + verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture()) + + assertEquals(Unit, geckoDelegateCaptor.value.onEnabled(extension)) + val extensionCaptor = argumentCaptor() + 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() + verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture()) + + assertEquals(Unit, geckoDelegateCaptor.value.onInstallationFailed(extension, exception)) + + val extensionCaptor = argumentCaptor() + val exceptionCaptor = argumentCaptor() + + 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() + verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture()) + + assertEquals(Unit, geckoDelegateCaptor.value.onDisabled(extension)) + val extensionCaptor = argumentCaptor() + 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() + verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture()) + + assertEquals(Unit, geckoDelegateCaptor.value.onUninstalled(extension)) + val extensionCaptor = argumentCaptor() + 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() + verify(webExtensionController).setAddonManagerDelegate(geckoDelegateCaptor.capture()) + + assertEquals(Unit, geckoDelegateCaptor.value.onInstalled(extension)) + val extensionCaptor = argumentCaptor() + 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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 = mutableListOf(geckoLanguageModel) + val geckoResult = GeckoResult>() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`>> { 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>() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`>> { 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() + val toLanguage = Language("de", "German") + val fromLanguage = Language("es", "Spanish") + val geckoResultValue = TranslationsController.RuntimeTranslation.TranslationSupport(listOf(fromLanguage), listOf(toLanguage)) + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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>() + val geckoResultValue = listOf("en", "es", "de") + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`>> { 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>() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`>> { 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() + + 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() + val geckoResultValue = "always" + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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() + val geckoResultValue = "NotAnExpectedResponse" + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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>() + 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`>> { 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>() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`>> { 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>() + val geckoResultValue = listOf("www.mozilla.org") + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`>> { 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>() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`>> { 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() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`> { 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>() + + Mockito.mockStatic(TranslationsController.RuntimeTranslation::class.java, Mockito.CALLS_REAL_METHODS).use { + mocked -> + mocked.`when`>> { 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() + 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 { + 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 { + 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 { + 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() + val geckoSession = mock() + val geckoView = mock() + + 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() + var thumbnail: Bitmap? = null + + var geckoResult = GeckoResult() + 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() + val geckoSession = mock() + val geckoView = mock() + + 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() + val geckoSession = mock() + val geckoView = mock() + + 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() + val geckoSession = mock() + val geckoView = mock() + + whenever(engineSession.geckoSession).thenReturn(geckoSession) + engineView.geckoView = geckoView + + engineView.render(engineSession) + + val captor = argumentCaptor() + 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() + val geckoSession = mock() + val geckoView = mock() + + whenever(engineSession.geckoSession).thenReturn(geckoSession) + engineView.geckoView = geckoView + + engineView.render(engineSession) + + val captor = argumentCaptor() + 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().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(IllegalStateException()).await() + } + + @Test + fun fromResult() = runTest { + val result = launchGeckoResult { 42 } + + result.then { + assertEquals(42, it) + GeckoResult.fromValue(null) + }.await() + } + + @Test + fun fromException() = runTest { + val result = launchGeckoResult { throw IllegalStateException() } + + result.then( + { + assertTrue("Invalid branch", false) + GeckoResult.fromValue(null) + }, + { + assertTrue(it is IllegalStateException) + GeckoResult.fromValue(null) + }, + ).await() + } + + @Test + fun asCancellableOperation() = runTest { + val geckoResult: GeckoResult = 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() + val mockGeckoSession = mock() + 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() + val mockGeckoSession = mock() + 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() + 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() + val contentPermission = mock() + + 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() + + 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() + val storageController = mock() + + 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() + val geckoResult = GeckoResult>() + + 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() + val session = GeckoEngineSession(runtime, geckoSessionProvider = { mockGeckoSession }) + + val contentPermission = + geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW) + val storageController = mock() + val geckoResult = GeckoResult>() + 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() + val geckoResult = GeckoResult>() + var exceptionList: List? = 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() + val mockGeckoSession = mock() + var containsException = false + val contentPermission = + geckoContentPermission("https://example.com/", PERMISSION_TRACKING, VALUE_ALLOW) + val storageController = mock() + val geckoResult = GeckoResult>() + + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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() + 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 = 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().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 = 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 = 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().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>() + 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 { + return GeckoResult().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().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>() + 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 { + return GeckoResult().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>() + 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 { + return GeckoResult().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().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().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() + 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() + 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() + 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 + + 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>() + whenever(geckoResult.poll(anyLong())).thenThrow(TimeoutException::class.java) + @Suppress("UNCHECKED_CAST") + whenever(geckoWebExecutor!!.fetch(any(), anyInt())).thenReturn(geckoResult as GeckoResult) + + super.get200WithReadTimeout() + } + + @Test + override fun get200WithStringBody() { + mockRequest() + mockResponse(200, body = "Hello World") + + super.get200WithStringBody() + } + + @Test + override fun get302FollowRedirects() { + mockResponse(200) + + val request = mock() + 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() + 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>() + whenever(geckoResult.poll(anyLong())).thenReturn(null) + @Suppress("UNCHECKED_CAST") + whenever(geckoWebExecutor!!.fetch(any(), anyInt())).thenReturn(geckoResult as GeckoResult) + + val request = mock() + 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() + 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() + 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() + 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() + 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() + 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() + 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? = null, body: String? = null, method: String = "GET") { + val server = mock() + whenever(server.url(any())).thenReturn(mock()) + val request = mock() + 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? = null, + body: String? = null, + charset: Charset = Charsets.UTF_8, + ) { + val executor = mock() + 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() { + + 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) { + 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>() + + 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>() + + 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() + + 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() + + 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() + + 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() + + 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() + + 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() + + 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() + + 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() + ReflectionUtils.setField(updatedPrompt, "choices", arrayOf()) + + 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() + + 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() + + 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() + 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() + + 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() + + 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() + + 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>() + + 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>() + + 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, + ): 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 = 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 = 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 = 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, + ): GeckoSession.PromptDelegate.AutocompleteRequest { + val prompt: GeckoSession.PromptDelegate.AutocompleteRequest = mock() + ReflectionUtils.setField(prompt, "options", loginArray) + return prompt + } + + @Suppress("UNCHECKED_CAST") + private fun geckoLoginSavePrompt( + login: Array, + ): GeckoSession.PromptDelegate.AutocompleteRequest { + val prompt = Mockito.mock( + GeckoSession.PromptDelegate.AutocompleteRequest::class.java, + Mockito.RETURNS_DEEP_STUBS, // for testing prompt.delegate + ) as GeckoSession.PromptDelegate.AutocompleteRequest + + 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, + ): GeckoSession.PromptDelegate.AutocompleteRequest { + val prompt: GeckoSession.PromptDelegate.AutocompleteRequest = + mock() + ReflectionUtils.setField(prompt, "options", creditCards) + return prompt + } + + private fun geckoSelectAddressPrompt( + addresses: Array, + isComplete: Boolean = false, + ): GeckoSession.PromptDelegate.AutocompleteRequest { + val prompt: GeckoSession.PromptDelegate.AutocompleteRequest = + mock() + whenever(prompt.isComplete).thenReturn(isComplete) + ReflectionUtils.setField(prompt, "options", addresses) + return prompt + } + + @Suppress("UNCHECKED_CAST") + private fun geckoCreditCardSavePrompt( + creditCard: Array, + ): GeckoSession.PromptDelegate.AutocompleteRequest { + val prompt = Mockito.mock( + GeckoSession.PromptDelegate.AutocompleteRequest::class.java, + Mockito.RETURNS_DEEP_STUBS, // for testing prompt.delegate + ) as GeckoSession.PromptDelegate.AutocompleteRequest + + 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 = 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() + + assertNull(GeckoSelectionActionDelegate.maybeCreate(mock(), customDelegate)) + assertNull(GeckoSelectionActionDelegate.maybeCreate(mock(), customDelegate)) + } + + @Test + fun `maybe create with null delegate context should return null`() { + assertNull(GeckoSelectionActionDelegate.maybeCreate(mock(), null)) + } + + @Test + fun `maybe create with expected inputs should return non-null`() { + assertNotNull(GeckoSelectionActionDelegate.maybeCreate(mock(), 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 = 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): Array { + 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 = 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): Array { + 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() + 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() + 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() + val portCaptor = argumentCaptor() + val portDelegateCaptor = argumentCaptor() + + 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() + val portCaptor = argumentCaptor() + val portDelegateCaptor = argumentCaptor() + + 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() + + 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() + 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() + val browserActionCaptor = argumentCaptor() + val pageActionCaptor = argumentCaptor() + 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() + val browserActionCaptor = argumentCaptor() + val pageActionCaptor = argumentCaptor() + 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() + val engineSessionCaptor = argumentCaptor() + + 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() + + 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(), 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 = emptyArray(), + optionalPermissions: Array = emptyArray(), + grantedOptionalPermissions: Array = emptyArray(), + grantedOptionalOrigins: Array = emptyArray(), + optionalOrigins: Array = emptyArray(), + origins: Array = 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() + 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() + 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() + 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 { 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() + val mockExperimentDelegate = mock() + val mockGeckoSetting = mock() + 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 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 + +``` + +`SystemEngineView ` can render any `SystemEngineSession` using the `render()` method. + +```Kotlin +val engineSession = engine.createSession() +val engineView = view.findViewById(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 @@ + + 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?, + ) { + 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(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) -> 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 + // 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>) { + // 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>?, + 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) -> 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("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("mozac_system_engine_fullscreen") + val webView = findViewWithTag("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 { + 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 { + 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() + 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() + 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 + internal val enabledCategories = HashSet() + + private val safelist: Safelist? + private val previouslyMatched = HashSet() + private val previouslyUnmatched = HashSet() + + constructor(patterns: Array) { + categories = HashMap() + safelist = null + + val defaultCategory = Trie.createRootNode() + patterns.forEach { defaultCategory.put(it.reverse()) } + categories[DEFAULT] = defaultCategory + enabledCategories.add(DEFAULT) + } + + internal constructor( + enabledCategories: Set, + supportedCategories: Set, + categoryMap: MutableMap, + 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) { + 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 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 { + 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 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 { + 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 = 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 = 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 = supportedCategories, + ): UrlMatcher { + val categoryMap = HashMap() + + 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, + override: Boolean = false, + ): Map { + 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, override: Boolean) { + reader.beginObject() + + val socialOverrides = LinkedList() + 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) { + 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 .", + "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 @@ + + + + በ%1$s ላይ ያለው ገጽ እንዲህ ይላል፡- + + %2$s የእርስዎን የተጠቃሚ ስም እና የይለፍ ቃል እየጠየቀ ነው። ድረ-ገፁ እንዲህ ይላል፡- "%1$s" + + %1$s የእርስዎን የተጠቃሚ ስም እና የይለፍ ቃል እየጠየቀ ነው። + 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 @@ + + + + La pachina en %1$s diz: + + %2$s te ye pedindo lo tuyo nombre d’usuario y clau. Lo puesto diz: “%1$s” + + %1$s te ye pedindo lo tuyo nombre d’usuario y clau. + 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 @@ + + + + Akpọk òkup me %1$s ìbe: + + %2$s ìkido erieen̄ òsikwaan̄ kwun̄ mè ikọ-atafia. Akpatan̄ ya ìbe: “%1$s” + + %1$s ìkido erieen̄ òsikwaan̄ kwun̄ mè ikọ-atafia. + 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 @@ + + + + تقول الصفحة في %1$s: + + يطلب الوسيط %2$s اسم مستخدم و كلمة سر. يقول الموقع: ”%1$s“ + + يطلب %1$s اسم المستخدم و كلمة السر. + 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 @@ + + + + La páxina de «%1$s» diz: + + «%2$s» solicita un nome d\'usuariu y una contraseña. El sitiu diz «%1$s» + + «%1$s» solicita un nome d\'usuariu y una contraseña. + 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 @@ + + + + %1$s səhifəsi deyir ki: + + %2$s istifadəçi adı və parolunuzu istəyir. Sayt deyir ki: “%1$s” + + %1$s istifadəçi adı və parolunuzu istəyir. + 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 @@ + + + + %1$s یارپاغیندا دئییلیر: + + %2$s قوللانیجی آدینیزی و رمزینیزی ایستییر. سایت‌دا دئییلیر: ”%1$s“ + + %1$s قوللانیجی آدینیزی و رمزینیزی ایستییر. + 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 @@ + + + + Kaca ring %1$s mapikobet: + + %2$s ngidih aran sang anganggé miwah kruna sandi Ragané. Situs nyuratang: “%1$s” + + %1$s ngidih aran sang anganggé miwah sandi Ragané. + 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 @@ + + + + Старонка на %1$s паведамляе: + + %2$s запытвае імя карыстальніка і пароль. Сайт паведамляе: “%1$s” + + %1$s запытвае імя карыстальніка і пароль. + 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 @@ + + + + Страницата на %1$s казва: + + %2$s пита за вашето потребителско име и парола. Сайтът казва: „%1$s“ + + %1$s пита за вашето потребителско име и парола. + 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 @@ + + + + %1$s পাতায় বলা হয়েছে: + + %2$s আপনার ব্যবহারকারী নাম এবং পাসওয়ার্ডের জন্য অনুরোধ করছে। সাইটটি বলছে: “%1$s” + + %1$s আপনার ব্যবহারকারী নাম এবং পাসওয়ার্ড অনুরোধ করছে। + 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 @@ + + + + Ar bajenn war %1$s a lâr: + + Emañ ar proksi %2$s ocʼh azgoulenn un anv arveriad hag ur ger-tremen. Emañ al lecʼhienn o lavarout: “%1$s” + + Emañ ar proksi %1$s ocʼh azgoulenn un anv arveriad hag ur ger-tremen. + 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 @@ + + + + Stranica pri %1$s kaže: + + %2$s zahtijeva korisničko ime i lozinku. Stranica vraća odgovor: “%1$s” + + %1$s zahtijeva tvoje korisničko ime i lozinku. + 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 @@ + + + + La pàgina a %1$s diu: + + %2$s sol·licita el vostre nom d’usuari i contrasenya. El lloc diu: «%1$s» + + %1$s sol·licita el vostre nom d’usuari i contrasenya. + 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 @@ + + + + Ri ruxaq %1$s nub\'ij: + + %2$s nuk\'utuj rub\'i\' winäq chuqa\' jun ewan tzij. Ri ruxaq k\'amaya\'l nub\'ij: “%1$s” + + %1$s nuk\'utuj ri rub\'i\' ataqoya\'l chuqa\' ewan atzij. + 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 @@ + + + + Ang page sa %1$s ingon: + + %2$s nangayo sa imong username ug password. Ang site ingon: “%1$s” + + %1$s nangayo sa imong username ug password. + 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 @@ + + + + پەڕە لە %1$s دەڵێت: + + %2$s داوای ناوی بەکارهێنەر و وشەی تێپەڕبوون دەکات. ماڵپەڕەکە دەڵێت: “%1$s” + + %1$s داوای ناوی بەکارهێنەر و ووشەی تێپەڕبوون دەکات. + 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 @@ + + + + Annunziamentu di a pagina %1$s :find in page + + U situ %2$s richiede u vostru nome d’utilizatore è a vostra parolla d’intesa. U site indica : « %1$s » + + U situ %1$s richiede u vostru nome d’utilizatore è a vostra parolla d’intesa. + 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 @@ + + + + Sdělení stránky %1$s: + + %2$s požaduje vaše uživatelské jméno a heslo. Sdělení serveru: „%1$s“ + + %1$s požaduje vaše uživatelské jméno a heslo. + 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 @@ + + + + Mae tudalen yn %1$s yn dweud: + + Mae %2$s yn gofyn am enw defnyddiwr a chyfrinair. Mae’r wefan yn dweud: “%1$s” + + Mae %1$s yn gofyn am eich enw defnyddiwr a chyfrinair. + 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 @@ + + + + Siden på %1$s siger: + + %2$s beder om dit brugernavn og din adgangskode. Webstedet siger: "%1$s" + + %1$s beder om dit brugernavn og din adgangskode. + 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 @@ + + + + Die Seite mit der Adresse %1$s meldet: + + %2$s verlangt einen Benutzernamen und ein Passwort. Ausgabe der Website: „%1$s“ + + %1$s verlangt einen Benutzernamen und ein Passwort. + 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 @@ + + + + Bok na %1$s groni: + + %2$s pomina wužywarske mě a gronidło. Sedło groni: "%1$s" + + %1$s pomina wašo wužywarske mě a gronidło. + 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 @@ + + + + Η σελίδα στο %1$s δηλώνει: + + Το %2$s ζητά όνομα χρήστη και κωδικό πρόσβασής. Ο ιστότοπος δηλώνει: «%1$s» + + Το %1$s ζητά το όνομα χρήστη και τον κωδικό πρόσβασής σας. + 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 @@ + + + + The page at %1$s says: + + %2$s is requesting your username and password. The site says: “%1$s” + + %1$s is requesting your username and password. + 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 @@ + + + + The page at %1$s says: + + %2$s is requesting your username and password. The site says: “%1$s” + + %1$s is requesting your username and password. + 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 @@ + + + + La paĝo ĉe %1$s diras: + + %2$s petas vian nomon de uzanto kaj pasvorton. La retejo diras "%1$s" + + %1$s petas vian nomon de uzanto kaj pasvorton. + 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 @@ + + + + La página en %1$s dice: + + %2$s está pidiendo tu nombre de usuario y contraseña. El sitio dice: “%1$s” + + %1$s te está pidiendo tu nombre de usuario y contraseña. + 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 @@ + + + + La página en %1$s dice: + + %2$s está solicitando tu usuario y contraseña. El sitio dice: “%1$s” + + %1$s está solicitando tu usuario y contraseña. + 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 @@ + + + + La página en %1$s dice: + + %2$s te está pidiendo tu nombre de usuario y contraseña. El sitio dice: “%1$s” + + %1$s te está pidiendo tu nombre de usuario y contraseña. + 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 @@ + + + + La página en %1$s dice: + + %2$s está solicitando tu nombre de usuario y contraseña. El sitio dice: “%1$s” + + %1$s está solicitando tu nombre de usuario y contraseña. + 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 @@ + + + + La página en %1$s dice: + + %2$s te está pidiendo tu nombre de usuario y contraseña. El sitio dice: “%1$s” + + %1$s te está pidiendo tu nombre de usuario y contraseña. + 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 @@ + + + + Veebileht aadressil %1$s ütleb: + + Sait aadressil %2$s nõuab kasutajanime ja parooli. Teade saidilt: “%1$s” + + %1$s nõuab kasutajanime ja parooli. + 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 @@ + + + + %1$s helbideko orriak hau dio: + + %2$s guneak erabiltzaile-izen eta pasahitza eskatzen ditu. Guneak hau dio: "%1$s" + + %1$s guneak erabiltzaile-izen eta pasahitza eskatzen ditu. + 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 @@ + + + + صفحهٔ %1$s می‌گوید: + + %2$s درخواست نام کاربری و گذرواژهٔ شما را دارد. این پایگاه می‌گوید: «%1$s» + + %1$s درخواست نام کاربری و گذرواژهٔ شما را دارد. + 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 @@ + + + + Hello wonngo to %1$s wiyi: + + %2$s ena naamnii innde kuutoro maa e finnde. Lowre ndee wiyi: “%1$s” + + %1$s ena naamnii innde kuutoro maa e finnde. + 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 @@ + + + + Sivu osoitteessa %1$s sanoo: + + %2$s pyytää käyttäjätunnusta ja salasanaa. Sivusto sanoo: ”%1$s” + + %1$s pyytää käyttäjätunnusta ja salasanaa. + 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 @@ + + + + Annonce de la page %1$s : + + Le site %2$s demande votre nom d’utilisateur et votre mot de passe. Le site indique : « %1$s » + + %1$s demande votre nom d’utilisateur et votre mot de passe. + 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 @@ + + + + La pagjine su %1$s e dîs: + + %2$s al domande il to non utent e la password. Il sît al dîs: “%1$s” + + %1$s al domande il to non utent e la password. + 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 @@ + + + + De side op %1$s meldt: + + %2$s freget om jo brûkersnamme en wachtwurd. De website meldt: ‘%1$s’ + + %1$s freget om jo brûkersnamme en wachtwurd. + 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 @@ + + + + Deir an leathanach ag %1$s: + + Tá %2$s ag iarraidh ainm úsáideora agus focal faire uait. Deir an suíomh: “%1$s” + + Tá %1$s ag iarraidh ainm úsáideora agus focal faire uait. + 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 @@ + + + + Tha an duilleag aig %1$s ag ràdh: + + Tha am progsaidh %2$s ag iarraidh ainm-cleachdaiche is facal-faire. Tha an làrach ag ràdh: “%1$s” + + Tha %1$s ag iarraidh an ainm-chleachdaiche is an fhacail-fhaire agad. + 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 @@ + + + + A páxina en %1$s di: + + %2$s solicita o seu nome de usuario e o contrasinal. O sitio di: «%1$s» + + %1$s solicita o seu nome de usuario e o contrasinal. + 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 @@ + + + + Pe kuatiarogue %1$s pegua he’i: + + %2$s ojerure poruhára réra ha avei ñe’ẽñemi. Pe tenda he’i: “%1$s” + + %1$s ojerure nde poruhára réra ha ñe’ẽñemi. + 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 @@ + + + + %1$s આગળનું પાનું આમ કહે છે: + + %2$s તમારું વપરાશકર્તા નામ અને પાસવર્ડની વિનંતી કરી રહ્યું છે. આ સાઇટ કહે છે: “%1$s” + + %1$s તમારા વપરાશકર્તા નામ અને પાસવર્ડની વિનંતી કરી રહ્યું છે. + 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 @@ + + + + %1$s पर मौजूद पृष्ठ का कहना है: + + %2$s आपके उपयोगकर्ता नाम और पासवर्ड का अनुरोध कर रहा है। साइट का कहना है: “%1$s” + + %1$s आपके उपयोगकर्ता नाम और पासवर्ड का अनुरोध कर रहा है। + 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 @@ + + + + Ang pahina sa %1$s nagasiling: + 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 @@ + + + + Stranica pri %1$s kaže: + + %2$s zahtijeva korisničko ime i lozinku. Stranica vraća odgovor: “%1$s” + + %1$s zahtijeva tvoje korisničko ime i lozinku. + 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 @@ + + + + Strona na %1$s praji: + + %2$s žada sej waše wužiwarske mjeno a hesło. Sydło praji: “%1$s” + + %1$s sej waše wužiwarske mjeno a hesło žada. + 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 @@ + + + + Az oldal a(z) %1$s helyen azt mondja: + + A(z) %2$s felhasználónevet és jelszót kér. A webhely üzenete: „%1$s” + + A(z) %1$s felhasználónevet és jelszót kér. + 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 @@ + + + + %1$s-ից էջը հաղորդում է` + + %2$s-ը պահանջում է օգտվողի անուն և գաղտնաբառ: Կայքը հաղորդում է` "%1$s" + + %1$s-ը հարցնում է օգտվողի Ձեր անունը և գաղտնաբառը + 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 @@ + + + + Le pagina sur %1$s dice: + + %2$s requesta tu nomine de usator e contrasigno. Le sito dice: “%1$s” + + %1$s requesta tu nomine de usator e contrasigno. + 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 @@ + + + + Laman dari %1$s menjelaskan: + + %2$s meminta nama pengguna dan sandi anda. Situs ini berkata: “%1$s” + + %1$s meminta nama pengguna dan sandi anda. + 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 @@ + + + + Síðan %1$s segir: + + %2$s er að biðja um notandanafnið þitt og lykilorð. Tilkynningin frá vefsvæðinu er: “%1$s” + + %1$s er að biðja um notandanafnið þitt og lykilorð. + 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 @@ + + + + La pagina sul server %1$s riporta: + + %2$s richiede un nome utente e una password. Il sito riporta: “%1$s” + + %1$s richiede un nome utente e una password. + 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 @@ + + + + הדף %1$s אומר: + + השרת %2$s מבקש את שם המשתמש והססמה שלך. מהאתר נמסר: “%1$s” + + השרת %1$s מבקש את שם המשתמש והססמה שלך. + 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 @@ + + + + %1$s のページから: + + %2$s がユーザー名とパスワードを要求しています。サイトからのメッセージ: “%1$s” + + %1$s がユーザー名とパスワードを要求しています。 + 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 @@ + + + + გვერდი %1$s გამცნობთ: + + %2$s ითხოვს მომხმარებლის სახელსა და პაროლს. საიტი გამცნობთ: „%1$s“ + + %1$s ითხოვს მომხმარებლის სახელსა და პაროლს. + 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 @@ + + + + %1$s betinde aytılıwınsha: + + %2$s paydalanıwshı atın hám parolin sorap atır. Sayt aytıwınsha: “%1$s” + + %1$s paydalanıwshı atın hám parolin sorap atır. + 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 @@ + + + + Asebter deg %1$s yeqqar-d: + + %2$s yessutur isem-ik n useqdac akked wawal-ik uffir. Asmel yaqqar: "%1$s" + + %1$s yessutur isem-ik n useqdac akked wawal-ik uffir. + 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 @@ + + + + %1$s адресіндегі бет хабарлайды: + + %2$s сайты сіздің пайдаланушы атын мен паролін сұрайды. Сайт айтады: "%1$s" + + %1$s сіздің пайдаланушы атын және паролін сұрап тұр. + 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 @@ + + + + Rûpela %1$s’ê dibêje: + + %2$s navê bikarhêner û pêborîna te dixwaze. Malper dibêje: “%1$s” + + %1$s navê bikarhêner û pêborîna te dixwaze. + 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 @@ + + + + %1$s ನಲ್ಲಿರುವ ಪುಟವು ಹೀಗೆ ಹೇಳುತ್ತದೆ: + + %2$s ಎಂಬ ಪ್ರಾಕ್ಸಿಯು ಒಂದು ಬಳಕೆದಾರ ಪದ ಹಾಗು ಗುಪ್ತಪದಕ್ಕಾಗಿ ಮನವಿ ಸಲ್ಲಿಸಿದೆ. ತಾಣವು ಹೀಗೆ ಹೇಳುತ್ತದೆ: “%1$s” + + %1$s ನಿಮ್ಮ ಬಳಕೆದಾರನ ಹೆಸರು ಮತ್ತು ಪ್ರವೇಶ ಪದ ಕೇಳುತ್ತಿದೆ. + 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 @@ + + + + %1$s 페이지의 메세지: + + %2$s가 사용자 이름과 비밀번호를 요청하고 있습니다. 사이트 메시지: “%1$s” + + %1$s가 사용자 이름과 비밀번호를 요청하고 있습니다. + 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 @@ + + + + An folen orth %1$s a lever: + + %2$s a bys a\'th hanow devnydhyer ha ger tremena. An wiasva a lever: “%1$s” + + %1$s a bys a\'th hanow devnydhyer ha ger tremena. + 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 @@ + + + + A pagina in %1$s a dixe: + + %2$s o domanda o teu nomme utente e paròlla segreta. O scito o dixe: “%1$s” + + %1$s o domanda o teu nomme utente e paròlla segreta. + 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 @@ + + + + ຫນ້າເວັບທີ່ %1$s ລະບຸວ່າ: + + %2$s ຕ້ອງການຊື່ຜູ້ໃຊ້ ແລະ ລະຫັດຜ່ານຂອງທ່ານ. ເວັບໄຊທລະບຸວ່າ: “%1$s” + + %1$s ຕ້ອງການຊື່ຜູ້ໃຊ້ ແລະ ລະຫັດຜ່ານຂອງທ່ານ. + 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 @@ + + + + Tinklalapis %1$s praneša: + + %2$s reikalauja jūsų vardo ir slaptažodžio. Svetainės pranešimas: „%1$s“ + + %1$s reikalauja jūsų vardo iš slaptažodžio. + 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 @@ + + + + Página nu %1$s katyi: + + %2$s tsiki sivi tsi tu un se e. Ndaka tu in: “%1$s” + + %1$s tsiki sivi tsi tu un se e. + 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 @@ + + + + %1$s ൽ ഉള്ള പേജ് പറയുന്നത്: + + %2$s ന് നിങ്ങളുടെ ഉപയോക്തൃനാമവും രഹസ്യവാക്കും ആവശ്യമാണ് . സൈറ്റ് പറയുന്നത് : “%1$s” + + %1$s നിങ്ങളുടെ ഉപയോക്തൃനാമവും രഹസ്യവാക്കും ആവശ്യപെടുന്നു. + 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 @@ + + + + %1$s वरील पृष्ठ म्हणते: + + %2$s वापरकर्तानाव व पासवर्डसाठी विनंती करत आहे. स्थळ असे म्हणते: “%1$s” + + %1$s आपल्या वापरकर्तानाव आणि पासवर्डसाठी विनंती करत आहे. + 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 @@ + + + + %1$s ရှိစာမျက်နှာက - + + %2$s သည်သင်၏သုံးစွဲသူအမည်နှင့်စကားဝှက်ကိုတောင်းနေသည်။ ဆိုဒ်တွင်“%1$s” ဆိုထားသည်။ + + %1$s သည်သင်၏သုံးစွဲသူအမည်နှင့်စကားဝှက်ကိုတောင်းနေသည်။ + 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 @@ + + + + Nettsiden på %1$s sier: + + %2$s ber om brukernavn og passord. Nettstedet sier: «%1$s» + + %1$s krever brukernavn og passord. + 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 @@ + + + + %1$s मा रहेको पृष्ठले भन्छ: + + %2$s ले प्रयोगकर्ता नाम र पासवर्ड अनुरोध गरिरहेको छ। साइट भन्छ: “%1$s” + + %1$s ले तपाईँको प्रयोगकर्ता नाम र पासवर्ड अनुरोध गरेको छ। + 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 @@ + + + + De pagina op %1$s zegt: + + %2$s vraagt om uw gebruikersnaam en wachtwoord. De website zegt: ‘%1$s’ + + %1$s vraagt om uw gebruikersnaam en wachtwoord. + 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 @@ + + + + Nettsida på %1$s seier: + + %2$s ber om brukarnamn og passord. Nettstaden seier: «%1$s» + + %1$s krev brukarnamn og passord. + 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 @@ + + + + Níłch’i naaltsoos -gi ’ání %1$s: + 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 @@ + + + + Anóncia de la pagina %1$s : + + %2$s demanda un nom d’utilizaire e un senhal. Lo site indica : « %1$s » + + %1$s demanda vòstres nom d’utilizaire e senhal. + 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 @@ + + + + %1$s ରେ ଥିବା ପୃଷ୍ଠା କହେ: + 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 @@ + + + + %1$s ਉੱਤੇ ਸਫ਼ਾ ਦਰਸਾਉਂਦਾ ਹੈ: + + %2$s ਤੁਹਾਡੇ ਵਰਤੋਂਕਾਰ-ਨਾਂ ਅਤੇ ਪਾਸਵਰਡ ਦੀ ਮੰਗ ਕਰ ਰਹੀ ਹੈ। ਸਾਈਟ ਕਹਿੰਦੀ ਹੈ: “%1$s” + + %1$s ਤੁਹਾਡੇ ਵਰਤੋਂਕਾਰ-ਨਾਂ ਅਤੇ ਪਾਸਵਰਡ ਦੀ ਮੰਗ ਕਰ ਰਹੀ ਹੈ। + 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 @@ + + + + پتے %1$s دے صفحے توں سنیہا: + + %2$s تہاڈے ورتنوالے دا ناں تے پاس‌ورڈ دی منگ کر رہی اے۔ ایتھوں سنیہا اے – ”%1$s“ + + %1$s تہاڈے ورتنوالے دا ناں تے پاس‌ورڈ دی منگ کر رہی اے۔ + 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 @@ + + + + Komunikat ze strony %1$s: + + %2$s prosi o podanie nazwy użytkownika i hasła. Komunikat witryny: „%1$s” + + %1$s prosi o podanie nazwy użytkownika i hasła. + 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 @@ + + + + Ne iswat tik %1$s ina: + + %2$s metztajtanilia muusuariojtukay wan muichtakataketzalis. Ne sitioj ina: “%1$s” + + %1$s metztajtanilia muusuariojtukay wan muichtakataketzalis. + 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 @@ + + + + A página em %1$s diz: + + %2$s está solicitando seu nome de usuário e senha. O site diz: “%1$s” + + %1$s está solicitando seu nome de usuário e senha. + 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 @@ + + + + A página %1$s diz: + + %2$s está a solicitar o seu nome de utilizador e a palavra-passe. O site diz: “%1$s” + + %1$s está a solicitar o seu nome de utilizador e a palavra-passe. + 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 @@ + + + + La pagina cun l\'adressa %1$s di: + + %2$s dumonda tes num d\'utilisader e pled-clav. La pagina di: «%1$s» + + %1$s dumonda tes num d\'utilisader e pled-clav. + 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 @@ + + + + Pagina de la %1$s spune: + + %2$s solicită numele de utilizator și parola. Site-ul spune: „%1$s” + + %1$s solicită numele de utilizator și parola. + 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 @@ + + + + Страница на %1$s сообщает: + + %2$s запрашивает имя пользователя и пароль. Сайт сообщает: «%1$s» + + %1$s запрашивает имя пользователя и пароль. + 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 @@ + + + + %1$s ᱴᱷᱮᱱ ᱢᱮᱱᱟᱜ ᱥᱟᱦᱴᱟ ᱢᱮᱱᱮᱜᱼᱟᱭ: + + %2$s ᱫᱚ ᱟᱢᱟᱜ ᱭᱩᱡᱟᱹᱨᱱᱮᱢ ᱟᱨ ᱯᱟᱥᱥᱣᱟᱹᱨᱰ ᱠᱷᱚᱡ ᱠᱟᱱᱟᱭ ᱾ ᱥᱟᱭᱴ ᱮ ᱢᱮᱱᱮᱜᱼᱟᱭ: “%1$s” + + %1$s ᱫᱚ ᱟᱢᱟᱜ ᱭᱩᱡᱟᱹᱨᱱᱮᱢ ᱟᱨ ᱯᱟᱥᱣᱟᱹᱨᱰ ᱠᱷᱚᱡ ᱠᱟᱱᱟᱭ ᱾ + 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 @@ + + + + Sa pàgina in %1$s narat: + + %2$s rechedet su nòmine tuo e sa crae. Su situ narat: “%1$s” + + %1$s rechedet su nòmine tuo e sa crae. + 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 @@ + + + + %1$s හි පිටුව මෙසේ පවසයි: + + %2$s ඔබගේ පරිශීලක නාමය සහ මුරපදය ඉල්ලා සිටියි. අඩවිය පවසන්නේ: “%1$s” + + %1$s ඔබගේ පරිශීලක නාමය සහ මුරපදය ඉල්ලා සිටියි. + 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 @@ + + + + Oznámenie stránky %1$s: + + %2$s požaduje zadanie vášho používateľského mena a hesla. Oznámenie stránky: „%1$s“ + + %1$s požaduje zadanie vášho používateľského mena a hesla. + 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 @@ + + + + %1$s تے ورقہ آہدے: + + %2$s ورتݨ ناں تے پاسورڈ دی ارداس کریندا پئے۔ سائٹ آہدی ہے: “%1$s” + + %1$s تہاݙے ورتݨ ناں تے پاسورڈ دی ارداس کریندا پئے۔ + 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 @@ + + + + Stran na %1$s sporoča: + + %2$s zahteva uporabniško ime in geslo. Sporočilo strani: “%1$s” + + %1$s zahteva uporabniško ime in geslo. + 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 @@ + + + + Faqja te %1$s thotë: + + %2$s po kërkon emrin tuaj të përdoruesit dhe fjalëkalimin. Sajti thotë: “%1$s” + + %1$s po kërkon emrin tuaj të përdoruesit dhe fjalëkalimin. + 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 @@ + + + + Порука са странице %1$s гласи: + + %2$s захтева ваше корисничко име и лозинку. Страница поручује: “%1$s” + + %1$s захтева ваше корисничко име и лозинку. + 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 @@ + + + + Kaca di %1$s nyebutkeun: + + %2$s ménta sandiasma jeung kecap konci anjeun. Situsna nyebutkeun: “%1$s” + + %1$s ménta sandiasma jeung kecap konci anjeun. + 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 @@ + + + + Sidan på %1$s säger: + + %2$s efterfrågar ditt användarnamn och lösenord. Webbplatsen säger: “%1$s” + + %1$s efterfrågar ditt användarnamn och lösenord. + 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 @@ + + + + Strōna %1$s dowo znać: + + %2$s prosi ô twoje miano używocza i hasło. Kōmunikat strōny: „%1$s” + + %1$s prosi ô twoje miano używocza i hasło. + 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 @@ + + + + பக்கம் %1$s இல் சொல்வது: + + %2$s உங்கள் பயனர்பெயர் மற்றும் கடவுச்சொல்லைக் கோருகிறது. தளம் கூறுகிறது: “%1$s” + + %1$s உங்கள் பயனர்பெயர் மற்றும் கடவுச்சொல்லைக் கோருகிறது. + 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 @@ + + + + %1$s వద్ద పేజీ అంటోంది: + + %2$s మీ వాడుకరి పేరును, సంకేతపదాన్ని అడుగుతోంది. సైటు ఇలా అంటుోంది: “%1$s” + + %1$s మీ వాడుకరి పేరును, సంకేతపదాన్ని అడుగుతోంది. + 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 @@ + + + + Саҳифа дар %1$s хабар медиҳад: + + %2$s номи корбар ва ниҳонвожаи шуморо дархост мекунад. Сомона хабар медиҳад: “%1$s” + + %1$s номи корбар ва ниҳонвожаи шуморо дархост мекунад. + 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 @@ + + + + หน้าที่ %1$s ระบุว่า: + + %2$s กำลังขอชื่อผู้ใช้และรหัสผ่านของคุณ ไซต์ระบุว่า: “%1$s” + + %1$s กำลังขอชื่อผู้ใช้และรหัสผ่านของคุณ + 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 @@ + + + + Ang pahina sa %1$s ay nagsasabi ng: + + %2$s ay humihingi ng iyong username at password. Ang sabi ng site ay: “%1$s” + + Hinihingi ng %1$s ang iyong username at password. + 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 @@ + + + + lipu %1$s li toki: + + %2$s li wile e nimi sina e nimi open sina. lipu li toki e ni: "%1$s" + + %1$s li wile e nimi sina e nimi open sina. + 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 @@ + + + + %1$s sayfası diyor ki: + + %2$s kullanıcı adı ve parolanızı istiyor. Site diyor ki: “%1$s” + + %1$s kullanıcı adı ve parolanızı istiyor. + 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 @@ + + + + Pajinâ nū riña %1$s taj: + + %2$s nachin\' man si yuguît ngà da\'nga\' huì arâj sunt. Sa tāj sitiô nan huin: “%1$s” + + %1$s nachin\' man si yuguît ngà da\'nga\' huì arâj sunt. + 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 @@ + + + + %1$s сәхифәсеннән хәбәр: + + %2$s кулланучы исемен һәм серсүзен сорый. Сайт хәбәре: “%1$s” + + %1$s кулланучы исемен һәм серсүзен сорый. + 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 @@ + + + + Da tettini tasna g %1$s: + + Issutur-ak %2$s ism d tguri n uzray-nnek. Da ittini usit: “%1$s” + + Issutur-ak %1$s isem d tguri n uzray-nnek. + 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 @@ + + + + بەت %1$s تە دېيىلگىنى: + + %2$s ئىشلەتكۈچى ئىسمى بىلەن ئىمنى تەلەپ قىلىۋاتىدۇ. تور بېكەتتە دېيىلگىنى: «%1$s» + + %1$s ئىشلەتكۈچى ئىسمى بىلەن ئىمنى تەلەپ قىلىۋاتىدۇ. + 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 @@ + + + + Сторінка на %1$s повідомляє: + + %2$s запитує ваше ім’я користувача і пароль. Повідомлення сайту: “%1$s” + + %1$s запитує ім’я користувача і пароль. + 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 @@ + + + + %1$s پر صفحہ کہتا ہے: + + %2$s صارف کا نام اور پاسورڈ کی درخواست کر رہا ہے۔ سائٹ کہتی ہے: “%1$s” + + %1$s صارف کا نام اور پاسورڈ کی درخواست کر رہا ہے۔ + 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 @@ + + + + %1$s sahifasi xabari: + + %2$s login va parolingizni soʻrayapti. Sayt xabari: “%1$s” + + %1$s login va parolingizni soʻrayapti. + 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 @@ + + + + Ƚa pàgina so’l server %1$s ƚa riporta: + + %2$s el dimanda on nòme utente e na password. El sito el riporta: “%1$s” + + %1$s dimanda on nòme utente e na password. + 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 @@ + + + + Trang %1$s cho biết: + + %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” + + %1$s yêu cầu tên người dùng và mật khẩu của bạn. + 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 @@ + + + + Ojú-ìwé %1$s sọ pé: + + %2$s ń béèrè orúkọ ìṣàmúlò àti kóòdù rẹ. Ìkànnì náà sọ pé: “%1$s” + + %1$s ń béère orúkọ ìwọlé àti pásíwọọ̀dù + 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 @@ + + + + 域名为 %1$s 的页面提示: + + %2$s 要求您输入用户名和密码。该网站提示:“%1$s” + + %1$s 要求您输入用户名和密码。 + 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 @@ + + + + %1$s 這一頁說: + + %2$s 要求您輸入帳號密碼。此網站說:「%1$s」 + + %1$s 要求您輸入帳號與密碼。 + 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 @@ + + + + + The page at %1$s says: + + %2$s is requesting your username and password. The site says: “%1$s” + + %1$s is requesting your username and password. + 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(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(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? = null + + val engineSession = spy(SystemEngineSession(testContext)) + val webView = spy( + object : WebView(testContext) { + override fun loadUrl(url: String, additionalHttpHeaders: MutableMap) { + loadedUrl = url + loadHeaders = additionalHttpHeaders + } + }, + ) + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + whenever(webView.settings).thenReturn(settings) + + engineSession.loadData("Hello!") + verify(webView, never()).loadData(anyString(), eq("text/html"), eq("UTF-8")) + + engineSession.webView = webView + + engineSession.loadData("Hello!") + verify(webView).loadData(eq("Hello!"), 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() + val settings = mock() + whenever(webView.settings).thenReturn(settings) + engineSession.register( + object : EngineSession.Observer { + override fun onLoadData() { + onLoadDataTriggered = true + } + }, + ) + engineSession.webView = webView + + engineSession.loadData("") + + assertTrue(onLoadDataTriggered) + } + + @Test + fun stopLoading() { + val engineSession = spy(SystemEngineSession(testContext)) + val webView = mock() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val webView = mock() { + 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() + val settings = mock() + + 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() + 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() + 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() + whenever(webView.context).thenReturn(testContext) + + val webViewSettings = mock() + 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("

Hello World

") + } + } + + 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("

Hello World

", 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("

Hello World

") + } + } + + 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() + val webViewSettings = mock() + 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() + val settings = mock() + whenever(webView.settings).thenReturn(settings) + val webViewSettings = mock() + 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() + val settings = mock() + whenever(webView.settings).thenReturn(settings) + val webViewSettings = mock() + 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() + + val webViewSettings = mock() + 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() + + val webViewSettings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + + 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() + val settings = mock() + 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() + 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() + val settings = mock() + 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 = 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() + engineSession.webView.webViewClient.onPageFinished(view, "http://mozilla.org") + assertEquals(Triple(false, null, null), observedSecurityChange) + + val certificate = mock() + val dName = mock() + 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() + val bundle = mock() + 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() + val bundle = mock() + + 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() + val bundle = mock() + + 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): List { + fail() + return emptyList() + } + + override suspend fun getVisited(): List { + 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> = 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() + 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() + 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() + 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() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + val settings = mock() + 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() + 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() + 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() + val settings = mock() + 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() + val customViewCallback = mock() + 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() + + 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() + + 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() + + // When the fullscreen view isn't available + engineSession.webView.webChromeClient!!.onShowCustomView(view, customViewCallback) + engineView.findViewWithTag("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("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 = 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() + val certificate = mock() + val dName = mock() + 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> { + 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() + + 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() + + 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() + + 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() + 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() + + 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() + + 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() + 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() + 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() + 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() + 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() + 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(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() + 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() + val suppportedCategories = mutableSetOf() + val enabledCategories = mutableSetOf() + 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() + val newWebView = mock() + val newEngineSession = mock() + 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() + val newWebView = mock() + val settings = mock() + + 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() + val newWebView = mock() + val resultMsg = mock() + val newEngineSession = mock() + + 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() + 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 + + + + + %pageTitle% + + + +
+

%messageShort%

+
+ + +``` + +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 @@ + + 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 @@ + + + + + + + + + + + + + + +
+ + + + + +
+

+
+ + +
+
+
+ + + + + + + + + + +
+
+

+
+ +
+
+ +
+
+
+
+ + + + + + 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("
    ".urlEncode(), "
      ".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 @@ + + + + እንደገና ሞክር + + + ጥያቄን ማጠናቀቅ አይቻልም + + + ስለዚህ ችግር ወይም ስህተት ተጨማሪ መረጃ በአሁኑ ጊዜ አይገኝም።

      + ]]>
      + + + ደህንነቱ የተጠበቀ ግንኙነት አልተሳካም + + + +
    • ሊያዩት የሞከሩት ገጽ ሊታይ አይችልም ምክንያቱም የሚያቀርበው ውሂብ ትክክለኛነት ሊረጋገጥ አልቻለም።
    • +
    • እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።
    • +
    + ]]> + + + ደህንነቱ የተጠበቀ ግንኙነት አልተሳካም + + + +
  • ይህ በአገልጋዩ ውቅር ላይ ያጋጠመ ችግር ሊሆን ይችላል ወይም ደግሞ አገልጋዩን ለመምሰል የሚሞክር ሰው ሊሆን ይችላል።
  • +
  • ከዚህ አገልጋይ ጋር ከዚህ ቀደም በተሳካ ሁኔታ ከተገናኙ ስህተቱ ጊዜያዊ ሊሆን ይችላል እና ቆይተው እንደገና መሞከር ይችላሉ።
  • +
+ ]]>
+ + + የላቀ… + + አንድ ሰው ድረ-ገፁን ለማስመሰል እየሞከረ ሊሆን ይችላል እና እርስዎ መቀጠል የለብዎትም። +

+ + ]]>
+ + ተመለስ (የሚመከር) + + አደጋውን ይቀበሉ እና ይቀጥሉ + + + ይህ ድረ-ገፅ ደህንነቱ የተጠበቀ ግንኙነት ይፈልጋል። + + + +
  • ለማየት የሞከሩት ገጽ ሊታይ አይችልም ምክንያቱም ይህ ድረ-ገጽ ደህንነቱ የተጠበቀ ግንኙነት ስለሚያስፈልገው።
  • +
  • ጉዳዩ ከድረ-ገፁ ሊሆን ይችላል፣ እና እሱን ለመፍታት እርስዎ ምንም ማድረግ አይችሉም።
  • +
  • ስለ ችግሩ የድረ-ገፁን አስተዳዳሪ ማሳወቅ ይችላሉ።
  • + + ]]>
    + + + የላቀ… + + + %1$s HTTP Strict Transport Security (HSTS) የሚባል የደህንነት ፖሊሲ አለው፣ ይህ ማለት %2$s ደህንነቱ በተጠበቀ ሁኔታ ብቻ ነው መገናኘት የሚችለው። ይህንን ድረ-ገጽ ለመጎብኘት የተለየ ፈቃድ ማከል አይችሉም። + ]]> + + ወደኋላ ተመለስ + + + ግንኙነቱ ተቋርጧል + + + አሳሹ በተሳካ ሁኔታ ተገናኝቷል፣ ነገር ግን መረጃ በሚተላለፍበት ጊዜ ግንኙነቱ ተቋርጧል። እባክዎ እንደገና ይሞክሩ።

    +
      +
    • ድረ-ገጹ ለጊዜው የማይገኝ ወይም በጣም ስራ የበዛበት ሊሆን ይችላል። ከጥቂት ጊዜ በኋላ እንደገና ይሞክሩ።
    • +
    • ምንም ገጾችን መጫን ካልቻሉ የመሣሪያዎን ውሂብ ወይም የWi-Fi ግንኙነት ያረጋግጡ።
    • +
    + ]]>
    + + + ግንኙነቱ ጊዜው አልፎበታል + + + የተጠየቀው ድረ-ገጽ ለግንኙነት ጥያቄ ምላሽ አልሰጠም እና አሳሹ ምላሽ መጠበቅ አቁሟል።

    +
      +
    • አገልጋዩ ከፍተኛ ፍላጎት ወይም ጊዜያዊ መቋረጥ እያጋጠመው ሊሆን ይችላል? ቆይተው እንደገና ይሞክሩ።
    • +
    • ሌሎች ድረ-ገጾችን ማሰስ አይችሉም? የመሳሪያውን የአውታረ መረብ ግንኙነት ይፈትሹ።
    • +
    • የእርስዎ መሣሪያ ወይም አውታረ መረብ በፋየርዎል ወይም በፕሮክሲ የተጠበቀ ነው? ትክክል ያልሆኑ ቅንብሮች በድር አሰሳ ላይ ጣልቃ ሊገቡ ይችላሉ።
    • +
    • አሁንም ችግር አለ? ለእርዳታ የአውታረ መረብ አስተዳዳሪዎን ወይም የበይነመረብ አቅራቢዎን ያማክሩ።
    • +
    + ]]>
    + + + መገናኘት አልተቻለም + + + +
  • ጣቢያው ለጊዜው የማይገኝ ወይም በጣም ስራ የበዛበት ሊሆን ይችላል። ከጥቂት ጊዜ በኋላ እንደገና ይሞክሩ።
  • +
  • ምንም ገጾችን መጫን ካልቻሉ የመሣሪያዎን ውሂብ ወይም የWi-Fi ግንኙነት ያረጋግጡ።
  • + + ]]>
    + + + ከሰርቨር ያልተጠበቀ ምላሽ + + + ድረ-ገፁ ለኔትወርክ ጥያቄው ባልተጠበቀ መልኩ ምላሽ ሰጥቷል እና አሳሹ መቀጠል አልቻለም።

    + ]]>
    + + + ገጹ በትክክል እየተዘዋወረ አይደለም + + + አሳሹ የተጠየቀውን ንጥል ሰርስሮ ለማውጣት መሞከሩን አቁሟል። ድረ-ገጹ ጥያቄውን በማያጠናቅቅ መንገድ እየመራው ነው።

    +
      +
    • በዚህ ድረ-ገጽ የሚፈለጉ ኩኪዎችን አሰናክለዋል ወይም አግደዋል?
    • +
    • የድረ-ገጹን ኩኪዎች መቀበል ችግሩን ካልፈታው፣ ምናልባት የእርስዎ መሣሪያ ሳይሆን የአገልጋይ ውቅር ችግር ነው።
    • +
    + ]]>
    + + + ከመስመር ውጭ ሁነታ + + + አሳሹ የሚሰራው ከመስመር ውጭ በሆነው ሁነታ ነው እና ከተጠየቀው ንጥል ጋር መገናኘት አይችልም።

    +
      +
    • መሣሪያው ከገባሪ አውታረ መረብ ጋር የተገናኘ ነው?
    • +
    • ወደ የመስመር ላይ ሁነታ ለመቀየር እና ገጹን እንደገና ለመጫን “እንደገና ሞክር”ን ይጫኑ።
    • +
    + ]]>
    + + + ለደህንነት ሲባል ወደብ ተገድቧል + + + የተጠየቀው አድራሻ ወደብ (ምሣ. mozilla.org:80 በmozilla.org ላይ ወደብ 80) በተለምዶ ከድር አሰሳ ውጭ ለሌላ ጥቅም ላይ ይውላል። ለእርስዎ ጥበቃ እና ደህንነት አሳሹ ጥያቄውን ሰርዟል።

    + ]]>
    + + + ግንኙነቱ ዳግም ተጀምሯል + + + በግንኙነት ላይ ሲደራደሩ የአውታረ መረቡ ማገናኛ ተቋርጧል። እባክዎ እንደገና ይሞክሩ።

    +
      +
    • ድረ-ገጹ ለጊዜው የማይገኝ ወይም በጣም ስራ የበዛበት ሊሆን ይችላል። ከጥቂት ጊዜ በኋላ እንደገና ይሞክሩ።
    • +
    • ምንም ገጾችን መጫን ካልቻሉ የመሣሪያዎን ውሂብ ወይም የWi-Fi ግንኙነት ያረጋግጡ።
    • +
    + ]]>
    + + + ደህንነቱ ያልተጠበቀ የፋይል አይነት + + + +
  • እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።
  • + + ]]>
    + + + የተበላሸ የይዘት ስህተት + + + ለማየት እየሞከሩት ያለው ገጽ ሊታይ አይችልም ምክንያቱም በመረጃ ስርጭቱ ላይ ስህተት ስለተገኘ።

    +
      +
    • እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።
    • +
    + ]]>
    + + + ይዘት ተበላሽቷል + + ለማየት እየሞከሩት ያለው ገጽ ሊታይ አይችልም ምክንያቱም በመረጃ ስርጭቱ ላይ ስህተት ስለተገኘ።

    +
      +
    • እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።
    • +
    + ]]>
    + + + የይዘት መቀየር ስህተት + ለማየት እየሞከሩት ያለው ገጽ ልክ ያልሆነ ወይም የማይደገፍ የማመቅ ዘዴ ስለሚጠቀም ሊታይ አይችልም።

    +
      +
    • እባክዎ ይህንን ችግር ለማሳወቅ የድረ-ገፁን ባለቤቶች ያነጋግሩ።
    • +
    + ]]>
    + + + አድራሻ አልተገኘም + + + አሳሹ ለተጠቀሰው አድራሻ አስተናጋጅ አገልጋይ ማግኘት አልቻለም።

    +
      +
    • ለትየባ ስህተቶች አድራሻውን ያረጋግጡ ለምሣሌ + ww.example.com + በwww.example.com ምትክ።
    • +
    • ምንም ገጾችን መጫን ካልቻሉ የመሣሪያዎን ውሂብ ወይም የWi-Fi ግንኙነት ያረጋግጡ።
    • +
    + ]]>
    + + + ምንም የበይነመረብ ግንኙነት የለም + + የአውታረ መረብ ግንኙነትዎን ይፈትሹ ወይም ገጹን ከጥቂት ጊዜ በኋላ እንደገና ለመጫን ይሞክሩ። + + እንደገና ጫን + + + ልክ ያልሆነ አድራሻ + የቀረበው አድራሻ በሚታወቅ ቅርጸት አይደለም። እባክዎ ለስህተቶች የአካባቢ አሞሌን ያረጋግጡ እና እንደገና ይሞክሩ።

    + ]]>
    + + አድራሻው ልክ አይደለም + + + +
  • የድር አድራሻዎች ብዙውን ጊዜ እንደ http://www.example.com/
  • ይጻፋሉ +
  • እዝባሮችን እየተጠቀሙ መሆንዎን ያረጋግጡ (ማለትም /)።
  • + + ]]>
    + + + ያልታወቀ ፕሮቶኮል + + አድራሻው ፕሮቶኮልን ይገልጻል (ለምሳሌ wxyz://) አሳሹ ይህን ስለማያቀው በትክክል ከድረ-ገጹ ጋር መገናኘት አይችልም።

    +
      +
    • መልቲሚዲያ ወይም ሌሎች የጽሑፍ ያልሆኑ አገልግሎቶችን ለማግኘት እየሞከሩ ነው? ለተጨማሪ መስፈርቶች ድረ-ገጹን ይፈትሹ።
    • +
    • አንዳንድ ፕሮቶኮሎች በአሳሹ ከመታወቃቸው በፊት የሶስተኛ ወገን ሶፍትዌር ወይም ቅጥያዎችን ሊፈልጉ ይችላሉ።
    • +
    + ]]>
    + + + ሰነዱ አልተገኘም + + +
  • ንጥሉ ሊሰየም፣ ሊወገድ ወይም ወደ ሌላ ቦታ ሊዛወር ይችል ነበር?
  • +
  • በአድራሻው ውስጥ የፊደል ግድፈት፣ አጣጣል ወይም ሌላ የአጻጻፍ ስህተት አለ?
  • +
  • ለተጠየቀው ንጥል በቂ የመዳረሻ ፍቃድ አለዎት?
  • + + ]]>
    + + + የፋይሉ መዳረሻ ተከልክሏል + +
  • ተወግዷል፣ ተንቀሳቅሷል ወይም የፋይል ፍቃዶች መዳረሻን እየከለከሉ ሊሆን ይችላል።
  • + + ]]>
    + + + ተኪ አገልጋይ ግንኙነትን ውድቅ አድርጓል + + አሳሹ ተኪ አገልጋይ እንዲጠቀም ተዋቅሯል፣ ነገር ግን ተኪው ግንኙነትን ውድቅ አደረገ።

    +
      +
    • የአሳሹ ተኪ ውቅር ትክክል ነው? ቅንብሮቹን ይፈትሹ እና እንደገና ይሞክሩ።
    • +
    • የተኪ አገልግሎቱ ከዚህ አውታረ መረብ ጋር መገናኘትን ይፈቅዳል?
    • +
    • አሁንም ችግር አለ? ለእርዳታ የአውታረ መረብ አስተዳዳሪዎን ወይም የበይነመረብ አቅራቢዎን ያማክሩ።
    • +
    + ]]>
    + + + ተኪ አገልጋይ አልተገኘም + + አሳሹ ተኪ አገልጋይ እንዲጠቀም ተዋቅሯል፣ ነገር ግን ተኪው ሊገኝ አልቻለም።

    +
      +
    • የአሳሹ ተኪ ውቅር ትክክል ነው? ቅንብሮቹን ይፈትሹ እና እንደገና ይሞክሩ።
    • +
    • መሣሪያው ከገባሪ አውታረ መረብ ጋር የተገናኘ ነው?
    • +
    • አሁንም ችግር አለ? ለእርዳታ የአውታረ መረብ አስተዳዳሪዎን ወይም የበይነመረብ አቅራቢዎን ያማክሩ።
    • +
    + ]]>
    + + + ጎጂ ድረ-ገጽ ችግር + + በ%1$s ያለው ድረ-ገፅ የጥቃት ድረ-ገፅ ተብሎ ሪፖርት ተደርጓል እና በእርስዎ የደህንነት ምርጫዎች መሠረት ታግዷል።

    + ]]>
    + + + የማይፈለግ የድረ-ገፅ ጉዳይ + + + በ%1$s ያለው ድረ-ገጽ ያልተፈለገ ሶፍትዌር እንደሚያቀርብ ሪፖርት ተደርጓል እና በእርስዎ የደህንነት ምርጫዎች መሰረት ታግዷል።

    + ]]>
    + + + ጎጂ የድረ-ገፅ ጉዳይ + + በ%1$s ያለው ድረ-ገፅ ጎጂ ሊሆን የሚችል ድረ-ገፅ ተብሎ ሪፖርት ተደርጓል እና በእርስዎ የደህንነት ምርጫዎች መሠረት ታግዷል።

    + ]]>
    + + + አታላይ የድረ-ገፅ ጉዳይ + + + ይህ በ%1$s ላይ ያለው ድረ-ገጽ እንደ አታላይ ድረ-ገጽ ሪፖርት ተደርጓል እና በእርስዎ የደህንነት ምርጫዎች መሠረት ታግዷል።

    + ]]>
    + + + ደህንነቱ የተጠበቀ ድረ-ገፅ የለም + + %1$s HTTPS ስሪት የለም።]]> + + ወደ HTTP ድረ-ገፅ ይቀጥሉ + 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 @@ + + + + + Tornar a intentar-lo + + + No se puede completar la petición + + Actualment no i hai información adicional disponible pa este problema u error.

    + ]]>
    + + + Connexión segura fallida + + +
  • La pachina que yes intentando veyer no se puede amostrar perque no s’ha puesto verificar l’autenticidat d’os datos recibius.
  • +
  • Contacta con os propietarios d’o puesto web pa informar-les d’este problema.
  • + ]]>
    + + + Connexión segura fallida + + +
  • 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.
  • +
  • Si ya t’hebas connectau dinantes a este servidor, la error podría estar temporal, y podrás tornar a intentar-lo mas tarde.
  • + + ]]>
    + + + Abanzadas… + + Belún podría estar intentando fer-se pasar per lo puesto y no habrías de continar. +

    + ]]>
    + + Tornar enta zaga (recomendau) + + Acceptar lo risgo y continar + + + La connexión ha estau interrumpida + + Lo navegador s’ha connectau con exito, pero la connexión s’ha interrumpiu mientres se transferiba la información. Torna a intentar-lo.

    +
      +
    • Lo puesto podría no estar disponible temporalment u estar masiau ocupau. Torna a intentar-lo en bells menutos.
    • +
    • Si no puetz cargar garra pachina, revisa la connexión wifi u de datos d’o tuyo dispositivo mobil.
    • +
    ]]>
    + + + S’ha pasau lo tiempo d’espera d’a connexión + + Lo puesto solicitau no respondió a una petición de connexión y lo navegador ha deixau d’asperar una respuesta.

    +
      +
    • Podría estar experimentando lo servidor una alta demanda u un corte temporal? Torna a intentar-lo mas tarde.
    • +
    • No puetz navegar per atros puestos? Compreba la connexión de ret de l’equipo.
    • +
    • Lo tuyo ret u equipo ye protechiu per un firewall u un proxy? Una configuración incorrecta puede interferir con a navegación web.
    • +
    • Encara tiens problemas? Consulta con l’administrador de ret u furnidor d’Internet pa obtener asistencia tecnica.
    • +
    + ]]>
    + + + No se puede connectar + + +
  • Lo puesto podría no estar disponible temporalment u estar masiau ocupau. Torna a intentar-lo en bell minuto.
  • +
  • Si no puetz cargar garra pachina, revisa la connexión wifi u de datos d’o dispositivo mobil.
  • + ]]>
    + + + Respuesta inasperada d’o servidor + + Lo puesto respondió a la solicitut de ret d’una forma inesperada y lo navegador no puede continar.

    ]]>
    + + + La pachina no ye reendrezando adequadament + + 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.

    +
      +
    • Tiens desactivadas u blocadas las cookies que ameneste este puesto?
    • +
    • 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.
    • +
    ]]>
    + + + Modo sin connexión + + Lo navegador ye operando en modo sin connexión y no puede connectar-se con l’elemento solicitau.

    +
      +
    • Ye connectau l’equipo a un ret activo?
    • +
    • Preta "Tornar a intentar-lo" pa pasar a lo modo con connexión y recargar la pachina.
    • +
    + ]]>
    + + + Puerto restrinchiu per razons de seguridat + + L’adreza solicitada especificaba un puerto (p. eix., mozilla.org:80 pa lo puerto 80 de mozilla.org) que gosa usar-se pa propositos distintos a navegar per Internet. Lo navegador ha cancelau la solicitut pa la tuya protección y seguridat.

    + ]]>
    + + + La connexión ha estau reiniciada + + Lo vinclo con o ret s’ha interrumpiu mientres se negociaba una connexión. Torna a intentar-lo.

    +
      +
    • Lo puesto podría no estar disponible temporalment u estar masiau ocupau. Torna a intentar-lo en bell minuto.
    • +
    • Si no puetz cargar garra pachina, revisa la connexión wifi u de datos d’o dispositivo mobil.
    • +
    ]]>
    + + + Tipo de fichero no seguro + + +
  • Mete-te en contacto con os propietarios d’o puesto web pa informar-les d’este problema.
  • + + ]]>
    + + + Error de conteniu malmeso + + La pachina que yes intentando veyer no puede amostrar-se perque se detectó una error en a transmisión d’os datos.

    +
      +
    • Mete-te en contacto con os propietarios d’o puesto web pa informar-les d’este problema.
    • +
    ]]>
    + + + Conteniu blocau + La pachina que yes intentando veyer no puede amostrar-se perque se detectó una error en a transmisión d’os datos.

    +
      +
    • Mete-te en contacto con os propietarios d’o puesto web pa informar-les d’este problema.
    • +
    ]]>
    + + + Error de codificación de conteniu + La pachina que yes mirando de veyer no puede amostrar-se perque usa una forma no valida u no admitida de compresión.

    +
      +
    • Mete-te en contacto con os propietarios d’o puesto web pa informar-les d’este problema.
    • +
    ]]>
    + + + No se trobó l’adreza + + Lo navegador no podió trobar lo servidor pa l’adreza proporcionada.

    +
      +
    • Compreba que l’adreza no contienga errors, per eixemplo, ww.eixemplo.com en cuenta de www.eixemplo.com.
    • +
    • Si no puetz cargar garra pachina, revisa la connexión wifi u de datos d’o dispositivo mobil.
    • +
    + ]]>
    + + + No i hai connexión a Internet + + Verifica la tuya connexión de ret u intenta tornar a cargar la pachina en uns momentos. + + Recargar + + + L’adreza no ye valida + L’adreza proporcionada no ye en un formato reconoixiu. Compreba si i hai errors en a barra d’adrezas y torna a intentar-lo.

    + ]]>
    + + L’adreza no ye valida + + +
  • Las adrezas web gosan escribir-se asinas: http://www.ejemplo.com/
  • +
  • Asegura-te d’estar usando barras inclinadas enta adebant (p. eix., /).
  • + + ]]>
    + + + Protocolo desconoixiu + L’adreza especifica un protocolo (p. eix., wxyz://) que lo navegador no reconoixe, asinas que lo navegador no puede connectar-se correctament con o puesto.

    +
      +
    • Yes mirando d’acceder a conteniu multimedia u atros servicios que no son de texto? Compreba los requisitos adicionals d’o puesto.
    • +
    • Qualques protocolos pueden amenester software u plugins de tercers antes que lo navegador pueda reconoixer-los.
    • +
    ]]>
    + + + No s’ha trobau lo fichero + +
  • Ye posible que l’elemento s’haiga renombrau, eliminau u cambiau de rota?
  • +
  • I hai bella error d’ortografía, d’uso de mayusclas u de qualsequier atro tipo en l’adreza?
  • +
  • Tiens privilechios d’acceso suficients pa l’elemento solicitau?
  • + + ]]>
    + + + L’acceso a lo fichero ha estau denegau + +
  • Puede haber-se eliminau u moviu, u los suyos permisos de fichero pueden estar impedindo l’acceso.
  • + + ]]>
    + + + Lo servidor proxy refusó la connexión + Lo navegador ye configurau pa usar un servidor proxy, pero lo proxy refusó la connexión.

    +
      +
    • Ye correcta la configuración de proxy d’o navegador? Compreba la configuración y torna a intentar-lo.
    • +
    • Permite lo servicio proxy connexions dende este ret?
    • +
    • Encara tiens problemas? Consulta con l’administrador de ret u furnidor d’Internet pa obtener asistencia tecnica.
    • +
    + ]]>
    + + + No se trobó lo servidor proxy + Lo navegador ye configurau pa usar un servidor proxy, pero no se podió trobar lo servidor proxy.

    +
      +
    • Ye correcta la configuración de proxy d’o navegador? Compreba la configuración y torna a intentar-lo.
    • +
    • Ye connectau l’equipo a un ret activo?
    • +
    • Encara tiens problemas? Consulta con l’administrador de ret u furnidor d’Internet pa obtener asistencia tecnica.
    • +
    + ]]>
    + + + Problema de puesto de malware + + Lo puesto en %1$s s’ha identificau como un puesto atacant y s’ha blocau, seguindo las tuyas preferencias de seguridat.

    ]]>
    + + + Problema de puesto no deseyau + + 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.

    ]]>
    + + + Problema de puesto nocivo + + Lo puesto en %1$s s’ha identificau como un puesto potencialment nocivo y s’ha blocau, seguindo las tuyas preferencias de seguridat.

    ]]>
    + + + Problema de puesto enganyoso + + Lo puesto en %1$s s’ha identificau como un puesto potencialment nocivo y s’ha blocau, seguindo las tuyas preferencias de seguridat

    ]]>
    + +
    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 @@ + + + + Kpọk Sa + + + Kpekọt Ìrọ Mbeek Ìsan̄a + + Kpunu ofifi etip òfolek ufialek mè ìre ǹlilọ yi.

    + ]]>
    + + + Ìkakọt ìtibi ìnin̄ me utelelek + + +
  • 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.
  • +
  • Please contact the website owners to inform them of this problem.
  • + + ]]>
    + + + +
  • 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.
  • +
  • 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ọ.
  • + + ]]>
    + + + Òdọdọk… + + + Gwu kom (Mîrọ inye) + + + Chieek Unan ya mè Fo isi + + + Mîkput ntibi-ntet ya + + Òwọlọ-etip îtibi itet inwọn, ire, mîgbugbana ntibi-ntet ya mgbọ îkiria etip.Soso kpọk sa.

    +
      +
    • Môkọt ire akpatan̄ yaìkakup mgbọ keyi mè ìre ìkirọ owuwa ikwaan̄. Kpọk sa me mgbidim mgbọ.
    • +
    • Ire òkakọt ìchili akpọk geege, kpọ data okup me okwukwut kwun̄ mè ìyaka ire ntibi-ntet Wi-Fi kwun̄.
    • +
    + ]]>
    + + + Okike mgbọ esun̄be inyi ntibi-ntet yi îraka + + + Ere akpatan̄ ya edobe ìkanyi ifọọk ntibi-ntet eweekbe, òwọlọ-etip si îta ikukup iban ifọọk.

    +
      +
    • Ìre môkọt ire owuwa iweweek okisi lek òbeme-etip? Kpọk sa ofifi mgbọ.
    • +
    • Ìre òkakọt ìwọlọ ere akpatan̄ yi? Kpọ ntibi-ntet eyi okwukwut kwun̄.
    • +
    • Ìre firewall sà ìre proxy okibem okwukwut kwun̄? Onineen̄ eyi ìkatatge môkọt igbugbana iwọwọlọ olik etip.
    • +
    • Owu gwa okikpọk ikaan̄ ufialek? Chichini ogwu àdìmin njin-etip kwun̄.
    • +
    + ]]>
    + + + Ìkakọt ìtibi ìtet + + + +
  • Môkọt ire ere akpatan̄ ìkakup me mgbidim mgbọ mè ìyaka ire ìkirọ owuwa ikwaan̄. Kpọk sa lek me mgbidim mgbọ.
  • +
  • Ire òkakọt ìchili akpọk geege, kpọ data òkup me okwukwut kwun̄ mè ìyaka ire Wi-Fi kwun̄.
  • + + ]]>
    + + + Ifọọk eyi kpekpọ chieen̄ ònan̄a me òbeme-etip + + Ere akpatan̄ ya ìfọọk me oniin̄ kpekpọ chieen̄, eya orọ òwọlọ-etip ìkayaka ìkọt ìje ìfo isi.

    + ]]>
    + + + Akpọk ya ìkagwu ìsi ìtat + + + Ò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.

    +
      +
    • Ìre oniin̄ sà ìre ogban cookies eyi akpatan̄ yi okidobe?
    • +
    • Ire ichechieek cookies ìkarọ ufialek yi ita, môkọt ire onineen̄ òbeme-etip, ìkare okwukwut kwun̄.
    • +
    + ]]>
    + +
    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 @@ + + + + + أعِد المحاولة + + + تعذّر إكمال الطلب + + + لا يوجد حاليا المزيد من المعلومات حول هذه المشكلة أو الخطأ.

    ]]>
    + + + فشل الاتصال الآمن + + +
  • لا يمكن عرض الصفحة التي تحاول زيارتها لعدم إمكانية الاستيثاق من البيانات المستقبلة.
  • من فضلك اتصل بمالكي الموقع لإعلامهم بهذه المشكلة.
  • ]]>
    + + + فشل الاتصال الآمن + + +
  • قد يكون هذا بسبب مشكلة في إعدادات الخادوم، أو أنّ أحدًا يحاول انتحال هوية الخادوم
  • إن كنت قد اتصلتَ بهذا الموقع بنجاح في الماضي، قد يكون هذا الخطأ مؤقتًا، لذا أعِد المحاولة في وقت لاحق.
  • ]]>
    + + + متقدم… + + هناك احتمال بمحاولة أحد الأشخاص انتحال هوية هذا الموقع، وبذلك عليك عدم المواصلة. +

    +]]>
    + + عُد للخلف (يُنصح به) + + أقبلُ المخاطرة فتابِع + + + يتطلب هذا الموقع اتصالاً آمنًا. + + + متقدم… + + + %1$s سياسة النقل الصارمة (HSTS)، ما يعني بأن %2$s لا يستطيع الاتصال به إلا بأمان. لا يمكنك إضافة استثناء لزيارة هذا الموقع. + ]]> + + عُد للخلف + + + قُوطِع الاتصال + + + اتصل المتصفح بنجاح، لكن قطع الاتصال أثناء نقل البيانات. من فضلك أعد المحاولة.

    +
      +
    • قد يكون الموقع متوقفًا مؤقتًا أو مشغولا جدًا. حاول ثانية بعد عدّة دقائق.
    • +
    • إن لم تكن تستطيع تحميل أي صفحة، تحقق من اتصال المحمول بشبكة البيانات أو الشبكة اللاسلكية.
    • +
    ]]>
    + + + انتهت مهلة الاتصال + + + لم يستجب الموقع المطلوب لطلب الاتصال، وتوقف المتصفح عن انتظار رده.

    +
      +
    • ربما يواجه الخادوم طلبا متزايدا أو يعاني من انقطاع مؤقت؟ أعد المحاولة فيما بعد.
    • +
    • ألا تستطيع تصفح المواقع الأخرى؟ راجع اتصال الجهاز بالشبكة.
    • +
    • جهازك أو شبكتك محمية بجدار ناري أو وسيط؟ الإعدادات الخطأ قد تتعارض مع تصفح الوِب.
    • +
    • أمازلت تواجه مشاكل؟ راجع مدير الشبكة أو مزود الخدمة للمساعدة
    • +
    ]]>
    + + + تعذّر الاتصال + + + +
  • قد يكون الموقع غير متاح مؤقتًا أو مشغولًا جدًا. حاول مجددًا بعد قليل.
  • +
  • إن لم تكن تستطيع تحميل أي صفحة، تحقق من اتصال المحمول بشبكة البيانات أو الشبكة اللاسلكية.
  • +]]>
    + + + استجابة غير متوقعة من الخادوم + + + استجاب الموقع لطلب الشبكة بطريقة غير متوقعة ولا يستطيع المتصفح المواصلة.

    ]]>
    + + + لا تعيد الصفحة التوجيه بشكل سليم + + + توقف المتصفح عن محاولة جلب العنصر المطلوب. يعيد الموقع توجيه الطلب بصورة لن تتم أبدا.

    • هل عطّلت أو حجبتَ الكعكات من هذا الموقع؟
    • إذا كان قبول كعكات هذا الموقع لا يحل مشكلتك، فهذه على الأرجح مشكلة في إعدادات الخادوم وليس حاسوبك.
    ]]>
    + + + وضع بدون اتصال + + + المتصفح يعمل في وضع اللا اتصال ولا يستطيع تحميل العنصر المطلوب.

    +
      +
    • هل الجهاز متّصل بشبكة نشِطة؟
    • +
    • انقر ”حاول مجددًا“ للتبديل إلى وضع الاتصال وإعادة تحميل الصفحة.
    • +
    ]]>
    + + + المنفذ ممنوع لأسباب الأمان + + + حدَّد العنوان المطلوب منفذا (مثال mozilla.org:80 من أجل المنفذ 80 على mozilla.org) يُستخدم عادة لأسباب غير تصفّح الوِب. ألغى المتصفّح الطّلب لحمايتك وأمنك.

    ]]>
    + + + صُفِّر الاتصال + + + قُوطع الاتصال الشبكي أثناء التفاوض على الاتصال. من فضلك أعد المحاولة.

    +
      +
    • قد يكون الموقع متوقفًا مؤقتًا أو مشغولا جدًا. حاول ثانية بعد عدّة دقائق.
    • +
    • إذا كنت غير قادر على تحميل أي صفحة، افحص اتصال الحاسوب بالشبكة.
    • +
    ]]>
    + + + نوع ملف غير آمن + + + +
  • من فضلك اتصل بمالكي الموقع لإعلامهم بهذه المشكلة.
  • + ]]>
    + + + خطأ محتوى فاسد + + + تعذر عرض الصفحة التي تريد مشاهدتها بسبب خطأ أثناء نقل البيانات.

    • الرجاء التواصل مع مالك الموقع لإبلاغه بهذه المشكلة.
    ]]>
    + + + انهار المحتوى + + تعذر عرض الصفحة التي تريد مشاهدتها بسبب خطأ أثناء نقل البيانات.

    • الرجاء التواصل مع مالك الموقع لإبلاغه بهذه المشكلة.
    ]]>
    + + + خطأ في ترميز المحتوى + + الصفحة التي تحاول فتحها لا يمكن عرضها لأنها تستخدم صيغة ضغط غير سليمة أو غير مدعومة.

    • الرجاء الاتصال أصحاب الموقع لإخبارهم بهذه المشكلة.
    ]]>
    + + + العنوان غير موجود + + + لم يجد المتصفح الخادوم المستضيف للعنوان المعطى.

    +
      +
    • هل قمت بخطأ في كتابة النطاق؟ (مثل ww.example.org بدلا من www.example.org)
    • +
    • أمتأكد من وجود عنوان النطاق هذا؟ ربما قد يكون انتهى تسجيله.
    • +
    • إن لم تكن تستطيع تحميل أي صفحة، تحقق من اتصال المحمول بشبكة البيانات أو الشبكة اللاسلكية.
    • +
    ]]>
    + + + لا يوجد اتصال بالإنترنت + + افحص اتصال الشبكة أو جرّب إعادة تحميل الصفحة بعد قليل. + + أعِد التحميل + + + عنوان غير صحيح + تنسيق العنوان المُعطى غير معروف. من فضلك راجع شريط العنوان بحثا عن أخطاء ثم أعد المحاولة.

    ]]>
    + + العنوان غير صالح + + + +
  • تُكتب عناوين الوِب عادة على الشكل الآتي ‪http://www.example.com/‬
  • +
  • تأكد أنك تستخدم الشرطة المائلة إلى اليمين (أي /).
  • +]]>
    + + + بروتوكول مجهول + + يحدد العنوان بروتوكولا (مثلا: wxyz://) لا يتعرفه المتصفح، لذا لا يستطيع المتصفح الاتصال بالموقع بشكل سليم.

    • هل تحاول الوصول لمحتوى متعدد الوسائط أو خدمة غير نصية؟ راجع الموقع لمتطلبات إضافية.
    • قد تحتاج بعض البروتوكولات لبرامج إضافية أو ملحقات ليستطيع المتصفح التعرف عليها.
    ]]>
    + + + الملف غير موجود + +
  • ربما حُذف العنصر أو تغيّر اسمه أو مكانه؟
  • هل هناك خطأ إملائي أو مطبعي في العنوان؟
  • هل لديك صلاحيات كافية للنفاذ إلى العنصر المطلوب؟
  • ]]>
    + + + مُنِع الوصول للملف + +
  • قد يكون حُذف أو نُقل أو أن صلاحيّات الملف تمنع الوصول إليه.
  • ]]>
    + + + رفض الخادوم الوسيط الاتصال + + ضُبط المتصفّح ليستخدم خادوما وسيطا، لكن الوسيط رفض الاتصال.

    • هل إعدادات الوسيط سليمة؟ تأكد من الإعدادات وأعد المحاولة.
    • هل يسمح الخادوم الوسيط بالاتصالات من هذه الشبكة؟
    • أما زلت تواجه المشاكل؟ راجع مدير الشبكة أو مزود الخدمة للمساعدة.
    ]]>
    + + + لم يُعثر على خادوم وسيط + + ضُبط المتصفّح ليستخدم خادوما وسيطا، لكن لم يوجد الوسيط.

    • هل إعدادات الوسيط سليمة؟ تأكد من الإعدادات وأعد المحاولة.
    • هل الجهاز متصل بشبكة نشطة؟
    • أما زلت تواجه المشاكل؟ راجع مدير الشبكة أو مزود الخدمة للمساعدة.
    ]]>
    + + + مشكلة موقع يحتوي برمجيات خبيثة + + أُبلِغ عن أن الموقع %1$s موقع هجمات و حُجب بناء على تفضيلات الأمن.

    ]]>
    + + + مشكلة موقع غير مرغوب فيه + + أُبلِغ عن أن الموقع %1$s يقدم برمجيات غير مرغوب فيها و حُجب بناء على تفضيلات الأمن.

    ]]>
    + + + مشكلة موقع ضار + + أُبلِغ عن أن الموقع %1$s موقع خطر محتمل و حُجِبَ بناء على تفضيلات الأمن.

    ]]>
    + + + مشكلة موقع مخادع + + أُبلِغ عن أن الموقع %1$s موقع مخادع و حُجِبَ بناء على تفضيلات الأمن.

    ]]>
    + + + خاصية الموقع الآمن غير متاحة + + %1$s.]]> + + واصِل نحو نسخة HTTP +
    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 @@ + + + + + Retentar + + + Nun se pue completar la solicitú + + + La información adicional tocante a esti problema o error nun ta disponible.

    ]]>
    + + + La conexón segura falló + + + +
  • Nun se pue amosar la páxina que tentes de ver porque nun se pudo verificar l\'autenticidá de los datos recibíos.
  • +
  • Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.
  • + ]]>
    + + + La conexón segura falló + + + +
  • Esto podría ser un problema cola configuración del sirvidor o que daquién tea tentando de suplantalu.
  • +
  • Si nel pasáu te conectesti correutamente al sirvidor, ye posible que l\'error seya temporal polo qu\'anueva la páxina dempués.
  • + ]]>
    + + + Opciones avanzaes… + + Daquién podría tar tentando de suplantar el sitiu ya nun habríes siguir. +

    + ]]>
    + + Dir p\'atrás (aconséyase) + + Aceptar el riesgu ya siguir + + + Esti sitiu web rique una conexón segura. + + +
  • Nun se pue amosar la páxina que tentes de ver porque esti sitiu web rique una conexón segura.
  • +
  • Ye mui probable que\'l problema seya del sitiu web ya nun puedas facer nada pa igualu.
  • +
  • Pues avisar a l\'alministración del sitiu web pa informar del problema.
  • + + ]]>
    + + + Opciones avanzaes… + + + %1$s tien una política de seguranza llamada HSTS (HTTP Strict Transport Security), lo que significa que %2$s namás se pue conectar con seguranza al sitiu. Nun pues amestar nenguna esceición pa visitar esti sitiu. + ]]> + + Dir p\'atrás + + + Torgóse la conexón + + + El restolador conectóse correutamente mas la conexón torgóse mentanto se tresfería información. Volvi tentalo.

    +
      +
    • Seique\'l sitiu ta temporalmente non disponible o perocupáu. Volvi probar nun momentu.
    • +
    • Si nun yes a cargar nenguna páxina, comprueba la conexón Wi-Fi o móvil del preséu.
    • +
    ]]>
    + + + Escosó\'l tiempu d\'espera de la conexón + + + El sitiu solicitáu nun respondió a una solicitú de conexón ya\'l restolador dexó d\'esperar una rempuesta.

    +
      +
    • ¿Pue ser que\'l sirvidor tea sufriendo una demanda alta o una cayida temporal? Volvi tentalo dempués.
    • +
    • ¿Nun yes a restolar per otros sitios? Comprueba la conexón del preséu a la rede.
    • +
    • ¿El preséu ta protexíu por un tornafuéu o proxy? Una configuración incorreuta pue afeutar al restolar de la web.
    • +
    • ¿Sigues teniendo problemes? Consulta al to alministrador de redes o fornidor d\'internet pa consiguir más asistencia.
    • +
    ]]>
    + + + Nun ye posible conectase + + + +
  • Seique\'l sitiu tea temporalmente non disponible o perocupáu. Volvi tentalo nun momentu.
  • +
  • Si nun yes a cargar nenguna páxina, comprueba la conexón Wi-Fi o móvil del preséu.
  • + ]]>
    + + + Rempuesta inesperada del sirvidor + + El sitiu respondió d\'una forma inesperada a la solicitú de rede ya\'l restolador nun pue siguir.

    ]]>
    + + + La páxina nun ta redirixendo afayadizamente + + + 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.

    +
      +
    • ¿Desactivesti o bloquiesti les cookies riquíes por esti sitiu?
    • +
    • 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.
    • +
    ]]>
    + + + Mou ensin conexón + + El restolador ta trabayando nel mou ensin conexón y nun pue conectase al elementu solicitáu.

    +
      +
    • ¿El preséu ta conectáu a una rede activa?
    • +
    • Primi «Retentar» pa cambiar pal mou conexón y volver cargar la páxina.
    • +
    ]]>
    + + + Torgóse un puertu por seguranza + + + La direición solicitada especificaba un puertu (por exemplu, softastur.org:80 pal puertu 80 en softastur.org) que davezu tien otru propósitu distintu al de restolar la web. El restolador encaboxó la solicitú pa la to proteición y seguranza.

    ]]>
    + + + Reanicióse la conexón + + + Torgóse l\'enllaz a la rede mentanto se negociaba una conexón. Volvi probar.

    +
      +
    • Seique\'l sitiu tea temporalmente non disponible o perocupáu. Volvi tentalo nun momentu.
    • +
    • Si nun yes a cargar nenguna páxina, comprueba la conexón Wi-Fi o móvil del preséu.
    • +
    ]]>
    + + + El tipu de ficheru ye inseguru + + +
  • Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.
  • + ]]>
    + + + Error de conteníu toyíu + + Nun se pue amosar la páxina que tentes de ver porque se detectó un error na tresmisión de los datos.

    +
      +
    • Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.
    • +
    ]]>
    + + + Cascó\'l conteníu + + Nun se pue amosar la páxina que tentes de ver porque se detectó un error na tresmisión de los datos.

    +
      +
    • Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.
    • +
    ]]>
    + + + Error de la codificación del conteníu + + 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.

    +
      +
    • Ponte en contautu colos propietarios del sitiu web pa informalos d\'esti problema.
    • +
    ]]>
    + + + Nun s\'atopó la direición + + + El restolador nun pudo atopar l\'agospiador de la direición apurrida.

    +
      +
    • Comprueba que nun s\'introduxeren fallos al teclexar la direición: + ww.softastur.org en cuentes de + www.softastur.org.
    • +
    • Si nun yes a cargar nenguna páxina, comprueba la conexón Wi-Fi o móvil del preséu.
    • +
    ]]>
    + + + Nun hai conexón a internet + + + Comprueba la conexón a la rede o tenta de recargar la páxina nun momentu. + + + Recargar + + + La direición nun ye válida + La direición apurrida nun ta nun formatu reconocíu. Comprueba si hai fallos na barra de direiciones y volvi a tentalo.

    ]]>
    + + La direición nun ye válida + + + +
  • Les direiciones web suelen escribise asina https://www.softastur.org/
  • +
  • Asegúrate de que tas usando barres inclinaes a la derecha (ye dicir, /).
  • + ]]>
    + + + Desconozse\'l protocolu + + La direición especifica un protocolu (por exemplu, wxyz://) que\'l restolador nun reconoz, polo que nun se pue conectar afayadizamente al sitiu.

    +
      +
    • ¿Tas tentando d\'acceder a servicios multimedia o que nun son de testu? Comprueba\'l sitiu pa ver más requirimientos.
    • +
    • Dalgunos protocolos riquen software o plugins de terceros pa que\'l restolador pueda reconocelos.
    • +
    ]]>
    + + + Nun s\'atopó\'l ficheru + + +
  • ¿Pue ser que l\'elementu se renomare, quitare o moviere?
  • +
  • ¿La direición tien dalguna falta u otru error tipográficu?
  • +
  • ¿Tienes abondos permisos p\'acceder al elementu solicitáu?
  • + ]]>
    + + + Negóse l\'accesu al ficheru + + +
  • Seique se desaniciare, moviere o los permisos del ficheru eviten l\'accesu.
  • + ]]>
    + + + El sirvidor del proxy refugó la conexón + + El restolador ta configuráu pa usar un sirvidor proxy mas esti últimu refugó la conexón.

    +
      +
    • ¿La configuración del proxy del restolador ye correuta? Comprueba los axustes y volvi tentalo.
    • +
    • ¿El serviciu del proxy permite les conexones dende esta rede?
    • +
    • ¿Sigues teniendo problemes? Consulta al to alministrador de redes o fornidor d\'internet pa más asistencia.
    • +
    ]]>
    + + + Nun s\'atopó\'l sirvidor del proxy + + El restolador ta configuráu pa usar un sirvidor proxy mas nun se pudo atopar.

    +
      +
    • ¿La configuración del proxy del restolador ye correuta? Comprueba los axustes y volvi tentalo.
    • +
    • ¿El preséu ta conectáu a una rede activa?
    • +
    • ¿Sigues teniendo problemes? Consulta al to alministrador de redes o fornidor d\'internet pa más asistencia.
    • +
    ]]>
    + + + Problema de sitiu con malware + + Informóse que %1$s ye un sitiu atacador y bloquióse según les tos preferencies de seguranza.

    ]]>
    + + + Problema de sitiu indeseable + + Informóse que\'l sitiu de %1$s ta sirviendo software indeseable y bloquióse según les tos preferencies de seguranza.

    ]]>
    + + + Problema de sitiu peligrosu + + Informóse que\'l sitiu de %1$s ye potencialmente peligrosu y bloquióse según les tos preferencies de seguranza.

    ]]>
    + + + Problema de sitiu engañosu + + Informóse de que la páxina web de %1$s ye engañosa y bloquióse según les tos preferencies de seguranza.

    ]]>
    + + + El sitiu seguru nun ta disponible + + %1$s nun ta disponible.]]> + + Siguir col sitiu HTTP +
    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 @@ + + + + Təkrar Yoxla + + + İstək yerinə yetirilə bilmir + + Bu xəta və problem haqqında ətraflı məlumat mövcud deyil

    ]]>
    + + + Təhlükəsiz bağlantı qurula bilmədi + + +
  • Görmək istədiyiniz səhifə alınan məlumatlar təsdiqlənə bilmədi deyə göstərilə bilmir.
  • +
  • Lütfən sayt sahibləri ilə əlaqə saxlayın və bu problemdən xəbərdar edin.
  • + ]]>
    + + + Təhlükəsiz bağlantı qurula bilmədi + + +
  • 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.
  • +
  • Ə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.
  • + ]]>
    + + + Təkmilləşmiş… + + Geri get (Məsləhətlidir) + + Riski qəbul et və davam et + + + Bağlantı kəsildi + + + Şəbəkə gözləmə müddəti bitdi + + Açmaq istədiyiniz sayt cavab vermədiyi üçün gözləmə ləğv edildi.

    +
      +
    • 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.
    • +
    • Digər səhifələrdə açılmaya bilər. Əgər elədirsə bağlantınızı yoxlayın.
    • +
    • 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.
    • +
    • 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.
    • +
    ]]>
    + + + Əlaqə cəhdi uğursuz oldu + + + Serverdən gözlənilməz cavab + + + Səhifə düzgün yönləndirilmir + + + Oflayn rejim + + + Təhlükəsizlik səbəblərindən port məhdudlaşdırılıb + + + Əlaqə sıfırlandı + + + Etibarsız fayl növü + + + Zədəli məzmun xətası + + + Məzmun çökdü + + + Məzmun kodlama səhvi + + + Ünvan tapılmadı + + + İnternet bağlantısı yoxdur + + Şəbəkə əlaqənizi yoxlayın və ya səhifəni az sonra təkrar yeniləməyi yoxlayın. + + Yenilə + + + Xətalı ünvan + 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.

    ]]>
    + + Ünvan səhvdir + + + Naməlum protokol + + + Fayl tapılmadı + + + Faylın işlədilməsinə icazə verilmədi + + + Proxy server bağlantını rədd etdi + + + Proxy server tapılmadı + + + Ziyanverici sayt problemi + + + İstənməyən sayt problemi + + + Zərərli sayt problemi + + + Aldadıcı sayt problemi + +
    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 @@ + + + + یئنی‌دن چالیش + + + ایستک قورتارا بیلمیر + + + بو موشکول و خطا ایله ایلگیلی آرتیقراق بیلگی هله یوخدور.

    ]]>
    + + + گوونلی باغلانتی اوغورسوز اولدو + + + گوونلی باغلانتی اوغورسوز اولدو + + + قاباغجیل + + قاییت(توصیه اولونور) + + ریسکی قبول ائلییرک دوام ائدین + + + بو سایت گوونلی باغلانتی ایسته ییر. + + + قاباغجیل… + + + دالیا گئت + + + باغلانتی کسیلدی + + + باغلانتی‌نین زامان قیسیتی قوتولدی + + + باغلانانمیر + + + سروردن گودولمه‌ین جواب گلدی + + + صفحه دوزگون یؤنلندیریلمیر + + + آفلاین حالت + + + باغلانتی یئنی‌دن قورولدی + + + گوونسیز سند تیپی + + + کورلانمیش ایچریک خطاسی + + + ایچریک سیندی + + + ایچریک کدلاما خطاسی + + + آدرس تاپیلمادی + + + اینترنت باغلانتی‌سی یوخدور + + شبکه باغلانتیزی یوخلایین و یا آز سونرا صفحه‌نی یئنیله‌مه‌یی دئنه‌یین. + + یئنیله + + + گئچرسیز آدرس + + آدرس گئچرلی دئییل + + + تانینمایان پروتکل + + + ایستنمیه‌ن سایت سورونو + + + ضررلی سایت سورونو + + + آلداتان سایت سورونو + + + گوونلی سایت موجود دئییل + + HTTP سایتینا دوام ائت +
    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 @@ + + + + Coba malih + + + Pinunas Ragané Ten Prasida Puput + + Informasi tambahan indik pikobet puniki utawi galat sané mangkin durung kasedia.

    + ]]>
    + + + Konéksi Aman Gagal + + +
  • Kaca sané ragané coba cingak tan prasida katampilang mawinan kaaslian data sané katerima tan prasida kavérifikasi.
  • +
  • Durus hubungin sané ngadruénang situs wéb antuk nganikain indik pikobet puniki.
  • + + ]]>
    + + + Konéksi Aman Gagal + + +
  • Puniki minab pikobet sareng setélan peladén, utawi minab wénten sané nyoba nuutin peladén.
  • +
  • Pinaka ragané sampun maasil mahubung nuju peladén puniki sadurungné, galat minab abaan nyané ajebos, miwah ragané prasida nyoba malih nyanan.
  • + + ]]>
    + + + Lanturan… + + Minab wénten sané nyoba nyamar dados situs miwah ragané tan dados ngalanturang. +

    + + ]]>
    + + Mawali (Kanikayang) + + Tampi Résiko miwah Lanturang + + + Soroh Berkas Tan Aman + + + Tanpa konéksi internét + + Muat malih + + + Protokol Tan Kauningin + + + Berkas Tan Katemu + + + Aksés nuju berkas tan kalugra + + + Lanturang ka Situs HTTP +
    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 @@ + + + + + Паспрабаваць зноў + + + Немагчыма скончыць запыт + + Дадатковыя звесткі пра гэтую праблему або памылку зараз недаступны.

    + ·]]>
    + + + Няўдача бяспечнага злучэння + + + +
  • Старонка, якую вы спрабуеце адкрыць, не можа быць паказана, бо сапраўднасць атрыманых звестак нельга пацвердзіць.
  • +
  • Калі ласка, паведаміце ўладальніку сайта пра гэтую праблему.
  • + ]]>
    + + + Няўдача бяспечнага злучэння + + +
  • Гэта можа быць праблемай налад сервера ці, магчыма, +хтосьці спрабуе прыкінуцца гэтым серверам
  • +
  • Калі вы паспяхова злучаліся з гэтым серверам раней, памылка можа +быць часовай, таму вы можаце паспрабаваць зноў пазней.
  • + ]]>
    + + + Дадаткова… + + Хтось можа спрабаваць падмяніць гэты вэб-сайт. Вам лепш не працягваць. +

    + ]]>
    + + Вярнуцца (рэкамендуецца) + + Прыняць рызыку і працягнуць + + + Гэты вэб-сайт патрабуе бяспечнага злучэння. + + + +
  • Старонку, якую вы спрабуеце прагледзець, немагчыма паказаць, бо для гэтага вэб-сайта патрабуецца бяспечнае злучэнне.
  • +
  • Праблема, хутчэй за ўсё, звязана з вэб-сайтам, і вы нічога не можаце зрабіць, каб яе вырашыць.
  • +
  • Вы можаце паведаміць адміністратарам вэб-сайта аб праблеме.
  • + + ]]>
    + + + Дадаткова… + + + %1$s мае палітыку бяспекі, што называецца HTTP Strict Transport Security (HSTS), і гэта азначае, што %2$s можа звязвацца з ім толькі абароненым злучэннем. Вы не можаце дадаць выключэнне для наведвання гэтага сайта. + ]]> + + Вярнуцца + + + Злучэнне перарвана + + + Браўзер паспяхова падлучыўся, але злучэнне было перарвана падчас перадачы інфармацыі. Паспрабуйце, калі ласка, зноў.

    +
      +
    • Магчыма, сайт часова недаступны ці перагружаны запытамі. Пачакайце некаторы час і паспрабуйце зноў.
    • +
    • Калі вы не можаце загрузіць ніводную старонку, праверце злучэнне вашай прылады з мабільнай або Wi-Fi сеткай.
    • +
    ]]>
    + + + Час чакання злучэння выйшаў + + Запатрабаваны сайт не адказаў на запыт злучэння і браўзер скончыў чакаць адказ.

    +
      +
    • Ці не можа сервер быць перагружаным або часова спыненым? Паспрабуйце зноў пазней.
    • +
    • Вы не здольны аглядаць іншыя сайты? Праверце злучэнне прылады з сеткай.
    • +
    • Ваша прылада альбо сетка абараняецца фаерволам або проксі? Няправільныя налады могуць замінаць агляданню ў сеціве.
    • +
    • Дагэтуль маеце праблемы? Парайцеся з адмістратарам сеткі або дастаўшчыком паслугаў Інтэрнэту.
    • +
    ]]>
    + + + Нельга злучыцца + + +
  • Магчыма, сайт часова недаступны ці перагружаны запытамі. Пачакайце некаторы час і паспрабуйце зноў.
  • +
  • Калі вы не можаце загрузіць ніводную старонку, праверце злучэнне вашай прылады з мабільнай або Wi-Fi сеткай.
  • + ]]>
    + + + Нечаканы адказ сервера + + Сайт адказаў на сеткавы запыт нечаканым спосабам, таму браўзер не можа працягваць.

    ]]>
    + + + Старонка няправільна перанакіроўваецца + + Браўзер спыніў спробы атрымаць запатрабаваную адзінку. Сайт перанакіроўвае запыты да сябе спосабам, які ніколі не будзе завершаны.

    +
      +
    • Магчыма вы забаранілі або блакавалі кукі, якія патрэбны гэтаму сайту?
    • +
    • Калі разблакаванне кук сайта не вырашае праблему, дык гэта хутчэй праблема налад сервера, а не вашай прылады.
    • +
    ]]>
    + + + Пазасеткавы рэжым + + Браўзер працуе ў пазасеткавым рэжыме і не можа злучыцца з запатрабаванай адзінкай.

    +
      +
    • Прылада злучана з дзейнай сеткай?
    • +
    • Націсніце “Паспрабаваць зноў”, каб пераключыцца ў сеткавы рэжым і абнавіць старонку.
    • +
    ]]>
    + + + Порт абмежаваны дзеля бяспекі + + Запатрабаваны адрас прызначае порт (напрыклад, mozilla.org:80 – порт 80 на mozilla.org) , які звычайна ўжываецца ў іншых мэтах, а не для аглядання ў Сеціве. Браўзер скасаваў гэты запыт дзеля вашай аховы і бяспекі.

    ]]>
    + + + Злучэнне скінута + + + Сеткавае злучэнне абарвана падчас яго наладжвання. Паспрабуйце, калі ласка, зноў.

    +
      +
    • Магчыма, сайт часова недаступны ці перагружаны запытамі. Пачакайце некаторы час і паспрабуйце зноў.
    • +
    • Калі вы не можаце загрузіць ніводную старонку, праверце злучэнне вашай прылады з мабільнай або Wi-Fi сеткай.
    • +
    ]]>
    + + + Небяспечны тып файла + + +
  • Паведамце, калі ласка, уладальнікам вэб-сайта пра гэтую праблему.
  • + ]]>
    + + + Памылка пашкоджанага змесціва + + Старонка, якую вы спрабуеце адкрыць, не можа быць паказана, бо выяўлена памылка перадачы дадзеных.

    +
      +
    • Калі ласка, паведаміце ўладальніку сайта пра гэтую праблему.
    • +
    ]]>
    + + + Крах змесціва + Старонка, якую вы спрабуеце адкрыць, не можа быць паказана, бо выяўлена памылка перадачы дадзеных.

    +
      +
    • Калі ласка, паведаміце ўладальніку сайта пра гэтую праблему.
    • +
    ]]>
    + + + Памылка кадавання змесціва + Старонку, якую вы спрабуеце пабачыць, немагчыма паказаць, бо яна выкарыстоўвае недапушчальную або непадтрымальную форму сціскання.

    +
      +
    • Паведамце, калі ласка, уладальнікам вэб-сайта пра гэтую праблему.
    • +
    ]]>
    + + + Адрас не знойдзены + + Браўзер не змог знайсці хост-сервер па ўказаным адрасе.

    +
      +
    • Праверце адрас на памылкі ўводу такія як + ww.example.com замест + www.example.com.
    • +
    • Калі вы не можаце загрузіць ніводную старонку, праверце злучэнне вашай прылады з мабільнай або Wi-Fi сеткай.
    • +
    ]]>
    + + + Няма інтэрнэт-злучэння + + Праверце падлучэнне да сеткі або паспрабуйце перазагрузіць старонку праз некаторы час. + + Перачытаць + + + Несапраўдны адрас + Фармат дадзенага адраса не апазнаны. Праверце, калі ласка, ці няма памылак на паліцы месцазнаходжання і паспрабуйце зноў.

    ]]>
    + + Несапраўдны адрас + + +
  • Адрас Сеціва звычайна пішацца, як http://www.example.com/
  • +
  • Упэўніцеся, што вы ўжываеце простыя косыя рыскі (г.зн. /).
  • + ]]>
    + + + Невядомы пратакол + + Адрас вызначае пратакол (напрыклад: wxyz://), які не распазнаецца браўзерам, таму браўзер не можа злучыцца з сайтам належным чынам.

    +
      +
    • Вы спрабуеце даступіцца да мультымедыйных або нетэкставых службаў? Праверце, ці існуюць на сайце дадатковыя патрабаванні.
    • +
    • Асобныя пратаколы могуць патрабаваць пабочныя праграмы або плагіны, каб браўзер мог распазнаваць іх.
    • +
    ]]>
    + + + Файл не знойдзены + +
  • Ці не была адзінка перайменавана, выдалена або перамешчана?
  • +
  • Можа існуе нейкая памылка ў адрасе, як прапушчаная/лішняя літара або вялікая літара замест малой, ці інакшая?
  • +
  • Вы маеце дастатковыя дазволы для доступу да запатрабаванай адзінкі?
  • + ]]>
    + + + Доступ да файла забаронены + +
  • Магчыма, што ён быў выдалены або перамешчаны, або дазволы на файл не даюць атрымаць да яго доступ.
  • + ]]>
    + + + Проксі-сервер адмовіўся злучацца + + Браўзер наладжаны карыстацца проксі-серверам, але проксі адхіліў злучэнне.

    +
      +
    • Ці налады проксі браўзера правільныя? Праверце налады і паспрабуйце зноў.
    • +
    • Ці дазваляе проксі-сервер падлучэнні з гэтай сеткі?
    • +
    • Дагэтуль маеце праблемы? Парайцеся з адміністратарам сеткі або пастаўшчыком паслуг Інтэрнэту.
    • +
    ]]>
    + + + Проксі-сервер не знойдзены + + Браўзер наладжаны карыстацца проксі-серверам, але проксі не знойдзены.

    +
      +
    • Ці налады проксі браўзера правільныя? Праверце налады і паспрабуйце зноў.
    • +
    • Ці падключана прылада да дзейнай сеткі?
    • +
    • Дагэтуль маеце праблемы? Парайцеся з адміністратарам сеткі або пастаўшчыком паслуг Інтэрнэту.
    • +
    ]]>
    + + + Шкоднасны сайт + + Сайт %1$s вядомы як нападнік і заблакаваны згодна з вашымі наладамі бяспекі.

    ]]>
    + + + Непажаданы сайт + + + Сайт па адрасе %1$s вядомы як пляцоўка для непажаданых праграм, заблакаваны ў адпаведнасці з вашымі наладамі бяспекі.

    ]]>
    + + + Шкоднасны сайт + + + Сайт па адрасе %1$s вядомы як патэнцыйна шкодны, заблакаваны ў адпаведнасці з вашымі наладамі бяспекі.

    ]]>
    + + + Падроблены сайт + + Старонка сеціва па адрасе %1$s вядома як падманлівы сайт, заблакавана ў адпаведнасці з вашымі наладамі бяспекі.

    ]]>
    + + + Бяспечны сайт недаступны + + %1$s недаступная.]]> + + Перайсці на HTTP-сайт +
    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 @@ + + + + + Повторен опит + + + Заявката не може да бъде завършена + + Допълнителна информация за този проблем или грешка в момента не е налична.

    ]]>
    + + + Неуспешно установяване на шифрована връзка + +
  • Страницата не може да бъде показана, защото достоверността на получените данни не може да бъде проверена.
  • Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.
  • ]]>
    + + + Неуспешно установяване на шифрована връзка + +
  • Това е или проблем с настройките на сървъра, или някой се опитва се представи за него.
  • Ако и преди сте се свързвали успешно с него, има вероятност грешката да е временна и може да опитате по-късно.
  • ]]>
    + + + Разширени… + + Някой може би се опитва да подмени истинската страница и по-добре да не продължавате. +

    +]]>
    + + Връщане назад (препоръчително) + + Продължаване въпреки риска + + + Страницата изисква защитена връзка. + + + +
  • Страницата не може да бъде показана, защото изисква защитена връзка.
  • +
  • Проблемът най-вероятно е в страницата и нищо не може да направите.
  • +
  • Може да уведомите администратора на страницата за него.
  • + + ]]>
    + + + Разширени… + + + %1$s използва политика за сигурност наречена HTTP Strict Transport Security (HSTS), което означава, че %2$s може да използва само сигурни връзки. Не може да добавяте изключение при посещение на тази страница.]]> + + Назад + + + Връзката е прекъсната + + + Четецът осъществи връзка, но тя е прекъсната по време на прехвърляне на информация. Опитайте отново.

    +
      +
    • Страницата може да е временно недостъпна или натоварена. Опитайте отново след малко.
    • +
    • Ако и други страници не се отварят - проверете връзката за данни или Wi-Fi.
    • +
    ]]>
    + + + Времето за изчакване на връзка изтече + + Страницата не отговаря на заявките за свързване и четецът спря да чака отговор.

    • Дали сървърът не е претоварен, или изключен? Опитайте отново по-късно.
    • Можете ли да разглеждате други страници? Проверете връзката си с интернет.
    • Устройството намира ли се зад защитна стена, или мрежов посредник? Неправилните настройки могат да попречат на разглеждането.
    • Ако все още има проблем, обърнете се за помощ към мрежовия администратор или доставчика си на интернет.
    ]]>
    + + + Не е установена връзка + + +
  • Страницата може да е временно недостъпна или натоварена. Опитайте отново след малко.
  • +
  • Ако и други страници не се отварят - проверете връзката за данни или Wi-Fi.
  • +]]>
    + + + Неочакван отговор от сървъра + + Страницата отговори на мрежово запитване по неочакван начин и четецът не може да продължи.

    ]]>
    + + + Страницата не пренасочва правилно + + + Четецът спря опитите за зареждане. Страницата препраща заявката по начин, който никога не завършва.

    • Да не би да сте изключили или спрели бисквитките на страницата?
    • Ако разрешаването на бисквитките не разреши проблема, вероятно става въпрос за грешка в настройките на сървъра, а не във вашето устройство.
    ]]>
    + + + Работа извън мрежата + + Четецът е настроен да работи в този режим и затова не може да се свърже.

    • Работи ли мрежата към която е свързано устройството?
    • Изберете „Повторен опит“ за свързване към мрежата и презареждане на страницата.
    ]]>
    + + + Достъп до порта ограничен от съображения за сигурност + + + Търсеният адрес съдържа порт (напр., mozilla.org:80 за порт 80 на mozilla.org), който обикновено се използва за цели, различни от разглеждане. От съображения за сигурност мрежовият четец прекъсна заявката.

    ]]>
    + + + Връзката е прекъсната + + + Връзка е прекъсната по време на свързване. Моля, опитайте отново.

    +
      +
    • Страницата може да е временно недостъпна или натоварена. Опитайте отново след малко.
    • +
    • Ако и други страници не се отварят - проверете връзката за данни или Wi-Fi.
    • +
    ]]>
    + + + Опасен вид файл + +
  • Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.
  • ]]>
    + + + Грешка поради повредено съдържание + + Страницата не може да бъде показана поради грешка при прехвърляне на данните.

    • Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.
    ]]>
    + + + Съдържанието се срина + Страницата не може да бъде показана поради грешка при прехвърляне на данните.

    • Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.
    ]]>
    + + + Грешка в кодировката на съдържанието + Страницата не може да бъде показана, защото използва невалиден или неподдържан вид компресия.

    • Моля, свържете се със собствениците на сайта, за да ги информирате за проблема.
    ]]>
    + + + Адресът не е намерен + + + Четецът не намира сървър на този адрес.

    +
      +
    • Проверете го за грешки при въвеждане, например ww.example.com вместо www.example.com.
    • +
    • Ако и други страници не се отварят - проверете връзката за данни или Wi-Fi.
    • +
    ]]>
    + + + Няма връзка с интернет + + Проверете мрежовата си свързаност или опитайте да презаредите страницата след малко. + + Презареждане + + + Недействителен адрес + Адресът е в неразпознаваема форма. Моля, проверете адресната лента за грешки и опитайте отново.

    ]]>
    + + Адресът е недействителен + + +
  • Адресите обикновено изглеждат така http://www.example.com/
  • +
  • Уверете се, че използвате прави наклонени черти (т.е. /).
  • +]]>
    + + + Неизвестен протокол + Адресът използва протокол (напр. wxyz://), който мрежовият четец не разпознава, така че не може да се установи връзка със страницата.

    • Опитвате ли да се свържете с мултимедиен ресурс или друга нетекстова услуга? Проверете страницата за допълнителни изисквания.
    • Някои протоколи изискват софтуер или приставка от трета страна.
    ]]>
    + + + Файлът не е намерен + +
  • Дали ресурсът не е преименуван, премахнат, или преместен?
  • +
  • Има ли грешка в правописа, регистъра на буквите, или друга техническа грешка?
  • +
  • Имате ли права за достъп до желания ресурс?
  • +]]>
    + + + Отказан достъп до файла + +
  • Може да е премахнат, преместен или правата му да ограничават достъпа.
  • +]]>
    + + + Мрежовият посредник отказа свързване + Четецът е настроен да използва мрежов посредник, но той отказва връзката.

    • Правилно ли е настроен? Проверете настройките и опитайте отново.
    • Мрежовият посредник разрешава ли свързване от вашата мрежа?
    • Ако все още има проблем, се обърнете за помощ към мрежовия администратор или своя доставчик на интернет.
    ]]>
    + + + Мрежовият посредник не е намерен + Четецът е настроен да използва мрежов посредник, но той не може да бъде намерен.

    • Правилно ли е настроен? Проверете настройките и опитайте отново.
    • Устройството свързано ли е към работеща мрежа?
    • Ако все още има проблем, се обърнете за помощ към мрежовия администратор или своя доставчик на интернет.
    ]]>
    + + + Враждебна страница + + Страницата %1$s е докладвана като враждебна и е блокирана спрямо вашите настройки за безопасност.

    ]]>
    + + + Нежелана страница + + Страницата %1$s е докладвана за сервиране на нежелан софтуер и е блокирана спрямо вашите настройки за безопасност.

    ]]>
    + + + Зловредна страница + + Страницата %1$s е докладвана като потенциално зловредна и е блокирана спрямо вашите настройки за безопасност.

    ]]>
    + + + Измамническа страница + + Страницата %1$s е докладвана като измамническа и е блокирана спрямо вашите настройки за безопасност.

    ]]>
    + + + Сигурната версия на сайта не е налична + + %1$s през HTTPS не е налична.]]> + + Продължаване с HTTP +
    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 @@ + + + + + আবার চেষ্টা করুন + + + অনুরোধ পূরণ করা সম্ভব নয় + + + এই সমস্যা বা ত্রুটি সম্পর্কে অতিরিক্ত তথ্য বর্তমানে নেই।

    ]]>
    + + + নিরাপদ সংযোগ স্থাপনে ব্যর্থ হয়েছে + + + +
  • আপনি যে পাতাটি দেখার চেষ্টা করছেন সেটি প্রদর্শিত হবে না কারণ প্রাপ্ত ডাটার সত্যতা যাচাই করা যায়নি।
  • +
  • এই সমস্যা সম্পর্কে তাদের অবহিত করতে ওয়েবসাইটের মালিকদের সাথে যোগাযোগ করুন।
  • + ]]>
    + + + নিরাপদ সংযোগ স্থাপনে ব্যর্থ হয়েছে + + + +
  • সম্ভবত সার্ভারের কনফিগারেশনে সমস্যা হয়েছে অথবা কোনো ব্যক্তি এই সার্ভারের পরিচয় জাল করার চেষ্টা করছে।
  • +
  • আপনি যদি আগে এই সার্ভারের সাথে সফলভাবে সংযোগ স্থাপন করে থাকেন, তাহলে সম্ভবত কোনো অস্থায়ী কারণে এই সমস্যা দেখা দিয়েছে এবং কিছুক্ষণ পর আবার চেষ্টা করুন।
  • + + ]]>
    + + + উন্নতপর্যায়ের… + + কেউ সাইটটি ছদ্মবেশ তৈরি করার চেষ্টা করছেন এবং আপনার চালিয়ে যাওয়া উচিত নয় +

    + + ]]>
    + + ফিরে যান (প্রস্তাবিত) + + ঝুঁকি নিন এবং চালিয়ে যান + + + সংযোগ বিঘ্নিত হয়েছে + + + সংযোগের সময়সীমা উত্তীর্ণ হয়ে গেছে + + + সংযোগ স্থাপন করতে ব্যর্থ + + + সার্ভার থেকে অপ্রত্যাশিত প্রতিক্রিয়া দেখাচ্ছে + + + সাইটটি একটি নেটওয়ার্ক অনুরোধের অপ্রত্যাশিত উত্তর দিয়েছে যার ফলে ব্রাউজার এটি চালিয়ে যেতে পারছে না।

    + ]]>
    + + + পাতাটি সঠিকভাবে পুনঃনির্দেশনা দিচ্ছে না + + + অফলাইন মোড + + + নিরাপত্তাজনিত কারণে পোর্টটি সীমাবদ্ধ করা হয়েছে + + + সংযোগটি পুনস্থাপিত করা হয়েছে + + + অনিরাপদ শ্রেণীর ফাইল + + + +
  • অনুগ্রহ করে এই সমস্যা সম্পর্কে ওয়েব সাইট নির্মাতাদের অবগত করুন।
  • + ]]>
    + + + ক্ষতিগ্রস্ত কন্টেন্টের ত্রুটি + + + কন্টেন্ট ক্র্যাশ করেছে + + + কন্টেন্টের এনকোডিং-এ ত্রুটি + + + ঠিকানা পাওয়া যায়নি + + + কোনো ইন্টারনেট সংযোগ নেই + + + আপনার নেটওয়ার্কের সংযোগটি পরীক্ষা করুন বা কিছুক্ষণ পর পাতাটি আবার লোড করার চেষ্টা করুন। + + পুনরায় লোড করুন + + + অকার্যকর ঠিকানা + + ঠিকানাটি কার্যকর নয় + + + অপরিচিত প্রোটোকল + + + ফাইল পাওয়া যায়নি + + + ফাইলে প্রবেশাধিকার প্রত্যাখ্যাত হয়েছে + + + প্রক্সি সার্ভার সংযোগ প্রত্যাখ্যান করেছে + + + প্রক্সি সার্ভার পাওয়া যায়নি + + + ম্যালওয়্যার সাইটের সমস্যা + + + অযাচিত সাইট সমস্যা + + + ক্ষতিকারক সাইটের সমস্যা + + + বিভ্রান্তিকর সাইটের সমস্যা + +
    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 @@ + + + + + Klask en-dro + + + Nʼhaller ket echuiñ an azgoulenn-mañ + + Titouroù ouzhpenn diwar-benn ar gudenn-mañ pe ar fazi-mañ nʼint ket hegerz evit poent.

    + ]]>
    + + + Cʼhwitadenn war ar cʼhennaskañ diarvar + + +
  • Nʼhall ket bezañ diskouezet ar bajennad emaocʼh o klask gwelout rak nʼhall ket bezañ gwiriet dilested ar roadennoù bet degemeret.
  • +
  • Kit e darempred gant percʼhenned al lecʼhienn evit kas keloù dezho a-zivout ar gudenn-mañ.
  • + + ]]>
    + + + Cʼhwitadenn war ar cʼhennaskañ diarvar + + + +
  • Marteze e vefe ur gudenn gant kefluniadur an dafariad, pe unan bennak o klask dreveziñ an dafariad.
  • +
  • Mard ocʼh bet kennasket ouzh an dafariad gant berzh a-raok na vefe eus ur fazi padennek moarvat, klaskit diwezhatocʼh.
  • + + ]]>
    + + + Kempleshocʼh… + + Gallout a rafe bezañ unan bennak o klask en em lakaat e plas al lecʼhienn ha ne rankfecʼh ket kendercʼhel ganti. +

    + + ]]>
    + + Distreiñ (Erbedet) + + Asantiñ ar riskl ha kendercʼhel + + + Ur c’hennask diogel a zo goulennet evit al lec’hienn-mañ. + + +
  • N’haller ket skrammañ ar bajenn a glaskit gwelet dre ma vez goulennet ur c’hennask diogel gant al lec’hienn.
  • +
  • Ar gudenn a zo gant al lec’hienn moarvat, ha n’eus netra a c’hallit ober.
  • +
  • Gallout a rit kemenn ardoer al lec’hienn ez eus ur gudenn.
  • + +]]>
    + + + Kempleshoc’h… + + + %1$s a zo gantañ ur politikerezh surentez anvet HTTP Strict Transport Security (HSTS), pezh a dalv ne c’hall %2$s kevreañ outañ nemet en un doare sur. Ne c’hallit ket ouzhpennañ un nemedenn evit gweladenniñ al lec’hienn-mañ. + ]]> + + Distreiñ + + + Harzet eo bet treuzkas ar roadennoù + + + Kennasket eo ar merdeer gant berzh, met harzhet eo bet ar c’hennask pa oa o treuzkas titouroù. Klaskit en-dro mar plij.

    +
      +
    • Gallout a rafe al lec’hienn-mañ bezañ sac’het pe ac’hubet. Klaskit en-dro benn nebeut.
    • +
    • Ma n’hoc’h ket gouest da gargañ pajenn ebet, gwiriekait ho kennask Wi-Fi pe roadennoù ho trevnad.
    • +
    + ]]>
    + + + Diamzeret eo ar cʼhennask + + + Nʼhe deus ket respontet al lecʼhienn azgoulennet dʼan azgoulenn kennaskañ ha ne cʼhortoz ket mui ar merdeer evit ar respont.

    +
      +
    • Marteze eo soulgarget pe sacʼhet an dafariad? Klaskit diwezhatocʼh.
    • +
    • Nʼocʼh ket evit gweladenniñ lecʼhiennoù all? Gwiriañ ar cʼhennaskañ ouzh ar rouedad.
    • +
    • Ha gwarezet eo hocʼh urzhiataer gant un tanvoger pe ur proksi? Arventennoù fall a cʼhallfe gwallemellout gant ho merdeiñ.
    • +
    • Trubuilhoù cʼhoazh? Kit e darempred gant ardoer ho reizhiad pe ho pourchaser internet evit kaout skoazell.
    • +
    + ]]>
    + + + Nʼhaller ket kennaskañ + + + +
  • Al lecʼhienn a cʼhallfe bezañ dihegerz pe acʼhubet betek re e vefe evit poent. Klaskit adarre bremaik.
  • +
  • Ma nʼhocʼh ket evit kargañ pajennad ebet, gwiriit kennask ar roadennoù pe Wi-Fi ho trevnad.
  • + + ]]>
    + + + Respont dicʼhortoz a-berzh an dafariad + + + 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.

    + ]]>
    + + + Adheñchañ ar bajenn nʼeo ket dereat + + + 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.

    +
      +
    • Ha diweredekaet pe cʼhennet hocʼh eus an toupinoù azgoulennet gant al lecʼhienn-mañ?
    • +
    • 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.
    • +
    + ]]>
    + + + Mod ezlinenn + + + Emañ ar merdeer o labourat gant e vod ezlinenn ha nʼhall ket kennaskañ ouzh an ergorenn bet goulennet.

    +
      +
    • Ha kennasket eo an trevnad ouzh ur rouedad oberiant?
    • +
    • Pouezit war "Klask en-dro" evit trecʼhaoliñ etrezek ar mod enlinenn hag adkargañ ar bajennad.
    • +
    + ]]>
    + + + Dindan strishadurioù emañ ar porzh-mañ + + + Gant ar chomlecʼh goulennet ez eus bet erspizet ur porzh (d.l.e. mozilla.org:80 evit ar porzh 80 war mozilla.org) arveret dre voaz evit palioù all eget ar merdeiñ war internet. Dilezet eo bet an azgoulenn gant ar merdeer evit ho kwarez hag ho tiogelroez.

    + ]]>
    + + + Ehanet eo bet ar cʼhennaskañ + + + Harzet eo bet al liamm rouedad en ur glask krouiñ ur cʼhennask. Klaskit en-dro mar plij.

    +
      +
    • Gallout a rafe al lecʼhienn-mañ bezañ sacʼhet pe acʼhubet. Klaskit en-dro benn nebeut.
    • +
    • Ma nʼhocʼh ket gouest da gargañ pajenn ebet, gwiriekait ho kennask Wi-Fi pe roadennoù.
    • +
    + ]]>
    + + + Rizh restr arvarus + + + +
  • Kit e darempred gant percʼhennerien al lecʼhienn evit kas keloù dezho a-zivout ar gudenn-mañ.
  • + + ]]>
    + + + Fazi a-fet endalcʼhad bet kontronet + + + Ar bajenn emaocʼh o klask gweladenniñ nʼhall ket bezañ skrammet rak degouezhet ez eus bet ur fazi e-pad treuzkas ar roadennoù.

    +
      +
    • Mar plij, kit e darempred gant percʼhenned al lecʼhienn a-benn kelaouiñ anezho eus ar gudenn-mañ.
    • +
    + ]]>
    + + + Sacʼhet eo an endalcʼhad + Ar bajenn emaocʼh o klask gweladenniñ nʼhall ket bezañ skrammet rak degouezhet ez eus bet ur fazi e-pad treuzkas ar roadennoù.

    +
      +
    • Mar plij, kit e darempred gant percʼhenned al lecʼhienn a-benn kelaouiñ anezho eus ar gudenn-mañ.
    • +
    + ]]>
    + + + Fazi enrinegañ an endalcʼhad + Ar bajennad emaocʼh o klask gwelout nʼhall ket bezañ diskouezet rak un doare koazhañ anskor pe didalvoudek zo arveret ganti.

    +
      +
    • Kit e darempred gant percʼhenned al lecʼhienn evit kas keloù dezho a-zivout ar gudenn-mañ.
    • +
    + ]]>
    + + + Nʼeo ket bet kavet ar chomlecʼh + + + Nʼeo ket ar merdeer evit kavout an dafariad ostiz evit ar chomlecʼh roet.

    +
      +
    • Gwiriekait ar chomlecʼh evit fazioù biziata evel + ww.skouer.bzh e plas + www.skouer.bzh.
    • +
    • Ma nʼhocʼh ket gouest da gargañ pajennoù, gwiriekait kennask roadennoù pe Wi-Fi ho trevnad.
    • +
    + ]]>
    + + + Kennask internet ebet + + Gwiriekait ho kennask ouzh ar rouedad pe klaskit adkargañ ar bajenn a-benn nebeud. + + Adkargañ + + + Nʼeo ket talvoudek ar chomlecʼh + Nʼeo ket anavezet mentrezh ar chomlecʼh pourvezet. Gwiriit barrenn al lecʼhiadur evit kavout ur fazi ha klaskit en-dro.

    + ]]>
    + + Nʼeo ket talvoudek ar chomlecʼh + + + +
  • Peurvuiañ e vez skrivet ar chomlecʼhioù web evel-mañ http://www.example.com/
  • +
  • Gwiriekait e vez implijet barennoù stouet ganeocʼh (d.l.e /).
  • + + ]]>
    + + + Komenad dianav + + Ur cʼhomenad (d.l.e. wxyz://) 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.

    +
      +
    • Hag emaocʼh o klask tizhout liesvedia pe gwazerezhioù andestenn? Gwiriit war al lecʼhienn mar bez ezhomm paramantadurioù all.
    • +
    • Goulennet e vez meziantoù ouzhpenn pe enlugelladoù gant komenadoù zo a-raok ma vo gouest ar merdeer dʼo anavezout.
    • +
    + ]]>
    + + + Restr dianav + + +
  • Gwiriañ ha dilecʼhiet eo bet, adanvet pe dilamet an ergorenn?
  • +
  • Gwiriañ anv ar chomlecʼh rak marteze ez eus fazioù pennlizherennoù pe fazioù skrivañ all?
  • +
  • Hag an aotreoù hocʼh eus evit haeziñ an ergorenn goulennet?
  • + + ]]>
    + + + Nacʼhet eo bet haeziñ dʼar restr + + +
  • Gallout a ra bezañ dilamet, dilecʼhiet, pe nʼeus ket ar gwirioù a-zere evit an haeziñ.
  • + + ]]>
    + + + Dafariad ar proksi en deus nacʼhet ar cʼhennaskañ + + Kefluniet eo ar merdeer evit ober gant un dafariad proksi, met nacʼhet eo bet ar cʼhennaskañ gant ar proksi.

    +
      +
    • Ha kefluniet mat eo proksi ar merdeer? Gwiriit an arventennoù ha klaskit en-dro.
    • +
    • Ha gwazerezh ar proksi a aotre kennaskadurioù diouzh ar rouedad-mañ?
    • +
    • Trubuilhoù cʼhoazh? Kit e darempred gant ardoer ar rouedad pe ho pourchaser internet a-benn kaout skoazell.
    • +
    + ]]>
    + + + Nʼeo ket bet kavet an dafariad proksi + + Kefluniet eo ar merdeer evit ober gant un dafariad proksi, met nʼeo ket bet kavet ar proksi.

    +
      +
    • Ha kefluniet mat eo proksi ar merdeer? Gwiriit an arventennoù ha klaskit en-dro.
    • +
    • Ha kennasket eo an trevnad ouzh ur rouedad oberiant?
    • +
    • Trubuilhoù cʼhoazh? Kit e darempred gant ardoer ar rouedad pe ho pourchaser internet a-benn kaout skoazell.
    • +
    + ]]>
    + + + Kudenn lecʼhienn dagus + + Al lecʼhienn e %1$s zo bet marilhet evel ul lecʼhienn dagus ha harzet eo bet gant ho kwellvezioù diogelroez.

    + ]]>
    + + + Kudenn lecʼhienn dicʼhoantaet + + 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.

    + ]]>
    + + + Fazi lecʼhienn noazus + + + 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.

    + ]]>
    + + + Fazi lecʼhienn douellus + + Al lecʼhienn e %1$s zo bet marilhet evel ul lecʼhienn douellus ha harzet eo bet gant ho kwellvezioù diogelroez.

    + ]]>
    + + + Lec’hienn diogel dihegerz + + %1$s .]]> + + Kenderc’hel etrezek al lec’hienn HTTP +
    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 @@ + + + + Pokušaj ponovo + + + Nije moguće dovršiti zahtjev + + Dodatne informacije o ovom problemu ili grešci trenutno nisu dostupne.

    ]]>
    + + + Sigurna veza nije uspjela + + +
  • Stranicu kojoj pokušavaš pristupiti nije moguće prikazati jer nije moguće provjeriti autentičnost primljenih podataka.
  • +
  • Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.
  • + ]]>
    + + + Sigurna veza nije uspjela + + +
  • Možda se radi o problemu s postavkama na serveru ili možda neko pokušava oponašati ovaj server.
  • +
  • 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.
  • + ]]>
    + + + Napredno… + + Netko možda pokušava lažno predstavljati web stranicu, stoga je bolje da ne nastaviš. +        
    +        ]]>
    + + Idi nazad (preporučeno) + + Prihvati rizik i nastavi + + + Ova web stranica zahtijeva sigurnu vezu. + + +
  • Stranica koju pokušavate pogledati se ne može prikazati jer ova web stranica zahtijeva sigurnu vezu.
  • +
  • Problem je najvjerovatnije na web stranici i ne možete ništa učiniti da ga riješite.
  • +
  • Možete obavijestiti administratora web stranice o problemu.
  • + + ]]>
    + + + Napredno… + + + %1$s ima sigurnosnu politiku koja se zove HTTP Strict Transport Security (HSTS), što znači da se %2$s može samo sigurno povezati na njega. Ne možete dodati izuzetak da posjetite ovu stranicu. + ]]> + + Idi nazad + + + Veza je prekinuta + + Browser se uspješno povezao, ali veza je prekinuta tokom prenosa informacija. Pokušaj ponovo.

    +      
      +        
    • Stranica je možda privremeno nedostupna ili prezauzeta. Pokušaj ponovo za nekoliko trenutaka.
    • +        
    • Ako ne možeš učitati nijednu stranicu, provjeri podatke tvog uređaja ili Wi-Fi vezu.
    • +      
    ]]>
    + + + Vezi je isteklo vrijeme + + Zatražena stranica nije odgovorila na zahtjev i browser je prestao čekati na odgovor.

    +
      +
    • Možda je server opterećen velikom količinom zahtjeva ili je privremeno ostao bez napajanja? Pokušaj ponovo kasnije.
    • +
    • Možeš li pregledavati ostale stranice? Provjeri mrežnu vezu svog računara.
    • +
    • Jesu li tvoje računalo ili mreža zaštićeni vatrozidom ili proxyjem? Neispravne postavke mogu prouzročiti probleme prilikom pregledavanja weba.
    • +
    • Ukoliko još uvijek imaš probleme, za pomoć se obrati administratoru mreže ili serveru internetske usluge.
    • +
    ]]>
    + + + Povezivanje nije moguće + + +
  • Stranica je možda privremeno nedostupna ili preopterećena. Pokušaj ponovo malo kasnije.
  • +
  • Ako ne možeš učitati niti jednu stranicu, provjeri podatke svog uređaja ili Wi-Fi vezu.
  • + ]]>
    + + + Neočekivani odgovor od servera + + Stranica je na mrežni zahtjev odgovorila na neočekivani način, zbog čega preglednik ne može nastaviti.

    ]]>
    + + + Stranica ne preusmjerava ispravno + + Preglednik je prestao dohvaćati zatražene stranice. Stranica preusmjerava zahtjev na takav način, da se on nikada ne može ispuniti.

    +
      +
    • Jesu li kolačići za ovu stranicu deaktivirani ili blokirani?
    • +
    • 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.
    • +
    ]]>
    + + + Izvanmrežni način rada + + Browser je u izvanmrežnom načinu rada i ne može se spojiti na traženu stavku.

    +
      +
    • Je li uređaj spojen na aktivnu mrežu?
    • +
    • Klikni na „Pokušaj ponovo” za prebacivanje na mrežni način rada i ponovo učitaj stranicu.
    • +
    ]]>
    + + + Priključak je iz sigurnosnih razloga ograničen + + + Zatražena adresa ima definiran priključak (npr. mozilla.org:80 za priključak 80 na mozilla.org) koji se inače koristi za druge radnje, a ne za pregledavanje weba. Browser je prekinuo zahtjev radi tvoje zaštite i sigurnosti.

    ]]>
    + + + Veza je resetovana + + Mrežna veza je prekinuta tijekom povezivanja. Pokušaj ponovo.

    +      
      +        
    • Stranica je možda privremeno nedostupna ili prezauzeta. Pokušaj ponovo za nekoliko trenutaka.
    • +        
    • Ako ne možeš učitati nijednu stranicu, provjeri podatke tvog uređaja ili Wi-Fi vezu.
    • +      
    ]]>
    + + + Nesigurna vrsta datoteke + + +
  • Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.
  • + ]]>
    + + + Greška oštećenog sadržaja + + + Stranicu kojoj pokušavaš pristupiti nije moguće prikazati zbog greške u prijenosu podataka.

    +
      +
    • Obavijesti vlasnike web stranice o ovom problemu.
    • +
    ]]>
    + + + Greška u sadržaju + Stranicu kojoj pokušavaš pristupiti nije moguće prikazati zbog greške u prenosu podataka.

    +
      +
    • Obavijesti vlasnike web stranice o ovom problemu.
    • +
    ]]>
    + + + Greška u enkodiranju sadržaja + Stranica koju pokušavaš vidjeti ne može biti prikazana jer koristi neispravni ili nepodržani oblik komprimiranja.

    +
      +
    • Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.
    • +
    ]]>
    + + + Adresa nije pronađena + + Browser nije mogao pronaći server domaćina za navedenu adresu.

    +      
      +        
    • Pazi da nemaš greške u tipkanju, kao što su +          ww.primjer.ba umjesto +          www.primjer.ba.
    • +
    • Ako ne možeš učitati nijednu stranicu, provjeri podatke svog uređaja ili Wi-Fi vezu.
    • +
    ]]>
    + + + Nema internet konekcije + + Provjeri mrežnu vezu ili pokušaj ponovo učitati stranicu za nekoliko trenutaka. + + Učitaj ponovo + + + Neispravna adresa + Navedena adresa nije u poznatrom formatu. Molimo provjerite lokaciju sa greškama i pokušajte ponovo.

    ]]>
    + + Adresa je nevažeća + + +
  • Adrese web stranice se obično pišu u formatu poput http://www.example.com/
  • +
  • Pazi na način pisanja kose crte (tj. /).
  • + ]]>
    + + + Nepoznat protokol + + Adresa navodi protokol (npr., wxyz://) koji browser ne prepoznaje, pa se browser ne može pravilno povezati na stranicu.

    +
      +
    • Da li pokušavate pristupiti multimediji ili drugim netekstualnim servisima? Provjerite stranicu za dodatne zahtjeve.
    • +
    • Neki protokoli mogu zahtijevati softver trećeg lica ili plugine prije nego ih browser može prepoznati.
    • +
    ]]>
    + + + Fajl nije pronađen + + +
  • Da li je stavka preimenovana, uklonjena ili premještena?
  • +
  • Da li postoji pravopisna ili neka druga greška u adresi?
  • +
  • Da li imate potrebne dozvole za pristup zatraženoj stavci?
  • + ]]>
    + + + Pristup fajlu je odbijen + + +
  • Možda je uklonjen, premješten ili vam dozvole za fajl onemogućuju pristup.
  • + ]]>
    + + + Proxy server je odbio povezivanje + + Browser je konfigurisan da koristi proxy server, ali je proxy odbio povezivanje.

    +
      +
    • Da li je proxy konfiguracija browsera ispravna? Provjerite postavke i pokušajte ponovo.
    • +
    • Da li proxy usluga dozvoljava povezivanje sa ove mreže?
    • +
    • I dalje imate problem? Posavjetujte se sa vašim mrežnim administratorom ili Internet provajderom.
    • +
    ]]>
    + + + Proxy server nije pronađen + + Browser je konfigurisan da koristi proxy server, ali je proxy odbio povezivanje.

    +
      +
    • Da li je proxy konfiguracija browsera ispravna? Provjerite postavke i pokušajte ponovo.
    • +
    • Da li je uređan povezan na aktivnu mrežu?
    • +
    • I dalje imate problem? Posavjetujte se sa vašim mrežnim administratorom ili Internet provajderom.
    • +
    ]]>
    + + + Problem sa malware stranicom + + Stranica %1$s je prijavljena kao napadačka i blokirana je na osnovu vaših sigurnosnih postavki.

    ]]>
    + + + Problem sa neželjenom stranicom + + Stranica %1$s je prijavljena da servira neželjeni softver i blokirana je na osnovu vaših sigurnosnih postavki.

    ]]>
    + + + Problem sa štetnom stranicom + + Stranica %1$s je prijavljena kao potencijalno štetna i blokirana je na osnovu vaših sigurnosnih postavki.

    ]]>
    + + + Problem sa obmanjujućom stranicom + + Stranica %1$s je prijavljena kao obmanjujuća i blokirana je na osnovu vaših sigurnosnih postavki.

    ]]>
    + + + Sigurna stranica nije dostupna + + %1$s nije dostupna.]]> + + Nastavite na HTTP stranicu +
    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 @@ + + + + + Torna-ho a provar + + + No es pot completar la sol·licitud + + No hi ha informació disponible actualment sobre aquest problema o error.

    + ]]>
    + + + Ha fallat la connexió segura + + + +
  • La pàgina que esteu intentant veure no es pot mostrar perquè no s’ha pogut verificar l’autenticitat de les dades rebudes.
  • +
  • Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.
  • + + ]]>
    + + + Ha fallat la connexió segura + + +
  • Això podria ser un problema amb la configuració del servidor, o bé podria ser que algú estigués intentant fer-se passar pel servidor.
  • +
  • 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.
  • + + ]]>
    + + + Avançat… + + És possible que algú estigui intentant suplantar el lloc i no hauríeu de continuar. +

    + ]]>
    + + Vés enrere (recomanat) + + Accepto el risc i vull continuar + + + Aquest lloc web requereix una connexió segura. + + +
  • La pàgina que esteu intentant veure no es pot mostrar perquè aquest lloc requereix una connexió segura.
  • +
  • Molt probablement, l’error és del lloc web i no hi podeu fer res per resoldre’l.
  • +
  • Podeu notificar el problema a l’administrador del lloc web.
  • + + ]]>
    + + + Avançat… + + + %1$s té una política de seguretat anomenada «HTTP Strict Transport Security» (Seguretat estricta de transport HTTP, o HSTS), que vol dir que el %2$s només pot connectar-s’hi de forma segura. No podeu afegir cap excepció per visitar aquest lloc. + +]]> + + Vés enrere + + + S’ha interromput la connexió + + El navegador s’ha connectat correctament, però la connexió s’ha interromput mentre es transferia informació. Torneu-ho a provar.

    +
      +
    • El lloc web podria estar temporalment no disponible o massa ocupat. Torneu-ho a provar d’aquí a uns moments.
    • +
    • Si no podeu carregar cap pàgina, comproveu la connexió de dades o Wi-Fi del vostre dispositiu.
    • +
    + ]]>
    + + + S’ha esgotat el temps d’espera de la connexió + + El lloc sol·licitat no ha respost a la sol·licitud de connexió i el navegador ha deixat d’esperar-ne una resposta.

    +
      +
    • Pot ser que el servidor estigui experimentant una alta demanda o una suspensió temporal? Torneu-ho a provar més tard.
    • +
    • No podeu navegar per altres llocs? Comproveu la connexió del vostre navegador a la xarxa.
    • +
    • L’ordinador està protegit amb un tallafoc o servidor intermediari (proxy)? Si hi ha cap paràmetre incorrecte, podria afectar la navegació web.
    • +
    • Encara teniu problemes? Consulteu el vostre administrador de xarxes o demaneu assistència al vostre proveïdor d’Internet.
    • +
    + ]]>
    + + + No s’ha pogut connectar + + +
  • El lloc web podria estar temporalment no disponible o massa ocupat. Torneu-ho a provar d’aquí a uns moments.
  • +
  • Si no podeu carregar cap pàgina, comproveu la connexió de dades o Wi-Fi del vostre dispositiu.
  • + ]]>
    + + + Resposta inesperada del servidor + + El lloc ha respost a la sol·licitud de la xarxa d’una manera inesperada i el navegador no podrà continuar.

    + ]]>
    + + + La pàgina no està redirigint correctament + + + 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.

    +
      +
    • Heu inhabilitat o blocat les galetes que necessita el lloc?
    • +
    • 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.
    • +
    + ]]>
    + + + Mode fora de línia + + El navegador està treballant fora de línia i no es pot connectar a l’element sol·licitat.

    +
      +
    • L’ordinador està connectat a una xarxa activa?
    • +
    • Premeu «Torna-ho a provar» per canviar al mode en línia i tornar a carregar la pàgina.
    • +
    + ]]>
    + + + El port s’ha restringit per motius de seguretat + + + L’adreça sol·licitada especifica un port (per exemple, mozilla.org:80 per al port 80 a mozilla.org) que normalment s’utilitza per a propòsits diferents de la navegació web. El navegador ha cancel·lat la sol·licitud per garantir la vostra protecció i seguretat.

    + ]]>
    + + + S’ha reiniciat la connexió + + + S’ha interromput l’enllaç a la xarxa mentre es negociava una connexió. Torneu-ho a provar.

    +
      +
    • El lloc web podria estar temporalment no disponible o massa ocupat. Torneu-ho a provar d’aquí a uns moments.
    • +
    • Si no podeu carregar cap pàgina, comproveu la connexió de dades o Wi-Fi del vostre dispositiu.
    • +
    + ]]>
    + + + Tipus de fitxer insegur + + +
  • Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.
  • + + ]]>
    + + + Error de contingut malmès + + + La pàgina que esteu intentant veure no es pot mostrar perquè s’ha produït un error en la transmissió de les dades.

    +
      +
    • Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.
    • +
    + ]]>
    + + + El contingut ha fallat + La pàgina que esteu intentant veure no es pot mostrar perquè s’ha produït un error en la transmissió de les dades.

    +
      +
    • Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.
    • +
    + ]]>
    + + + Error de codificació del contingut + La pàgina que esteu intentant veure no es pot mostrar perquè utilitza una forma de compressió no vàlida o incompatible.

    +
      +
    • Poseu-vos en contacte amb els propietaris del lloc web per informar-los del problema.
    • +
    + ]]>
    + + + No s’ha trobat l’adreça + + + El navegador no ha pogut trobar l’ordinador central per a l’adreça proporcionada.

    +
      +
    • Comproveu que no s’hagin introduït errors en teclejar l’adreça, p. ex. + ww.example.com en lloc de + www.example.com.
    • +
    • Si no podeu carregar cap pàgina, comproveu la connexió de dades o Wi-Fi del vostre dispositiu.
    • +
    + ]]>
    + + + No hi ha connexió a Internet + + Comproveu la vostra connexió a la xarxa o proveu de tornar a carregar la pàgina d’aquí a uns moments. + + Torna a carregar + + + L’adreça no és vàlida + 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.

    + ]]>
    + + L’adreça no és vàlida + + + +
  • Les adreces web normalment s’escriuen així: http://www.example.com/
  • +
  • Assegureu-vos que utilitzeu les barres inclinades (/).
  • + + ]]>
    + + + Protocol desconegut + + L’adreça especifica un protocol (p. ex. wxyz://) que el navegador no reconeix, així doncs, no es pot connectar correctament al lloc.

    +
      +
    • Esteu provant d’accedir a contingut multimèdia o bé a altres serveis que no són text? Comproveu els requisits addicionals del lloc.
    • +
    • Alguns protocols poden requerir programari o connectors de tercers per tal que el navegador els pugui reconèixer.
    • +
    + ]]>
    + + + No s’ha trobat el fitxer + +
  • Pot ser que l’element hagi canviat de nom, s’hagi esborrat o canviat d’ubicació?
  • +
  • Hi ha cap error ortogràfic, de majúscules/minúscules, o bé tipogràfic a l’adreça?
  • +
  • Teniu suficients permisos d’accés per accedir a l’element sol·licitat?
  • + + ]]>
    + + + S’ha denegat l’accés al fitxer + + +
  • Pot ser que s’hagi eliminat, que s’hagi traslladat o que els permisos del fitxer n’impedeixin l’accés.
  • + + ]]>
    + + + S’ha rebutjat la connexió al servidor intermediari + S’ha configurat el navegador perquè utilitzi un servidor intermediari, però s’ha rebutjat la connexió.

    +
      +
    • És correcta la configuració del servidor intermediari? Comproveu els seus paràmetres i torneu-ho a provar.
    • +
    • Esteu segur que el servidor intermediari permet connexions des d’aquesta xarxa?
    • +
    • Encara teniu problemes? Demaneu ajuda al vostre administrador de xarxa o al vostre proveïdor d’Internet.
    • +
    + ]]>
    + + + No s’ha trobat el servidor intermediari + S’ha configurat el navegador perquè utilitzi un servidor intermediari, però aquest no s’ha pogut trobar.

    +
      +
    • És correcta la configuració del servidor intermediari? Comproveu els seus paràmetres i torneu-ho a provar.
    • +
    • L’ordinador està connectat a una xarxa activa?
    • +
    • Encara teniu problemes? Demaneu ajuda al vostre administrador de xarxa o al vostre proveïdor d’Internet.
    • +
    + ]]>
    + + + Problema de lloc amb programari maliciós + + S’ha informat que %1$s és un lloc atacant i s’ha blocat d’acord amb les vostres preferències de seguretat.

    + ]]>
    + + + Problema de lloc amb programari indesitjable + + S’ha informat que el lloc %1$s conté programari indesitjable i s’ha blocat d’acord amb les vostres preferències de seguretat.

    + ]]>
    + + + Problema de lloc maliciós + + 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.

    + ]]>
    + + + Problema de lloc enganyós + + 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.

    + ]]>
    + + + Lloc segur no disponible + + %1$s.]]> + + Ves al lloc HTTP +
    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 @@ + + + + + Titojtob\'ëx chik + + + Man xtikïr ta xutz\'aqatisaj ri k\'utunïk + + Majun na\'oj k\'o chi rij re k\'ayewal o sachoj re\'.

    ]]>
    + + + Xq\'ate\' ri Ütz Okem + + +
  • 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.
  • +
  • Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.
  • + ]]>
    + + + Xqate\' ri Ütz Okem + + + +
  • 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.
  • +
  • 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.
  • + ]]>
    + + + Taq Q\'axinäq… + + + Rik\'in jub\'a\' k\'o noxk\'ob\'en ri ruxaq ruma ri\' man ta chik nasamajij qa. +

    + ]]>
    + + Titzolïx (Chilab\'en) + + Tak\'ulu\' ri K\'ayewal chuqa\' Tinsamajij el + + + Re ruxaq k\'amaya\'l nrajo\' jun jikïl okem. + + + +
  • Ri ruxaq natojtob\'ej natz\'ët man tz\'etel ta ruma chi ri ruxaq k\'amaya\'l nrajo\' jun jikïl okem.
  • +
  • Rik\'in jub\'a\' ri k\'ayewal xa ruma ri ruxaq ajk\'amaya\'l, majun yatikïr nab\'än chuwäch richin nasöl.
  • +
  • Yatikïr naya\' rutzijol chi re ri runuk\'samajel ruxaq k\'amaya\'l chi rij ri k\'ayewal.
  • + + ]]>
    + + + Q\'axinäq… + + + %1$sk\'o jun runa\'ojil jikomal b\'ina\'am HTTP Jikïl Ruk\'waxik Jikomal (HSTS), ri nuq\'ajuj chi ri %2$s xa xe nitikïr nok pa ütz jikomal. Man yatikïr ta natz\'aqatisaj jun man relik ta richin natz\'ët re ruxaq re\'. + +]]> + + Titzolin + + + Xq\'at ri okem + + + Ütz xok ri okik\'amaya\'l, xa xe chi xnaq ri okem toq niq\'axäx ri etamab\'äl. Tatojtob\'ej chik.

    +
      +
    • Rik\'in jub\'a\' man wachel ta ri ruxaq wakami o yalan rusamaj. Tatojb\'ej pa jun chik ti ramaj.
    • +
    • We majun ruxaq nisamäj, tanik\'oj ri Wi-Fi Okem o taq rutzij ri awokisab\'al oyonib\'äl.
    • +
    ]]>
    + + + Xxik\'o ruq\'ijul ri okem + + + 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.

    +
      +
    • ¿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.
    • +
    • ¿La man nitikïr ta nok pa juley chik ruxaq k’amaya’l? Tanik’oj ri rokem pa k’amab’ey awokisab’al.
    • +
    • ¿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.
    • +
    • ¿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.
    • +
    ]]>
    + + + Man nitikïr ta nok + + + +
  • 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
  • +
  • We man yatikïr ta nasamajij jujun taq ruxaq k\'amaya\'l, tanik\'oj ri rutzij oyonib\'äl okisaxel o ri Wi-Fi awokem.
  • + ]]>
    + + + Man oyob\'en ta re rutzijol nuya\' pe ri ruk\'u\'x samaj + + Ri okik\'amaya\'l man nitikïr ta chik nusamajib\'ej ri tzolin rutzij ruxaq k\'amaya\'l.

    ]]>
    + + + Man ütz ta niq\'axan ri ruxaq k\'amaya\'l + + + 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.

    +
      +
    • ¿La e chupül o eq\'aton ri taq kaxlanwey yek\'atzin chi re re ruxaq k\'amaya\'l re\'?
    • +
    • 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.
    • +
    ]]>
    + + + Pa Rub\'eyal Majun Okem + + + 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.

    +
      +
    • ¿La okinäq ri okisab\'äl pa jun tzijïl k\'amab\'ey?
    • +
    • 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.
    • +
    ]]>
    + + + Q\'aton ri b\'ey ruma ri nichajïx + + + Ri ochochib\'äl nik\'utüx xuya\' retal jun b\'ey (achi\'el mozilla.org:80 richin ri b\'ey 80 richin mozilla.org) okisan kichin jalajöj okem pa k\'amaya\'l. Ri okik\'amaya\'l xuq\'ät ri taqowuj ruma ri ato\'ik chuqa\' achajixïk.

    ]]>
    + + + Xtikirisäx chik ri okem + + + Xnaq ri ruk\'amab\'ey ximonel toq nitzijöx jun okem . Tatojtob\'ej chik.

    +
      +
    • Rik\'in jub\'a\' man wachel ta ri ruxaq wakami o yalan rusamaj. Tatojb\'ej pa jun chik ti ramaj.
    • +
    • We majun ruxaq nisamäj, tanik\'oj ri Wi-Fi Okem o taq rutzij ri awokisab\'al oyonib\'äl.
    • +
    ]]>
    + + + Itzel Ruwäch Chi Yakb\'äl + + + +
  • Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.
  • +]]>
    + + + Rusachoj Itzelan Rupam + + + 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.

    +
      +
    • Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.
    • +
    ]]>
    + + + Q\'aton rupam + + 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.

    +
      +
    • Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.
    • +
    ]]>
    + + + Rusachoj Rucholajil Rupam + + 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.

    +
      +
    • Tab\'ana\' utzil, taya\' rutzijol chi rij re k\'ayewal re\' chi ke ri rajaw ruxaq k\'amaya\'l.
    • +
    ]]>
    + + + Man Xilitäj Ta Ri Ochochib\'äl + + + Man xril ta ri ruk\'u\'x samaj ochochib\'äl ri okik\'amaya\'l.

    +
      +
    • Tanik\'oj chi majun sachoj ruk\'wan ri ochochib\'äl achi\'el + ww.example.com pa ruk\'exel ri + www.example.com.
    • +
    • We man yatikïr ta nasamajib\'ej jun ruxaq, tanik\'oj ri Wi-Fi awokem o rutzij awokisab\'al.
    • +
    ]]>
    + + + Majun Okem pa K\'amaya\'l + + Tanik\'oj ri awokem pa k\'amab\'ey o tatojtob\'ej nasamajij chik ri ruxaq pa jun ti mej. + + Tisamajïx chik + + + Man Okel Ta Ri Ochochib\'äl + + 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.

    ]]>
    + + Man nuxïm ta ri\' ri ochochib\'äl + + + +
  • Rochochib\'al Ajk\'amaya\'l achi\'el jantape\' nitz\'ib\'äx achi\'el http://www.example.com/
  • +
  • Tajikib\'a\' chi ye\'awokisaj ri q\'eq\'el taq juch\' (achi\'el /).
  • + ]]>
    + + + Man Etaman Ta Ruwäch Ri Rub\'eyal Samaj + + Ri ochochib\'äl nuya\' retal jun rub\'eyal samaj (achi\'el wxyz://) 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.

    +
      +
    • ¿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.
    • +
    • Jujun taq rub\'eyal samaj rik\'in jub\'a\' yekajo\' taq kema\' o taq tz\'aqat kichin aj rox winaqi\' richin ütz yesamäj.
    • +
    ]]>
    + + + Man Xilitäj Ta ri Yakb\'äl + + +
  • ¿Rik\'in jub\'a\' chi nisik\'ïx chik, xyujtäj o xk\'ex rub\'ey ri ch\'akulal?
  • +
  • ¿La k\'o jun sachoj pa ruwi\' rutz\'ib\'axik, nimatz\'ib\' o jun chik chi sachoj toq xtz\'ib\'äx ri ochochib\'äl?
  • +
  • ¿La kiya\'on aq\'ij richin yatok chupam ri ch\'akulal xk\'utüx?
  • +]]>
    + + + Xq\'at rutz\'etik ri yakb\'äl + + +
  • Rik\'in jub\'a\' xyuj el, xsilöx el o ri niya\'on q\'ij chi ke ri yakb\'äl niq\'ato rutz\'etik.
  • +]]>
    + + + Xxutüx ri okem pa k\'amaya\'l ruma ri ruproxi ruk\'u\'x samaj + + Nuk\'un ri okik\'amaya\'l richin nrokisaj jun proxi ruk\'u\'x samaj, xa xe chi xtzolïx pe ri okem.

    +
      +
    • ¿La ütz ri runuk\'ulem ruproxi okik\'amaya\'l? Tanik\'oj ri runuk\'ulem chuqa\' tatojtob\'ej chik.
    • +
    • ¿Nuya\' q\'ij ri proxi ruk\'u\'x samaj taq okem pa re k\'amab\'ey re\'?
    • +
    • ¿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.
    • +
    ]]>
    + + + Man Xilitäj Ta Ri Proxi Ruk\'u\'x Samaj + + 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.

    +
      +
    • ¿La ütz ri runuk\'ulem ruproxi okik\'amaya\'l? Tanik\'oj ri runuk\'ulem chuqa\' tatojtob\'ej chik.
    • +
    • ¿La tzijïl ri okisab\'äl pa jun tzijïl k\'amab\'ey?
    • +
    • ¿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.
    • +
    ]]>
    + + + Ruk\'ayewal ruxaq rik\'in Malware + + Re ruxaq k\'amaya\'l pa %1$s xya\' rutzijol chi niqeleb\'en, ruma ri\' toq xq\'at ruma ach\'ojin achajinik.

    ]]>
    + + + Man oyob\'en ta reruk\'ayewal ruxaq re\' + + + 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.

    ]]>
    + + + Ruk\'ayewal tz\'ilanel ruxaq + + + Re ruxaq k\'amaya\'l pa %1$s xya\' rutzijol chi niqeleb\'en, ruma ri\' toq xq\'at ruma ach\'ojin achajinik.

    ]]>
    + + + Ruk\'ayewal q\'olonel ruxaq + + Re ruxaq k\'amaya\'l pa %1$s xya\' rutzijol chi niq\'eleb\'en, ruma ri\' toq xq\'at ruma ach\'ojin chajinïk.

    ]]>
    + + + Majun jikïl ruxaq + + %1$s man wachel ta.]]> + + Tik\'oje\' na pa HTTP Ruxaq +
    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 @@ + + + + Usba + + + Dili makumpleto ang Request + + + Wala pa\'y dugang detalye para ani nga problema o error.

    + · ]]>
    + + + Secure Connection Pakyas + + + ·
  • Ang page nga imong buot lantawon dili mapakita kay ang authenticity sa nadawat nga data dili ma-verify.
  • + ·
  • Palihug pahibal-a ang tag-iya sa website mahitungod ani nga problema.
  • + · + · ]]>
    + + + Secure Connection Pakyas + + + + ·
  • Basin problema ni sa server configuration, o basin naa\'y gusto mo-impersonate ani nga server.
  • + ·
  • Kung naka-konek ka ani nga server sa niagi, basin temporary ra ni nga error, sulayi lang ug balik unya.
  • + · + · ]]>
    + + + Advanced… + + Basin naay ga-impersonate sa site ug dili ka dapat mopadayon. + ·

    + · + · ]]>
    + + Balik (Rekomendado) + + Dawata ang Risgo ug Padayon + + + Advanced… + + + Ang koneksyon naputol + + + Nakakonek ang browser, pero naputol ang koneksyon kamulong balhin sa impormasyon. Palihog sulayi usab.

    + ·
      + ·
    • Basin ang site dili available kadyot o busy lang. Sulay balik karon taudtaod.
    • + ·
    • Kung wala pa gihapon mogawas nga page, tan-awa imong device data o Wi-Fi connection.
    • + ·
    + · ]]>
    + + + Ang koneksyon ni-time out. + + + Ang gi-request nga site wala nitubag sa request ug niundang na ang browser ug paghulat sa tubag.

    + ·
      + ·
    • Basin nakasinati ang server ug taas nga demand o kasamtangang pagkapalong? Sulayi balik unya.
    • + ·
    • DIli ba ka maka-browse ug uban site? Tan-awa ang device network connection.
    • + ·
    • Aduna ba\'y firewall o proxy ang imong device o network? Ang sayop nga setting makaputol sa Web browsing.
    • + ·
    • Problema pa gihapon? Tawagi imong network administrator o Internet provider para sa dugang tabang.
    • + ·
    + · ]]>
    + + + Dili ka-konek + + + ·
  • Ang site dili ma-open o busy kaayo. Sulayi balik taudtaod.
  • + ·
  • Kung wala pa gihapon mogawas nga page, tan-awa imong device data o koneksyon sa Wi-Fi.
  • + · + · ]]>
    + + + Wala damha nga tubag sa server + + + Ang site nitubag sa network request sa wala damha nga pamaagi ug ang browser dili na makapadayon.

    + · ]]>
    + + + Ang page dili ka-redirect ug tarong + + + Ni-undang ang browser ug kuha sa mga gi-request nga mga item. Ang site ga-redirect sa pamaagi nga dili makumpleto.

    + ·
      + ·
    • Imo ba gi-hunongan o gi-block ang mga cookie nga kinahanglan ani nga site?
    • + ·
    • Kung ang pagdawat sa mga cookie sa site wala makasulbad sa problema, basin problema kini sa server configuration ug dili sa imong device.
    • + ·
    + · ]]>
    + + + Offline Mode + + Ang browser naka-offline mode karon ug dili makakuha sa gipangayo nga item.

    + ·
      + ·
    • Ang device konektado ba sa active nga network?
    • + ·
    • Pinduta ang “Sulayi Balik” para maka-online mode ug ablihi balik ang page.
    • + ·
    + · ]]>
    + + + Port gidid-an tungod sa seguridad + + + Ang girequest nga address duna\'y port (e.g., mozilla.org:80 para port 80 sa mozilla.org) gigamit para lang sa ubang tuyo dili Web browsing. Gi-undang sa browser ang request alang sa imong proteksyon ug seguridad.

    + · ]]>
    + + + Na-reset ang koneksyon + + Naputol ang network link samtang gasulay ug konek. Palihog sulay balik.

    + ·
      + ·
    • Ang site dili ma-open o busy kaayo. Sulayi balik taudtaod.
    • + ·
    • Kung wala pa gihapon mogawas nga page, tan-awa imong device data o koneksyon sa Wi-Fi.
    • + ·
    + · ]]>
    + + + Delikado nga File Type + + + ·
  • Palihog ug kontak sa mga tag-iya sa website aron ipahibalo ang problema.
  • + · + · ]]>
    + + + Guba nga Content Error + + Ang page nga gisulayan ug pakita dili makita tungod kay naa\'y error nga nabantayan kamulong transmission.

    + ·
      + ·
    • Palihog ug kontak sa mga tag-iya sa website aron ipahibalo ang problema.
    • + ·
    + · ]]>
    + + + Daot nga Content + Ang page nga gisulayan ug pakita dili makita tungod kay naa\'y error nga nabantayan kamulong transmission.

    + ·
      + ·
    • Palihog ug kontak sa mga tag-iya sa website aron ipahibalo ang problema.
    • + ·
    + · ]]>
    + + + Content Encoding Error + Ang page nga gisulayan ug pakita dili makita tungod kay gagamit kini ug sayop nga porma sa compression.

    + ·
      + ·
    • Palihog ug kontak sa mga tag-iya sa website aron ipahibalo ang problema.
    • + ·
    + · ]]>
    + + + Wala\'y Address + + + Dili makita sa browser ang host server sa gihatag nga address.

    + ·
      + ·
    • Susiha ang address kung naa\'y sayup sa pag-type sama sa + · ww.example.com imbis + · www.example.com.
    • + ·
    • Kung wala pa gihapon mogawas nga page, tan-awa imong device data o koneksyon sa Wi-Fi.
    • + ·
    + · ]]>
    + + + Wala\'y koneksyon sa internet + + Tan-awa imong koneksyon sa network o sulayi ug reload ang page taudtaod. + + + Reload + + + Sayop ang Address + Ang gihatag nga address sayop ang format. Palihog ug tan-aw sa location bar kung naa\'y sayop ug sulay balik.

    + · ]]>
    + + Sayop ang address + + + ·
  • Ang kasagaran sa mga Web address gisulat sama sa http://www.example.com/
  • + ·
  • Siguruha nga nigamit ka ug mga forward slash (i.e. /).
  • + · + · ]]>
    + + + Di inila nga Protocol + Ang address nagbungat ug protocol (eg wxyz://) nga wa ilha sa browser, mao nga ang browser dili maka-konek ug tarong sa site.

    +
      +
    • Nagsulay ba ka ug access ug multimedia o laing non-text nga mga service? i-Check ang site alang sa dugang requirement.
    • +
    • Ubang mga protocol nag-require ug third-party software o mga plugin aron makaila ang browser kanila.
    • +
    + ]]>
    + + + Wala Makita ang File + +
  • Basin ang item kay na-rename, na-remove, o nabalhin?
  • +
  • Naa ba\'y spelling, capitalization, o ubang sayop sa pag-sulat sa address?
  • +
  • Naa ba ka\'y igo nga access permission sa gi-request nga item?
  • + + ]]>
    + + + Access sa File gi-deny + + +
  • Basin kini na-remove, nabalhin, o ang mga file permission nagpugong sa access.
  • + + ]]>
    + + + Ang Proxy Server Balibad sa Koneksyon + Ang browser na-configure nga mogamit ug proxy server, pero ang proxy gabalibad ug koneksyon.

    +
      +
    • Sakto ba ang proxy configuration sa browser? Susiha ang mga setting ug sulay balik.
    • +
    • Ang proxy service gadawat ba ug koneksyon gikan ani nga network?
    • +
    • Aduna pa\'y problema? Konsulta sa inyong network administrator o internet service provider ug tabang.
    • +
    + ]]>
    + + + Proxy Server Wa Makita + + Ang browser na-configure nga mogamit ug proxy server, pero ang proxy dili makita.

    +
      +
    • Sakto ba ang proxy configuration sa browser? Susiha ang mga setting ug sulay balik.
    • +
    • Ang proxy service gadawat ba ug koneksyon gikan ani nga network?
    • +
    • Aduna pa\'y problema? Konsulta sa inyong network administrator o internet service provider ug tabang.
    • +
    + ]]>
    + + + Malware site issue + + + Ang site sa %1$s nareport nga usa ka-attack site busa na-block base sa imong mga security preference.

    + ]]>
    + + + Inayran nga site issue + + Ang site sa %1$s nareport nga nagsilbi ug kadudahang software busa kini na-block base sa imong mga security preference.

    + ]]>
    + + + Daotan nga site issue + + Ang site sa %1$s nareport nga gidudahang makadaot nga site busa na-block kini base sa imong mga security preference.

    +]]>
    + + + Makailad nga site issue + + Kini nga webpage sa %1$s nareport nga mailarong site busa kini nablock base sa imong mga security preference.

    + ]]>
    + +
    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 @@ + + + + دووبارە هەوڵ بدەرەوە + + + ناتوانرێت داواکاری تەواو بکرێت + + زانیاری زیاتر دەربارەی ئەم کێشەیە یان ئەم هەڵەیە ئێستا بەردەست نیە.

    + ]]>
    + + + پەیوەندی پارێزراو سەرکەوتوو نەبوو + + + پەیوەندی پارێزراو سەرکەوتوو نەبوو + + + پێشکەوتوو… + + بڕۆ دواوە (پێشنیارکراوە) + + مەترسیەکە وەردەگرم و بەردەوام بە + + + پەیوەندی جیگیر نیە + + + پەیوەندی بەسەرچوو + + + نەتوانرا پەیوەندی بکا + + + وەڵامێکی چاوەڕواننەکراو لە ڕاژەوە + + + ماڵپەڕ بە شێوەیەکی نائاسایی وەڵامی دایەوە بۆیە وێبگەڕ بەردەوام نابێت.

    + ]]>
    + + + پەڕە بەشێوەیەکی ڕاست دووبارە ناردنەوە ئەنجام نادات + + + دۆخی دەرهێڵ + + + دەرەچە ڕێگەپێنەدراوە لەبەر هۆکاری پاراستن + + + پەیوەندی نوێکرایەوە + + + جۆری پەڕگە پارێزراو نیە + + + +
  • تکایە پەیوەندی بکە بە بەرپرسی ماڵپەڕەکەوە بۆ ئاگادادارکردنەوەی لەم کێشەیە.
  • + +]]>
    + + + هەڵەی ناوەڕۆکی تێکشکاو + + + ئەو پەڕەیەیی کە دەتەوێت بیبینیت ناتوانرێت پیشانبدرێت لەبەرئەوەی گواستنەوەی زانیاری تێدا بە دیدەکرێت.

    +
      +
    • تکایە پەیوەندی بکە بە خاوەنی ماڵپەڕەکەوە و کێشەکەیان پێ ڕابگەیەنە.
    • +
    + ]]>
    + + + ناوەڕۆک تێکشکا + + ئەو پەڕەیەیی کە دەتەوێت بیبینیت ناتوانرێت پیشانبدرێت لەبەرئەوەی گواستنەوەی زانیاری تێدا بە دیدەکرێت.

    +
      +
    • تکایە پەیوەندی بکە بە خاوەنی ماڵپەڕەکەوە و کێشەکەیان پێ ڕابگەیەنە.
    • +
    + ]]>
    + + + هەڵەی کۆدکردنی ناوەڕۆک + + ئەو پەڕەیەیی کە دەتەوێت بیبینیت ناتوانرێت پیشانبدرێت لەبەرئەوەی فۆرمێکی هەڵە یان پشتگیر نەکراو بەکرادێنێت بۆ پەستاندن .

    +
      +
    • تکایە پەیوەندی بکە بە خاوەنی ماڵپەڕەکەوە و کێشەکەیان پێ ڕابگەیەنە.
    • +
    + ]]>
    + + + ناونیشان نەدۆزرایەوە + + + پەیوەندی ئینتەرنێت نیە + + تکایە دڵنیابە ئینتەرنێتەکەت کار دەکات یاخوود پەڕە دووبارە نوێبکەرەوە. + + بارکردنەوە + + + ناونیشان ڕاست نیە + + ناونیشان گونجاو نیە + + + +
  • ناونیشانی ماڵپەڕ بەم شێوەیە دەنووسرێت http://www.example.com/
  • +
  • دڵنیابە کە لارەهێڵی ڕاست بەکاردێنیت (بۆ نموونە /).
  • + + ]]>
    + + + پرۆتۆکۆڵی نەناسراو + + + پەڕگە نەدۆزرایەوە + + + چوونەناوی پەڕگە ڕەتکرایەوە + + + ڕاژەخوازی پرۆکسی نەدۆزرایەوە + + + گرفتی ماڵپەڕی داوانامەی خراپ + + + گرفتی ماڵپەڕی نەویستراو + + + گرفتی ماڵپەڕی بەزیان + + + گرفتی ماڵپەڕی فێڵاوی + +
    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 @@ + + + + + Ripruvà + + + Ùn si pò compie a richiesta + + Nisuna infurmazione ùn hè dispunibule à st’ora apprupositu di stu penseru o di stu sbagliu.

    ]]>
    + + + Fiascu di a cunnessione assicurizata + + +
  • A pagina chì vò circate à fighjà ùn pò micca esse affissata, perchè a sputichezza di i dati ricevuti ùn pò micca esse verificata.
  • +
  • Ci vuleria à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.
  • + + ]]>
    + + + Fiascu di a cunnessione assicurizata + + +
  • Stu penseru pò vene di a cunfigurazione di u servitore, osinnò ghjè qualchissia forse chì prova d’impatrunissi di u servitore.
  • +
  • S’è vo avete dighjà riesciutu à cunnettevi à stu servitore, u sbagliu pò esse timpurariu, è pudete pruvà torna da quì à pocu.
  • + + ]]>
    + + + Espertu… + + Qualchissia puderia pruvà d’impatrunissi di u situ web è seria più sicuru d’ùn micca cuntinuà. +

    + + ]]>
    + + Ritornu (ricumandatu) + + Accettà u risicu è cuntinuà + + + Stu situ web richiede una cunnessione sicura. + + + +
  • A pagina chì vò circate à fighjà ùn pò micca esse affissata perchè u situ web richiede una cunnessione sicura.
  • +
  • Sicuramente, stu prublema hè cagiunatu da u situ web, è ùn pudete fà nunda per què.
  • +
  • Ma pudete cuntattà l’amministratore di stu situ web per infurmallu di stu prublema.
  • + + ]]>
    + + + Espertu… + + + %1$s impiega una strategia di sicurità chjamata HTTP Strict Transport Security (HSTS), vole si dì chì %2$s pò solu ci cunnettesi di manera assicurizata. Ùn pudete micca aghjunghje un’eccezzione per visità stu situ. + ]]> + + Ritornu + + + A cunnessione hè stata interrotta + + U navigatore s’hè cunnessu bè, ma a cunnessione hè stata interrotta durante u trasferimentu d’infurmazioni. Ci vole à pruvà torna.

    +
      +
    • Forse u site hè timpurariamente indispunibule o sopraccarcu. Pruvate torna da quì à pocu.
    • +
    • S’ella ùn hè mancu pussibule di navigà nant’à un situ, verificate a cunnessione di dati o Wi-Fi di u vostru apparechju.
    • +
    + ]]>
    + + + U cumportu d’attesa hè statu ecciditu + + + U navigatore hà troppu aspettatu durante a cunnessione à u situ è hà fermatu d’aspettà una risposta.

    +
      +
    • Forse u servitore tratta dumande numerose o hè timpurariamente in panna ? Pruvate torna da quì à pocu.
    • +
    • Ùn pudete micca navigà nant’à d’altri siti ? Verificate a cunnessione à a reta di u vostru apparechju.
    • +
    • 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.
    • +
    • Avete sempre penseri ? Cuntattate u vostru amministratore di a reta o u vostru furnidore d’accessu à internet per ottene un aiutu.
    • +
    ]]>
    + + + Impussibule di cunnettesi + + +
  • Forse u site hè timpurariamente indispunibule o sopraccarcu. Pruvate torna da quì à pocu.
  • +
  • S’ella ùn hè mancu pussibule di navigà nant’à un situ, verificate a cunnessione di dati o Wi-Fi di u vostru apparechju.
  • + + ]]>
    + + + Risposta inespettata da u servitore + + U situ hà rispostu à a dumanda di a reta da una manera inaspettata è u navigatore ùn pò micca cuntinuà.

    + ]]>
    + + + A pagina ùn hè micca ridirettata currettamente + + + 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.

    +
      +
    • Avete disattivatu o bluccatu i canistrelli richiesti da stu situ ?
    • +
    • 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.
    • +
    ]]>
    + + + Modu fora di cunnessione + + + U navigatore funziuneghja in u modu fora di cunnessione è ùn si pò cunnette à l’indirizzu indicatu.

    +
      +
    • L’apparechju hè cunnessu à a reta ?
    • +
    • Sciglite « Pruvà torna » per cambià ver di u modu in linea è ricaricà a pagina.
    • +
    ]]>
    + + + Portu ristrintu per raghjone di sicurità + + + L’indirizzu richiesta indicheghja un portu (i.e. : mozilla.org:80 per u portu 80 nant’à mozilla.org) chì hè di solitu impiegatu per d’altri scopi chè a navigazione nant’à u Web. U navigatore hà abbandunatu a richiesta per a vostra prutezzione è a vostra sicurità.

    + ]]>
    + + + A cunnessione hè stata rilanciata + + + U liame cù a reta hè statu interrottu durante a neguziazione di a cunnessione. Ci vole à pruvà torna.

    +
      +
    • Forse u site hè timpurariamente indispunibule o sopraccarcu. Pruvate torna da quì à pocu.
    • +
    • S’ella ùn hè mancu pussibule di navigà nant’à un situ, verificate a cunnessione di dati o Wi-Fi di u vostru apparechju.
    • +
    + ]]>
    + + + Tipu di schedariu micca sicuru + + +
  • Ci vole à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.
  • + + ]]>
    + + + Sbagliu di cuntenutu alteratu + + A pagina chì vò circate à fighjà ùn pò micca esse affissata, perchè un sbagliu in a trasmissione di dati hè statu scupertu.

    +
      +
    • Ci vole à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.
    • +
    + ]]>
    + + + U cuntenutu s’hè lampatu + A pagina chì vò circate à fighjà ùn pò micca esse affissata, perchè un sbagliu in a trasmissione di dati hè statu scupertu.

    +
      +
    • Ci vole à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.
    • +
    + ]]>
    + + + Sbagliu di cudificazione di cuntenutu + A pagina chì vò circate à fighjà ùn pò micca esse affissata, perchè ella impiega una forma di cumpressione scunnisciuta o micca accettata.

    +
      +
    • Ci vole à cuntattà i prupietarii di u situ web per infurmalli di stu penseru.
    • +
    + ]]>
    + + + L’indirizzu ùn esiste micca + + U navigatore ùn pò micca truvà u servitore ospite per l’indirizzu indicatu.

    +
      +
    • Verificate a sintassa di l’indirizzu per circà i sbaglii cum’è + ww.esempiu.com invece di + www.esempiu.com.
    • +
    • S’ella ùn hè mancu pussibule di navigà nant’à un situ, verificate a cunnessione di dati o Wi-Fi di u vostru apparechju.
    • +
    + ]]>
    + + + Alcuna cunnessione internet + + Verificate a vostra cunnessione internet o pruvate di ricaricà a pagina da quì à pocu. + + Attualizà + + + Indirizzu inaccettevule + 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.

    + ]]>
    + + L’indirizzu ùn hè accettevule + + + +
  • A sintassa di l’indirizzi hè di solitu http://www.esempiu.com/
  • +
  • Assicuratevi d’impiegà barre sbieche (i.e. /).
  • + + ]]>
    + + + Protocollu scunnisciutu + L’indirizzu indicheghja un protocollu (i.e. : wxyz://) scunnisciutu da u navigatore chì ùn riesce micca à cunnettesi currettamente à u situ.

    +
      +
    • >Pruvate d’accede à cuntenutu multimedia o à d’altri servizii non-testu ? Verificate nant’à u situ i prugrammi chì sò richiesti.
    • +
    • Certi protocolli ponu richiede un terzu prugramma o moduli d’estensione per chì u navigatore possi ricunnoscelli.
    • +
    ]]>
    + + + U schedariu ùn esiste micca + +
  • Forse l’elementu hè statu rinuminatu, cacciatu o dispiazzatu ?
  • +
  • Ci hè un sbagliu d’ortugrafia, di maiuscula o un altru sbagliu di tipografia ?
  • +
  • Avete abbastanza permessi d’accessu per st’elementu ?
  • + + ]]>
    + + + L’accessu à u schedariu hè statu ricusatu + +
  • Forse u schedariu hè statu cacciatu, dispiazzatu, o i so permessi ùn permettenu d’accedeci.
  • + + ]]>
    + + + A cunnessione hè stata ricusata da u servitore proxy + + U navigatore hè cunfiguratu per impiegà un servitore proxy, ma quessu hà ricusatu a cunnessione.

    +
      +
    • Hè curretta a cunfigurazione proxy di u navigatore ? Verificate e preferenze è pruvate torna.
    • +
    • U serviziu proxy permette e cunnessioni da sta reta ?
    • +
    • Avete sempre penseru ? Cuntattate u vostru amministratore di a reta o u vostru furnidore d’accessu à internet per ottene un aiutu.
    • +
    + ]]>
    + + + U servitore proxy ùn si trova micca + U navigatore hè cunfiguratu per impiegà un servitore proxy, ma quessu ùn si trova micca.

    +
      +
    • Hè curretta a cunfigurazione proxy di u navigatore ? Verificate e preferenze è pruvate torna.
    • +
    • Hè cunnessu à una reta attiva, l’apparechju ?
    • +
    • Avete sempre penseru ? Cuntattate u vostru amministratore di a reta o u vostru furnidore d’accessu à internet per ottene un aiutu.
    • +
    ]]>
    + + + Prublema di prugramma animosu + + U situ web à l’indirizzu %1$s hè statu signalatu cum’è una surghjente d’affronti è hè statu bluccatu secondu à e vostre preferenze di sicurità.

    + ]]>
    + + + Prublema di situ indesiderevule + + U situ web à l’indirizzu %1$s hè statu signalatu cum’è cuntenendu prugrammi indesiderevule è hè statu bluccatu secondu à e vostre preferenze di sicurità.

    + ]]>
    + + + Prublema di situ periculosu + + U situ web à l’indirizzu %1$s hè statu signalatu cum’è putenzialmente periculosu è hè statu bluccatu secondu à e vostre preferenze di sicurità.

    + ]]>
    + + + Prublema di situ ingannatore + + U situ web à l’indirizzu %1$s hè statu signalatu cum’è essendu ingannatore è hè statu bluccatu secondu à e vostre preferenze di sicurità.

    ]]>
    + + + U situ sicurizatu ùn hè micca dispunibule + + %1$s.]]> + + Cuntinuà nant’à u situ HTTP +
    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 @@ + + + + + Zkusit znovu + + + Nepodařilo se dokončit požadavek + + Další informace o této chybě nejsou bohužel dostupné.

    + ]]>
    + + + Chyba zabezpečeného spojení + + +
  • Požadovanou stránku nelze zobrazit, protože nelze ověřit autenticitu přijatých dat.
  • +
  • Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.
  • + + ]]>
    + + + Chyba zabezpečeného spojení + + +
  • Tato chyba může být způsobena chybnou konfigurací serveru nebo někým, +kdo se snaží vydávat za server.
  • +
  • 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.
  • + + ]]>
    + + + Rozšířené… + + Někdo se může snažit vydávat za zmiňovaný server a proto byste neměli v připojení pokračovat. +

    + + ]]>
    + + Zpátky (doporučeno) + + Beru na vědomí a chci pokračovat + + + Tento web vyžaduje zabezpečené připojení. + + +
  • Stránka, kterou se snažíte načíst, nemůže být zobrazena, protože vyžaduje zabezpečené připojení.
  • +
  • Příčina tohoto problému je pravděpodobně na straně serveru a vy ji bohužel nemůžete odstranit.
  • +
  • O problému můžete informovat správce webu.
  • + + ]]>
    + + + Rozšířené… + + + %1$s má nastaveno bezpečnostní pravidlo HTTP Strict Transport Security (HSTS), které od aplikace %2$s vyžaduje používání pouze zabezpečeného spojení. Pro připojení k této stránce nelze udělit výjimku.]]> + + Zpátky + + + Spojení bylo přerušeno + + Podařilo se připojit k serveru, ale spojení bylo v průběhu přenosu přerušeno. Opakujte akci.

    +
      +
    • Stránka může být dočasně nedostupná nebo zaneprázdněná. Zkuste to znovu za pár okamžiků.
    • +
    • Pokud nemůžete načíst žádnou stránku, zkontrolujte svá mobilní data nebo Wi-Fi připojení.
    • +
    + ]]>
    + + + Vypršel čas spojení + + + Požadovaný server neodpověděl na požadavek o připojení a prohlížeč ukončil čekání na tuto odpověď.

    +
      +
    • Server může být velmi vytížen. Opakujte akci později.
    • +
    • Funguje načítání ostatních webových stránek? Zkontrolujte síťové připojení vašeho zařízení.
    • +
    • 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.
    • +
    • Pokud problém přetrvává, poraďte se se správcem vaší sítě, nebo poskytovatelem připojení k internetu.
    • +
    ]]>
    + + + Chyba spojení + + +
  • Stránka může být dočasně nedostupná nebo zaneprázdněná. Zkuste to znovu za pár okamžiků.
  • +
  • Pokud nemůžete načíst žádnou stránku, zkontrolujte svá mobilní data nebo Wi-Fi připojení.
  • + + ]]>
    + + + Neplatná odpověď serveru + + Server odpověděl na požadavek neočekávaným způsobem a prohlížeč tak nemohl pokračovat.

    + ]]>
    + + + Smyčka při přesměrování + + 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í.

    +
      +
    • Je možné, že stránka vyžaduje ukládání cookies, které máte zakázané nebo je pro tento server blokujete.
    • +
    • Většinou se ale jedná o problém konfigurace serveru a není to tak problém vašeho zařízení.
    • +
    ]]>
    + + + Režim offline + + Prohlížeč je teď v režimu offline a k požadované položce se nelze připojit.

    +
      +
    • Je zařízení připojeno k funkční síti?
    • +
    • Pro přechod do režimu online a opětovné načtení stránky klepněte na tlačítko „Zkusit znovu“.
    • +
    ]]>
    + + + Omezení přístupu na port + + V požadované adrese (URL) byl zadán port (např. mozilla.org:80 pro port 80 na serveru mozilla.org), který se obvykle používá pro jiné internetové služby než je prohlížení webových stránek. Prohlížeč zrušil požadavek z důvodů vaší ochrany.

    + ]]>
    + + + Spojení přerušeno + + Spojení bylo v průběhu otevírání komunikačního kanálu se serverem neočekávaně přerušeno. Opakujte akci.

    +
      +
    • Server je dočasně nedostupný. Zkuste to prosím znovu za chvíli.
    • +
    • Pokud nemůžete načíst žádnou stránku, zkontrolujte svá mobilní data nebo Wi-Fi připojení.
    • +
    + ]]>
    + + + Nebezpečný typ souboru + + +
  • Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.
  • + + ]]>
    + + + Chyba v obsahu stránky + + Požadovanou stránku nelze zobrazit, protože při přenosu dat došlo k chybě.

    +
      +
    • Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.
    • +
    + ]]>
    + + + Pád obsahu + Požadovanou stránku nelze zobrazit, protože při přenosu dat došlo k chybě.

    +
      +
    • Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.
    • +
    + ]]>
    + + + Chyba znakové sady obsahu + Požadovanou stránku nelze zobrazit, protože používá neplatný či nepodporovaný způsob komprese dat.

    +
      +
    • Kontaktujte prosím vlastníky webového serveru a informujte je o tomto problému.
    • +
    + ]]>
    + + + Adresa nenalezena + + URL adresa neodpovídá známému serveru a nelze ji načíst.

    +
      +
    • Zkontrolujte, že je adresa napsaná správně a neobsahuje chyby jako + ww.example.com místo www.example.com.
    • +
    • Pokud se vám nezobrazují ani ostatní stránky, zkontrolujte své připojení přes mobilní data nebo Wi-Fi.
    • +
    + ]]>
    + + + Připojení k internetu není dostupné + + + Zkontrolujte připojení k síti nebo zkuste stránku za chvilku znovu načíst. + + Znovu načíst + + + Neplatná adresa + Adresa (URL) není platná a nelze ji načíst. Zkontrolujte prosím, že je adresa napsána správně.

    + ]]>
    + + Neplatná adresa + + +
  • Webové adresy jsou obvykle psány jako http://www.example.com/
  • +
  • Ujistěte se, že používáte běžná lomítka (tj. /).
  • + + ]]>
    + + + Neznámý protokol + Adresu (URL) určuje protokol (např. wxyz://), který nebyl prohlížečem rozpoznán, a proto se k ní nemůže korektně připojit.

    +
      +
    • Zkoušíte přistupovat k multimédiím či jiné netextové službě? Podívejte se, jaké další věci stránka vyžaduje.
    • +
    • Některé protokoly mohou vyžadovat software třetích stran nebo zásuvné moduly dříve, než je prohlížeč může rozpoznat.
    • +
    + ]]>
    + + + Soubor nenalezen + +
  • Je možné, že byl smazán, přejmenován nebo přesunut.
  • +
  • Zkontrolujte prosím, že je adresa napsána správně, a to včetně velikosti písmen.
  • +
  • Jste-li autorem tohoto souboru, ověřte, že daný soubor na serveru existuje a že má příslušná práva na zobrazení.
  • + + ]]>
    + + + Přístup k souboru byl odepřen + +
  • Možná byl smazán, přesunut nebo jeho oprávnění zabraňují přístupu.
  • + + ]]>
    + + + Proxy server odmítl spojení + Prohlížeč je nastaven, aby používal proxy server, který odmítá spojení.

    +
      +
    • Zkontrolujte v prohlížeči nastavení proxy serveru a akci opakujte.
    • +
    • Je možné, že proxy server nepovoluje připojení z vaší sítě.
    • +
    • Pokud problém přetrvává, poraďte se se správcem vaší sítě, nebo poskytovatelem připojení k internetu.
    • +
    +]]>
    + + + Proxy server nenalezen + + Prohlížeč je nastaven, aby používal proxy server, který nelze nalézt.

    +
      +
    • Zkontrolujte v prohlížeči nastavení proxy serveru a akci opakujte.
    • +
    • Zkontrolujte síťové připojení vašeho zařízení.
    • +
    • Pokud problém přetrvává, poraďte se se správcem vaší sítě, nebo poskytovatelem připojení k internetu.
    • +
    + ]]>
    + + + Problém se škodlivým softwarem + + Stránka %1$s byla nahlášena jako útočná a byla zablokována na základě vašeho bezpečnostního nastavení.

    + ]]>
    + + + Problém s nežádoucí webovou stránkou + + 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í.

    + ]]>
    + + + Problém se škodlivou stránkou + + Stránka %1$s byla nahlášena jako útočná a byla zablokována na základě vašeho bezpečnostního nastavení.

    + ]]>
    + + + Problém s klamavou stránkou + + 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í.

    + ]]>
    + + + Nelze navázat zabezpečené spojení + + %1$s není dostupná.]]> + + Pokračovat přes nezabezpečené spojení +
    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 @@ + + + + + Ceisiwch Eto + + + Methu Cyflawni’r Cais + + Nid oes gwybodaeth ar gael am y gwall neu broblem yma ar hyn o bryd.

    +]]>
    + + + Methodd y Cysylltiad Diogel + + + +
  • Nid oes modd dangos y dudalen rydych yn ceisio ei darllen am nad oes modd dilysu’r data rydych wedi ei dderbyn.
  • +
  • Cysylltwch â pherchnogion y wefan i’w hysbysu o\'r anhawster.
  • + +]]>
    + + + Methodd y Cysylltiad Diogel + + +
  • Gall hwn fod yn anhawster gyda ffurfweddiad y gweinydd neu gall fod yn rhywun sy\'n ceisio dynwared y gweinydd.
  • +
  • Os ydych wedi cysylltu’n llwyddiannus gyda’r gweinydd yn y gorffennol, efallai mai gwall dros dro ydyw ac i geisio eto.
  • + ]]>
    + + + Uwch… + + Gallai rhywun fod yn ceisio dynwared y wefan hon a dylech chi ddim mynd ymhellach. +        

    +         +    ]]>
    + + Ewch Nôl (Argymell) + + Derbyn y Perygl a Pharhau + + + Mae angen cysylltiad diogel ar y wefan hon. + + + +
  • Nid oes modd dangos y dudalen rydych yn ceisio ei gweld oherwydd bod angen cysylltiad diogel ar y wefan hon.
  • +
  • Mae’r broblem yn fwyaf tebygol gyda’r wefan, a does dim byd y gallwch chi ei wneud i’w ddatrys.
  • +
  • Gallwch roi gwybod i weinyddwr y wefan am y broblem.
  • + + ]]>
    + + + Uwch… + + + %1$s bolisi diogelwch o’r enw HTTP Strict Transport Security (HSTS), sy’n golygu mai dim ond yn ddiogel y gall %2$s gysylltu ag ef. Ni allwch ychwanegu eithriad i ymweld â’r wefan hon. +]]> + + Ewch Nôl + + + Cafodd y cysylltiad ei darfu + + + Cysylltodd y porwr yn llwyddiannus, ond amharwyd ar y cysylltiad wrth drosglwyddo gwybodaeth. Rhowch gynnig arall arni.

    +      
      +        
    • Gallai’r wefan fod ar gael dros dro neu’n rhy brysur. Rhowch gynnig arall arni mewn ychydig eiliadau.
    • +        
    • Os na allwch lwytho unrhyw dudalennau, gwiriwch ddata neu gysylltiad Wi-Fi eich dyfais.
    • +      
    ]]>
    + + + Mae cyfnod y cyswllt wedi dod i ben + + Nid yw’r wefan wedi ymateb i gais am gyswllt ac mae’r porwr wedi peidio aros am ateb.

    +
      +
    • Gall fod y gweinydd yn profi galw sylweddol neu ataliad dros dro? Ceisiwch eto.
    • +
    • A ydych yn methu pori i wefannau eraill? Gwiriwch gysylltiad rhwydwaith y ddyfais.
    • +
    • A yw eich dyfais wedi ei ddiogelu gan fur cadarn neu ddirprwy? Gall osodiadau anghywir effeithio ar bori\'r we.
    • +
    • Yn dal yn cael problemau? Cysylltwch â’ch gweinyddwr rhwydwaith neu ddarparwr rhyngrwyd am gymorth.
    • +
    ]]>
    + + + Methu cysylltu + + +
  • Mae’n bosib fod y wefan yn rhy brysur neu ddim ar gael dros dro. Ceisiwch eto cyn bo hir.
  • +
  • Os nad oes modd i chi lwytho unrhyw dudalennau, gwiriwch gysylltiad data neu Wi-Fi eich dyfais.
  • + ]]>
    + + + Ymateb annisgwyl gan y gweinydd + + + Ymatebodd y wefan i’r cais rhwydwaith mewn ffordd annisgwyl ac nid yw’r porwr yn gallu parhau.

    + ]]>
    + + + Nid yw’r dudalen yn ailgyfeirio’n iawn + + 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.

    +      
      +        
    • A ydych wedi analluogi neu rwystro cwcis sy’n ofynnol gan y wefan hon?
    • +        
    • 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.
    • +      
    ]]>
    + + + Modd All-lein + + + Mae’r porwr yn gweithredu yn ei fodd all-lein ac nid yw’n gallu cysylltu â’r eitem y gofynnwyd amdani.

    +      
      +        
    • A yw’r ddyfais wedi’i chysylltu â rhwydwaith gweithredol?
    • +        
    • Pwyswch “Ceisiwch Eto” i newid i’r modd ar-lein ac ail-lwytho’r dudalen
    • +      
    ]]>
    + + + Porth wedi’i gyfyngu am resymau diogelwch + + Gofynnodd y cyfeiriad am borth (e.e., mozilla.org:80 sef, porth 80 ar mozilla.org) sy’n cael ei ddefnyddio am resymau heblaw pori’r We. Mae’r porwr wedi diddymu eich cais er eich diogelwch.

    +]]>
    + + + Cafodd y cysylltiad ei ailosod + + Amharwyd ar y cysylltiad rhwydwaith wrth negodi cysylltiad. Rhowch gynnig arall arni.

    +      
      +        
    • Gallai’r wefan fod ar gael dros dro neu’n rhy brysur. Rhowch gynnig arall arni mewn ychydig eiliadau.
    • +        
    • Os na allwch lwytho unrhyw dudalennau, gwiriwch ddata neu gysylltiad Wi-Fi eich dyfais.
    • +      
    +    ]]>
    + + + Math Anniogel o Ffeil + + +
  • Cysylltwch â pherchnogion y wefan i’w hysbysu o’r anhawster.
  • + + ]]>
    + + + Gwall Cynnwys Llygredig + + Nid oes modd dangos y dudalen rydych yn ceisio ei gweld yn sgil canfod gwall trosglwyddo data.

    +
      +
    • Cysylltwch â pherchnogion y wefan i’w hysbysu o’r anhawster.
    • +
    +]]>
    + + + Chwalodd y cynnwys + Nid oes modd dangos y dudalen rydych yn ceisio ei gweld yn sgil canfod gwall trosglwyddo data.

    +
      +
    • Cysylltwch â pherchnogion y wefan i’w hysbysu o’r anhawster.
    • +
    +]]>
    + + + Gwall Amgodio Cynnwys + + 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.

    +
      +
    • Cysylltwch â pherchnogion y wefan i’w hysbysu o’r anhawster.
    • +
    + ]]>
    + + + Heb Ganfod Cyfeiriad + + Nid yw’r porwr yn gallu dod o hyd i’r gweinydd cynnal ar gyfer y cyfeiriad hwn.

    +      
      +        
    • Gwiriwch y cyfeiriad am wallau teipio fel +          ww.example.com yn lle +          www.example.com.
    • +        
    • Os na allwch lwytho unrhyw dudalennau, gwiriwch ddata neu gysylltiad Wi-Fi eich dyfais
    • +      
    +    ]]>
    + + + Dim cysylltiad rhyngrwyd + + + Gwiriwch eich cysylltiad rhwydwaith neu ceisiwch ail-lwytho’r dudalen ymhen ychydig eiliadau. + + Ail-lwytho + + + Cyfeiriad Annilys + + Nid yw’r cyfeiriad yma mewn fformat adnabyddus. Gwiriwch y bar lleoliad a cheisiwch eto.

    + ]]>
    + + Nid yw’r cyfeiriad yn ddilys + + + +
  • Fel rheol mae cyfeiriadau Gwe’n cael eu hysgrifennu fel http://www.example.com/
  • +
  • Gwnewch yn siŵr eich bod yn defnyddio blaen slaes (h.y. /).
  • + +]]>
    + + + Protocol Anhysbys + Mae’r cyfeiriad yn pennu protocol (e.e. wxyz://) nad yw’r porwr yn ei adnabod, felly nid yw’r porwr yn gallu cysylltu’n iawn â’r wefan.

    +
      +
    • Ydych chi’n ceisio cael mynediad at wasanaethau aml-gyfrwng neu destun yn unig? Gwiriwch y wefan am yr anghenion ychwanegol hyn.
    • +
    • Mae rhai protocolau angen meddalwedd trydydd parti neu ategion cyn bod modd i’r porwr eu hadnabod.
    • +
    + ]]>
    + + + Heb Ganfod Ffeil + +
  • Efallai bod yr eitem wedi cael ei hailenwi, ei thynnu neu ei symud?
  • +
  • Oes yna wall sillafu, llythrennu neu wall teipio arall yn y cyfeiriad?
  • +
  • A oes gennych hawl digonol i gael mynediad at yr eitem hon?
  • + + ]]>
    + + + Mae mynediad i’r ffeil wedi ei wrthod + +
  • Gall ei fod wedi ei dynnu, symud neu fod caniatâd ffeiliau yn rhwystro mynediad.
  • + +]]>
    + + + Gwrthododd y Gweinydd Dirprwyol y Cysylltiad + Mae’r porwr wedi ei ffurfweddu i ddefnyddio gweinydd dirprwy, ond mae’r dirprwy wedi gwrthod cysylltiad.

    +
      +
    • A yw ffurfweddiad dirprwy’r porwr yn gywir? Gwiriwch y gosodiadau a cheisio eto.
    • +
    • A yw’r gwasanaeth dirprwy yn caniatáu cysylltiadau o’r rhwydwaith?
    • +
    • Dal yn cael problemau? Cysylltwch â’ch gweinyddwr rhwydwaith neu ddarparwr rhyngrwyd am gymorth.
    • +
    + ]]>
    + + + Heb Ganfod y Gweinydd Dirprwyol + + Mae’r porwr wedi ei ffurfweddu i ddefnyddio gweinydd dirprwy, ond nid oes modd canfod y dirprwy.

    +
      +
    • A yw ffurfweddiad dirprwy’r porwr yn gywir? Gwiriwch y gosodiadau a cheisio eto.
    • +
    • A yw’r ddyfais wedi ei chysylltu i rwydwaith weithredol?
    • +
    • Dal yn cael problemau? Cysylltwch â’ch gweinyddwr rhwydwaith neu ddarparwr rhyngrwyd am gymorth.
    • +
    ]]>
    + + + Mater gwefan drwgwar + + Mae gwefan %1$s yn hysbys fel gwefan ymosod ac wedi cael ei rwystro ar sail eich dewisiadau diogelwch.

    + ]]>
    + + + Mater gwefan diangen + + Mae gwefan %1$s yn hysbys fel gwefan sy’n cyflwyno meddalwedd diangen ac wedi cael ei rwystro ar sail eich dewisiadau diogelwch.

    + ]]>
    + + + Mater gwefan niweidiol + + Mae gwefan %1$s yn hysbys fel gwefan a allai fod yn niweidiol ac wedi cael ei rwystro ar sail eich dewisiadau diogelwch.

    + ]]>
    + + + Mater gwefan twyllodrus + + Mae gwefan %1$s yn hysbys fel gwefan twyllodrus ac wedi cael ei rwystro ar sail eich dewisiadau diogelwch.

    + ]]>
    + + + Gwefan Ddiogel Ddim ar Gael + + %1$s ar gael.]]> + + Ymlaen i’r Wefan HTTP +
    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 @@ + + + + + Prøv igen + + + Kan ikke færdiggøre forespørgsel + + + Yderligere information om dette problem eller denne fejl er ikke tilgængelig lige nu.

    + ]]>
    + + + Sikker forbindelse mislykkedes + + + +
  • Siden kunne ikke vises, da autenticiteten af de modtagne data ikke kunne bekræftes.
  • +
  • Kontakt ejerne af webstedet omkring dette problem.
  • + + ]]>
    + + + Sikker forbindelse mislykkedes + + + +
  • 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.
  • +
  • Hvis du har kunnet tilgå serveren tidligere kan dette problem være midlertidigt og du anbefales at prøve igen senere.
  • + + ]]>
    + + + Avanceret… + + Nogen kan have lavet en falsk version af webstedet, og du bør ikke fortsætte. +

    + + ]]>
    + + Gå tilbage (anbefalet) + + Accepter risikoen og fortsæt + + + Dette websted kræver en sikker forbindelse. + + +
  • Siden kan ikke vises, da webstedet kræver en sikker forbindelse.
  • +
  • Problemet skyldes højst sandsynligt webstedet, og du kan ikke selv løse problemet.
  • +
  • Du kan prøve at kontakte webstedets administrator for at gøre opmærksom på problemet.
  • + + ]]>
    + + + Avanceret… + + + %1$s bruger en sikkerhedspolitik kaldet HTTP Strict Transport Security (HSTS), hvilket betyder, at %2$s kun kan oprette en sikker forbindelse til webstedet. Du kan ikke tilføje en undtagelse for at besøge webstedet. + ]]> + + Gå tilbage + + + Forbindelsen blev afbrudt + + + Browseren oprettede forbindelsen korrekt, men den blev afbrudt under overførsel af data. Prøv igen.

    +
      +
    • Webstedet kan være midlertidigt utilgængelig eller optaget. Prøv igen senere.
    • +
    • Hvis du ikke kan indlæse nogen sider, skal du kontrollere din enheds data- eller wi-fi-forbindelse.
    • +
    + ]]>
    + + + Forbindelsens tidsfrist udløb + + + Det anmodede websted svarede ikke på en anmodning om forbindelse, og browseren er stoppet med at vente på et svar.

    +
      +
    • Kan serveren opleve stor efterspørgsel eller være midlertidigt nede? Prøv igen senere.
    • +
    • Er du ikke i stand til at besøge andre websteder? Kontrollér enhedens netværksforbindelse.
    • +
    • Er din enhed eller dit netværk beskyttet af en firewall eller en proxy? Forkerte indstillinger kan forstyrre din webbrowsing.
    • +
    • Har du stadig problemer? Kontakt din netværksadministrator eller din internetudbyder for at få hjælp.
    • +
    ]]>
    + + + Kan ikke oprette forbindelse + + +
  • Webstedet kan være midlertidigt utilgængeligt eller travlt optaget. Prøv igen senere.
  • +
  • Hvis du ikke kan indlæse nogen websider overhovedet, kontrollér da din enheds data- eller wi-fi-forbindelse.
  • + + ]]>
    + + + Uventet svar fra server + + + Webstedet svarede på netværksforespørgslen på en uventet måde, og browseren kan ikke fortsætte.

    + ]]>
    + + + Denne side viderestiller ikke forespørgslen korrekt + + Browseren har afbrudt forespørgslen, fordi webstedet viderestiller forespørgslen på den måde, der forhindrer den i nogensinde at blive færdig.

    +
      +
    • Har du deaktiveret eller blokeret cookies for webstedet?
    • +
    • Hvis det ikke hjælper at aktivere cookies for webstedet, så skyldes problemet sandsynligvis en fejl på webserveren og ikke din enhed.
    • +
    ]]>
    + + + Offline-tilstand + + Browseren er sat i offline-tilstand og kan ikke oprette den anmodede forbindelse.

    +
      +
    • Er enheden forbundet til et aktivt netværk?
    • +
    • Tryk på "Prøv igen" for at skifte til online-tilstand og genindlæse siden.
    • +
    ]]>
    + + + Port begrænset af sikkerhedshensyn + + Den angivne adresse specificerede en port (fx mozilla.org:80 for port 80 på mozilla.org), der normalt anvendes til andre formål end visning af websider. Browseren har afbrudt forespørgslen af sikkerhedshensyn.

    + ]]>
    + + + Forbindelsen blev nulstillet + + + Netværkslinket blev afbrudt under forhandlinger om en forbindelse. Prøv igen.

    +
      +
    • Webstedet kan være midlertidig utilgængeligt eller have for travlt. Prøv igen senere.
    • +
    • Hvis du ikke kan indlæse nogen sider overhovedet, så skal du kontrolle din enheds data eller wi-fi-forbindelse.
    • +
    + ]]>
    + + + Usikker filtype + + +
  • Kontakt webstedets ejere omkring dette problem.
  • + + ]]>
    + + + Fejlbehæftet indhold + + Siden, du forsøger at se, kan ikke vises, da der er fundet en fejl i overførslen af data.

    +
      +
    • Kontakt webstedets ejere omkring dette problem.
    • +
    + ]]>
    + + + Indholdet kunne ikke vises + Siden, du forsøger at se, kan ikke vises, da der er fundet en fejl i overførslen af data.

    +
      +
    • Kontakt webstedets ejere omkring dette problem.
    • +
    + ]]>
    + + + Fejl i indholdskodning + Siden kunne ikke vises, da den bruger en ugyldig eller ikke-understøttet form for komprimering.

    +
      +
    • Kontakt webstedets ejere omkring dette problem.
    • +
    + ]]>
    + + + Adressen blev ikke fundet + + Browseren kunne ikke finde host-serveren til den angivne adresse.

    +
      +
    • Kontroller adressen for tastefejl som fx + ww.eksempel.dk i stedet for + www.eksempel.dk.
    • +
    • Hvis du ikke kan indlæse nogen sider overhovedet, skal du kontrollere din enheds data- eller wi-fi-forbindelse.
    • +
    + ]]>
    + + + Ingen internetforbindelse + + Kontrollér din internetforbindelse, eller prøv at indlæse siden igen om et øjeblik. + + Genindlæs + + + Ugyldig adresse + Adressen er ikke angivet i et genkendt format. Undersøg, om der er fejl i adressen, og prøv igen.

    + ]]>
    + + + Adressen er ikke gyldig + + + +
  • Webadresser skrives normalt som http://www.eksempel.dk/
  • +
  • Kontrollér at du ikke benytter divisionstegn (dvs. /).
  • + + ]]>
    + + + Ukendt protokol + + Adressen starter med en protokol (fx wxyz://), der ikke genkendes af browseren. Det betyder, at browseren ikke kan oprette en korrekt forbindelse til webstedet.

    +
      +
    • Prøver du at få adgang til multimedia eller andet indholder, der ikke er tekst? Kontrollér, om webstedet stiller særlige krav.
    • +
    • Nogle protokoller kan kræve, at du installere tredjeparts-software eller -plugins, før browseren kan genkende protokollen.
    • +
    + ]]>
    + + + Fil ikke fundet + +
  • Er elementet blive omdøbt, fjernet eller flyttet?
  • +
  • Er der en stavefejl, fejl med store/små bogstaver eller andre typografiske fejl i adressen?
  • +
  • Har du de rette adgangstilladelser til det anmodede element?
  • + + ]]>
    + + + Adgang til filen blev nægtet + +
  • Den kan være blevet slettet, flyttet eller tilladelserne for filen kan forhindre adgang.
  • + + ]]>
    + + + Proxyserveren afviste forbindelse + + Browseren er indstillet til at anvende en proxyserver, men proxyserveren afviste forbindelsen.

    +
      +
    • Er browserens proxy-indstillinger korrekte? Kontrollér indstillingerne og prøv igen.
    • +
    • Tillader proxy-tjenesten forbindelser fra dette netværk?
    • +
    • Har du stadig problemer? Kontakt din netværksadministrator eller din internetudbyder for at få hjælp.
    • +
    + ]]>
    + + + Proxyserver blev ikke fundet + + Browseren er indstillet til at anvende en proxyserver, men serveren kunne ikke findes.

    +
      +
    • Er browserens proxy-indstillinger korrekte? Kontrollér indstillingerne og prøv igen.
    • +
    • Er enheden forbundet til et aktivt netværk?
    • +
    • Har du stadig problemer? Kontakt din netværksadministrator eller din internetudbyder for at få hjælp.
    • +
    ]]>
    + + + Problem med malware-websted + + Webstedet %1$s er blevet anmeldt som et angrebswebsted og er blevet blokeret som følge af dine sikkerhedsindstillinger.

    + ]]>
    + + + Problem med uønsket software + + Webstedet %1$s er blevet anmeldt for at tilbyde uønsket software og er blevet blokeret som følge af dine sikkerhedsindstillinger.

    + ]]>
    + + + Problem med skadeligt websted + + Webstedet %1$s er blevet anmeldt som et potentielt skadeligt websted og er blevet blokeret som følge af dine sikkerhedsindstillinger.

    + ]]>
    + + + Problem med vildledende websted + + Webstedet %1$s er blevet anmeldt som et vildledende websted og er blevet blokeret som følge af dine sikkerhedsindstillinger .

    + ]]>
    + + + Sikkert websted er ikke tilgængeligt + + %1$s er ikke tilgængelig.]]> + + Fortsæt til HTTP-websted +
    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 @@ + + + + + Erneut versuchen + + + Anfrage kann nicht abgeschlossen werden + + Weitere Informationen zu diesem Problem oder Fehler sind momentan nicht verfügbar.

    + ]]>
    + + + Sichere Verbindung fehlgeschlagen + + +
  • Die Webseite kann nicht angezeigt werden, da die Authentizität der erhaltenen Daten nicht verifiziert werden konnte.
  • +
  • Kontaktieren Sie bitte den Inhaber der Website, um ihn über dieses Problem zu informieren.
  • + + ]]>
    + + + Sichere Verbindung fehlgeschlagen + + +
  • Dies könnte ein Problem mit der Konfiguration des Servers sein oder jemand, der vorgibt, der Server zu sein.
  • +
  • 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.
  • + + ]]>
    + + + Erweitert… + + Jemand könnte versuchen, sich als diese Website auszugeben, daher sollten Sie nicht fortfahren. +

    + ]]>
    + + Zurück (empfohlen) + + Risiko akzeptieren und fortfahren + + + Diese Website erfordert eine sichere Verbindung. + + +
  • Die Seite, die Sie aufrufen möchten, kann nicht angezeigt werden, da für diese Website eine sichere Verbindung erforderlich ist.
  • +
  • Das Problem liegt höchstwahrscheinlich bei der Website, und Sie können nichts tun, um es zu lösen.
  • +
  • Sie können den Administrator der Website über das Problem informieren.
  • + + ]]>
    + + + Erweitert… + + + %1$s hat eine Sicherheitsrichtlinie namens HTTP Strict Transport Security (HSTS), was bedeutet, dass sich %2$s nur über eine sichere Verbindung mit der Website verbinden kann. Daher kann keine Ausnahme für die Website hinzugefügt werden. + ]]> + + Zurück + + + Fehler: Datenübertragung unterbrochen + + Der Browser hat die Verbindung erfolgreich hergestellt, diese ist jedoch während der Datenübertragung abgebrochen. Bitte versuchen Sie es erneut.

    +
      +
    • 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.
    • +
    • Wenn Sie auch keine andere Website aufrufen können, überprüfen Sie bitte die Daten- oder WLAN-Verbindung.
    • +
    + ]]>
    + + + Fehler: Netzwerk-Zeitüberschreitung + + Die angeforderte Website hat auf eine Verbindungsanfrage nicht reagiert und der Browser wartet inzwischen nicht mehr auf eine Antwort.

    +
      +
    • Ist der Server eventuell zu stark ausgelastet oder vorübergehend ausgefallen? Versuchen Sie es später erneut.
    • +
    • Können Sie auch keine anderen Websites aufrufen? Prüfen Sie die Netzwerkverbindung des Computers.
    • +
    • Wird Ihr Computer oder Ihr Netzwerk von einer Firewall oder einem Proxy geschützt? Durch falsche Einstellungen kann das Surfen im Internet behindert werden.
    • +
    • Treten immer noch Probleme auf? Bitten Sie Ihren Netzwerkadministrator oder Internetanbieter um Hilfestellung.
    • +
    + ]]>
    + + + Fehler: Verbindung fehlgeschlagen + + +
  • 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.
  • +
  • Wenn Sie auch keine andere Website aufrufen können, überprüfen Sie bitte die Daten- oder WLAN-Verbindung.
  • + + ]]>
    + + + Fehler: Unerwartete Antwort + + Die aufgerufene Website hat in einer unerwarteten Art geantwortet, sodass die Verbindung nicht fortgesetzt werden kann.

    + ]]>
    + + + Fehler: Umleitungsfehler + + Der Browser versucht nicht mehr, das angeforderte Element zu öffnen. Die aufgerufene Website leitet die Anfrage so um, dass sie nie beendet werden kann.

    +
      +
    • Haben Sie Cookies, die von dieser Website benötigt werden, deaktiviert oder blockiert?
    • +
    • 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.
    • +
    + ]]>
    + + + Offline-Modus + + Der Browser arbeitet im Offline-Modus und kann daher keine Verbindung mit dem angefragten Element aufbauen.

    +
      +
    • Ist der Computer mit einem aktiven Netzwerk verbunden?
    • +
    • Wählen Sie „Erneut versuchen“, um in den Online-Modus zu wechseln und die Seite erneut zu laden.
    • +
    + ]]>
    + + + Port aus Sicherheitsgründen gesperrt + + Die angeforderte Adresse hat einen Port (z. B. mozilla.org:80 für Port 80 auf mozilla.org) übergeben, der normalerweise für andere Zwecke als für das Surfen im Internet verwendet wird. Der Browser hat die Anforderung zu Ihrem Schutz und Ihrer Sicherheit abgebrochen.

    + ]]>
    + + + Fehler: Verbindung unterbrochen + + Die Netzwerkverbindung wurde während des Verbindungsaufbaus unterbrochen. Bitte versuchen Sie es erneut.

    +
      +
    • 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.
    • +
    • Wenn Sie auch keine andere Website aufrufen können, überprüfen Sie bitte die Daten- oder WLAN-Verbindung.
    • +
    + ]]>
    + + + Unsicherer Dateityp + + +
  • Kontaktieren Sie bitte die Betreiber der Website, um sie über dieses Problem zu informieren.
  • + + ]]>
    + + + Fehler: Beschädigte Inhalte + + Die Seite, die Sie zu öffnen versuchen, kann nicht angezeigt werden, da ein Fehler in der Datenübertragung festgestellt wurde.

    +
      +
    • Bitte kontaktieren Sie die Betreiber der Website, um sie über dieses Problem zu informieren.
    • +
    + ]]>
    + + + Inhalt abgestürzt + Die Seite, die Sie zu öffnen versuchen, kann nicht angezeigt werden, da ein Fehler in der Datenübertragung festgestellt wurde.

    +
      +
    • Bitte kontaktieren Sie die Betreiber der Website, um sie über dieses Problem zu informieren.
    • +
    + ]]>
    + + + Content-Encoding-Fehler + Die aufgerufene Seite kann nicht angezeigt werden, da sie eine ungültige oder nicht unterstützte Form der Kompression verwendet.

    +
      +
    • Bitte kontaktieren Sie die Website-Betreiber, um sie über dieses Problem zu informieren.
    • +
    + ]]>
    + + + Adresse nicht gefunden + + Der Browser konnte den Host-Server für die angegebene Adresse nicht finden.

    +
      +
    • Bitte überprüfen Sie die Adresse auf Tippfehler, wie ww.example.com statt www.example.com.
    • +
    • Wenn Sie auch keine andere Website aufrufen können, überprüfen Sie bitte die Daten- oder WLAN-Verbindung.
    • +
    ]]>
    + + + Keine Internetverbindung + + Überprüfen Sie Ihre Netzwerkverbindung oder versuchen Sie in wenigen Augenblicken, die Seite neu zu laden. + + Neu laden + + + Ungültige Adresse + Das Format der angegebenen Adresse wurde nicht erkannt. Bitte überprüfen Sie die Adressleiste auf Fehler und versuchen Sie es erneut.

    + ]]>
    + + Fehler: Ungültige Adresse + + +
  • Web-Adressen sehen gewöhnlich folgendermaßen aus: http://www.example.com/
  • +
  • Bitte stellen Sie sicher, dass Sie nicht den umgekehrten, sondern den einfachen Schrägstrich (/) verwenden.
  • + + ]]>
    + + + Unbekanntes Protokoll + In der Adresse wird ein Protokoll angegeben (z. B. wxyz://), das der Browser nicht erkennt und ihn daran hindert, eine funktionierende Verbindung zu der Website herzustellen.

    +
      +
    • Versuchen Sie, auf Multimedia-Inhalte oder andere nicht textorientierte Dienste zuzugreifen? Prüfen Sie, ob für die Website zusätzliche Anforderungen gelten.
    • +
    • Manche Protokolle benötigen unter Umständen Drittanbieter-Software oder Plugins, damit sie von Browsern erkannt werden.
    • +
    + ]]>
    + + + Datei nicht gefunden + +
  • Könnte der Eintrag umbenannt, gelöscht oder verschoben worden sein?
  • +
  • Enthält die Adresse einen Rechtschreib-, Groß-/Kleinschreibungs- oder anderen Schreibfehler?
  • +
  • Haben Sie ausreichende Zugriffsrechte für den angeforderten Eintrag?
  • + + ]]>
    + + + Zugriff auf die Datei wurde verweigert + +
  • Sie wurde möglicherweise entfernt, verschoben, oder fehlende Dateiberechtigungen könnten den Zugriff verhindern.
  • + + ]]>
    + + + Proxy-Server verweigert die Verbindung + Der Browser ist für die Verwendung eines Proxy-Servers konfiguriert, doch der Proxy verweigert die Verbindung.

    +
      +
    • Stimmt die Proxy-Konfiguration des Browsers? Überprüfen Sie die Einstellungen und versuchen Sie es erneut.
    • +
    • Lässt der Proxy-Dienst es zu, dass aus diesem Netzwerk heraus Verbindungen hergestellt werden?
    • +
    • Bestehen die Probleme weiterhin? Bitten Sie Ihren Netzwerkadministrator oder Internetanbieter um Hilfestellung.
    • +
    + ]]>
    + + + Proxy-Server nicht gefunden + Der Browser ist für die Verwendung eines Proxy-Servers konfiguriert, doch der Proxy konnte nicht gefunden werden.

    +
      +
    • Stimmt die Proxy-Konfiguration des Browsers? Überprüfen Sie die Einstellungen und versuchen Sie es erneut.
    • +
    • Ist der Computer mit einem aktiven Netzwerk verbunden?
    • +
    • Bestehen die Probleme weiterhin? Bitten Sie Ihren Netzwerkadministrator oder Internetanbieter um Hilfestellung.
    • +
    + ]]>
    + + + Malware-Website blockiert + + Die Website auf %1$s wurde als attackierende Website gemeldet und aufgrund Ihrer Sicherheitseinstellungen blockiert.

    ]]>
    + + + Unerwünschte Website blockiert + + Die Website auf %1$s wurde als Lieferant von unerwünschter Software gemeldet und aufgrund Ihrer Sicherheitseinstellungen blockiert.

    + ]]>
    + + + Gefährdende Website blockiert + + Die Website auf %1$s wurde als potenziell gefährdende Seite gemeldet und aufgrund Ihrer Sicherheitseinstellungen blockiert.

    + ]]>
    + + + Betrügerische Website blockiert + + Die Website auf %1$s wurde als betrügerische Website gemeldet und aufgrund Ihrer Sicherheitseinstellungen blockiert.

    + ]]>
    + + + Sichere Website nicht verfügbar + + %1$s verfügbar.]]> + + Weiter zur HTTP-Website +
    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 @@ + + + + + Hyšći raz wopytaś + + + Napšašowanje njedajo se dokóńcyś + + Pśidatne informacije wó toś tom problemje abo zmólce njestoje tuchylu k dispoziciji.

    + ]]>
    + + + Zwisk njejo se raźił + + +
  • Bok, kótaryž cośo se woglědaś, njedajo se pokazaś, dokulaž awtentiskosć dostanych datow njedajo se pśeglědaś.
  • +
  • Pšosym stajśo se z wobsejźarjami websedła do zwiska, aby jich wó toś tym problemje informěrował.
  • + + ]]>
    + + + Zwisk njejo se raźił + + + +
  • To by mógło problem z konfiguraciju serwera byś, abo by mógło byś, až něchten wopytujo serwer imitěrowaś.
  • +
  • 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ś.
  • + + ]]>
    + + + Rozšyrjone… + + Něchten mógał wopytaś, sedło za swójo wudaś, togodla njeměł wy z tym pókšacowaś. +

    + + ]]>
    + + Slědk (dopórucony) + + Riziko akceptěrowaś a pókšacowaś + + + Toś to websedło se wěsty zwisk pomina. + + +
  • Bok, kótaryž se cośo woglědaś, njedajo se pokazaś, dokulaž toś to websedło se wěsty zwisk pomina.
  • +
  • Problem se nejskerjej pśez websedło zawinujo a togodla njedajo nic, což móžośo cyniś, aby jen rozwězał.
  • +
  • Móžośo administratoroju websedła problem k wěsći daś.
  • + + ]]>
    + + + Rozšyrjony… + + + %1$s ma wěstotne pšawidło z mjenim HTTP Strict Transport Security (HSTS), kótarež wóznamjenijo, až %2$s móžo se jano wěsće zwězaś. Njamóžśo wuwześe pśidaś, aby se k toś tomu sedłoju woglědał. + ]]> + + Slědk + + + Zwisk jo se pśetergnuł + + 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.

    +
      +
    • Sedło njejo snaź nachylu k dispoziji abo jo pśeśěžone. Wopytajśo za někotare wokognuśa hyšći raz.
    • +
    • Jolic njamóžośo boki zacytaś, pśeglědajśo daty swójogo rěda abo WLAN-zwisk.
    • +
    + ]]>
    + + + Zwisk jo cas pśekšocył + + + Pominane sedło njejo na zwiskowe napšašowanje wótegroniło a wobglědowak jo pśestał na wótegrono cakaś.

    +
      +
    • Móžo byś, až serwer jo pśeśěžony abo ma nachylne mólenje? Wopytajśo pózdźej hyšći raz.
    • +
    • Njamóžośo druge sedła pśeglědowaś? Pśespytajśo seśowy zwisk rěda.
    • +
    • Šćita se waš rěd abo waša seś z wognjoweju murju abo proksy? Njekorektne nastajenja mógu webowemu pśeglědowanjeju zajźowaś.
    • +
    • Maśo hyšći śěže? Konsultěrujśo swójogo seśowego administratora abo internetnego póbitowarja za pódpěru.
    • +
    ]]>
    + + + Zwisk njejo móžny + + +
  • Sedło njestoj snaź nachylu k dispoziciji abo jo pśeśěžone. Wopytajśo za mało wokognuśow hyšći raz.
  • +
  • Jolic njamóžośo boki zacytaś, pśeglědajśo datowy abo WLAN-zwisk swójogo rěda.
  • + + ]]>
    + + + Njewótčakane wótegrono ze serwera + + 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ś.

    + ]]>
    + + + Bok pšawje njepósrědnja + + + 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.

    +
      +
    • Sćo cookieje znjemóžnił abo zablokěrował, kótarež su trěbne za tós to sedło?
    • +
    • Jolic akceptěrowanje sedłowych cookiejow njerozwězujo ten problem, jo to nejskerjej problem serwereje konfiguracije a nic wašogo rěda.
    • +
    ]]>
    + + + Offline-modus + + + Wobglědowak źěła offline a njamóžo z pominanym objektom zwězaś.

    +
      +
    • Jo rěd z aktiwneju seśu zwězany?
    • +
    • Klikniśo na „Hyšći raz“, aby do online-modusa pśejšeł a bok znowego zacytał.
    • +
    ]]>
    + + + Port z pśicynow wěstoty wobgranicowany + + Pominana adresa jo port pódała (na pś. mozilla.org:80 za port 80 na mozilla.org), kótaryž wužywa se normalnje za druge zaměry nježli webpśeglědowanje. Wobglědowak jo napšašowanje za waš šćit a wěstotu pśetergnuł.

    + ]]>
    + + + Zwisk jo se slědk stajił + + Syśowy zwisk jo se pśetergnuł, mjaztym až sćo wopytał, zwisk nawězaś. Pšosym wopytajśo hyšći raz.

    +
      +
    • Sedło njejo snaź nachylu k dispoziji abo jo pśeśěžone. Wopytajśo za někotare wokognuśa hyšći raz.
    • +
    • Jolic njamóžośo boki zacytaś, pśeglědajśo daty swójogo rěda abo WLAN-zwisk.
    • +
    + ]]>
    + + + Njewěsty datajowy typ + + +
  • Pšosym stejśo se z wobsejźarjami websedła do zwiska, aby je wó toś tom problemje informěrował.
  • + + ]]>
    + + + Zmólka - wobškóźone wopśimjeśe + + Bok, kótaryž cośo se woglědaś, njedajo se pokazaś, dokulaž jo se zmólka pśi pśenosowanju datow namakała.

    +
      +
    • Pšosym stajśo se z wobsejźarjami websedła do zwiska, aby je wó toś tom problemje informěrował.
    • +
    + ]]>
    + + + Wopśimjeśe jo se wowaliło + Bok, kótaryž cośo se woglědaś, njedajo se pokazaś, dokulaž jo se zmólka pśi pśenosowanju datow namakała.

    +
      +
    • Pšosym stajśo se z wobsejźarjami websedła do zwiska, aby je wó toś tom problemje informěrował.
    • +
    + ]]>
    + + + Zmólka pśi koděrowanju wopśimjeśa + Bok, kótaryž cośo se woglědaś, njedajo se pokazaś, dokulaž wužywa njepłaśiwu abo njepódpěranu formu kompresije.

    +
      +
    • Pšosym stajśo se z wobsejźarjami websedła do zwiska, aby je wó toś tom problemje informěrował.
    • +
    + ]]>
    + + + Adresa njejo se namakała + + Wobglědowak njejo mógał hostowy serwer za pódanu adresu namakaś.

    +
      +
    • Pśeglědajśo adresu za pisańskimi zmólkami ako + ww.example.com město + www.example.com.
    • +
    • Jolic njamóžośo boki zacytaś, pśeglědajśo daty swójogo rěda abo WLAN-zwisk.
    • +
    + ]]>
    + + + Žeden internetny zwisk + + Pśeglědajśo swój seśowy zwisk abo zacytajśo bok za mało wokognuśow znowego. + + Znowego zacytaś + + + Njepłaśiwa adresa + 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.

    + ]]>
    + + Adresa njejo płaśiwa + + + +
  • Webadrese se zwětšego ako http://www.example.com/ pišu.
  • +
  • Zawěsććo, až wužywaśo doprědka schylone nakósne smužki (t. gr. /).
  • + + ]]>
    + + + Njeznaty protokol + Adresa pódawa protokol (na pś. wxyz://), kótaryž wobglědowak njepśipóznawa, tak až wobglědowak njamóžo pšawje ze sedłom zwězaś.

    +
      +
    • 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.
    • +
    • Někotare protokole trjebaju programy tśeśich abo tykace, nježli až wobglědowak móžo je pśipóznaś.
    • +
    + ]]>
    + + + Dataja njejo se namakała + +
  • Jo móžno, až objekt jo se pśemjenił, wótpórał abo pśesunuł?
  • +
  • Jo zmólka pšawopisa, wjelikopisanja, abo hynakša typografiska zmólka w adresy?
  • +
  • Maśo dosěgajuce pśistupne pšawa za pominany objekt?
  • + + ]]>
    + + + Pśistup na dataju jo se wótpokazał + +
  • Snaź jo se wótpórała, pśesunuła, abo datajowe pšawa zajźuju pśistupoju.
  • + + ]]>
    + + + Proksyjowy serwer jo zwisk wótpokazał + Wobglědowak jo konfigurěrowany, aby serwer proksy wužywał, ale proksy jo zwisk wótpokazał.

    +
      +
    • Jo konfiguracija proksy wobglědowaka korektna? Pśeglědajśo nastajenja a wopytajśo hyšći raz.
    • +
    • Dowólujo proksyjowa słužba zwiski z toś teje seśi?
    • +
    • Maśo hyšći śěže? Konsultěrujśo swójogo seśowego administratora abo internetnego póbitowarja za pódpěru.
    • +
    + ]]>
    + + + Proksyjowy serwer njenamakany + Wobglědowak jo se konfigurěrował, aby se wužywa proksyjowy serwer, ale proksy njedajo se namakaś.

    +
      +
    • Jo konfiguracija proksyja wobglědowaka korektna? Pśeglědajśo nastajenja a wopytajśo hyšći raz.
    • +
    • Jo rěd z aktiwneju seśu zwězany?
    • +
    • Maśo hyšći śěže? Stajśo ze swójim seśowym administratorom abo internetny póbitowarjom za pódpěru do zwiska.
    • +
    ]]>
    + + + Problem ze sedłom ze škódneju softwaru + + + 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.

    + ]]>
    + + + Problem z njewitanym sedłom + + 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.

    + ]]>
    + + + Problem z wobgrozujucym sedłom + + 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.

    + ]]>
    + + + Problem z wobšudnym sedłom + + 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.

    + ]]>
    + + + Wěste sedło njejo k dispoziciji + + %1$s njejo k dispoziciji.]]> + + Dalej k HTTP-sedłoju +
    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 @@ + + + + + Δοκιμή ξανά + + + Αδυναμία ολοκλήρωσης αιτήματος + + Αυτή τη στιγμή, δεν διατίθενται επιπρόσθετες πληροφορίες σχετικά με αυτό το πρόβλημα ή σφάλμα.

    ]]>
    + + + Αποτυχία ασφαλούς σύνδεσης + + + +
  • Η σελίδα που προσπαθείτε να προβάλετε δεν μπορεί να εμφανιστεί, επειδή δεν ήταν δυνατή η επαλήθευση της αυθεντικότητας των ληφθέντων δεδομένων.
  • +
  • Παρακαλούμε επικοινωνήστε με τους ιδιοκτήτες της ιστοσελίδας για να τους ενημερώσετε για αυτό το πρόβλημα.
  • + ]]>
    + + + Αποτυχία ασφαλούς σύνδεσης + + + +
  • Ενδέχεται να υπάρχει πρόβλημα με τη διαμόρφωση του διακομιστή, ή κάποιος προσπαθεί να μιμηθεί το διακομιστή.
  • +
  • Αν έχετε συνδεθεί με επιτυχία σε αυτό το διακομιστή στο παρελθόν, το σφάλμα ίσως είναι προσωρινό και μπορείτε να δοκιμάσετε ξανά αργότερα.
  • + + ]]>
    + + + Σύνθετα… + + Κάποιος ίσως προσπαθεί να μιμηθεί την ιστοσελίδα και δεν πρέπει να συνεχίσετε. +

    + + ]]>
    + + Επιστροφή (Προτείνεται) + + Αποδοχή κινδύνου και συνέχεια + + + Αυτός ο ιστότοπος απαιτεί ασφαλή σύνδεση. + + + +
  • Δεν είναι δυνατή η εμφάνιση της σελίδας που προσπαθείτε να προβάλετε, επειδή αυτός ο ιστότοπος απαιτεί ασφαλή σύνδεση.
  • +
  • Το ζήτημα οφείλεται κατά πάσα πιθανότητα στον ιστότοπο και δεν μπορείτε να κάνετε τίποτα για να το επιλύσετε.
  • +
  • Μπορείτε να ειδοποιήστε τον διαχειριστή του ιστοτόπου σχετικά με το πρόβλημα.
  • + + ]]>
    + + + Σύνθετα… + + + Το %1$s διαθέτει μια πολιτική ασφαλείας, η οποία ονομάζεται «HTTP Strict Transport Security (HSTS)», που σημαίνει ότι το %2$s μπορεί να συνδεθεί μόνο με ασφάλεια σε αυτό. Δεν μπορείτε να προσθέσετε εξαίρεση για να επισκεφθείτε τον ιστότοπο αυτό. + ]]> + + Επιστροφή + + + Η σύνδεση διακόπηκε + + + Το πρόγραμμα περιήγησης συνδέθηκε με επιτυχία, αλλά η σύνδεση διακόπηκε κατά τη μεταφορά πληροφοριών. Παρακαλούμε προσπαθήστε ξανά.

    +
      +
    • Η σελίδα μπορεί να είναι προσωρινά μη διαθέσιμη ή πολύ απασχολημένη. Δοκιμάστε ξανά σε λίγα λεπτά.
    • +
    • Aν δεν μπορείτε να φορτώσετε καμία σελίδα, ελέγξτε τα δεδομένα ή τη σύνδεση Wi-Fi της συσκευής σας.
    • +
    + ]]>
    + + + Το χρονικό όριο της σύνδεσης έληξε + + + Η ζητούμενη ιστοσελίδα δεν αποκρίθηκε στο αίτημα σύνδεσης και το πρόγραμμα περιήγησης σταμάτησε να περιμένει απάντηση.

    +
      +
    • Μήπως ο διακομιστής αντιμετωπίζει υψηλή ζήτηση ή προσωρινή διακοπή λειτουργίας; Δοκιμάστε ξανά αργότερα.
    • +
    • Δεν μπορείτε να περιηγηθείτε σε άλλες ιστοσελίδες; Ελέγξτε + τη σύνδεση δικτύου της συσκευής σας.
    • +
    • Προστατεύεται η συσκευή ή το δίκτυό σας από τείχος προστασίας ή διακομιστή μεσολάβησης; Οι εσφαλμένες ρυθμίσεις μπορούν να επηρεάσουν την περιήγηση.
    • +
    • Έχετε ακόμη πρόβλημα; Συμβουλευτείτε το διαχειριστή δικτύου ή τον πάροχό σας για βοήθεια.
    • +
    + ]]>
    + + + Αδυναμία σύνδεσης + + + +
  • Η σελίδα ενδέχεται να είναι προσωρινά μη διαθέσιμη ή πολύ απασχολημένη. Δοκιμάστε ξανά σε λίγο.
  • +
  • Αν δεν μπορείτε να φορτώσετε καμία σελίδα, ελέγξτε τη σύνδεση δεδομένων ή Wi-Fi σας.
  • + + ]]>
    + + + Απρόσμενη απάντηση από το διακομιστή + + + Ο ιστότοπος απάντησε με απρόσμενο τρόπο στο αίτημα δικτύου και το πρόγραμμα περιήγησης δεν μπορεί να συνεχίσει.

    + ]]>
    + + + Η σελίδα δεν ανακατευθύνει σωστά + + + Το πρόγραμμα περιήγησης έχει σταματήσει την προσπάθεια ανάκτησης του ζητούμενου στοιχείου. Η ιστοσελίδα ανακατευθύνει το αίτημα με τρόπο που δεν θα ολοκληρωθεί.

    +
      +
    • Έχετε απενεργοποιήσει ή αποκλείσει τα cookie που απαιτούνται από την ιστοσελίδα;
    • +
    • Αν η αποδοχή των cookie δεν επιλύει το πρόβλημα, μάλλον οφείλεται σε ζήτημα ρύθμισης του διακομιστή και όχι στη συσκευή σας.
    • +
    + ]]>
    + + + Λειτουργία εκτός σύνδεσης + + + Το πρόγραμμα περιήγησης είναι σε λειτουργεία εκτός σύνδεσης και δεν μπορεί να συνδεθεί στο ζητούμενο στοιχείο.

    +
      +
    • Είναι συνδεδεμένη η συσκευή σε ενεργό δίκτυο;
    • +
    • Πατήστε “Δοκιμή ξανά” για σύνδεση στο διαδίκτυο και ανανέωση της σελίδας.
    • +
    + ]]>
    + + + Περιορισμός θύρας για λόγους ασφαλείας + + Η ζητούμενη διεύθυνση καθόρισε θύρα (π.χ., mozilla.org:80 για τη θύρα 80 στο mozilla.org) που δεν χρησιμοποιείται συνήθως για περιήγηση στο διαδίκτυο. Το πρόγραμμα περιήγησης ακύρωσε το αίτημα για να σας προστατεύσει.

    + ]]>
    + + + Έγινε επαναφορά της σύνδεσης + + + Ο σύνδεσμος δικτύου διακόπηκε κατά τη διαδικασία σύνδεσης. Παρακαλούμε δοκιμάστε ξανά.

    +
      +
    • Η ιστοσελίδα ίσως είναι προσωρινά μη διαθέσιμη ή πολύ απασχολημένη. Δοκιμάστε ξανά σε λίγο.
    • +
    • Αν δεν μπορείτε να φορτώσετε καμία σελίδα, ελέγξτε τη σύνδεση δεδομένων ή Wi-Fi της συσκευής σας.
    • +
    + ]]>
    + + + Μη ασφαλής τύπος αρχείου + + + +
  • Παρακαλώ επικοινωνήστε με τους ιδιοκτήτες του ιστοτόπου για να τους ενημερώσετε σχετικά με αυτό το πρόβλημα.
  • + + ]]>
    + + + Σφάλμα κατεστραμμένου περιεχομένου + + + Η σελίδα που προσπαθείτε να δείτε δεν μπορείτε να εμφανιστεί λόγω σφάλματος κατά τη μεταγωγή δεδομένων.

    +
      +
    • Παρακαλούμε ενημερώστε τους ιδιοκτήτες της ιστοσελίδας για αυτό το πρόβλημα.
    • +
    + ]]>
    + + + Το περιεχόμενο καταστράφηκε + + Η σελίδα που προσπαθείτε να δείτε δεν μπορείτε να εμφανιστεί λόγω σφάλματος κατά τη μεταγωγή δεδομένων.

    +
      +
    • Παρακαλούμε ενημερώστε τους ιδιοκτήτες της ιστοσελίδας για αυτό το πρόβλημα.
    • +
    + ]]>
    + + + Σφάλμα κωδικοποίησης περιεχομένου + + Η σελίδα που προσπαθείτε να δείτε δεν μπορεί να εμφανιστεί επειδή δεν χρησιμοποιεί έγκυρη ή υποστηριζόμενη μορφή συμπίεσης.

    +
      +
    • Παρακαλώ επικοινωνήστε με τους ιδιοκτήτες του ιστοτόπου για να τους ενημερώσετε σχετικά με αυτό το πρόβλημα.
    • +
    + ]]>
    + + + Η διεύθυνση δεν βρέθηκε + + + Το πρόγραμμα περιήγησης δεν βρήκε τον κεντρικό διακομιστή για την παρεχόμενη διεύθυνση.

    +
      +
    • Ελέγξτε τη διεύθυνση για τυχόν λάθη, όπως + ww.example.com αντί για + www.example.com.
    • +
    • Αν δεν μπορείτε να φορτώσετε καμία σελίδα, ελέγξτε τα δεδομένα ή τη σύνδεση Wi-Fi της συσκευής σας.
    • +
    + ]]>
    + + + Δεν υπάρχει σύνδεση στο διαδίκτυο + + Ελέγξτε τη σύνδεση δικτύου σας ή δοκιμάστε να φορτώσετε εκ νέου τη σελίδα σε λίγα λεπτά. + + Ανανέωση + + + Μη έγκυρη διεύθυνση + Η διεύθυνση δεν είναι σε αναγνωρισμένη μορφή. Παρακαλούμε ελέγξτε τη γραμμή διευθύνσεων για λάθη και δοκιμάστε ξανά.

    ]]>
    + + Η διεύθυνση δεν είναι έγκυρη + + + +
  • Οι διευθύνσεις ιστού γράφονται συνήθως ως εξής: http://www.example.com/
  • +
  • Βεβαιωθείτε ότι χρησιμοποιείτε τις σωστές καθέτους (δηλ. /).
  • + ]]>
    + + + Άγνωστο πρωτόκολλο + + Η διεύθυνση καθορίζει ένα πρωτόκολλο (π.χ. «wxyz://») που δεν αναγνωρίζει το πρόγραμμα περιήγησης, επομένως δεν είναι δυνατή η σωστή σύνδεση στον ιστότοπο.

    +
      +
    • Προσπαθείτε να προσπελάσετε πολυμέσα ή άλλες υπηρεσίες χωρίς κείμενο; Ελέγξτε τον ιστότοπο για επιπλέον απαιτήσεις.
    • +
    • Ορισμένα πρωτόκολλα ενδέχεται να απαιτούν λογισμικό ή αρθρώματα τρίτων πριν μπορέσουν να αναγνωριστούν από το πρόγραμμα περιήγησης.
    • +
    + ]]>
    + + + Το αρχείο δεν βρέθηκε + + +
  • Μήπως το στοιχείο έχει μετονομαστεί, αφαιρεθεί, ή μετακινηθεί;
  • +
  • Υπάρχουν σφάλματα ορθογραφίας, κεφαλαίων, ή άλλα τυπογραφικά λάθη στη διεύθυνση;
  • +
  • Έχετε επαρκή δικαιώματα για πρόσβαση στο ζητούμενο στοιχείο;
  • + + ]]>
    + + + Άρνηση πρόσβασης στο αρχείο + + +
  • Ίσως έχει αφαιρεθεί, μετακινηθεί ή τα δικαιώματα αρχείου εμποδίζουν την πρόσβαση.
  • + ]]>
    + + + Άρνηση σύνδεσης διαμεσολαβητή + + Το πρόγραμμα περιήγησης έχει ρυθμιστεί ώστε να χρησιμοποιεί διακομιστή μεσολάβησης, αλλά ο διακομιστής μεσολάβησης αρνήθηκε τη σύνδεση.

    +
      +
    • Έχει ρυθμιστεί σωστά ο διακομιστής μεσολάβησης; Ελέγξτε τις ρυθμίσεις και δοκιμάστε ξανά.
    • +
    • Επιτρέπει ο διακομιστής μεσολάβησης συνδέσεις από αυτό το δίκτυο;
    • +
    • Έχετε ακόμα πρόβλημα; Συμβουλευτείτε το διαχειριστή δικτύου ή τον πάροχό σας για βοήθεια.
    • +
    + ]]>
    + + + Δεν βρέθηκε διακομιστής μεσολάβησης + + Το πρόγραμμα περιήγησης έχει ρυθμιστεί ώστε να χρησιμοποιεί διακομιστή μεσολάβησης, αλλά δεν βρέθηκε διακομιστής μεσολάβησης.

    +
      +
    • Έχει ρυθμιστεί σωστά ο διακομιστής μεσολάβησης; Ελέγξτε τις ρυθμίσεις και δοκιμάστε ξανά.
    • +
    • Είναι συνδεδεμένη η συσκευή σε ενεργό δίκτυο;
    • +
    • Έχετε ακόμη πρόβλημα; Συμβουλευτείτε το διαχειριστή δικτύου σας ή τον πάροχό σας για βοήθεια.
    • +
    + ]]>
    + + + Ζήτημα κακόβουλου ιστοτόπου + + + Ο ιστότοπος στο %1$s έχει αναφερθεί ως ιστότοπος επιθέσεων και έχει αποκλειστεί βάσει των προτιμήσεων ασφαλείας σας.

    + ]]>
    + + + Ζήτημα ανεπιθύμητου ιστοτόπου + + Ο ιστότοπος στο %1$s έχει αναφερθεί για διανομή ανεπιθύμητου λογισμικού και έχει αποκλειστεί βάσει των προτιμήσεων ασφαλείας σας.

    + ]]>
    + + + Ζήτημα επιβλαβούς ιστοτόπου + + + Ο ιστότοπος στο %1$s έχει αναφερθεί ως δυνητικά επιβλαβής και έχει αποκλειστεί βάσει των προτιμήσεων ασφαλείας σας.

    + ]]>
    + + + Ζήτημα παραπλανητικού ιστοτόπου + + Η ιστοσελίδα στο %1$s έχει αναφερθεί ως παραπλανητικός ιστότοπος και έχει αποκλειστεί βάσει των προτιμήσεων ασφαλείας σας.

    + ]]>
    + + + Δεν διατίθεται ασφαλής ιστότοπος + + %1$s.]]> + + Συνέχεια σε ιστότοπο HTTP +
    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 @@ + + + + + Try Again + + + Cannot Complete Request + + Additional information about this problem or error is currently unavailable.

    + ]]>
    + + + Secure Connection Failed + + +
  • The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.
  • +
  • Please contact the website owners to inform them of this problem.
  • + + ]]>
    + + + Secure Connection Failed + + +
  • This could be a problem with the server’s configuration, or it could be someone trying to impersonate the server.
  • +
  • If you have connected to this server successfully in the past, the error may be temporary, and you can try again later.
  • + + ]]>
    + + + Advanced… + + Someone could be trying to impersonate the site and you should not continue. +

    + + ]]>
    + + Go Back (Recommended) + + Accept the Risk and Continue + + + This website requires a secure connection. + + + +
  • The page you are trying to view cannot be shown because this website requires a secure connection.
  • +
  • The issue is most likely with the website, and there is nothing you can do to resolve it.
  • +
  • You can notify the website’s administrator about the problem.
  • + + ]]>
    + + + Advanced… + + + %1$s has a security policy called HTTP Strict Transport Security (HSTS), which means that %2$s can only connect to it securely. You can’t add an exception to visit this site. + ]]> + + Go Back + + + The connection was interrupted + + The browser connected successfully, but the connection was interrupted while transferring information. Please try again.

    +
      +
    • The site could be temporarily unavailable or too busy. Try again in a few moments.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + The connection has timed out + + The requested site did not respond to a connection request and the browser has stopped waiting for a reply.

    +
      +
    • Could the server be experiencing high demand or a temporary outage? Try again later.
    • +
    • Are you unable to browse other sites? Check the device’s network connection.
    • +
    • Is your device or network protected by a firewall or proxy? Incorrect settings can interfere with Web browsing.
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    ]]>
    + + + Unable to connect + + +
  • The site could be temporarily unavailable or too busy. Try again in a few moments.
  • +
  • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
  • + + ]]>
    + + + Unexpected response from server + + The site responded to the network request in an unexpected way and the browser cannot continue.

    + ]]>
    + + + The page isn’t redirecting properly + + The browser has stopped trying to retrieve the requested item. The site is redirecting the request in a way that will never complete.

    +
      +
    • Have you disabled or blocked cookies required by this site?
    • +
    • If accepting the site’s cookies does not resolve the problem, it is likely a server configuration issue and not your device.
    • +
    ]]>
    + + + Offline Mode + + The browser is operating in its offline mode and cannot connect to the requested item.

    +
      +
    • Is the device connected to an active network?
    • +
    • Press “Try Again” to switch to online mode and reload the page.
    • +
    ]]>
    + + + Port restricted for security reasons + + The requested address specified a port (e.g., mozilla.org:80 for port 80 on mozilla.org) normally used for purposes other than Web browsing. The browser has cancelled the request for your protection and security.

    + ]]>
    + + + The connection was reset + + The network link was interrupted while negotiating a connection. Please try again.

    +
      +
    • The site could be temporarily unavailable or too busy. Try again in a few moments.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + Unsafe File Type + + +
  • Please contact the website owners to inform them of this problem.
  • + + ]]>
    + + + Corrupted Content Error + + The page you are trying to view cannot be shown because an error in the data transmission was detected.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Content crashed + The page you are trying to view cannot be shown because an error in the data transmission was detected.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Content Encoding Error + The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Address Not Found + + The browser could not find the host server for the provided address.

    +
      +
    • Check the address for typing errors such as + ww.example.com instead of + www.example.com.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + No internet connection + + Check your network connection or try reloading the page in a few moments. + + Reload + + + Invalid Address + The provided address is not in a recognized format. Please check the location bar for mistakes and try again.

    + ]]>
    + + The address isn’t valid + + +
  • Web addresses are usually written like http://www.example.com/
  • +
  • Make sure that you’re using forward slashes (i.e. /).
  • + + ]]>
    + + + Unknown Protocol + The address specifies a protocol (e.g., wxyz://) the browser does not recognize, so the browser cannot properly connect to the site.

    +
      +
    • Are you trying to access multimedia or other non-text services? Check the site for extra requirements.
    • +
    • Some protocols may require third-party software or plugins before the browser can recognize them.
    • +
    + ]]>
    + + + File Not Found + +
  • Could the item have been renamed, removed, or relocated?
  • +
  • Is there a spelling, capitalization, or other typographical error in the address?
  • +
  • Do you have sufficient access permissions to the requested item?
  • + + ]]>
    + + + Access to the file was denied + +
  • It may have been removed, moved, or file permissions may be preventing access.
  • + + ]]>
    + + + Proxy Server Refused Connection + The browser is configured to use a proxy server, but the proxy refused a connection.

    +
      +
    • Is the browser’s proxy configuration correct? Check the settings and try again.
    • +
    • Does the proxy service allow connections from this network?
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    + ]]>
    + + + Proxy Server Not Found + The browser is configured to use a proxy server, but the proxy could not be found.

    +
      +
    • Is the browser’s proxy configuration correct? Check the settings and try again.
    • +
    • Is the device connected to an active network?
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    ]]>
    + + + Malware site issue + + The site at %1$s has been reported as an attack site and has been blocked based on your security preferences.

    + ]]>
    + + + Unwanted site issue + + The site at %1$s has been reported as serving unwanted software and has been blocked based on your security preferences.

    + ]]>
    + + + Harmful site issue + + The site at %1$s has been reported as a potentially harmful site and has been blocked based on your security preferences.

    + ]]>
    + + + Deceptive site issue + + This web page at %1$s has been reported as a deceptive site and has been blocked based on your security preferences.

    + ]]>
    + + + Secure Site Not Available + + %1$s is not available.]]> + + Continue to HTTP Site +
    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 @@ + + + + + Try Again + + + Cannot Complete Request + + Additional information about this problem or error is currently unavailable.

    + ]]>
    + + + Secure Connection Failed + + +
  • The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.
  • +
  • Please contact the web site owners to inform them of this problem.
  • + + ]]>
    + + + Secure Connection Failed + + +
  • This could be a problem with the server’s configuration, or it could be someone trying to impersonate the server.
  • +
  • If you have connected to this server successfully in the past, the error may be temporary, and you can try again later.
  • + + ]]>
    + + + Advanced… + + Someone could be trying to impersonate the site and you should not continue. +

    + + ]]>
    + + Go Back (Recommended) + + Accept the Risk and Continue + + + This web site requires a secure connection. + + +
  • The page you are trying to view cannot be shown because this web site requires a secure connection.
  • +
  • The issue is most likely with the web site, and there is nothing you can do to resolve it.
  • +
  • You can notify the web site’s administrator about the problem.
  • + + ]]>
    + + + Advanced… + + + %1$s has a security policy called HTTP Strict Transport Security (HSTS), which means that %2$s can only connect to it securely. You can’t add an exception to visit this site. + ]]> + + Go Back + + + The connection was interrupted + + The browser connected successfully, but the connection was interrupted while transferring information. Please try again.

    +
      +
    • The site could be temporarily unavailable or too busy. Try again in a few moments.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + The connection has timed out + + The requested site did not respond to a connection request and the browser has stopped waiting for a reply.

    +
      +
    • Could the server be experiencing high demand or a temporary outage? Try again later.
    • +
    • Are you unable to browse other sites? Check the device’s network connection.
    • +
    • Is your device or network protected by a firewall or proxy? Incorrect settings can interfere with Web browsing.
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    ]]>
    + + + Unable to connect + + +
  • The site could be temporarily unavailable or too busy. Try again in a few moments.
  • +
  • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
  • + + ]]>
    + + + Unexpected response from server + + The site responded to the network request in an unexpected way and the browser cannot continue.

    + ]]>
    + + + The page isn’t redirecting properly + + The browser has stopped trying to retrieve the requested item. The site is redirecting the request in a way that will never complete.

    +
      +
    • Have you disabled or blocked cookies required by this site?
    • +
    • If accepting the site’s cookies does not resolve the problem, it is likely a server configuration issue and not your device.
    • +
    ]]>
    + + + Offline Mode + + The browser is operating in its offline mode and cannot connect to the requested item.

    +
      +
    • Is the device connected to an active network?
    • +
    • Press “Try Again” to switch to online mode and reload the page.
    • +
    ]]>
    + + + Port restricted for security reasons + + + The requested address specified a port (e.g., mozilla.org:80 for port 80 on mozilla.org) normally used for purposes other than Web browsing. The browser has cancelled the request for your protection and security.

    + ]]>
    + + + The connection was reset + + The network link was interrupted while negotiating a connection. Please try again.

    +
      +
    • The site could be temporarily unavailable or too busy. Try again in a few moments.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + Unsafe File Type + + +
  • Please contact the web site owners to inform them of this problem.
  • + + ]]>
    + + + Corrupted Content Error + + The page you are trying to view cannot be shown because an error in the data transmission was detected.

    +
      +
    • Please contact the web site owners to inform them of this problem.
    • +
    + ]]>
    + + + Content crashed + The page you are trying to view cannot be shown because an error in the data transmission was detected.

    +
      +
    • Please contact the web site owners to inform them of this problem.
    • +
    + ]]>
    + + + Content Encoding Error + The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.

    +
      +
    • Please contact the web site owners to inform them of this problem.
    • +
    + ]]>
    + + + Address Not Found + + The browser could not find the host server for the provided address.

    +
      +
    • Check the address for typing errors such as + ww.example.com instead of + www.example.com.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + No internet connection + + Check your network connection or try reloading the page in a few moments. + + Reload + + + Invalid Address + The provided address is not in a recognised format. Please check the location bar for mistakes and try again.

    + ]]>
    + + The address isn’t valid + + +
  • Web addresses are usually written like http://www.example.com/
  • +
  • Make sure that you’re using forward slashes (i.e. /).
  • + + ]]>
    + + + Unknown Protocol + The address specifies a protocol (e.g., wxyz://) the browser does not recognise, so the browser cannot properly connect to the site.

    +
      +
    • Are you trying to access multimedia or other non-text services? Check the site for extra requirements.
    • +
    • Some protocols may require third-party software or plugins before the browser can recognise them.
    • +
    + ]]>
    + + + File Not Found + +
  • Could the item have been renamed, removed, or relocated?
  • +
  • Is there a spelling, capitalisation, or other typographical error in the address?
  • +
  • Do you have sufficient access permissions to the requested item?
  • + + ]]>
    + + + Access to the file was denied + +
  • It may have been removed, moved, or file permissions may be preventing access.
  • + + ]]>
    + + + Proxy Server Refused Connection + The browser is configured to use a proxy server, but the proxy refused a connection.

    +
      +
    • Is the browser’s proxy configuration correct? Check the settings and try again.
    • +
    • Does the proxy service allow connections from this network?
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    + ]]>
    + + + Proxy Server Not Found + The browser is configured to use a proxy server, but the proxy could not be found.

    +
      +
    • Is the browser’s proxy configuration correct? Check the settings and try again.
    • +
    • Is the device connected to an active network?
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    ]]>
    + + + Malware site issue + + The site at %1$s has been reported as an attack site and has been blocked based on your security preferences.

    + ]]>
    + + + Unwanted site issue + + The site at %1$s has been reported as serving unwanted software and has been blocked based on your security preferences.

    + ]]>
    + + + Harmful site issue + + The site at %1$s has been reported as a potentially harmful site and has been blocked based on your security preferences.

    + ]]>
    + + + Deceptive site issue + + This web page at %1$s has been reported as a deceptive site and has been blocked based on your security preferences.

    + ]]>
    + + + Secure Site Not Available + + %1$s is not available.]]> + + Continue to HTTP Site +
    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 @@ + + + + + Klopodi denove + + + Ne eblas kompletigi la peton + + + Ne disponelbas pli da informo pri tiu ĉi problemo aŭ eraro.

    ]]>
    + + + Malsukcesa sekura konekto + + +
  • La paĝo, kiun vi klopodas vidi, ne povas esti montrita ĉar ne eblis kontroli la aŭtentikecon de la ricevitaj datumoj.
  • +
  • Bonvolu kontakti la posedantojn de la retejo por raporti al ili tiun ĉi problemon.
  • +]]>
    + + + Malsukcesa sekura konekto + + + +
  • Tio povus esti problemo en la agordo de la servilo, aŭ iu kiu klopodas preni la identecon de la servilo.
  • +
  • Se vi iam jam sukcese konektiĝis al tiu ĉi servilo, +la eraro povas esti tempa kaj vi povos klopodi denove poste.
  • +]]>
    + + + Spertula… + + + Iu eble klopodas ŝajnigi la retejon, vi ne devus daŭrigi. +

    + ]]>
    + + Reen (rekomendita) + + Akcepti la riskon kaj daŭrigi + + + Tiu ĉi retejo postulas sekuran konekton. + + +
  • La paĝo, kiun vi volas vidi, ne povas esti montrita ĉar la retejo postulas sekuran konekton.
  • +
  • La problemo plej verŝajne okazas en la retejo, kaj vi nenion povas fari por solvi ĝin.
  • +
  • Vi povas tamen sciigi la administranton de la retejo pri la problemo.
  • + + ]]>
    + + + Spertula… + + + %1$s havas sekurecan politikon, kiun oni nomas HTTP Strict Transport Security (HSTS), kiu signifas ke %2$s nur povas konektiĝi per sekura konekto. Vi ne povas aldoni escepton por viziti la retejon. + ]]> + + Iri reen + + + La konekto estis interrompita + + + La retumilo sukcese konektiĝis, sed la konekto estis interrompita dum transmeto de informoj. Bonvolu provi denove.

    +
      +
    • Tiu ĉi retejo povus esti provizore ne atingebla aŭ tro okupata. Klopodu denove post kelkaj momentoj.
    • +
    • Se vi ne kapablas ŝargi iun ajn paĝon, kontrolu la aparatan datuman aŭ sendratan (Wi-Fi) konekton.
    • +
    ]]>
    + + + Limtempo por konekto atingita + + La petita retejo ne respondis la konektan peton kaj la retumilo ĉesis atendi respondon.

    +
      +
    • Ĉu eble eble la servilo estas tro okupata aŭ provizore malaktiva? Reprovu poste.
    • +
    • Ĉu ankaŭ aliajn retejojn vi ne povas viziti? Kontrolu la aparatan retaliron.
    • +
    • Ĉu via komputilo aŭ reto estas protektataj de retbarilo aŭ retperanto? Malĝustaj agordoj povas malhelpi retumon.
    • +
    • Ĉu ankoraŭ estas problemoj? Petu helpon al via reta administranto aŭ retprovizanto.
    • +
    ]]>
    + + + Ne eblas konektiĝi + + +
  • La retejo povus esti nuntempe ne alirebla aŭ tro okupata. Bonvolu reprovi post iom da tempo.
  • +
  • Se vi ne povas viziti iun ajn paĝon, kontrolu la datuman aŭ sendratan (Wi-Fi) konekton de via poŝaparato.
  • + ]]>
    + + + Neatendita respondo el servilo + + La retejo respondis la retpeton en maniero neatendita kaj la retumilo ne povas daŭrigi.

    ]]>
    + + + La paĝo ne redirektiĝas bone + + La retumilo finis la klopodon akiri la petitan elementon. La retejo redirektas la peton tiamaniere ke ĝi neniam estos finita.

    +
      +
    • Ĉu vi malpermesis aŭ blokis kuketojn kiuj estas postulataj de tiu ĉi retejo?
    • +
    • 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.
    • +
    ]]>
    + + + Malkonektita reĝimo + + La retumilo funkcias nun en malkonektita reĝimo kaj ne povas konektiĝi al la petita elemento.

    +
      +
    • Ĉu la aparato estas konektita al aktiva reto?
    • +
    • Bonvolu premi “Klopodi denove” por iri al konektita reĝimo kaj reŝargi la paĝon.
    • +
    ]]>
    + + + Aliro al pordo limigita pro sekurecaj kialoj + + La petita retadreso enhavas pordon (e.g. mozilla.org:80 por pordo 80 en mozilla.org) kiu normale ne estas uzata por retumado sed por aliaj celoj. La retlegilo, celante vian sekurecon kaj protekton, nuligis vian peton.

    ]]>
    + + + La konekto esti rekomencita + + + La retaliro estis interrompita dum la konekto. Bonvolu klopodi denove.

    +
      +
    • La retejo povus esti provizore ne disponebla aŭ tro okupata. Klopodu denove post kelkaj momentoj.
    • +
    • Se vi ne kapablas ŝargi iun ajn paĝon, kontrolu la aparatan datuman aŭ sendratan (Wi-Fi) konekton.
    • +
    ]]>
    + + + Nesekura tipo de dosiero + + +
  • Bonvolu kontakti la retejajn posedantojn por raporti al ili tiun ĉi problemon.
  • + ]]>
    + + + Eraro pro difektita enhavo + + La paĝo, kiun vi klopodas vidi, ne povas esti montrita ĉar okazis eraro dum la transmeto de datumoj.

    • Bonvolu kontakti la posedantojn de la retejo por raporti al ili tiun ĉi problemon.
    ]]>
    + + + La enhavo paneis + + La paĝo, kiun vi klopodas vidi, ne povas esti montrita ĉar okazis eraro dum la transmeto de datumoj.

    • Bonvolu kontakti la posedantojn de la retejo por raporti al ili tiun ĉi problemon.
    ]]>
    + + + Eraro de enkodigo de enhavo + La paĝo, kiun vi klopodas vidi, ne povas esti montrita ĉar ĝi uzas nevalidan aŭ nesubtenatan kompaktigon.

    +
      +
    • Bonvolu kontakti la posedantojn de la retejo por raporti al ili tiun ĉi problemon.
    • +
    ]]>
    + + + Adreso ne trovita + + + La retumilo ne povis trovi la servilon asociata al la adreso indikita.

    +
      +
    • Kontrolu la adreson por vidi ĉu estas tajperaro, ekzemple + ww.example.com anstataŭ + www.example.com.
    • +
    • Se vi ne kapablas ŝargi iun ajn paĝon, kontrolu la aparatan datuman aŭ sendratan (Wi-Fi) konekton.
    • +
    ]]>
    + + + Sen retaliro + + Kontrolu vian retaliron kaj klopodu reŝargi la paĝon post kelkaj momentoj. + + Reŝargi + + + Nevalida adreso + La provizita adreso havas formaton nerekoneblan. Bonvolu kontroli ĉu en la adresa strio io estas mistajpita kaj klopodu denove.

    ]]>
    + + La adreso ne estas valida + + + +
  • Retadresoj estas kutime ĉi tiel skribitaj http://www.example.com/
  • +
  • Estu certa uzi la ĝustajn oblikvajn strekojn (i.e. /).
  • + ]]>
    + + + Nekonata protokolo + + La adreso specifas protokolon (ekzemple wxyz://) ne rekonata de la retumilo, kiu ne povas do taŭge konektiĝi al la retejo.

    +
      +
    • Ĉu vi klopodas aliri aŭdvidaĵon aŭ alian servon ne tekstan? Kontrolu la retejon por scii ĉu estas apartaj postuloj.
    • +
    • Kelkaj protokoloj povas postuli apartajn programojn aŭ kromprogramojn antaŭ ol la retumilo povos rekoni ilin.
    • +
    ]]>
    + + + Dosiero ne trovita + + +
  • Ĉu eble la elemento estis renomita, forigita aŭ translokita?
  • +
  • Ĉu estas literuma, majuskliga aŭ alia tajperaro en la adreso?
  • +
  • Ĉu vi havas sufiĉajn rajtojn aliri la petitan elementon?
  • + ]]>
    + + + Rifuzita aliro al dosiero + + +
  • Eble ĝi estis forigita, aŭ movita, aŭ la permesoj dosieraj evitas aliron al ĝi.
  • + ]]>
    + + + Rifuzita konekto al retperanto + + La retumilaj agordoj indikas uzon de retperanto, sed la retperanto rifuzis la konekton.

    +
      +
    • Ĉu la retumilaj agordoj pri retperanto estas ĝustaj? Kontrolu la agordojn kaj klopodu denove.
    • +
    • Ĉu la retperanta servo akceptas konektojn el tiu ĉi retejo?
    • +
    • Ĉu ankoraŭ estas problemoj? Kontaktu vian retan administraton aŭ retprovizanton por ricevi helpon.
    • +
    ]]>
    + + + Retperanto ne trovita + + La retumilaj agordoj indikas uzon de retperanto, sed la retperanto ne estis trovita.

    +
      +
    • Ĉu la retumilaj agordoj pri retperanto estas ĝustaj? Kontrolu la agordojn kaj klopodu denove.
    • +
    • Ĉu la aparato estas konektita al aktiva reto?
    • +
    • Ĉu ankoraŭ estas problemoj? Kontaktu vian retan administraton aŭ retprovizanton por ricevi helpon.
    • +
    ]]>
    + + + Problemo kun retejo kiu enhavas malicajn programojn + + + La retejo ĉe %1$s estis denuncita kiel ataka retejo, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.

    ]]>
    + + + Problemo kun programtruda retejo + + + La retejo ĉe %1$s estis denuncita kiel programtruda, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.

    ]]>
    + + + Problemo kun danĝera retejo + + + La retejo ĉe %1$s estis denuncita kiel eble danĝera retejo, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.

    ]]>
    + + + Problemo kun trompa retejo + + Tiu ĉi paĝo ĉe %1$s estis denuncita kiel trompa retejo, kaj ĝi estis blokita surbaze de viaj sekurecaj preferoj.

    ]]>
    + + + Sekura retejo ne disponebla + + %1$s ne estas disponebla.]]> + + Daŭrigi al retejo HTTP +
    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 @@ + + + + + Probar de nuevo + + + No se puede completar el pedido + + En este momento no hay información adicional disponible para este problema o error.

    + ]]>
    + + + Falló la conexión segura + + +
  • La página que estás intentando ver no se puede mostrar porque no se pudo verificar la autenticidad de los datos recibidos.
  • +
  • Por favor contactate con los propietarios del sitio web para informarles de este problema.
  • + ]]>
    + + + Falló la conexión segura + + +
  • Esto podría ser un problema con la configuración del servidor o podría ser alguien tratando de hacerse pasar por el servidor.
  • +
  • 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.
  • + +]]>
    + + + Avanzadas… + + + Alguien podría estar intentando imitar el sitio y no deberías continuar. +        

    +         +    ]]>
    + + Retroceder (recomendado) + + Aceptar el riesgo y continuar + + + Este sitio web requiere una conexión segura. + + +
  • La página que intentás ver no se puede mostrar porque este sitio web requiere una conexión segura.
  • +
  • Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que puedas hacer para resolverlo.
  • +
  • Podés avisarle al administrador del sitio web sobre el problema.
  • + + ]]>
    + + + Avanzadas… + + + %1$s tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), lo que significa que %2$s solo puede conectarse de forma segura. No podés añadir una excepción para visitar este sitio.]]> + + Retroceder + + + Se interrumpió la conexión + + El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería la información. Volvé a probar.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Volvé a probar en unos minutos.
    • +
    • Si no podés cargar ninguna página, revisa la conexión wifi o de datos de tu dispositivo móvil.
    • +
    + ]]>
    + + + La conexión tardó demasiado tiempo + + El sitio solicitado no respondió a una pedido de conexión y el navegador dejó de esperar una respuesta.

    +
      +
    • ¿El servidor podría estar experimentando una alta demanda o un corte temporal? Volvé a probar más tarde.
    • +
    • ¿No podés navegar por otros sitios? Verificá la conexión de red del dispositivo.
    • +
    • ¿Tu red o dispositivo está protegido por un firewall o un proxy? Una configuración incorrecta puede interferir con la navegación web.
    • +
    • ¿Todavía tenés problemas? Consultá con el administrador de la red o el proveedor de Internet para obtener asistencia técnica.
    • +
    ]]>
    + + + No se puede conectar + + + +
  • El sitio puede estar temporariamente inaccesible o demasiado ocupado. Intentá nuevamente en un rato.
  • +
  • Si no podés cargar ninguna página, verificá la conexión de datos o Wi-Fi de tu dispositivo.
  • + ]]>
    + + + Respuesta inesperada del servidor + + El sitio respondió al pedido de la red de una forma inesperada y el navegador no puede continuar.

    ]]>
    + + + La página no se redirecciona correctamente + + + El navegador dejó de tratar de transferir el ítem solicitado. El sitio está redirigiendo el pedido en una manera que nunca se completará.

    +
      +
    • ¿Deshabilitaste o bloqueaste cookies requeridas por este sitio?
    • +
    • Si aceptar las cookies del sitio no resuelve el problema, seguramente es un problema de configuración del servidor y no de tu dispositivo.
    • + +
    ]]>
    + + + Modo sin conexión + + + El navegador está funcionando en el modo sin conexión y no puede conectarse al ítem solicitado.

    • ¿El dispositivo está conectado a una red activa?
    • Presioná “Intentar de nuevo” para volver al modo con conexión y recargar la página.
    ]]>
    + + + Restricción del puerto por razones de seguridad + + La dirección solicitada especificaba un puerto (p. ej., mozilla.org:80 para el puerto 80 de mozilla.org) que suele usarse para propósitos distintos a navegar por Internet. El navegador canceló la solicitud para tu protección y seguridad.

    + ]]>
    + + + Se restableció la conexión + + + El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería la información. Volvé a probar.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Volvé a probar en unos minutos.
    • +
    • Si no podés cargar ninguna página, revisá la conexión wifi o de datos de tu dispositivo móvil.
    • +
    ]]>
    + + + Tipo de archivo no seguro + + + +
  • Contactate con los propietarios del sitio web para informarles de este problema.
  • + + ]]>
    + + + Error de contenido corrupto + + + La página que estás tratando de ver no puede ser mostrada porque se detectó un error en la transmisión de datos.

    +
      +
    • Contactá a los dueños del sitio web para informarles sobre este problema.
    • +
    + ]]>
    + + + El contenido falló + + La página que estás tratando de ver no puede ser mostrada porque se detectó un error en la transmisión de datos.

    +
      +
    • Contactá a los dueños del sitio web para informarles sobre este problema.
    • +
    + ]]>
    + + + Error de codificación de contenido + + La página que estás tratando de ver no puede mostrarse porque usa una forma de compresión inválida o no soportada.

    +
      +
    • Contactá a los dueños del sitio web para informarles sobre este problema.
    • + +
    + ]]>
    + + + No se encontró la dirección + + + El navegador no pudo encontrar el servidor de la dirección provista.

    +
      +
    • Verificá si la dirección no tiene errores de tipeo como + ww.example.com en lugar de + www.example.com.
    • +
    • Si no podés cargar ninguna página, verificá la conexión de datos o Wi-Fi de tu dispositivo.
    • +
    + ]]>
    + + + Sin conexión a Internet + + Verificá tu conexión a la red o intentá volver a cargar la página en un ratito. + + Recargar + + + La dirección no es válida + La dirección provista no tiene un formato reconocible. Mirá si no hay errores en la barra de direcciones e intentá nuevamente.

    + ]]>
    + + La dirección no es válida + + + +
  • Usualmente las direcciones web se escriben como http://www.example.com/
  • +
  • Asegurate de estar usando las barras correctas (ej: /).
  • + + ]]>
    + + + Protocolo desconocido + + La dirección especifica un protocolo (ej: wxyz://) que el navegador no reconoce, así que no puede conectarse adecuadamente al sitio.

    +
      +
    • ¿Estás tratando de acceder a multimedia o a otros servicios que no son de texto? Verifiá el sitio para requerimientos extra.
    • +
    • Algunos protocolos pueden requerir software de terceros o plugins antes que el navegador pueda reconocerlos.
    • +
    + ]]>
    + + + No se encontró el archivo + + +
  • ¿Puede ser que el ítem haya sido renombrado, removido o reubicado?
  • +
  • ¿Hay un error de ortografía, mayúsculas o algún error tipográfico en la dirección?
  • +
  • ¿Tenés suficientes permisos de acceso al ítem solicitado?
  • + + ]]>
    + + + Se denegó el acceso al archivo + + +
  • Puede haber sido eliminado, movido o los permisos del archivo pueden evitar el acceso.
  • + + ]]>
    + + + El servidor proxy rechazó la conexión + + El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.

    +
      +
    • ¿Es correcta la configuración del proxy? Verificá la configuración y volvé a intentarlo.
    • +
    • ¿El servidor proxy permite conexiones desde esta red?
    • +
    • ¿Aún tenés problemas? Consultá con el administrador de tu red o el proveedor de internet.
    • +
    ]]>
    + + + No se encontró el servidor proxy + + El navegador está configurado para usar un servidor proxy, pero el proxy no se encontró el servidor.

    • ¿La configuración del proxy del navegador es correcta? Verificá la configuración y probá de nuevo.
    • ¿El dispositivo está conectado a una red activa?
    • ¿Todavía tenés problemas? Consultá con tu administrador de red o tu proveedor de Internet para recibir asistencia.
    ]]>
    + + + Problema del sitio malicioso + + + Se informó el sitio %1$s como un sitio de ataque y se bloqueó basado en tus preferencias de seguridad.

    ]]>
    + + + Problema de sitio no deseado + + + Se informó el sitio %1$s por instalar software no deseado y se bloqueó basado en tus preferencias de seguridad.

    ]]>
    + + + Problema de sitio dañino + + + Se informó el sitio %1$s como potencialmente dañino y se bloqueó basado en tus preferencias de seguridad.

    ]]>
    + + + Problema de sitio engañoso + + Se informó la página web %1$s como un sitio engañoso y se bloqueó basado en tus preferencias de seguridad.

    ]]>
    + + + No hay sitios seguros disponibles + + %1$s no está disponible.]]> + + Continuar al sitio HTTP +
    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 @@ + + + + + Volver a intentarlo + + + No se puede completar solicitud + + Actualmente no hay información adicional disponible para este problema o error.

    + ]]>
    + + + Falló la conexión segura + + +
  • La página que intentas ver no puede ser mostrada porque la autenticidad de los datos recibidos no pudo ser verificada.
  • +
  • Por favor, contacta a los dueños del sitio para avisarles de este problema.
  • + + ]]>
    + + + Falló la conexión segura + + + +
  • Esto podría ser un problema con la configuración del servidor, o que alguien esté intentando suplantar al servidor.
  • +
  • Si te pudiste conectar con éxito en el pasado, el error puede ser temporal, y puedes volver a intentarlo en un rato.
  • + + ]]>
    + + + Avanzado… + + Alguien podría estar intentando hacerse pasar por el sitio y no debieras continuar. +

    + + ]]>
    + + Retroceder (recomendado) + + Aceptar el riesgo y continuar + + + Este sitio web requiere una conexión segura. + + + +
  • La página que estás intentando ver no puede ser mostrada porque este sitio web requiere una conexión segura.
  • +
  • Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que se puedas hacer para resolverlo.
  • +
  • Puedes notificar al administrador del sitio web acerca de este problema.
  • + + ]]>
    + + + Avanzado… + + + %1$s tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), lo que se traduce en que %2$s solo puede conectarse de forma segura. No puedes añadir una excepción para visitar este sitio.]]> + + Retroceder + + + La conexión fue interrumpida + + + El navegador se conectó exitosamente, pero la conexión fue interrumpida mientras se transfería información. Por favor, vuelve a intentarlo.

    +
      +
    • El sitio podría estar temporalmente no disponible o muy ocupado. Vuelve a intentarlo en un rato.
    • +
    • Si no puedes cargar ninguna página, revisa los datos de tu dispositivo o conexión Wi-Fi.
    • +
    + ]]>
    + + + La conexión ha caducado + + El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.

    +
      +
    • ¿Podría estar experimentando el servidor alta demanda o un corte temporal? Vuelve a intentarlo en un rato.
    • +
    • ¿No puedes navegar por otros sitios? Comprueba la conexión de red del computador.
    • +
    • ¿Tu computador está protegido por un proxy o un firewall? Una configuración incorrecta puede interferir con la navegación.
    • +
    • ¿Todavía con problemas? Consulta con tu administrador de red o proveedor de Internet para asistencia técnica.
    • +
    + ]]>
    + + + No se pudo conectar + + + +
  • El sitio podría estar temporalmente no disponible o demasiado ocupado. Vuelve a intentarlo en un rato.
  • +
  • Si no puedes cargar ninguna página, comprueba el servicio de datos de tu dispositivo o la conexión Wi-Fi.
  • + + ]]>
    + + + Respuesta inesperada del servidor + + El sitio respondió a la solicitud de red en una forma inesperada y el navegador no puede continuar.

    + ]]>
    + + + La página no está redirigiendo adecuadamente + + + 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.

    +
      +
    • ¿Tiene desactivadas o bloqueadas las cookies requeridas por este sitio?
    • +
    • Si aceptar las cookies del sitio no resuelve el problema, probablemente es un problema de configuración del servidor y no de su computador.
    • +
    + ]]>
    + + + Modo sin conexión + + + El navegador está actualmente funcionando en el modo sin conexión a la red y no puede conectarse al ítem solicitado.

    +
      +
    • ¿Está la computadora conectada a una red activa?
    • +
    • Presiona “Volver a intentarlo” para volver al modo con conexión y recargar la página.
    • +
    + ]]>
    + + + Puerto restringido por razones de seguridad + + + La dirección solicitada especificaba un puerto (ej. mozilla.org:80 para el puerto 80 de mozilla.org) usado normalmente para propósitos distintos a navegar por la web. El navegador ha cancelado la solicitud por su protección y seguridad.

    + ]]>
    + + + La conexión fue reiniciada + + + El enlace de red fue interrumpido mientras se negociaba una conexión. Por favor, vuelve a intentarlo.

    +
      +
    • El sitio podría estar temporalmente no disponible o muy ocupado. Vuelve a intentarlo en un rato.
    • +
    • Si no puedes cargar ninguna página, revisa los datos de tu dispositivo o conexión Wi-Fi.
    • +
    + ]]>
    + + + Tipo de archivo inseguro + + + +
  • Por favor, contacta a los dueños del sitio para avisarles de este problema.
  • + + ]]>
    + + + Error de contenido corrupto + + + La página que estás intentando ver no puede ser mostrada por que se detectó un error en la transmisión de datos.

    +
      +
    • Por favor, contacta a los dueños del sitio para avisarles de este problema.
    • +
    + ]]>
    + + + Fallo del contenido + + La página que estás intentando ver no puede ser mostrada por que se detectó un error en la transmisión de datos.

    +
      +
    • Por favor, contacta a los dueños del sitio para avisarles de este problema.
    • +
    + ]]>
    + + + Error de codificación de contenido + 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.

    +
      +
    • Por favor, contacta a los dueños del sitio para avisarles de este problema.
    • +
    + ]]>
    + + + Dirección no encontrada + + + El navegador no pudo encontrar el servidor de la dirección proporcionada.

    +
      +
    • Revisa la dirección por errores de tipeo como + ww. example.com en lugar de + www.example.com
    • +
    • Si no puedes cargar ninguna página, revisa los datos de tu dispositivo o conexión Wi-Fi.
    • +
    + ]]>
    + + + Sin conexión a internet + + Revisa tu conexión de red o vuelve a intentar cargar la página en un rato. + + Recargar + + + Dirección inválida + La dirección proporcionada no está en un formato reconocido. Por favor, comprueba errores en la barra de direcciones y vuelve a intentarlo.

    + ]]>
    + + La dirección no es válida + + + +
  • Las direcciones Web usualmente son escritas como http://www.example.com/
  • +
  • Asegúrate de que estás usando barras oblicuas (por ejemplo /).
  • + + ]]>
    + + + Protocolo desconocido + + La dirección especifica un protocolo (ej. wxyz://) que el navegador no reconoce, por esto el navegador no puede conectar adecuadamente al sitio.

    +
      +
    • ¿Está intentando acceder a servicios multimedia u otros que no sean de texto? Revise el sitio por requerimientos extra.
    • +
    • Algunos protocolos pueden requerir software de terceros o complementos antes de que el navegador pueda reconocerlos.
    • +
    + ]]>
    + + + Archivo no encontrado + + +
  • ¿Es posible que el elemento haya sido renombrado, eliminado o cambiado de ubicación?
  • +
  • ¿Hay algún error de ortografía, mayúsculas o cualquier otro error al escribir la dirección?
  • +
  • ¿Tienes privilegios de acceso suficientes para el elemento solicitado?
  • + + ]]>
    + + + El acceso al archivo fue denegado + + +
  • Puede haber sido removido o movido, o puede que los permisos del archivo prevengan el acceso.
  • + + ]]>
    + + + El servidor proxy rechazó la conexión + El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.

    +
      +
    • ¿Es correcta la configuración del proxy del navegador? Comprueba la configuración y vuelve a intentarlo.
    • +
    • ¿Permite el servicio proxy conexiones desde esta red?
    • +
    • ¿Todavía con problemas? Consulta con tu administrador de red o proveedor de Internet para asistencia técnica.
    • +
    + ]]>
    + + + Servidor Proxy no encontrado + + El navegador está configurado para usar un servidor proxy, pero no se pudo encontrar el servidor proxy.

    +
      +
    • ¿Es correcta la configuración del proxy del navegador? Comprueba la configuración y vuelve a intentarlo.
    • +
    • ¿El computador está conectado a una red activa?
    • +
    • ¿Todavía con problemas? Consulta con tu administrador de red o proveedor de Internet para asistencia técnica.
    • +
    + ]]>
    + + + Problema de sitio malicioso + + El sitio en %1$s ha sido reportado como un sitio atacante y ha sido bloqueado en base a tus preferencias de seguridad.

    + ]]>
    + + + Problema de sitio no deseado + + + El sitio en %1$s ha sido reportado por entregar software indeseado y ha sido bloqueado en base a tus preferencias de seguridad.

    + ]]>
    + + + Problema de sitio peligroso + + El sitio en %1$s ha sido reportado como un sitio potencialmente peligroso y ha sido bloqueado en base a tus preferencias de seguridad.

    + ]]>
    + + + Problema de sitio fraudulento + + El sitio en %1$s ha sido reportado como un sitio fraudulento y ha sido bloqueado en base a tus preferencias de seguridad.

    + ]]>
    + + + Sitio seguro no disponible + + %1$s no se encuentra disponible.]]> + + Continuar al sitio HTTP +
    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 @@ + + + + + Volver a intentarlo + + + No se puede completar la petición + + Actualmente no hay información adicional disponible para este problema o error.

    + ]]>
    + + + Conexión segura fallida + + +
  • La página que estás intentando ver no se puede mostrar porque no se ha podido verificar la autenticidad de los datos recibidos.
  • +
  • Contacta con los propietarios del sitio web para informarles de este problema.
  • + ]]>
    + + + Conexión segura fallida + + +
  • Esto podría deberse a un problema con la configuración del servidor, o podría ser alguien intentando hacerse pasar por el servidor.
  • +
  • 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.
  • + + ]]>
    + + + Avanzadas… + + Alguien podría estar intentando hacerse pasar por el sitio y no deberías continuar. +        

    +         +    ]]>
    + + Retroceder (recomendado) + + Aceptar el riesgo y continuar + + + Este sitio web requiere una conexión segura. + + +
  • La página que estás intentando ver no se puede mostrar porque este sitio web requiere una conexión segura.
  • +
  • Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que se pueda hacer para resolverlo.
  • +
  • Puedes notificar al administrador del sitio web sobre este problema.
  • + + ]]>
    + + + Avanzadas… + + + %1$s tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), que significa que %2$s solo puede conectarse a él de forma segura. No puedes añadir una excepción para visitar este sitio. + ]]> + + Retroceder + + + La conexión ha sido interrumpida + + El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería la información. Vuelve a intentarlo.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o de datos de tu dispositivo móvil.
    • +
    + ]]>
    + + + La conexión ha caducado + + El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.

    +
      +
    • ¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelve a intentarlo más tarde.
    • +
    • ¿No puedes navegar por otros sitios? Comprueba la conexión de red del equipo.
    • +
    • ¿Tu red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta puede interferir con la navegación web.
    • +
    • ¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    + ]]>
    + + + No se puede conectar + + +
  • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
  • +
  • Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.
  • + + ]]>
    + + + Respuesta inesperada del servidor + + El sitio respondió a la solicitud de red de una forma inesperada y el navegador no puede continuar.

    + ]]>
    + + + La página no está redirigiendo adecuadamente + + 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.

    +
      +
    • ¿Tienes desactivadas o bloqueadas las cookies que necesita este sitio?
    • +
    • 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.
    • +
    + ]]>
    + + + Modo sin conexión + + + El navegador está operando en modo sin conexión y no puede conectarse con el elemento solicitado.

    +
      +
    • ¿Está conectado el equipo a una red activa?
    • +
    • Pulsa "Volver a intentarlo" para pasar al modo con conexión y recargar la página.
    • +
    + ]]>
    + + + Puerto restringido por razones de seguridad + + La dirección solicitada especificaba un puerto (p. ej., mozilla.org:80 para el puerto 80 de mozilla.org) que suele usarse para propósitos distintos a navegar por Internet. El navegador ha cancelado la solicitud para tu protección y seguridad.

    + ]]>
    + + + La conexión ha sido reiniciada + + El enlace con la red se interrumpió mientras se negociaba una conexión. Vuelve a intentarlo.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.
    • +
    + ]]>
    + + + Tipo de archivo no seguro + + +
  • Ponte en contacto con los propietarios del sitio web para informarles de este problema.
  • + + ]]>
    + + + Error de contenido dañado + + La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.

    +
      +
    • Ponte en contacto con los propietarios del sitio web para informarles de este problema.
    • +
    + ]]>
    + + + Contenido bloqueado + La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.

    +
      +
    • Ponte en contacto con los propietarios del sitio web para informarles de este problema.
    • +
    + ]]>
    + + + Error de codificación de contenido + La página que estás intentando ver no puede mostrarse porque usa una forma no válida o no admitida de compresión.

    +
      +
    • Ponte en contacto con los propietarios del sitio web para informarles de este problema.
    • +
    + ]]>
    + + + No se encontró la dirección + + El navegador no pudo encontrar el servidor para la dirección proporcionada.

    +
      +
    • Comprueba que la dirección no contenga errores, por ejemplo, ww.ejemplo.com en lugar de www.ejemplo.com.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.
    • +
    + ]]>
    + + + No hay conexión a Internet + + Verifica tu conexión de red o intenta volver a cargar la página en unos momentos. + + Recargar + + + La dirección no es válida + La dirección proporcionada no está en un formato reconocido. Comprueba si hay errores en la barra de direcciones y vuelve a intentarlo.

    + ]]>
    + + La dirección no es válida + + +
  • Las direcciones web suelen escribirse así: http://www.ejemplo.com/
  • +
  • Asegúrate de estar usando barras inclinadas hacia adelante (p. ej., /).
  • + + ]]>
    + + + Protocolo desconocido + La dirección especifica un protocolo (p. ej., wxyz://) que el navegador no reconoce, así que el navegador no puede conectarse correctamente con el sitio.

    +
      +
    • ¿Estás intentando acceder a contenido multimedia u otros servicios que no son de texto? Comprueba los requisitos adicionales del sitio.
    • +
    • Algunos protocolos pueden necesitar software o plugins de terceros antes de que el navegador pueda reconocerlos.
    • +
    + ]]>
    + + + Archivo no encontrado + +
  • ¿Es posible que el elemento se haya renombrado, eliminado o cambiado de ruta?
  • +
  • ¿Hay algún error de ortografía, de uso de mayúsculas o de cualquier otro tipo en la dirección?
  • +
  • ¿Tienes privilegios de acceso suficientes para el elemento solicitado?
  • + + ]]>
    + + + El acceso al archivo ha sido denegado + +
  • Puede haberse eliminado o movido, o sus permisos de archivo pueden estar impidiendo el acceso.
  • + + ]]>
    + + + El servidor proxy rechazó la conexión + El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.

    +
      +
    • ¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.
    • +
    • ¿Permite el servicio proxy conexiones desde esta red?
    • +
    • ¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    + ]]>
    + + + No se encontró el servidor proxy + El navegador está configurado para usar un servidor proxy, pero no se pudo encontrar el servidor proxy.

    +
      +
    • ¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.
    • +
    • ¿Está conectado el equipo a una red activa?
    • +
    • ¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    + ]]>
    + + + Problema de sitio de malware + + El sitio en %1$s se ha identificado como un sitio atacante y se ha bloqueado, siguiendo tus preferencias de seguridad.

    + ]]>
    + + + Problema de sitio no deseado + + 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.

    + ]]>
    + + + Problema de sitio dañino + + El sitio en %1$s se ha identificado como un sitio potencialmente dañino y se ha bloqueado, siguiendo tus preferencias de seguridad.

    + ]]>
    + + + Problema de sitio engañoso + + La página web en %1$s se ha identificado como un sitio engañoso y se ha bloqueado, siguiendo tus preferencias de seguridad.

    + ]]>
    + + + Sitio seguro no disponible + + %1$s.]]> + + Continuar al sitio HTTP +
    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 @@ + + + + + Intenta de nuevo + + + No se puede completar la solicitud + + Información adicional acerca de este problema o error no está actualmente disponible.

    ]]>
    + + + La conexión segura ha fallado + + +
  • La página que estás intentando ver no se puede mostrar porque no se ha podido verificar la autenticidad de los datos recibidos.
  • +
  • Por favor, ponte en contacto con los propietarios del sitio web para informarles de este problema.
  • + ]]>
    + + + La conexión segura ha fallado + + +
  • Puede haber un problema en la configuración del servidor, o bien alguien podría estar intentando suplantar el servidor.
  • +
  • Si ya te has conectado a este servidor con éxito, el error puede ser temporal, por lo que puedes intentar nuevamente en breve.
  • + ]]>
    + + + Avanzado… + + Alguien podría estar intentando hacerse pasar por el sitio y no deberías continuar. +        

    +        ]]>
    + + Regresar (Recomendado) + + Aceptar el riesgo y continuar + + + Este sitio web requiere una conexión segura. + + +
  • La página que estás tratando de ver no se puede mostrar porque este sitio web requiere una conexión segura.
  • +
  • Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que se pueda hacer para resolverlo.
  • +
  • Puedes notificar al administrador del sitio web sobre este problema.
  • + + ]]>
    + + + Opciones avanzadas… + + + %1$s tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), lo que significa que %2$s solo puede conectarse de forma segura. No puedes agregar una excepción para visitar este sitio. + ]]> + + Regresar + + + La conexión fue interrumpida + + El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería información. Por favor, intenta de nuevo.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o los datos de tu dispositivo móvil.
    • +
    ]]>
    + + + La conexión ha expirado + + El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.

    +
      +
    • ¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelve a intentarlo más tarde.
    • +
    • ¿No puedes navegar por otros sitios? Comprueba la conexión de red del equipo.
    • +
    • ¿Tu red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta puede interferir con la navegación web.
    • +
    • ¿Todavía con problemas? Consulta con su administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    ]]>
    + + + No se puede conectar + + +
  • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
  • +
  • Si no puedes cargar ninguna página, revisa la conexión wifi o los datos de tu dispositivo móvil.
  • + ]]>
    + + + Respuesta inesperada del servidor + + El sitio ha respondido a la solicitud de red de una forma inesperada y el navegador no puede continuar.

    ]]>
    + + + La página no está redirigiendo adecuadamente + + 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.

    +
      +
    • ¿Tienes desactivadas o bloqueadas las cookies requeridas por este sitio?
    • +
    • 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.
    • +
    ]]>
    + + + Modo sin conexión + + El navegador está operando en modo sin conexión y no puede conectarse con el elemento solicitado.

    +
      +
    • ¿Estás conectado el equipo a una red activa?
    • +
    • Presiona "Volver a intentarlo" para pasar al modo con conexión y recargar la página.
    • +
    ]]>
    + + + Puerto restringido por razones de seguridad + + + La dirección solicitada especificaba un puerto (p. ej., mozilla.org:80 para el puerto 80 de mozilla.org) que suele usarse para propósitos distintos a navegar por Internet. El navegador ha cancelado la solicitud para tu protección y seguridad.

    ]]>
    + + + La conexión se ha reiniciado + + El enlace con la red se interrumpió mientras se negociaba una conexión. Por favor, intenta de nuevo.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o los datos de tu dispositivo móvil.
    • +
    ]]>
    + + + Tipo de archivo inseguro + + +
  • Por favor, comunícate con los propietarios del sitio web para informarles de este problema.
  • + ]]>
    + + + Error de contenido dañado + + La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.

    +
      +
    • Por favor, comunícate con los propietarios del sitio web para informarles de este problema.
    • +
    ]]>
    + + + Contenido bloqueado + La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de datos.

    +
      +
    • Por favor, comunícate con los propietarios del sitio web para informarles de este problema.
    • +
    ]]>
    + + + Error de codificación de contenido + + La página que estás intentando ver no puede mostrarse porque usa una forma no válida o no admitida de comprensión.

    +
      +
    • Por favor, comunícate con los propietarios del sitio web para informarles de este problema.
    • +
    ]]>
    + + + Dirección no encontrada + + + El navegador no pudo encontrar el servidor para dirección proporcionada.

    +
      +
    • Verifica que la dirección no contenga error, por ejemplo: + ww.ejemplo.com en lugar de + www.ejemplo.com.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o los datos de tu dispositivo móvil.
    • +
    ]]>
    + + + No hay conexión a internet + + Verifica tu conexión de red o intenta volver a cargar la página en unos momentos. + + Recargar + + + Dirección inválida + La dirección proporcionada no está en un formato reconocido. Comprueba si hay errores en la barra de direcciones y vuelve a intentarlo.

    ]]>
    + + La dirección no es válida + + +
  • Las direcciones web suelen escribirse así: http://www.ejemplo.com/
  • +
  • Asegúrate de estar usando barras inclinadas hacia adelante (p. ej., /).
  • + ]]>
    + + + Protocolo desconocido + La dirección especifica un protocolo (p. ej., wxyz://) que el navegador no reconoce, así que el navegador no puede conectarse correctamente con el sitio.

    +
      +
    • ¿Estás intentando acceder a contenido multimedia u otros servicios que no son de texto? Comprueba los requisitos adicionales del sitio.
    • +
    • Algunos protocolos pueden necesitar software o plugins de terceros antes de que el navegador pueda reconocerlos.
    • +
    ]]>
    + + + Archivo no encontrado + +
  • ¿Es posible que el elemento se haya renombrado, eliminado o cambiado de ruta?
  • +
  • ¿Hay algún error de ortografía, de uso de mayúsculas o de cualquier otro tipo en la dirección?
  • +
  • ¿Tienes privilegios de acceso suficientes para el elemento solicitado?
  • + ]]>
    + + + El acceso al archivo fue denegado + +
  • Puede haberse eliminado o movido, o los permisos del archivo pueden estar impidiendo el acceso.
  • + ]]>
    + + + El servidor proxy rechazó la conexión + El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.

    +
      +
    • ¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.
    • +
    • ¿Permite el servicio proxy conexiones desde esta red?
    • +
    • ¿Todavía con problemas? Consulta con tu administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    ]]>
    + + + Servidor proxy no encontrado + El navegador está configurado para usar un servidor proxy, pero no se pudo encontrar el servidor proxy.

    +
      +
    • ¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.
    • +
    • ¿Está conectado el equipo a una red activa?
    • +
    • ¿Todavía con problemas? Consulta con su administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    ]]>
    + + + Problema de sitio malicioso + + El sitio en %1$s se ha identificado como un sitio atacante y se ha bloqueado de acuerdo con tus preferencias de seguridad.

    ]]>
    + + + Problema de sitio no deseado + + 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.

    ]]>
    + + + Problema de sitio dañino + + 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.

    ]]>
    + + + Problema de sitio engañoso + + 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.

    ]]>
    + + + Sitio seguro no disponible + + %1$s.]]> + + Continuar al sitio HTTP +
    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 @@ + + + + + Volver a intentarlo + + + No se puede completar la petición + + Actualmente no hay información adicional disponible para este problema o error.

    + ]]>
    + + + Conexión segura fallida + + +
  • La página que estás intentando ver no se puede mostrar porque no se ha podido verificar la autenticidad de los datos recibidos.
  • +
  • Contacta con los propietarios del sitio web para informarles de este problema.
  • + ]]>
    + + + Conexión segura fallida + + +
  • Esto podría deberse a un problema con la configuración del servidor, o podría ser alguien intentando hacerse pasar por el servidor.
  • +
  • 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.
  • + + ]]>
    + + + Avanzadas… + + Alguien podría estar intentando hacerse pasar por el sitio y no deberías continuar. +        

    +         +    ]]>
    + + Retroceder (recomendado) + + Aceptar el riesgo y continuar + + + Este sitio web requiere una conexión segura. + + +
  • La página que estás intentando ver no se puede mostrar porque este sitio web requiere una conexión segura.
  • +
  • Lo más probable es que el problema esté relacionado con el sitio web y no hay nada que se pueda hacer para resolverlo.
  • +
  • Puedes notificar al administrador del sitio web sobre este problema.
  • + + ]]>
    + + + Avanzadas… + + + %1$s tiene una política de seguridad llamada HTTP Strict Transport Security (HSTS), que significa que %2$s solo puede conectarse a él de forma segura. No puedes añadir una excepción para visitar este sitio. + ]]> + + Retroceder + + + La conexión ha sido interrumpida + + El navegador se conectó con éxito, pero se interrumpió la conexión mientras se transfería la información. Vuelve a intentarlo.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o de datos de tu dispositivo móvil.
    • +
    + ]]>
    + + + La conexión ha caducado + + El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.

    +
      +
    • ¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelve a intentarlo más tarde.
    • +
    • ¿No puedes navegar por otros sitios? Comprueba la conexión de red del equipo.
    • +
    • ¿Tu red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta puede interferir con la navegación web.
    • +
    • ¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    + ]]>
    + + + No se puede conectar + + +
  • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
  • +
  • Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.
  • + + ]]>
    + + + Respuesta inesperada del servidor + + El sitio respondió a la solicitud de red de una forma inesperada y el navegador no puede continuar.

    + ]]>
    + + + La página no está redirigiendo adecuadamente + + 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.

    +
      +
    • ¿Tienes desactivadas o bloqueadas las cookies que necesita este sitio?
    • +
    • 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.
    • +
    + ]]>
    + + + Modo sin conexión + + + El navegador está operando en modo sin conexión y no puede conectarse con el elemento solicitado.

    +
      +
    • ¿Está conectado el equipo a una red activa?
    • +
    • Pulsa "Volver a intentarlo" para pasar al modo con conexión y recargar la página.
    • +
    + ]]>
    + + + Puerto restringido por razones de seguridad + + La dirección solicitada especificaba un puerto (p. ej., mozilla.org:80 para el puerto 80 de mozilla.org) que suele usarse para propósitos distintos a navegar por Internet. El navegador ha cancelado la solicitud para tu protección y seguridad.

    + ]]>
    + + + La conexión ha sido reiniciada + + El enlace con la red se interrumpió mientras se negociaba una conexión. Vuelve a intentarlo.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelve a intentarlo en unos minutos.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.
    • +
    + ]]>
    + + + Tipo de archivo no seguro + + +
  • Ponte en contacto con los propietarios del sitio web para informarles de este problema.
  • + + ]]>
    + + + Error de contenido dañado + + La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.

    +
      +
    • Ponte en contacto con los propietarios del sitio web para informarles de este problema.
    • +
    + ]]>
    + + + Contenido bloqueado + La página que estás intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.

    +
      +
    • Ponte en contacto con los propietarios del sitio web para informarles de este problema.
    • +
    + ]]>
    + + + Error de codificación de contenido + La página que estás intentando ver no puede mostrarse porque usa una forma no válida o no admitida de compresión.

    +
      +
    • Ponte en contacto con los propietarios del sitio web para informarles de este problema.
    • +
    + ]]>
    + + + No se encontró la dirección + + El navegador no pudo encontrar el servidor para la dirección proporcionada.

    +
      +
    • Comprueba que la dirección no contenga errores, por ejemplo, ww.ejemplo.com en lugar de www.ejemplo.com.
    • +
    • Si no puedes cargar ninguna página, revisa la conexión wifi o de datos del dispositivo móvil.
    • +
    + ]]>
    + + + No hay conexión a Internet + + Verifica tu conexión de red o intenta volver a cargar la página en unos momentos. + + Recargar + + + La dirección no es válida + La dirección proporcionada no está en un formato reconocido. Comprueba si hay errores en la barra de direcciones y vuelve a intentarlo.

    + ]]>
    + + La dirección no es válida + + +
  • Las direcciones web suelen escribirse así: http://www.ejemplo.com/
  • +
  • Asegúrate de estar usando barras inclinadas hacia adelante (p. ej., /).
  • + + ]]>
    + + + Protocolo desconocido + La dirección especifica un protocolo (p. ej., wxyz://) que el navegador no reconoce, así que el navegador no puede conectarse correctamente con el sitio.

    +
      +
    • ¿Estás intentando acceder a contenido multimedia u otros servicios que no son de texto? Comprueba los requisitos adicionales del sitio.
    • +
    • Algunos protocolos pueden necesitar software o plugins de terceros antes de que el navegador pueda reconocerlos.
    • +
    + ]]>
    + + + Archivo no encontrado + +
  • ¿Es posible que el elemento se haya renombrado, eliminado o cambiado de ruta?
  • +
  • ¿Hay algún error de ortografía, de uso de mayúsculas o de cualquier otro tipo en la dirección?
  • +
  • ¿Tienes privilegios de acceso suficientes para el elemento solicitado?
  • + + ]]>
    + + + El acceso al archivo ha sido denegado + +
  • Puede haberse eliminado o movido, o sus permisos de archivo pueden estar impidiendo el acceso.
  • + + ]]>
    + + + El servidor proxy rechazó la conexión + El navegador está configurado para usar un servidor proxy, pero el proxy rechazó la conexión.

    +
      +
    • ¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.
    • +
    • ¿Permite el servicio proxy conexiones desde esta red?
    • +
    • ¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    + ]]>
    + + + No se encontró el servidor proxy + El navegador está configurado para usar un servidor proxy, pero no se pudo encontrar el servidor proxy.

    +
      +
    • ¿Es correcta la configuración de proxy del navegador? Comprueba la configuración y vuelve a intentarlo.
    • +
    • ¿Está conectado el equipo a una red activa?
    • +
    • ¿Todavía tienes problemas? Consulta con el administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    + ]]>
    + + + Problema de sitio de malware + + El sitio en %1$s se ha identificado como un sitio atacante y se ha bloqueado, siguiendo tus preferencias de seguridad.

    + ]]>
    + + + Problema de sitio no deseado + + 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.

    + ]]>
    + + + Problema de sitio dañino + + El sitio en %1$s se ha identificado como un sitio potencialmente dañino y se ha bloqueado, siguiendo tus preferencias de seguridad.

    + ]]>
    + + + Problema de sitio engañoso + + La página web en %1$s se ha identificado como un sitio engañoso y se ha bloqueado, siguiendo tus preferencias de seguridad.

    + ]]>
    + + + Sitio seguro no disponible + + %1$s.]]> + + Continuar al sitio HTTP +
    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 @@ + + + + + Proovi uuesti + + + Päringut pole võimalik lõpetada + + Selle probleemi kohta pole lisainformatsiooni.

    ]]>
    + + + Turvalise ühenduse viga + + + +
  • Soovitud veebilehte pole võimalik näidata, kuna saadud andmete autentsust polnud võimalik kontrollida.
  • +
  • Palun võta veebilehe omanikuga ühendust ja teavita teda probleemist.
  • + ]]>
    + + + Turvalise ühenduse viga + + +
  • Tegemist võib olla serveri seadistuste probleemiga või siis üritab keegi antud +serverina välja paista.
  • +
  • Kui sa oled varem selle serveriga edukalt ühendunud, siis võib olla tegemist ajutise veaga +ja sa võid hiljem uuesti proovida.
  • + ]]>
    + + + Edasijõudnuile… + + + Keegi võib üritada selle saidina välja paista ja sa ei peaks jätkama. +

    + ]]>
    + + Mine tagasi (soovitatav) + + Nõustu riskiga ja jätka + + + See sait nõuab turvalist ühendust. + + +
  • Lehte, mida soovid vaadata, pole võimalik kuvada, sest see sait nõuab turvalist ühendust.
  • +
  • Probleem on suure tõenäosusega saidi poolel ja sa ei saa selle lahendamiseks midagi teha.
  • +
  • Sa võid sellest probleemist teavitada saidi administraatorit.
  • + + ]]>
    + + + Edasijõudnuile… + + + Mine tagasi + + + Ühendus katkes + + + Veebilehitsejal õnnestus edukalt ühenduda, kuid ühendust segati keset andmete edastamist. Palun proovi uuesti.

    +
      +
    • Veebileht võib olla ajutiselt kättesaamatu või liialt hõivatud. Proovi mõne aja pärast uuesti.
    • +
    • Kui sa ei saa avada ühtegi lehte, siis kontrolli oma seadme andmeside või Wi-Fi ühendust.
    • +
    ]]>
    + + + Ühendus aegus + + + Soovitud leht ei vastanud ühendusele ja veebilehitseja lõpetas vastuse ootamise.

    +
      +
    • Kas serveril võib olla probleeme suure koormusega või on tegu ajutise veaga? Proovi hiljem uuesti.
    • +
    • Kas ka teiste lehtede vaatamine ei õnnestu? Kontrolli seadme võrguühendust.
    • +
    • Kas sinu seade või võrk on kaitstud tulemüüriga? Vigased tulemüüri sätted võivad segada veebilehitsemist.
    • +
    • Endiselt probleemid? Konsulteeri oma võrguadministraatori või interneti teenusepakkujaga.
    • +
    ]]>
    + + + Viga ühendumisel + + +
  • Veebileht võib olla ajutiselt kättesaamatu või liialt hõivatud. Proovi mõne hetke pärast uuesti.
  • +
  • Kui sa ei saa avada ühtegi lehte, siis kontrolli oma seadme andmeside või Wi-Fi ühendust.
  • + ]]>
    + + + Ootamatu vastus serverilt + + + Veebisait andis päringule ootamatu vastuse ja veebilehitsejal pole võimalik jätkata.

    ]]>
    + + + Veebileht pole korralikult ümber suunatud + + Veebilehitseja lõpetas katsed objekti laadida. Veebileht suunab päringu edasi viisil, kuidas see kunagi ei õnnestu.

    +
      +
    • Kas oled keelanud või blokkinud selle saidi küpsised?
    • +
    • Kui küpsiste lubamine ei lahenda antud probleemi, siis on tõenäoliselt tegemist probleemiga serveris, mitte sinu seadmes.
    • +
    ]]>
    + + + Võrguta režiim + + Brauser on võrguta režiimis ega saa soovitud aadressiga ühenduda.

    +
      +
    • Kas seade on ühendatud töötavasse võrku?
    • +
    • Vajuta “Proovi uuesti”, et lülituda võrgurežiimi ning laadida leht uuesti.
    • +
    ]]>
    + + + Port on turvakaalutlustel keelatud + + + Soovitud aadress määratleb pordi (nt mozilla.org:80 ehk pordi 80 aadressil mozilla.org), mida muidu kasutatakse muul otstarbel kui veebilehitsemine. Veebilehitseja katkestas päringu sinu turvalisuse ja julgeoleku huvides.

    ]]>
    + + + Ühendus katkestati + + Ühenduse loomisel segati võrguliiklust. Palun proovi uuesti.

    +
      +
    • Veebileht võib olla ajutiselt kättesaamatu või liialt hõivatud. Proovi mõne aja pärast uuesti.
    • +
    • Kui sa ei saa avada ühtegi lehte, siis kontrolli oma seadme andmeside või Wi-Fi ühendust.
    • +
    ]]>
    + + + Ohtlik faili tüüp + + +
  • Palun võta ühendust saidi omanikuga, et informeerida teda antud probleemist.
  • + ]]>
    + + + Vigane sisu + + + Andmete edastamisel esinenud vea tõttu pole soovitud lehte võimalik kuvada.

    +
      +
    • Palun võta ühendust saidi omanikega, et teavitada neid sellest probleemist.
    • +
    ]]>
    + + + Sisu kuvamisel esines viga + Andmete edastamisel esinenud vea tõttu pole soovitud lehte võimalik kuvada.

    +
      +
    • Palun võta ühendust saidi omanikega, et teavitada neid sellest probleemist.
    • +
    ]]>
    + + + Sisu kodeeringu viga + Lehte, mida soovid vaadata, pole võimalik kuvada, kuna see kasutab vigast või mittetoetatud pakkimise vormingut.

    +
      +
    • Palun võta ühendust veebilehe omanikuga, et informeerida teda antud probleemist.
    • +
    ]]>
    + + + Aadressi ei leitud + + + Veebilehitseja ei leidnud sellele aadressile vastavat serverit.

    +
      +
    • Kontrolli, kas aadressis pole sisestusvigu, näiteks + ww.example.com + www.example.com.
    • asemel) +
    • Kui sa ei saa avada ühtegi lehte, siis kontrolli oma seadme andmeside või Wi-Fi ühendust.
    • +
    ]]>
    + + + Puudub internetiühendus + + Kontrolli oma internetiühendust või proovi lehte mõne aja pärast uuesti laadida. + + Laadi uuesti + + + Vigane aadress + Sisestatud aadress ei ole tunnustatud formaadis. Palun kontrolli aadressi korrektsust ja proovi uuesti.

    ]]>
    + + Aadress pole korrektne + + + +
  • Veebiaadressid on tavaliselt kirjutatud kujul http://www.example.com/
  • +
  • Kontrolli üle, et kasutad ikka õigetpidi kaldkriipse (näiteks /).
  • + ]]>
    + + + Tundmatu protokoll + + Soovitud aadress määratleb protokolli (nt wxyz://), mida veebilehitseja ei tunne, seega pole veebilehitsejal võimalik korralikult selle saidiga ühenduda.

    +
      +
    • Kas üritad avada multimeedia- või muud teenust, mis pole teksti kujul? Kontrolli veebilehe lisanõudeid.
    • +
    • Mõned protokollid võivad nõuda kolmanda osapoole tarkvara või pluginaid, enne kui veebilehitseja suudab nad tuvastada.
    • +
    ]]>
    + + + Faili ei leitud + + +
  • Kas soovitud objekt võib olla ümber nimetatud, kustutatud või mujale paigutatud?
  • +
  • Kas aadressis võib olla sisestusviga või probleeme suurtähtedega?
  • +
  • Kas sul on objektile ligipääsemiseks piisavalt õigusi?
  • + ]]>
    + + + Ligipääs failile keelati + +
  • Fail võib olla kustutatud, mujale liigutatud või on sellele seatud ligipääsu piiravad õigused.
  • + ]]>
    + + + Puhverserver keeldus ühendusest + Brauser on häälestatud kasutama puhverserverit, aga puhverserver keeldub ühendusest.

    +
      +
    • Kas brauseri puhverserveri sätted on korrektsed? Kontrolli sätteid ja proovi uuesti.
    • +
    • Kas puhverserver lubab ühendusi sellest võrgust?
    • +
    • Endiselt probleemid? Konsulteeri oma võrguadministraatori või interneti teenusepakkujaga.
    • +
    ]]>
    + + + Puhverserverit ei leitud + + Brauser on häälestatud kasutama puhverserverit, aga puhverserverit ei leitud.

    +
      +
    • Kas brauseri puhverserveri sätted on korrektsed? Kontrolli sätteid ja proovi uuesti.
    • +
    • Kas seade on ühendatud töötavasse võrku?
    • +
    • Endiselt probleemid? Konsulteeri oma võrguadministraatori või interneti teenusepakkujaga.
    • +
    ]]>
    + + + Pahavara saidi probleem + + Aadressil %1$s asuv veebileht on teadete kohaselt ründav leht ja see blokiti vastavalt sinu turvasätetele.

    ]]>
    + + + Soovimatu saidi probleem + + Aadressil %1$s asuv veebileht levitab teadete kohaselt soovimatut tarkvara ja see blokiti vastavalt sinu turvasätetele.

    ]]>
    + + + Kahjuliku saidi probleem + + Aadressil %1$s asuv veebileht on teadete kohaselt potentsiaalselt kahjulik ja see blokiti vastavalt sinu turvasätetele.

    ]]>
    + + + Veebivõltsingu probleem + + Aadressil %1$s asuv veebileht on teadete kohaselt veebivõltsingut sisaldav leht ja see blokiti vastavalt sinu turvasätetele.

    ]]>
    + + + Turvaline sait pole saadaval + + %1$s puudub HTTPSi tugi.]]> + + Mine HTTP saidile +
    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 @@ + + + + + Saiatu berriro + + + Ezin da eskaera osatu + + + Momentu honetan ez dago arazo edo errore honen inguruko argibide gehiago..

    + ]]>
    + + + Konexio seguruak huts egin du + + +
  • Ikusten saiatzen ari zaren orria ezin da erakutsi jasotako datuen egiazkotasuna ezin delako egiaztatu.
  • +
  • Mesedez jarri harremanetan webgunearen jabeekin arazoaren berri emateko.
  • + + ]]>
    + + + Konexio seguruak huts egin du + + + +
  • Hau zerbitzariaren konfigurazio arazo bat izan zitekeen, edo norbait zerbitzariaren nortasuna bidegabe bereganatzen saiatzen ibiltzea.
  • +
  • Zerbitzari honetara arazorik gabe konektatu bazara iraganean, errorea unekoa izan daiteke eta geroago saia zaitezke.
  • + + ]]>
    + + + Aurreratua… + + Baten bat gunea ordezten saiatzen egon liteke eta ez zenuke jarraitu behar. +

    + + ]]>
    + + Itzuli (gomendatua) + + Onartu arriskua eta jarraitu + + + Webgune honek konexio segurua eskatzen du. + + +
  • Ikusten saiatzen ari zaren orria ezin da erakutsi webguneak konexio segurua eskatzen duelako.
  • +
  • Arazoa ziurrenik webgunearena da eta ezin duzu ezer egin hau konpontzeko.
  • +
  • Webgunearen kudeatzaileari arazoaren berri eman diezaiokezu.
  • + + ]]>
    + + + Aurreratua… + + + %1$s webguneak HTTP Strict Transport Security (HSTS) izeneko segurtasun-politika dauka eta beraz %2$s aplikazioa modu seguruan konekta daiteke soilik. Ezin duzu gune hau bisitatzeko salbuespenik gehitu. + ]]> + + Itzuli + + + Konexioa eten egin da + + + Nabigatzaileak konexioa ondo sortu du, baina datuak jasotzen ari zela transferentzia eten egin da. Mesedez, saiatu berriro.

    +
      +
    • Gunea une batez desgaituta edo oso lanpetuta egon daiteke. Saiatu berriro minutu batzuen buruan.
    • +
    • Ezin baduzu beste orririk kargatu, egiaztatu zure gailuaren datu- edo WiFi-konexioa.
    • +
    + ]]>
    + + + Konexioaren denbora-muga gainditu da + + + Eskatutako guneak ez du eskaera erantzun eta nabigatzaileak itxaroteari utzi dio.

    +
      +
    • Zerbitzariak lan karga handia duelako gerta daiteke. Saiatu geroago.
    • +
    • Gauza bera gertatzen zaizu beste guneekin? Egiaztatu zure gailuaren sareko konexioa.
    • +
    • Zure gailua edo sarea suebaki edo proxy baten bitartez babestuta dago? Gaizki konfiguratutako ezarpenek web nabigazioa oztopa dezakete.
    • +
    • Arazoak oraindik? Jarri harremanetan zure sarearen kudeatzailearekin edo Internet hornitzailearekin.
    • +
    ]]>
    + + + Ezin da konektatu + + + +
  • Gunea une batez desgaituta edo oso lanpetuta egon daiteke. Saiatu berriro minutu batzuen buruan.
  • +
  • Ezin baduzu beste orririk kargatu, egiaztatu zure gailuaren datu- edo WiFi-konexioa.
  • + ]]>
    + + + Ustekabeko erantzuna zerbitzaritik + + Guneak emandako erantzuna ustekabekoa izan da eta nabigatzaileak ezin du aurrera jarraitu.

    + ]]>
    + + + Orriak ez du birbideraketa ondo egiten + + Nabigatzaileak eskatutako elementua berreskuratzen saiatzeari utzi dio. Gunea eskaera modu okerrean birbideratzen ari da.

    +
      +
    • Gune honetarako cookieak blokeatu edo ezgaitu dituzu?
    • +
    • Guneak bidalitako cookieak onartu eta gero arazoa konpontzen ez bada, zerbitzariaren konfigurazioaren arazoa izan daiteke eta ez zure gailuarena.
    • +
    ]]>
    + + + Lineaz kanpoko modua + + Nabigatzailea lineaz kanpo dago eta ezin du eskatutako elementuarekin konektatu.

    +
      +
    • Gailua sare aktibo batera konektatuta dago?
    • +
    • Sakatu "Saiatu berriro" botoia nabigatzailea linean jarri eta orria berritzeko.
    • +
    ]]>
    + + + Segurtasun neurriengatik ataka galarazita dago + + + Eskatutako helbideak zehaztutako ataka (adib. mozilla.org:80 mozilla.org guneko 80. atakarentzako) web nabigazioa ez den bestelako helburuetarako erabili ohi da. Nabigatzaileak eskaera bertan behera utzi du zure babes eta segurtasunerako.

    + ]]>
    + + + Konexioa berrezarri egin da + + + Sareko lotura eten egin da konexioa negoziatzerakoan. Mesedez saiatu berriro.

    +
      +
    • Gunea une batez desgaituta edo oso lanpetuta egon daiteke. Saiatu berriro minutu batzuen buruan.
    • +
    • Ezin baduzu beste orririk kargatu, egiaztatu zure gailuaren datu- edo WiFi-konexioa.
    • +
    + ]]>
    + + + Fitxategi mota ez-segurua + + + +
  • Jarri harremanetan webgunearen jabeekin arazo honen berri emateko.
  • + + ]]>
    + + + Hondatutako edukien errorea + + Ikusten saiatzen ari zaren orria ezin da erakutsi errore bat detektatu delako datu-transmisioan.

    +
      +
    • Jarri harremanetan gunearen arduradunarekin arazo honen berri emateko.
    • +
    + ]]>
    + + + Edukiak huts egin du + Ikusten saiatzen ari zaren orria ezin da erakutsi errore bat detektatu delako datu-transmisioan.

    +
      +
    • Jarri harremanetan gunearen arduradunarekin arazo honen berri emateko.
    • +
    + ]]>
    + + + Edukien kodeketa-errorea + + Ikusten saiatzen ari zaren orria ezin da ikusi baliogabeko edo onartzen ez den konpresio mota bat erabiltzen baitu.

    +
      +
    • Jarri harremanetan gunearen arduradunarekin arazo honen berri emateko.
    • +
    + ]]>
    + + + Helbidea ez da aurkitu + + + Nabigatzaileak ezin du ostalariko zerbitzaria aurkitu emandako helbidean.

    +
      +
    • Helbidea ondo begiratu mota honetako erroreak ekiditeko: ww.adibidea.eus www.adibidea.eus-en ordez.
    • +
    • Ezin baduzu inolako orririk kargatu, begiratu zure gailuaren datu- edo WiFi-konexioa.
    • +
    + ]]>
    + + + Interneterako konexiorik ez + + Egiaztatu zure sareko konexioa edo saiatu orria berritzen geroago. + + Berritu + + + Helbide baliogabea + Idatzitako helbidearen formatua ez da ulertzen. Egiaztatu helbide-barran ea akatsik dagoen eta saiatu berriro.

    + ]]>
    + + Helbidea ez da baliozkoa + + +
  • Web helbideak normalean http://www.adibidea.eus/ formatukoak dira
  • +
  • Ziurtatu aurrerako barrak erabiltzen dituzula (hau da, /).
  • + + ]]>
    + + + Protokolo ezezaguna + + Helbideak zehaztutako protokoloa (adib. wxyz://) ez du ezagutzen nabigatzaileak, beraz ezin da behar bezala konektatu gunera.

    +
      +
    • Multimedia edo testua ez den bestelako zerbitzuren bat atzitzen saiatzen ari zara? Egiaztatu guneak aparteko beharrik duen.
    • +
    • Zenbait protokolok hirugarrenen softwarea edo pluginak behar dituzte nabigatzaileak ezagutu ahal ditzan.
    • +
    + ]]>
    + + + Fitxategia ez da aurkitu + + +
  • Elementuaren izena aldatua, elementua bera ezabatuta edo lekuz aldatua egon daiteke?
  • +
  • Helbidea oker idatzi duzu?
  • +
  • Baduzu eskatutako elementua jasotzeko baimenik?
  • + + ]]>
    + + + Fitxategi-atzipena ukatu egin da + + +
  • Kendua edo lekuz aldatua egon liteke, edo fitxategi-baimenek sarrera eragotz lezakete.
  • + + ]]>
    + + + Proxy-zerbitzariak konexioa ukatu egin du + + Nabigatzailea proxy-zerbitzari bat erabiltzeko konfiguratuta dago, baina proxy-zerbitzariak konexioa ukatu egin du.

    +
      +
    • Proxy-zerbitzariaren ezarpenak ondo ezarrita daude? Egiaztatu ezarpenak eta saiatu berriro.
    • +
    • Proxy-zerbitzariak sare honetatik bideratutako konexioak baimenduta daude?
    • +
    • Arazoak oraindik? Jarri harremanetan zure sarearen kudeatzailearekin edo Internet hornitzailearekin.
    • +
    + ]]>
    + + + Proxy-zerbitzaria ez da aurkitu + + Nabigatzailea proxy-zerbitzari bat erabiltzeko konfiguratuta dago, baina proxy-zerbitzaria ez da aurkitu.

    +
      +
    • Proxy-zerbitzariaren ezarpenak ondo ezarrita daude? Egiaztatu ezarpenak eta saiatu berriro.
    • +
    • Gailua sare batera konektatua dago?
    • +
    • Arazoak oraindik? Jarri harremanetan zure sarearen kudeatzailearekin edo Internet hornitzailearekin.
    • +
    ]]>
    + + + Malware gunea + + %1$s gunea gune erasotzaile bezala salatua dago eta blokeatu egin da zure segurtasun-ezarpenetan oinarrituta.

    + ]]>
    + + + Nahi ez den gunea + + %1$s gunea nahi ez den softwarearen zerbitzari bezala salatua dago eta blokeatu egin da zure segurtasun-ezarpenetan oinarrituta.

    + ]]>
    + + + Gune arriskutsua + + %1$s gunea balizko gune arriskutsu bezala salatua dago eta blokeatu egin da zure segurtasun-ezarpenetan oinarrituta.

    + ]]>
    + + + Gune iruzurtia + + %1$s gune iruzurti gisa dago salatuta eta blokeatu egin da zure segurtasun-ezarpenetan oinarrituta.

    + ]]>
    + + + Gune segurua ez dago erabilgarri + + %1$s webgunearen HTTPS bertsioa ez dago erabilgarri.]]> + + Jarraitu HTTP gunera +
    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 @@ + + + + + تلاش دوباره + + + امکان تکمیل درخواست وجود ندارد + + + در حال حاضر اطلاعات بیشتری در مورد این ایراد یا خطا در دسترس نیست.

    + ]]>
    + + + برقراری پیوند ایمن شکست خورد + + +
  • صفحه‌ای که تلاش می‌کنید از آن بازدید کنید قابل نمایش نیست، زیرا امکان تأیید اعتبار داده‌های دریافتی از آن وجود ندارد.
  • +
  • لطفاً با صاحبان این وبگاه تماس بگیرید و آن‌ها را در جریان این مشکل قرار دهید.
  • + + ]]>
    + + + برپاسازی پیوند ایمن شکست خورد + + +
  • علت می‌تواند اشکالی در پیکربندی کارساز، یا تلاش فردی برای وانمود کردن خود به جای این کارساز باشد.
  • +
  • اگر در گذشته با موفقیت به این کارساز متصل شده‌اید، امکان دارد این اشکال موقتی باشد، و می‌توانید بعداً دوباره تلاش کنید.
  • + + ]]>
    + + + پیشرفته… + + شخصی می‌تواند در تلاش برای جعل هویت پایگاه باشد و شما نباید ادامه دهید. +

    + + ]]>
    + + بازگشت (پیشنهاد می‌شود) + + پذیرش خطر و ادامه + + + این وبگاه به اتصالی ایمن نیاز دارد. + + + +
  • صفحه‌ای را که می‌خواهید مشاهده کنید نمیتواند نمایش داده شود زیرا این وبگاه به اتصال ایمن نیاز دارد.
  • +
  • مشکل به احتمال زیاد مربوط به وبگاه است و کاری برای حل آن نمی توانید انجام دهید.
  • +
  • می‌توانید مشکل را به مدیر وبگاه اطلاع دهید.
  • + + ]]>
    + + + پیش‌رفته… + + + %1$s یک خط‌مشی امنیتی به نام HTTP Strict Transport Security (HSTS) دارد، به این معنی که %2$s فقط می‌تواند به صورت ایمن به آن متصل شود. شما نمی‌توانید استثنایی برای بازدید از این پایگاه اضافه کنید. + ]]> + + بازگشت + + + اتّصال قطع شد + + مرورگر با موفقیت متّصل شد، اما هنگام انتقال اطلاعات، اتّصال قطع شد. لطفاً دوباره تلاش کنید.

    +
      +
    • سایت ممکن است موقتاً در دسترس نباشد یا خیلی شلوغ باشد. چند لحظه بعد دوباره تلاش کنید.
    • +
    • اگر قادر به بارگزاری هیچ صفحه‌ای نیستید، اتّصال دادهٔ دستگاه یا وای‌فای خود را بررسی کنید.
    • +
    + ]]>
    + + + مهلت اتّصال تمام شد + + پایگاه درخواست شده به تقاضای اتّصال مرورگر پاسخ نداد و مرورگر انتظار برای پاسخ را متوقف کرد.

    +
      +
    • آیا ممکن است کارساز دچار تقاضای بیش از حد شده باشد یا موقتاً دارای مشکلی باشد؟ لطفاً بعداً دوباره تلاش کنید.
    • +
    • آیا قادر به مرور پایگاه‌های دیگر نیز نیستید؟ اتّصال شبکهٔ افزاره‌تان را بررسی کنید.
    • +
    • آیا افزاره یا شبکهٔ شما توسط دیوار آتش یا پیشکار محافظت می‌شود؟ تنظیمات نادرست آن مانع از مرور وب می‌شود.
    • +
    • هنوز هم مشکل دارید؟ برای دریافت کمک با مدیر شبکه یا فراهم‌کنندهٔ اینترنت خود مشورت کنید.
    • +
    + ]]>
    + + + قادر به برقراری اتصال نیست + + +
  • ممکن است پایگاه موقتاً در دسترس نباشد یا خیلی شلوغ باشد. چند لحظهٔ دیگر دوباره تلاش کنید.
  • +
  • اگر نمی‌توانید هیچ صفحه‌ای را باز کنید، اتّصال دادهٔ افزاره یا وای‌فای خود را بررسی کنید.
  • + + ]]>
    + + + جواب غیرمنتظره از کارساز + + این سایت به شیوهٔ غیر منتظره‌ای به درخواست شبکه پاسخ داد و مرورگر قادر به ادامه نیست.

    + ]]>
    + + + این صفحه درست تغییر مسیر نمی‌دهد + + مرورگر تلاش برای دریافت مورد درخواستی را پایان داده است. وبگاه به صورتی درخواست را تغییر مسیر می‌دهد که این کار هیچ‌گاه به پایان نخواهد رسید.

    +
      +
    • آیا کلوچک‌های احتمالی مورد نیاز این وبگاه را غیرفعال ساخته‌اید؟
    • +
    • اگر پذیرفتن کلوچک‌های وبگاه مشکل را حل نکرد، به احتمال زیاد این مشکلی در تنظیمات کارساز است و به رایانهٔ شما مربوط نیست.
    • +
    + ]]>
    + + + حالت برون‌خط + + مرورگر در حالت برون‌خط قرار دارد و نمی‌تواند به مورد درخواستی وصل شود.

    +
      +
    • آیا رایانه به یک شبکهٔ فعال متصل است؟
    • دکمهٔ «تلاش دوباره» را فشار دهید تا به حالت برخط رفته و صفحه را دوباره بار کنید.
    • +
    + ]]>
    + + + درگاه به دلایل امنیتی محدود شده است + + نشانی درخواست مشخصا(به عنوان مثالmozilla.org:80برای درگاه ۸۰ بر روی mozilla.org) ازدرگاهی استفاده می کندکه در حالت عادی به عنوان کاربردی به غیر از وبگردی استفاده می شود.مرورگر برای حفاظت و امنیت شما این درخواست را لغوکرد.

    ]]>
    + + + اتصال از نو برقرار شد + + مرورگر با موفقیت متصل شد ، اما هنگام انتقال اطلاعات ، اتصال قطع شد. لطفا دوباره امتحان کنید.

    +      
      +        
    • سایت ممکن است موقتاً در دسترس نباشد یا خیلی شلوغ باشد. در چند لحظه دوباره امتحان کنید.
    • +        
    • اگر قادر به بارگیری هیچ صفحه ای نیستید ، اطلاعات دستگاه یا اتصال Wi-Fi خود را بررسی کنید.
    • +      
    ]]>
    + + + نوع پرونده ناامن است + +
  • لطفاً با صاحبان پایگاه وب تماس بگیرید تا آنها را در جریان این مشکل قرار دهید.
  • ]]>
    + + + خطای خرابی محتوا + + صفحه‌ای که تلاش می‌کنید از آن بازدید کنید قابل نمایش نیست، زیرا خطایی در هنگام انتقال اطلاعات رُخ داده است.

    • لطفاً با صاحبان این پایگاه اینترنتی تماس بگیرید و آنها را در جریان این مشکل قرار دهید.
    ]]>
    + + + محتوا فروپاشید + صفحه‌ای که تلاش می‌کنید از آن بازدید کنید قابل نمایش نیست، زیرا خطایی در هنگام انتقال اطلاعات رخ داده است.

    +
      +
    • لطفاً با صاحبان این وبگاه تماس بگیرید و آن‌ها را در جریان این مشکل قرار دهید.
    • +
    + ]]>
    + + + خطای کدگذاری محتوا + قادر به نمایش صفحه‌ای که درخواست بازدید از آن را کرده‌اید نیست، زیرا از گونه‌ای ناشناخته یا پشتیبانی نشده از فشرده‌سازی استفاده می‌کند.

    • لطفاً با صاحبان این پایگاه اینترنتی تماس برقرار کنید و آنها را در جریان این مشکل قرار دهید.
    ]]>
    + + + نشانی پیدا نشد + + مرورگر نتوانست کارساز میزبان را برای نشانی ارائه شده پیدا کند.

    +
      +
    • نشانی را برای غلط‌های املایی بررسی کنید، مانند: + ww.example.com به جای + www.example.com.
    • +
    • اگر قادر به بارگیری هیچ صفحه‌ای نیستید، دادهٔ افزاره یا اتصال وای‌فای خود را بررسی کنید.
    • +
    + ]]>
    + + + اتصال اینترنت در دسترس نیست + + اتصال شبکهٔ خود را بررسی کنید یا پس از چند لحظه، بار کردن دوبارهٔ صفحه را بیازمایید. + + بار کردن دوباره + + + نشانی نامعتبر + نشانی فراهم‌شده دارای قالب معتبری نیست. لطفاً از عدم وجود اشتباه در نوار مکان اطمینان حاصل کنید و دوباره تلاش نمایید.

    ]]>
    + + نشانی معتبر نیست + + +
  • نشانی‌های وب معمولاً به این صورت نوشته می‌شوند http://www.example.com/
  • +
  • مطمئن شوید که از ممیز درست استفاده می‌کنید (یعنی /).
  • +]]>
    + + + نشانی قابل فهم نبود + نشانی قراردادی مثلا (e.g. wxyz://)‏ را مشخص کرده است که مرورگر قادر به شناسایی آن نیست، بنابراین نمی‌تواند به شیوه صحیح به وب‌گاه متصل شود.

    • آیا در حال تلاش برای دسترسی به خدمات چندرسانه‌ای یا غیر متنی هستید؟ این وب‌گاه را برای یافتن ملزومات اضافی جست‌وجو کنید.
    • برخی قراردادها ممکن است برای شناسایی نیاز به نرم‌افزار یا افزایه های دیگری جهت شناسایی داشته باشند.
    ]]>
    + + + پرونده پیدا نشد +
  • آیا امکان دارد پروندهٔ مورد نظر تغییر نام یا محل داده باشد یا حذف شده باشد؟
  • آیا اشکال املائی، بزرگی یا کوچکی حروف یا دیگر اشکالات نوشتاری در نشانی وجود دارد؟
  • آیا مجوزهای دسترسی کافی برای دست‌یابی به این نشانی را دارید؟
  • ]]>
    + + + دسترسی به این پرونده رد می‌شود +
  • ممکن است حذف،‌منتقل شده باشد یا مجوز‌های آن از دسترسی جلوگیری‌ می‌کند.
  • ]]>
    + + + کارساز پیشکار از اتصال خودداری می‌کند + مرورگر برای استفاده از یک کارساز پیشکار پیکربندی شده است، ولی پیشکار از اتصال خودداری می‌کند.

    +
      +
    • آیا تنظیمات کارساز پیشکار مرورگر صحیح است؟ تنظیمات را کنترل کنید و دوباره تلاش نمایید.
    • +
    • آیا پیشکار اجازهٔ اتصال از این شبکه را می‌دهد؟
    • +
    • هنوز هم مشکل دارید؟ برای دریافت کمک با مدیر شبکه یا فراهم‌کنندهٔ اینترنت خود مشورت کنید.
    • +
    + ]]>
    + + + کارساز پیشکار پیدا نشد + مرورگر برای استفاده از یک کارساز پیشکار پیکربندی شده است، ولی پیشکار پیدا نشد.

    +
      +
    • آیا تنظیمات کارساز پیشکار مرورگر صحیح است؟ تنظیمات را کنترل کنید و دوباره تلاش نمایید.
    • +
    • آیا افزاره به شبکه‌ای فعال متصل است؟
    • +
    • هنوز هم مشکل دارید؟ برای دریافت کمک با مدیر شبکه یا فراهم‌کنندهٔ اینترنت خود مشورت کنید.
    • +
    + ]]>
    + + + مشکل پایگاه بدافزار + + پایگاه%1$s به عنوان یک وب‌گاه تهاجمی گزارش شده و بر اساس ترجیحات امنیتی شما مسدود شده است.

    ]]>
    + + + مشکل پایگاه ناخواسته + + پایگاه %1$s به عنوان سایتی که اقدام به ارائه نرم‌افزارهای ناخواسته می‌کند گزارش شده و بر اساس ترجیحات امنیتی شما مسدود شده است.

    ]]>
    + + + مشکل پایگاه زیان‌آور + + پایگاه%1$s به عنوان یک وب‌گاه تهاجمی گزارش شده و بر اساس ترجیحات امنیتی شما مسدود شده است.

    ]]>
    + + + مشکل پایگاه گمراه‌کننده + + صفحه %1$s به عنوان یک پایگاه وب گمراه‌کننده گزارش شده و بر اساس ترجیحات امنیتی شما مسدود شده است.

    ]]>
    + + + پایگاه ایمن در دسترس نیست + + %1$s در دسترس نیست.]]> + + ادامه به این پایگاه اینترنتی به وسیله HTTP +
    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 @@ + + + + + Fuɗɗito + + + Horiima Timminde Ɗaɓɓitannde + + Humpito ɓeydorɗo baɗte ɗee caɗeele walla ndee juumre heɓotaako oo sahaa.

    ]]>
    + + + Ceŋagol Kisnangol Woorii + + +  
  • Hello ngo etoto-ɗaa naatnde ngoo waawaa hollireede sabu goongɗingol keɓe keɓaaɗe ɗee waawaa ƴeewteede.
  • +
  • Tiiɗno jokkondir e jeyɓe lowre geese ndee ngam humpitde-ɓe caɗeele ɗee.
  •   + ]]>
    + + + Ceŋagol Kisnangol Woorii + + + +
  • Ɗum ena waawi ummaade e teeltol sarworde ndee, walla ena waawi wonde won etotooɗo ñemmbitaade sarworde ndee.
  • +
  • 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.
  • + ]]>
    + + + Ceeɓtore… + + Neɗɗo ina waawi wonde e etaade wujjude hefinirde lowre ndee etee a fotaani jokkude. +

    + ]]>
    + + Rutto (Ena wasiyaa) + + Jaɓ tanaa oo njokkaa + + + Nde lowre ena laaɓndii ceŋol kisnangol. + + + Ceŋagol ngol taƴii + + + Wanngorde ndee seŋii haa moƴƴi, kono ceŋogol ngol taƴii saanga baylugol humpito. Tiiɗno, eto goɗngol.

    +
      +
    • Lowre ndee ina waasde heɓaade e mudda walla ko nde haljunde no feewi. Eto kadi ɗo e yeeso seeɗa.
    • +
    • O a horiima loowde hay hello wooto, ƴeewto keɓe kaɓirgal maa walla ceŋogol WI-FI.
    • +
    ]]>
    + + + Ceŋagol ngol honaama waktu + + + Lowre ɗaɓɓitaande ndee jaabaaki ɗaɓɓitaande ceŋol etee wanngorde ndee dartiima fadde jaabtol.

    +
      +
    • Mbar sarworde ndee wonaa heewraande walla ena taƴi oo sahaa? Fuɗɗito so ɓooyii.
    • +
    • A horiima naatde e lowe goɗɗe? Ƴeewto ceŋol laylaytol masiŋel ngel.
    • +
    • Mbar ordinateer maa suuraaki caggal ɓalal-jaynge walla proxy? Teelɗe ɗe peewaani ena mbaawi haɗde peeragol e geese.
    • +
    • Haa jonni ena waɗi caɗeele? Jokkondir e jiiloowo laylaytol maa walla jeeyoowo ceŋol maa ngam ɗaɓɓude ballal.
    • +
    ]]>
    + + + Horiima seŋaade + + +
  • dee lowre heɓotaako oo sahaa walla ena halji. Eto naatde e mayre ɗoo e yeeso seeɗa.
  • +
  • So a horiima loowde kelle fof, ƴeewto keɓe kaɓirgol maa walla seŋorde maa Wi-Fi.
  • + ]]>
    + + + Jaabawol sarworde ngol tijjaaka + + Lowre ndee jaabtiima ɗaɓɓitaande laylaytol ndol e mabydi kaawniindi etee wanngorde ndee waawaa jokkude.

    ]]>
    + + + Ngoo hello wonaani e yiiltude no feewiri + + + Wanngorde ndee dartinii aaftaade temre ɗaɓɓitaande ndee. Lowre ndee ɓenninii ɗaɓɓitaande ndee e mbaydi ndi jogoraani timmude.

    +
      +
    • Mbar a daaƴaani walla pali-ɗaa kukiije cokalaaɗe e ndee lowre?
    • +
    • So jaɓde kukiije lowre ndee ñawndaani caɗeele ɗee, Ena wona tawwa ko teeltol sarworde ndee kono wonaa kaɓirgal maa.
    • +
    ]]>
    + + + Mbaydi Ceŋtol + + + Wanngorde ndee woni ko e mbayka ceŋtol etee waawaa seŋaade e temre ɗaɓɓitaande ndee.

    +
      +
    • Mbar kaɓirgal maa ena seŋii e laylaytol caasngol?
    • +
    • Ñoƴƴu “eto kadi” ngam artirde a mbayka ceŋol, kesɗitinaa hello ngoo.
    • +
    ]]>
    + + + Ceŋagol ngol fuɗɗitaama + + + Ndee Fiilde Hoolnaaki + + +
  • Tiiɗno jokkondir e jeyɓe lowre ndee ngam humpitde-ɓe ɗee caɗeele.
  • + ]]>
    + + + Juumre loowdi pirndi + + + Hello ngo etot-ɗaa naatnde ngoo waawaa hollireede sabu juumre waɗii e baanjitagol keɓe ɗee.

    +
      +
    • Tiiɗno jokkondir e jeyɓe lowre geese ndee ngam humpitde-ɓe caɗeele ɗee.
    • +
    ]]>
    + + + Loowdi ndii hookii + Hello ngo etot-ɗaa naatnde ngoo waawaa hollireede sabu juumre waɗii e baanjitagol keɓe ɗee.

    +
      +
    • Tiiɗno jokkondir e jeyɓe lowre geese ndee ngam humpitde-ɓe caɗeele ɗee.
    • +
    ]]>
    + + + Ñiiɓirde Yiytaaka + + + Alaa ceŋol enternet + + + Yeewto ceŋogol laylaytol maa walla eto loowtugol hello ngoo ko ɓooyaani. + + Loowtu + + + Ñiiɓirde Moƴƴaani + Ñiiɓirde hokkaande ndee wonaani e mbaydi keftinaandi. Tiiɗno ƴeewto palal ñiiɓiɗe ndee ngam pergitte, puɗɗito-ɗaa.

    ]]>
    + + Ñiiɓirde ndee moƴƴaani + +
  • Ñiiɓirɗe geese keewi winndireede ko http://www.yeru.com/
  • Ƴeewto no feewi so a huutoriima laase yeeso (k.n. /).
  • + ]]>
    + + + Fiilde Yiytaaka + + + Ballagol fiilde salaama + +
  • Maa taw ko nde momtaande, dirtinaande, walla jamire fiilde ena kala ballagol.
  • + ]]>
    + + + Sawrorde Proxy Saliima Ceŋagol + + + Sarworde Proxy Yiytaaka + + + Ndee lowre to %1$s jaŋtaama wonde ko lowre njangu etee ko ko daaƴaa e cuɓoraaɗe kisal maa.

    ]]>
    + + + Caɗe lowre ɗe njiɗaaka + + Ndee lowre to %1$s jaŋtaama wonde sarwat topirɗe gañaaɗe etee koko daaƴaa e cuɓoraaɗe kisal maa.

    ]]>
    + + + Caɗe lowre bonnde + + Ndee lowre to %1$s jaŋtaama wonde ko lowre waawnde bonnude etee koko daaƴaa e cuɓoraaɗe kisal maa.

    ]]>
    + + + Caɗe lowre fuuntoore + + Ndee lowre to %1$s jaŋtaama wonde ko lowre fuuntoore etee koko daaƴaa e cuɓoraaɗe kisal maa.

    ]]>
    + + + Jooku to Lowre HTTP +
    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 @@ + + + + + Yritä uudelleen + + + Pyyntöä ei voi suorittaa + + + Lisätietoja tästä ongelmasta tai virheestä ei ole juuri nyt saatavilla.

    + ]]>
    + + + Suojattu yhteys epäonnistui + + + +
  • Avattavaa sivua ei voida näyttää, koska vastaanotetun datan alkuperää ei kyetty varmentamaan.
  • +
  • Ilmoitathan ongelmasta sivuston omistajalle.
  • + ]]>
    + + + Suojattu yhteys epäonnistui + + + +
  • Ongelma voi johtua palvelimen asetuksista tai jonkin toisen palvelimen vilpillisestä yrityksestä tekeytyä palvelimeksi.
  • +
  • Jos yhteyden muodostuminen palvelimeen on aiemmin onnistunut, vika voi olla väliaikainen. Yritä tällöin myöhemmin uudestaan.
  • + +]]>
    + + + Lisäasetukset… + + Jokin toinen osapuoli saattaa tekeytyä sivustoksi. Ei ole suositeltavaa jatkaa sivustolle. +

    +]]>
    + + Palaa (suositellaan) + + Ota riski ja jatka + + + Tämä sivusto vaatii suojatun yhteyden. + + + +
  • Sivua, jota yrität katsella, ei voida näyttää, koska tämä verkkosivusto vaatii suojatun yhteyden.
  • +
  • Ongelma johtuu todennäköisesti verkkosivustosta, etkä voi tehdä mitään ongelman ratkaisemiseksi.
  • +
  • Voit ilmoittaa ongelmasta verkkosivuston ylläpitäjälle.
  • + + ]]>
    + + + Lisäasetukset… + + + %1$s noudattaa tietoturvakäytäntöä nimeltä HTTP Strict Transport Security (HSTS), mikä tarkoittaa, että %2$s voi muodostaa vain suojatun yhteyden. Tälle sivustolle siirtymistä varten ei voi lisätä poikkeusta. + ]]> + + Palaa takaisin + + + Yhteys keskeytettiin + + + Selain muodosti yhteyden, mutta yhteys katkesi siirrettäessä tietoa. Yritä uudestaan.

    +
      +
    • Sivusto voi olla väliaikaisesti saavuttamattomissa tai kovan rasituksen alaisena. Yritä hetken kuluttua uudestaan.
    • +
    • Jos mitkään sivustot eivät toimi, tarkista laitteen data- tai Wi-Fi-yhteys.
    • +
    ]]>
    + + + Yhteys aikakatkaistiin + + + Palvelin ei vastannut yhteyspyyntöön, ja selain lopetti vastauksen odottamisen.

    • Palvelin voi olla kovan rasituksen alainen tai väliaikaisesti huollettavana. Yritä myöhemmin uudestaan.
    • Jos muutkaan sivustot eivät toimi, tarkista laitteen verkkoasetukset.
    • Onko laite tai verkko suojattu palomuurilla tai käytetäänkö välityspalvelinta? Virheelliset asetukset voivat haitata selaamista.
    • Jos ongelmat jatkuvat, ota yhteyttä verkon ylläpitoon tai verkkoyhteyden palveluntarjoajaan.
    ]]>
    + + + Yhdistäminen ei onnistu + + + +
  • Sivusto saattaa olla väliaikaisesti pois käytöstä tai ruuhkautunut. Yritä uudelleen hetken kuluttua.
  • +
  • Jos et voi avata mitään sivuja, tarkista laitteen data- ja wifi-yhteyksien tila.
  • + + ]]>
    + + + Odottamaton vastaus palvelimelta + + + Sivusto vastasi verkkopyyntöön odottamattomalla tavalla, eikä selain voi jatkaa.

    + ]]>
    + + + Sivu ei uudelleenohjaa oikein + + + Selain on lopettanut pyydetyn kohteen lataamisen. Palvelin uudelleenohjaa yhteyspyyntöjä loputtomasti.

    +
      +
    • Onko kaikki tai sivuston tarvitsemat evästeet estetty?
    • +
    • Jos sivuston evästeiden salliminen ei korjaa ongelmaa, vika on luultavasti palvelimen asetuksissa, eikä tässä laitteessa.
    • +
    ]]>
    + + + Yhteydetön tila + + + Selain on verkkoyhteydettömässä tilassa eikä voi muodostaa yhteyttä pyydettyyn kohteeseen.

    +
      +
    • Onko laitteen verkkoyhteys toiminnassa?
    • +
    • Aseta selain yhteystilaan ja yritä uudelleen.
    • +
    ]]>
    + + + Portin käyttöä rajoitettu tietoturvasyistä + + + Pyydetty osoite sisältää käytettävän portin (esim. mozilla.org:80 ottaa yhteyden mozilla.orgin porttiin 80), jota tavallisesti käytetään muuhun kuin verkkosivujen selaamiseen. Selain on perunut verkkopyynnön turvallisuussyistä.

    ]]>
    + + + Yhteys keskeytyi + + + Verkkoyhteys katkesi muodostettaessa yhteyttä. Yritä uudestaan.

    +
      +
    • Sivusto voi olla väliaikaisesti saavuttamattomissa tai kovan rasituksen alaisena. Yritä hetken kuluttua uudestaan.
    • +
    • Jos mitkään sivustot eivät toimi, tarkista laitteen data- tai Wi-Fi-yhteys.
    • +
    ]]>
    + + + Vaarallinen tiedostotyyppi + + + +
  • Ole yhteydessä sivuston omistajaan ja ilmoita kohtaamastasi ongelmasta.
  • + + ]]>
    + + + Vioittuneen sisällön virhe + + + Avattavaa sivua ei voida näyttää, koska tiedonsiirrossa tapahtui virhe.

    +
      +
    • Ilmoitathan ongelmasta sivuston omistajalle.
    • +
    ]]>
    + + + Sisältö kaatui + + Avattavaa sivua ei voida näyttää, koska tiedonsiirrossa tapahtui virhe.

    +
      +
    • Ilmoitathan ongelmasta sivuston omistajalle.
    • +
    ]]>
    + + + Sisällön koodausvirhe + + Avattavaa sivua ei voida näyttää, koska se käyttää virheellistä tai ei-tuettua pakkaustapaa.

    +
      +
    • Ilmoitathan ongelmasta sivuston omistajalle.
    • +
    ]]>
    + + + Osoitetta ei löytynyt + + + Selain ei löytänyt osoitteessa annettua palvelinta.

    +
      +
    • Tarkista osoite kirjoitusvirheiden varalta, esimerkiksi + ww.mozilla.org oikean muodon + www.mozilla.org sijaan.
    • +
    • Jos muutkaan sivustot eivät toimi, tarkista laitteen data- tai Wi-Fi-yhteys.
    • +
    ]]>
    + + + Ei internetyhteyttä + + Tarkista verkkoyhteys tai yritä päivittää sivu hetken kuluttua. + + Päivitä + + + Virheellinen osoite + Annettu osoite ei ole tunnistettavassa muodossa. Tarkista osoitepalkin sisältö virheiden varalta ja yritä uudelleen.

    + ]]>
    + + Osoite ei ole kelvollinen + + + +
  • Verkko-osoitteet ovat yleensä muodossa http://www.example.com/
  • +
  • Varmista että käytät vinoviivoja (esim. /).
  • + + ]]>
    + + + Tuntematon yhteyskäytäntö + + Selain ei tunnistanut osoitteessa käytettyä yhteyskäytäntöä (esim. wxyz://), minkä takia yhteyttä palvelimeen ei voida muodostaa.

    +
      +
    • Jos yhteyttä muodostetaan multimediaa tai jotain muuta kuin tekstiä tarjoavaan palveluun, tarkista palvelimen lisävaatimukset asiakasohjelmille.
    • +
    • Toimiakseen selaimessa jotkin yhteyskäytännöt vaativat kolmannen osapuolen tekemän ohjelman tai liitännäisen.
    • +
    ]]>
    + + + Tiedostoa ei löydy + + +
  • Tiedosto voi olla poistettu, siirretty tai nimetty uudelleen.
  • +
  • Onko tiedoston nimi ja sijainti kirjoitettu virheettömästi ja oikealla kirjainkoolla?
  • +
  • Onko käyttäjällä lukuoikeudet tiedostoon?
  • + ]]>
    + + + Pääsy tiedostoon estettiin + + +
  • Se saattaa olla poistettu, siirretty tai tiedoston oikeudet estävät sen käytön.
  • + + ]]>
    + + + Välityspalvelin kieltäytyi yhteydestä + + Selain on asetettu käyttämään välityspalvelinta, mutta välityspalvelin ei hyväksynyt yhteyttä.

    +
      +
    • Ovatko selaimen välityspalvelinasetukset oikeat? Tarkista asetukset ja yritä uudelleen.
    • +
    • Tulisiko välityspalvelimen hyväksyä yhteydet tästä verkkoyhteydestä?
    • +
    • Jos ongelmat jatkuvat, ota yhteyttä verkon ylläpitoon tai verkkoyhteyden palveluntarjoajaan.
    • +
    ]]>
    + + + Välityspalvelinta ei löytynyt + + Selain on asetettu käyttämään välityspalvelinta, mutta siihen ei saatu yhteyttä.

    +
      +
    • Ovatko selaimen välityspalvelinasetukset oikeat? Tarkista asetukset ja yritä uudelleen.
    • +
    • Onko tietokoneen verkkoyhteys toimintakykyinen?
    • +
    • Jos ongelmat jatkuvat, ota yhteyttä verkon ylläpitoon tai verkkoyhteyden palveluntarjoajaan.
    • +
    ]]>
    + + + Haittaohjelmia sisältävä sivusto + + + Sivu osoitteessa %1$s on ilmoitettu hyökkäyssivustoksi, ja on siksi estetty pohjautuen asettamiisi tietoturva-asetuksiin.

    + ]]>
    + + + Ei-toivottua sisältöä tarjoava sivusto + + + Sivun osoitteessa %1$s on ilmoitettu tarjoavan ei-haluttuja ohjelmistoja, ja on siksi estetty pohjautuen asettamiisi tietoturva-asetuksiin.

    + ]]>
    + + + Vahingollinen sivusto + + + Sivu osoitteessa %1$s on ilmoitettu mahdollisesti haitalliseksi sivustoksi, ja on siksi estetty pohjautuen asettamiisi tietoturva-asetuksiin.

    + ]]>
    + + + Harhaanjohtava sivusto + + Sivu osoitteessa %1$s on ilmoitettu harhaanjohtavaksi sivustoksi, ja on siksi estetty pohjautuen asettamiisi tietoturva-asetuksiin.

    + ]]>
    + + + Suojattu sivusto ei saatavilla + + %1$s ei ole käytettävissä.]]> + + Jatka HTTP-sivustolle +
    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 @@ + + + + + Réessayer + + + La requête ne peut aboutir + + Aucune autre information disponible concernant le problème ou l’erreur.

    + ]]>
    + + + Échec de la connexion sécurisée + + +
  • 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.
  • +
  • Veuillez contacter les propriétaires du site web pour les informer de ce problème.
  • + + ]]>
    + + + Échec de la connexion sécurisée + + +
  • Ceci peut être dû à un problème de configuration du serveur ou à une personne essayant d’usurper l’identité du serveur.
  • +
  • Si vous avez déjà pu vous connecter à ce serveur, l’erreur est peut-être temporaire et vous pouvez essayer à nouveau plus tard.
  • + + ]]>
    + + + Avancé… + + Quelqu’un pourrait essayer de se faire passer pour le site et vous ne devriez pas continuer. +

    +]]>
    + + Retour (recommandé) + + Accepter le risque et poursuivre + + + Ce site nécessite une connexion sécurisée. + + +
  • La page que vous essayez d’atteindre ne peut pas être affichée, car ce site web nécessite une connexion sécurisée.
  • +
  • Ce problème vient probablement du site web et il n’y a rien que vous puissiez faire pour le résoudre.
  • +
  • Vous pouvez informer l’administrateur du site web du problème.
  • + + ]]>
    + + + Avancé… + + + %1$s a recours à une stratégie de sécurité HTTP Strict Transport Security (HSTS), ce qui signifie que %2$s 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.]]> + + Retour + + + La connexion a été interrompue + + Le navigateur s’est correctement connecté, mais la connexion a été interrompue pendant le transfert d’informations. Veuillez réessayer.

    +
      +
    • Le site est peut-être temporairement indisponible ou surchargé. Réessayez plus tard.
    • +
    • Si vous n’arrivez à naviguer sur aucun site, vérifiez la connexion données ou Wi-Fi de votre appareil.
    • +
    + ]]>
    + + + Le délai d’attente a été dépassé + + Le navigateur a attendu trop longtemps lors de la connexion au site et a arrêté d’attendre une réponse.

    +
      +
    • Le serveur est peut-être en surcharge ou est temporairement en panne ? Réessayez plus tard.
    • +
    • D’autres sites sont aussi inaccessibles ? Vérifiez la connexion au réseau de votre appareil.
    • +
    • 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.
    • +
    • Vous avez toujours des problèmes ? Consultez votre administrateur réseau ou votre fournisseur d’accès à Internet pour obtenir de l’aide.
    • +
    ]]>
    + + + La connexion a échoué + + +
  • Le site est peut-être temporairement indisponible ou surchargé. Réessayez plus tard.
  • +
  • Si vous n’arrivez à naviguer sur aucun site, vérifiez la connexion données ou Wi-Fi de votre appareil.
  • + + ]]>
    + + + Réponse inattendue du serveur + + Le site a répondu à la requête réseau d’une façon inattendue et le navigateur ne peut continuer.

    + ]]>
    + + + La page n’est pas redirigée correctement + + 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.

    +
      +
    • Avez-vous désactivé ou bloqué les cookies nécessaires pour ce site ?
    • +
    • 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.
    • +
    ]]>
    + + + Mode hors connexion + + Le navigateur est en mode hors connexion et ne peut pas se connecter à l’adresse indiquée.

    +
      +
    • L’appareil est-il connecté au réseau ?
    • +
    • Cliquez sur le bouton « Réessayer » pour revenir en mode connecté et recharger la page.
    • +
    ]]>
    + + + Port restreint pour des raisons de sécurité + + L’adresse demandée indique un port (p. ex. mozilla.org:80 pour le port 80 sur mozilla.org) qui est normalement utilisé pour d’autres usages que la navigation sur le Web. Le navigateur a annulé la requête pour votre protection et votre sécurité.

    + ]]>
    + + + La connexion a été réinitialisée + + La liaison au réseau a été interrompue pendant la négociation d’une connexion. Veuillez réessayer.

    +
      +
    • Le site est peut-être temporairement indisponible ou surchargé. Réessayez plus tard.
    • +
    • Si vous n’arrivez à naviguer sur aucun site, vérifiez la connexion données ou Wi-Fi de votre appareil.
    • +
    + ]]>
    + + + Type de fichier non sûr + + +
  • Veuillez contacter les propriétaires du site web pour les informer de ce problème.
  • + + ]]>
    + + + Erreur due à un contenu corrompu + + 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.

    +
      +
    • Veuillez contacter les propriétaires du site web pour les informer de ce problème.
    • +
    + ]]>
    + + + Le contenu a planté + 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.

    +
      +
    • Veuillez contacter les propriétaires du site web pour les informer de ce problème.
    • +
    + ]]>
    + + + Erreur d’encodage de contenu + 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.

    +
      +
    • Veuillez contacter les propriétaires du site web pour les informer de ce problème.
    • +
    + ]]>
    + + + Adresse introuvable + + Le navigateur n’a pas pu trouver le serveur hôte pour l’adresse indiquée.

    +
      +
    • Vérifiez la syntaxe de l’adresse (saisie de ww.example.com au lieu de www.example.com par exemple) ;
    • +
    • Si vous n’arrivez à naviguer sur aucun site, vérifiez la connexion données ou Wi-Fi de votre appareil.
    • +
    + ]]>
    + + + Aucune connexion internet + + Vérifiez votre connexion réseau ou essayez d’actualiser la page dans quelques instants. + + Actualiser + + + Adresse invalide + 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.

    + ]]>
    + + L’adresse n’est pas valide + + +
  • La syntaxe des adresses web est généralement http://www.example.com/.
  • +
  • Assurez-vous de bien utiliser des barres obliques (c.-à-d. /).
  • + + ]]>
    + + + Le protocole n’a pas été reconnu + L’adresse indique un protocole (p. ex. wxyz://) inconnu du navigateur qui ne peut donc pas se connecter correctement au site.

    +
      +
    • Essayez-vous d’accéder à du contenu multimédia ou d’autres services non texte ? Vérifiez les prérequis logiciels du site.
    • +
    • Certains protocoles peuvent nécessiter un logiciel tiers ou des plugins pour que le navigateur puisse les reconnaître.
    • +
    + ]]>
    + + + Fichier introuvable + +
  • Le fichier a peut-être été renommé, supprimé ou déplacé ?
  • +
  • Y a-t-il une erreur d’orthographe, de majuscule ou une autre erreur typographique dans l’adresse ?
  • +
  • Avez-vous des permissions d’accès suffisantes pour ce fichier ?
  • + + ]]>
    + + + L’accès au fichier a été refusé + +
  • Il a peut-être été supprimé, déplacé ou les permissions associées au fichier ne permettent pas d’y accéder.
  • + + ]]>
    + + + La connexion a été refusée par le serveur proxy + Le navigateur est configuré pour utiliser un serveur proxy mais le proxy a refusé la connexion.

    +
      +
    • La configuration proxy du navigateur est-elle correcte ? Vérifiez les paramètres et réessayez.
    • +
    • Le service proxy autorise-t-il les connexions à partir de ce réseau ?
    • +
    • Vous avez toujours des problèmes ? Consultez votre administrateur réseau ou votre fournisseur d’accès à Internet pour obtenir de l’aide.
    • +
    + ]]>
    + + + Le serveur proxy est introuvable + Le navigateur est configuré pour utiliser un serveur proxy mais le proxy est introuvable.

    +
      +
    • La configuration proxy du navigateur est-elle correcte ? Vérifiez les paramètres et réessayez.
    • +
    • L’appareil est-il connecté au réseau ?
    • +
    • Vous avez toujours des problèmes ? Consultez votre administrateur réseau ou votre fournisseur d’accès à Internet pour obtenir de l’aide.
    • +
    ]]>
    + + + Problème de logiciel malveillant + + 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é.

    + ]]>
    + + + Problème de site indésirable + + 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é.

    + ]]>
    + + + Problème de site dangereux + + 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é.

    + ]]>
    + + + Problème de site trompeur + + 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é.

    + ]]>
    + + + Site sécurisé non disponible + + %1$s.]]> + + Continuer vers le site HTTP +
    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 @@ + + + + Torne prove + + + Impussibil completâ la richieste + + Pal moment no son disponibilis altris informazions su chest probleme o erôr.

    + ]]>
    + + + Conession sigure falide + + + +
  • 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.
  • +
  • Par plasê contate i proprietaris dal sît web par informâju di chest probleme.
  • + + ]]>
    + + + Conession sigure falide + + +
  • 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.
  • +
  • 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.
  • + + ]]>
    + + + Avanzadis… + + + Cualchidun al podarès cirî di sostituîsi al sît origjinâl e o disconseìn di continuâ. +

    + + ]]>
    + + Torne indaûr (conseât) + + Acete il risi e continue + + + Chest sît web al domande une conession sigure. + + +
  • Nol è pussibil mostrâ la pagjine che tu stâs cirint di visualizâ parcè che chest sît web al domande une conession sigure.
  • +
  • Al è probabil che il probleme al stedi tal sît web e duncje nol è nuie che tu podedis fâ par risolvilu.
  • +
  • Tu puedis però notificâ il probleme al aministradôr dal sît web.
  • + + ]]>
    + + + Avanzadis… + + + %1$s al à une politiche di sigurece clamade HTTP Strict Transport Security (HSTS), che al significhe che %2$s al pues conetisi dome in maniere sigure e no tu puedis zontâ une ecezion par visitâ chest sît. + ]]> + + Torne indaûr + + + La conession e je stade interote + + Il navigadôr si è conetût cun sucès, ma la conession e je stade interote dilunc il trasferiment des informazions. Torne prove.

    +
      +
    • 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.
    • +
    • Se no tu rivis a cjariâ nissune pagjine, controle i dâts dal dispositîf o la conession Wi-Fi.
    • +
    + ]]>
    + + + La conession e je lade fûr timp massim + + + Il sît domandât nol à rispuindût a une richieste di conession e il navigadôr al à fermât di spietâ une rispueste.

    +
      +
    • Isal pussibil che il servidôr al vedi une richieste masse elevade o une interuzion temporanie? Torne prove plui indenant.
    • +
    • Rivistu a visitâ altris sîts? Controle la conession di rêt dal to dispositîf.
    • +
    • 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.
    • +
    • Âstu ancjemò problemis? Consulte l’aministradôr de rêt o il furnidôr di acès a internet par vê assistence.
    • +
    + ]]>
    + + + Impussibil conetisi + + +
  • Al è pussibil che il sît nol sedi disponibil in maniere temporanie opûr masse ocupât. Torne prove chi di pôc.
  • +
  • Se no tu rivis a cjariâ nissune pagjine, controle i dâts dal to dispositîf o la conession Wi-Fi.
  • + + ]]>
    + + + Rispueste inspietade dal servidôr + + Ilsît al à rispuindût ae richieste di rêt intune maniere inspietade e il navigadôr nol pues continuâ.

    + ]]>
    + + + Il gnûf-dirotament di cheste pagjine nol funzione ben + + + 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.

    +
      +
    • Âstu disabilitât o blocât i cookies necessaris a chest sît?
    • +
    • 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.
    • +
    + ]]>
    + + + Modalitât fûr rêt + + Il navigadôr al sta lavorant in modalitât fûr rêt e nol pues conetisi al element domandât.

    +
      +
    • Il dispositîf isal conetût a une rêt ative?
    • +
    • Frache “Torne prove” par passâ ae modalitât in rêt e tornâ a cjariâ la pagjine.
    • +
    + ]]>
    + + + Puarte limitade par resons di sigurece + + + La direzion domandade e à specificât une puarte (p.e. mozilla.org:80 pe puarte 80 su mozilla.org) che di solit e ven doprade par altris finalitâts rispiet a chês di navigazion sul Web. Il navigadôr al à anulade la richieste pe tô protezion e sigurece.

    + ]]>
    + + + La conession e je stade anulade + + Il colegament ae rêt al è stât interot dilunc la negoziazion di une conession. Torne prove.

    +
      +
    • Al è pussibil che il sît nol sedi disponibil in maniere temporanie opûr masse ocupât. Torne prove chi di pôc.
    • +
    • Se no tu rivis a cjariâ nissune pagjine, controle i dâts dal dispositîf o la conession Wi-Fi.
    • +
    + ]]>
    + + + Gjenar di file no sigûr + + +
  • Contate il proprietari dal sît web par informâlu di chest probleme.
  • + + ]]>
    + + + Erôr di contignût comprometût + + 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.

    +
      +
    • Contate il proprietari dal sît web par informâlu di chest probleme.
    • +
    + ]]>
    + + + Contignût colassât + 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.

    +
      +
    • Contate il proprietari dal sît web par informâlu di chest probleme.
    • +
    + ]]>
    + + + Erôr te codifiche dal contignût + + 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.

    +
      +
    • Contate il proprietari dal sît web par informâlu di chest probleme.
    • +
    + ]]>
    + + + Direzion no cjatade + + Il navigadôr nol rive a cjatâ il servidôr host pe direzion indicade.

    +
      +
    • Controle la direzion par erôrs di scriture come + ww.esempli.com al puest di + www.esempli.com.
    • +
    • Se no tu rivis a cjariâ nissune pagjine, controle i dâts dal to dispositîf o la conession Wi-Fi.
    • +
    + ]]>
    + + + Nissune conession a internet + + Verifiche la tô conession di rêt o prove chi di pôc a tornâ a cjariâ la pagjine. + + Torne cjame + + + Direzion no valide + La direzion indicade no je intun formât ricognossût. Controle la sbare de direzion par cjatâ erôr e torne prove.

    + ]]>
    + + La direzion no je valide + + + +
  • Lis direzions dal web di solit a son scritis te forme http://www.esempli.com/
  • +
  • Controle di vê doprât lis sbaris justis (vâl a dî /).
  • + + ]]>
    + + + Protocol no cognossût + La direzion e specifiche un protocol (p.e. wxyz://) che il navigadôr nol ricognòs, duncje il navigadôr nol pues conetisi al sît in maniere juste.

    +
      +
    • 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.
    • +
    • Cualchi protocol al podarès domandâ software di tiercis parts o plugins par che il navigadôr ju podedi ricognossi.
    • +
    + ]]>
    + + + File no cjatât + +
  • Isal pussibil che l’element al vedi cambiât non, al sedi stât eliminât o spostât?
  • +
  • Isal un erôr di ortografie o scriture te direzion?
  • +
  • Âstu permès suficients par acedi al element domandât?
  • + + ]]>
    + + + Acès al file dineât + +
  • Al podarès jessi stât eliminât, spostât o i permès sul file a podaressin impedî l’acès.
  • + + ]]>
    + + + Il servidôr proxy al à refudât la conession + Il navigadôr al è configurât in mût di doprâ un servidôr proxy, ma il proxy al à refudât une conession.

    +
      +
    • Ise juste la configurazions proxy dal navigadôr? Controle lis impostazions e torne prove.
    • +
    • Il servizi proxy permetial lis conessions di cheste rêt?
    • +
    • Âstu ancjemò problemis? Consulte l’aministradôr di rêt o il furnidôr di acès a internet pe assistence.
    • +
    + ]]>
    + + + Impussibil contatâ il servidôr proxy + + Il navigadôr al è configurât in mût di doprâ un servidôr proxy, ma nol è stât pussibil cjatâ il proxy.

    +
      +
    • La configurazion proxy dal navigadôr ise juste? Controle lis impostazions e torne prove.
    • +
    • Il dispositîf isal colegât a une rêt ative?
    • +
    • Âstu ancjemò problemis? Consulte il to aministradôr di rêt o il furnidôr di acès internet pe assistence.
    • +
    + ]]>
    + + + Probleme di sît cun malware + + 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.

    + ]]>
    + + + Probleme di sît malvolût + + + 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.

    + ]]>
    + + + Probleme di sît pericolôs + + 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.

    + ]]>
    + + + Probleme di sît ingjanôs + + 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.

    + ]]>
    + + + Version sigure dal sît no disponibile + + %1$s .]]> + + Continue sul sît HTTP +
    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 @@ + + + + + Opnij probearje + + + Kin oanfraach net foltôgje + + Ekstra ynformaasje oer dit probleem of dizze flater is op dit stuit net beskikber.

    + + ]]>
    + + + Befeilige ferbining mislearre + + + +
  • De side dy’t jo besjen wolle kin net toand wurde, omdat de echtheid fan de ûntfongen gegevens net ferifiearre wurde kin.
  • + +
  • Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.
  • + + + + +]]>
    + + + Befeilige ferbining mislearre + + + + +
  • Dit kin in probleem mei de serverkonfiguraasje wêze, of ien probearret de server as in oar foar te dwaan.
  • + +
  • 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.
  • + + + + +]]>
    + + + Avansearre… + + Ien kin probearje de side nei te meitsjen en jo moatte net trochgean. +        

    +         +    ]]>
    + + Tebekgean (Oanrekommandearre) + + It risiko oanfurdigje en trochgean + + + Dizze webside fereasket in befeilige ferbining. + + + +
  • De side dy’t jo besykje te besjen kin net werjûn wurde, omdat dizze webside in befeilige ferbining fereasket.
  • +
  • It probleem leit nei alle gedachten by de website, en jo kinne neat dwaan om it op te lossen.
  • +
  • Jo kinne de behearder fan de website ynformearje oer it probleem.
  • + + ]]>
    + + + Avansearre… + + + %1$s hat in befeiligingsbelied mei de namme HTTP Strict Transport Security (HSTS), wat betsjut dat %2$s allinnich in befeilige ferbining dêrmei meitsje kin. Jo kinne gjin útsûndering tafoegje om dizze website te besykjen. + + ]]> + + Tebek + + + De ferbining waard ferbrutsen + + + De browser is suksesfol ferbûn, mar de ferbining waard ferbrutsen by it oerdragen fan ynformaasje. Probearje it opnij.

    + +
      + +
    • De website kin tydlik net beskikber of te drok wêze. Probearje it oer in pear mominten opnij. +
    • + +
    • As jo gjin siden lade kinne, kontrolearje dan de gegevens of de wifi-ferbining fan jo apparaat.
    • + +
    + + ]]>
    + + + De wachttiid foar de ferbining is ferstrutsen + + + De opfrege website hat net op in ferbiningsfersyk antwurde en de browser wachtet net mear op in antwurd.

    + +
      + +
    • Miskien ûnderfynt de server in hege fraach of in tydlike stroomûnderbrekking? Probearje it letter opnij.
    • + +
    • Kinne jo gjin oare websites besykje? Kontrolearje de netwurkferbining fan it apparaat.
    • + +
    • Wurdt jo apparaat of netwurk beskerme troch in firewall of proxy? Ferkearde ynstellingen kinne in goede wurking wylst it webbrowsen tsjin gean.
    • + +
    • Hawwe jo noch hieltyd problemen? Freegje jo netwurkbehearder of ynternetprovider foar assistinsje.
    • + +
    + + ]]>
    + + + Kin gjin ferbining meitsje + + + + +
  • Miskien is de website tydlik net beskikber of oerbelêste. Probearje it oer inkelde mominten opnij.
  • + +
  • As jo gjin inkelde side lade kinne, kontrolearje dan de gegevens- of wifi-ferbining fan jo apparaat.
  • + + + + ]]>
    + + + Unferwacht antwurd fan de server + + + De website antwurde op in ûnferwachte manier op de netwurkoanfraach en de browser kin net trochgean.

    + + ]]>
    + + + De side ferwiist net op in krekte wize troch + + + 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.

    + +
      + +
    • Hawwe jo cookies dy’t nedich binne foar dizze website útskeakele of blokkearre?
    • + +
    • As it akseptearjen fan cookies fan dizze website it probleem net oplost is it wierskynlik in serverkonfiguraasjeprobleem en net jo apparaat.
    • + +
    + + ]]>
    + + + Offlinemodus + + + De browser wurket op dit stuit sûnder ferbining en kin net ferbine mei it frege ûnderdiel.

    + +
      + +
    • Is it apparaat ferbûn mei in aktyf netwurk?
    • + +
    • Klik op ‘Opnij probearje’ om nei onlinemodus te gean en laad de side opnij.
    • + +
    + + ]]>
    + + + Poarte beheind om feilichheidsredenen + + + It opfrege adres spesifisearret in poarte (byg. mozilla.org:80 foar poarte 80 op mozilla.org) dy’t normaal sprutsen foar oare doeleinen as websneupjen brûkt wurdt. De browser hat it fersyk foar jo beskerming en feilichheid annulearre.

    + + ]]>
    + + + De ferbining waard opnij inisjalisearre + + + De netwurkkeppeling waard ûnderbrutsen wylst it ûnderhanneljen oer in ferbining. Probearje it opnij.

    + +
      + +
    • De website is mooglik tydlik net beskikber of te drok. Probearje it oer in oantal eagenblikken opnij.
    • + +
    • As jo gjin siden lade kinne, kontrolearje dan de gegevens- of wifi-ferbining fan jo apparaat.
    • + +
    + + ]]>
    + + + Unfeilich bestânstype + + + + +
  • Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.
  • + + + + +]]>
    + + + Skansearre-ynhâldsflater + + + De side dy’t jo besjen wolle kin net werjûn wurde, omdat der in flater yn de gegevensoerdracht detektearre is.

    + +
      + +
    • Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.
    • + +
    + + ]]>
    + + + Ynhâld ferûngelokke + + De side dy’t jo besjen wolle kin net werjûn wurde, omdat der in flater yn de gegevensoerdracht detektearre is.

    + +
      + +
    • Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.
    • + +
    + + ]]>
    + + + Ynhâldkodearringsflater + + 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.

    + +
      + +
    • Nim kontakt op mei de website-eigeners om se oer dit probleem te ynformearjen.
    • + +
    + + ]]>
    + + + Adres net fûn + + + De browser koe de hostserver foar it opjûne adres net fine.

    + +
      +
    • Kontrolearje it adres op typeflaters, lykas + + ww.example.com yn stee fan + + www.example.com.
    • + +
    • As jo gjin siden lade kinne, kontrolearje dan de gegevens- of wifi-ferbining fan jo apparaat.
    • + +
    + + ]]>
    + + + Gjin ynternetferbining + + Kontrolearje jo netwurkferbining of probearje de side oer in amerijke opnij te laden. + + Opnij lade + + + Unjildich adres + It opjûne adres hat gjin werkenbere yndieling. Kontrolearje de lokaasjebalke op flaters en probearje it opnij.

    + + ]]>
    + + It adres is net jildich + + + + +
  • Webadressen wurde trochgeans skreaun as http://www.example.com/
  • + +
  • Let der op dat jo foarweartse slashes brûke (d.y. /).
  • + + + + ]]>
    + + + Unbekend protokol + + It adres spesifisearret in protokol (byg. wxyz://) dat de browser net werkend, wêrtroch de browser net op in krekte manier mei de website ferbine kin.

    + +
      + +
    • Probearje jo tagong te krijen ta multimedia- of oare net-tekstservices? Kontrolearje de website op ekstra nedichheden.
    • + +
    • Guon protokollen kinne programmatuer of ynstekkers fan tredden fereaskje foardat de browser se werkenne kin.
    • + +
    + + ]]>
    + + + Bestân net fûn + + + +
  • Kin it item omneamd, fuortsmiten of ferpleatst wêze?
  • + +
  • Stiet der in stavering-, haadletter- of oare typografyske flater yn it adres?
  • + +
  • Hawwe jo genôch tagongsrjochten foar it opfrege item?
  • + + + + ]]>
    + + + Tagong ta it bestân is wegere + + + +
  • It kin fuortsmiten wêze, ferpleatst, of bestânsmachtigingen kinne tagong tsjingean.
  • + + + + + + +]]>
    + + + Proxyserver wegere de ferbining + + De browser is konfigurearre om in proxyserver te brûken, mar de proxy wegere in ferbining.

    + +
      + +
    • Is de proxykonfiguraasje fan de browser in oarder? Kontrolearje de ynstellingen en probearje it opnij.
    • + +
    • Stiet de proxyservice ferbiningen fan dit netwurk ta?
    • + +
    • Hawwe jo noch hieltyd problemen? Freegje jo netwurkbehearder of ynternetprovider foar assistinsje.
    • + +
    + + ]]>
    + + + Proxyserver net fûn + + De browser is konfigurearre om in proxyserver te brûken, mar de proxy koe net fûn wurde.

    + +
      + +
    • Is de proxykonfiguraasje fan de browser yn oarder? Kontrolearje de ynstellingen en probearje it opnij.
    • + +
    • Is it apparaat ferbûn mei in aktyf netwurk?
    • + +
    • Hawwe jo noch hieltyd problemen? Freegje jo netwurkbehearder of ynternetprovider foar assistinsje.
    • + +
    + + ]]>
    + + + Probleem mei malware op website + + + De website op %1$s is rapportearre as in fertochte side en is blokkearre op basis fan jo befeiligingsfoarkarren.

    + + ]]>
    + + + Probleem mei net winske website + + + De website op %1$s is rapportearre as in website dy’t net-winske software oanbiedt en is blokkearre op basis fan jo befeiligingsfoarkarren.

    + + ]]>
    + + + Probleem mei skealike website + + + De website op %1$s is rapportearre as in fertochte side en is blokkearre op basis fan jo befeiligingsfoarkarren.

    + + + + ]]>
    + + + Probleem mei misliedende website + + De website op %1$s is rapportearre as in misliedende website en is blokkearre op basis fan jo befeiligingsfoarkarren.

    + + ]]>
    + + + Befeilige website net beskikber + + %1$s is net beskikber.]]> + + Trochgean nei HTTP-website +
    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 @@ + + + + Bain Triail Eile As + + + Ní féidir an t-iarratas a chríochnú + + Níl eolas breise faoin bhfadhb nó faoin earráid seo ar fáil faoi láthair.

    + ]]>
    + + + Níorbh Fhéidir Ceangal Slán a Bhunú + + + +
  • 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ú.
  • +
  • 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.
  • + + ]]>
    + + + Níorbh Fhéidir Ceangal Slán a Bhunú + + +
  • 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í.
  • +
  • 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í.
  • + + ]]>
    + + + Ardroghanna… + + Seans go bhfuil duine éigin ag dul i mbréagriocht ar an suíomh, agus níor chóir duit leanúint ar aghaidh. +

    + + ]]>
    + + Amach As Seo (Molta) + + Tuigim an Baol. Ar Aghaidh Linn! + + + Idirbhriseadh an ceangal + + + D\'éirigh leis an mbrabhsálaí ceangal a bhunú, ach briseadh isteach air agus na sonraí á n-aistriú. Bain triail eile as.

    +
      +
    • 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.
    • +
    • Mura bhfuil tú in ann aon leathanach a lódáil, déan seiceáil ar do cheangal sonraí nó Wi-Fi.
    • +
    + ]]>
    + + + Ceangal imithe thar am + + Níor fhreagair an suíomh iarrtha iarratas ceangail agus tá an brabhsálaí éirithe as bheith ag fanacht ar fhreagra.

    +
      +
    • An féidir go bhfuil éileamh mór nó fadhb shealadach ar an bhfreastalaí? Triail arís é níos déanaí.
    • +
    • Mura bhfuil tú in ann suímh eile a bhrabhsáil, déan seiceáil ar cheangal líonra do ríomhaire.
    • +
    • 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.
    • +
    • 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.
    • +
    + ]]>
    + + + Ní féidir ceangal a bhunú + + +
  • 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.
  • +
  • Mura bhfuil tú in ann leathanach ar bith a lódáil, cinntigh ceangal sonraí nó Wi-Fi do ghléis.
  • + + ]]>
    + + + Freagra gan súil leis ón bhfreastalaí + + 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.

    + ]]>
    + + + Níl an leathanach ag atreorú i gceart + + + 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.

    +
      +
    • An bhfuil fianáin atá riachtanach don suíomh seo díchumasaithe nó coiscthe agat?
    • +
    • 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í.
    • +
    + ]]>
    + + + Mód As Líne + + Tá an brabhsálaí sa mhód as líne agus ní féidir leis ceangal leis an mhír iarrtha.

    +
      +
    • An bhfuil an ríomhaire ceangailte le líonra beo?
    • +
    • Brúigh “Bain Triail Eile As” chun dul ar líne agus an leathanach a lódáil arís.
    • +
    + ]]>
    + + + Tá an port srianta de bharr cúrsaí slándála + + Shonraigh an seoladh iarrtha port (m.sh. mozilla.org:80 le haghaidh poirt 80 ar mozilla.org) a úsáidtear de ghnáth le haghaidh cúiseanna seachas brabhsáil. Chealaigh an brabhsálaí d\'iarratas ar mhaithe le do shlándáil.

    + ]]>
    + + + Athshocraíodh an ceangal + + + Briseadh an nasc líonra agus ceangal á bhunú. Bain triail eile as.

    +
      +
    • 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.
    • +
    • Mura bhfuil tú in ann aon leathanach a lódáil, déan seiceáil ar do cheangal sonraí nó Wi-Fi.
    • +
    + ]]>
    + + + Cineál Comhaid Baolach + + +
  • 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.
  • + + ]]>
    + + + Earráid: Ábhar Truaillithe + + + An leathanach atá tú ag iarraidh a amharc, ní féidir é a thaispeáint toisc gur tharla earráid agus na sonraí á seoladh.

    +
      +
    • Téigh i dteagmháil le húinéir an tsuímh agus inis dóibh faoin fhadhb seo.
    • +
    + ]]>
    + + + Ábhar tuairteála + An leathanach atá tú ag iarraidh a amharc, ní féidir é a thaispeáint toisc gur tharla earráid agus na sonraí á seoladh.

    +
      +
    • Téigh i dteagmháil le húinéir an tsuímh agus inis dóibh faoin fhadhb seo.
    • +
    + ]]>
    + + + Earráid: Ionchódú Ábhair + 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.

    +
      +
    • Téigh i dteagmháil le húinéirí an tsuímh leis an bhfadhb a chur in iúl dóibh.
    • +
    + ]]>
    + + + Seoladh Gan Aimsiú + + + Níorbh fhéidir leis an mbrabhsálaí óstach don seoladh iarrtha a aimsiú.

    +
      +
    • Deimhnigh nach bhfuil botún clóscríofa sa seoladh, ar nós + ww.example.com in áit + www.example.com.
    • +
    • Mura bhfuil tú in ann aon leathanach a lódáil, déan seiceáil ar do cheangal sonraí nó Wi-Fi.
    • +
    + ]]>
    + + + Gan ceangal Idirlín + + Déan seiceáil ar do cheangal líonra, nó bain triail as an leathanach a athlódáil i gceann nóiméid. + + Athlódáil + + + Seoladh Neamhbhailí + Ní aithnítear formáid an tseolta sonraithe. Féach an bhfuil botúin i mbarra na suíomhanna agus bain triail eile as.

    + ]]>
    + + Níl an seoladh bailí + + +
  • De ghnáth scríobhtar seoltaí Gréasáin mar seo http://www.example.com/
  • +
  • Bí cinnte gur tulslaiseanna atá agat (.i. /).
  • + + ]]>
    + + + Prótacal Anaithnid + Sonraíonn an seoladh prótacal (m.sh. wxyz://) nach n-aithníonn an brabhsálaí agus ní féidir leis an mbrabhsálaí ceangal leis an suíomh i gceart dá bharr.

    +
      +
    • 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.
    • +
    • 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.
    • +
    + ]]>
    + + + Comhad Gan Aimsiú + +
  • An féidir go bhfuil an mhír athainmnithe, bainte, nó bogtha?
  • +
  • An bhfuil botún litrithe, nó ceannlitir mhícheart, nó botún eile den sórt sin, sa seoladh?
  • +
  • An bhfuil na ceadanna rochtana cuí agat don mhír iarrtha?
  • + + ]]>
    + + + Diúltaíodh rochtain ar an gcomhad + +
  • Seans nach bhfuil an comhad ann a thuilleadh, nó b\'fhéidir nach bhfuil cead agat é a rochtain.
  • + + ]]>
    + + + Dhiúltaigh an Seachfhreastalaí leis an gCeangal + Tá an brabhsálaí cumraithe le seachfhreastalaí a úsáid, ach dhiúltaigh an seachfhreastalaí le ceangal.

    +
      +
    • An bhfuil cumraíocht seachfhreastalaí cheart ar an mbrabhsálaí? Déan seiceáil ar na socruithe agus triail arís é.
    • +
    • An dtugann an tseirbhís seachfhreastalaí cead do cheangail ón líonra seo?
    • +
    • 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.
    • +
    + ]]>
    + + + Seachfhreastalaí Gan Aimsiú + + Tá an brabhsálaí socruithe chun seachfhreastalaí a úsáid, ach níorbh fhéidir an seachfhreastalaí a aimsiú.

    +
      +
    • An bhfuil cumraíocht an tseachfhreastalaí ceart? Déan seiceáil ar na socruithe agus triail arís é.
    • +
    • An bhfuil an ríomhaire ceangailte le líonra beo?
    • +
    • 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.
    • +
    + ]]>
    + + + Suíomh le bogearraí mailíseacha + + Tuairiscíodh gur suíomh ionsaithe é an suíomh %1$s agus tá cosc curtha air de bharr do roghanna slándála.

    + ]]>
    + + + Suíomh ar a bhfuil bogearraí gan iarraidh + + 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.

    + ]]>
    + + + Suíomh díobhálach + + 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.

    + ]]>
    + + + Suíomh cealgach + + Tuairiscíodh gur suíomh cealgach é an suíomh %1$s agus cuireadh cosc air de bharr do roghanna slándála.

    + ]]>
    + +
    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 @@ + + + + + Feuch ris a-rithist + + + Cha ghabh an t-iarrtas a choileanadh + + Chan eil fiosrachadh a bharrachd ann mun duilgheadas no mun mhearachd seo an-dràsta.

    ]]>
    + + + Dh’fhàillig an ceangal tèarainte + + +
  • Cha ghabh an duilleag a tha thu airson faicinn a shealltainn a chionn ’s nach gabh dearbhachd an dàta a fhuaradh a dhearbhadh.
  • +
  • Cuir fios gun fheadhainn aig a bheil an làrach-lìn gu bheil a leithid de dhuilgheadas ann.
  • + ]]>
    + + + Dh’fhàillig an ceangal tèarainte + + +
  • 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.
  • +
  • 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.
  • + ]]>
    + + + Adhartach… + + Dh’fhaoidte gu bheul cuideigin a’ leigeil orra gur iadsan an làrach sin agus cha bu chòir dhut leantainn air adhart. +

    + ]]>
    + + Air ais (Mholamaid seo) + + Tuigidh mi an cunnart, air adhart leam + + + Feumaidh an làrach-lìn seo ceangal tèarainte. + + +
  • Chan urrainn dhuinn an duilleag seo a shealltainn dhut a chionn ’s gum feum an làrach-lìn seo ceangal tèarainte.
  • +
  • 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.
  • +
  • Ach is urrainn dhut innse do rianaire na làraich-lìn gu bheil an duilgheadas seo ann.
  • + + ]]>
    + + + Adhartach… + + + Tha poileasaidh tèarainteachd aig %1$s air a bheil HTTP Strict Transport Security (HSTS), agus is ciall dha sin nach urrainn dha %2$s ach ceangal tèarainte a dhèanamh. Chan urrainn dhut eisgeachd a chur ris a thadhal air an làrach seo. + ]]> + + Air ais + + + Bhris rudeigin a-steach air a’ cheangal + + + Rinn am brabhsair ceangal ach bhris an ceangal nuair a bha fiosrachadh ga chur. Feuch ris a-rithist

    +
      +
    • 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.
    • +
    • Mur urrainn dhut duilleag sam bith a luchdadh, thoir sùil air a’ cheangal dàta no WiFi aig an uidheam agad.
    • +
    ]]>
    + + + Dh’fhalbh an ùine air a’ cheangal + + + 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.

    +
      +
    • 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.
    • +
    • Mura faic thu làraichean eile, cuir sùil air ceangal a’ choimpiutair agad ris an lìonra.
    • +
    • 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.
    • +
    • A bheil duilgheadas agad fhathast? Bruidhinn ri rianaire an lìonraidh agad no ris an fhrithealaiche-lìn airson cobhair.
    • +
    ]]>
    + + + Cha ghabh ceangal a dhèanamh ris + + + +
  • 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.
  • +
  • Mur urrainn dhut duilleag sam bith a ruigsinn, cuir sùil air ceangal dàta no WiFi an uidheim agad.
  • + ]]>
    + + + Freagairt ris nach robh dùil on fhrithealaiche + + 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.

    ]]>
    + + + Chan eil an duilleag ag ath-stiùireadh mar bu chòir + + + 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.

    +
      +
    • An do chuir briosgaidean à comas o chionn goirid a dh’fheumas an làrach seo no an do bhac thu iad?
    • +
    • 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.
    • +
    ]]>
    + + + Modh far loidhne + + + Tha am brabhsair agad sa mhodh far loidhne ’s chan urrainn dha ceangal ris an rud a dh’iarr thu.

    +
      +
    • A bheil an t-uidheam seo co-cheangailte ri lìonra beò?
    • +
    • Brùth “Feuch ris a-rithist” gus a dhol air loidhne is luchdaich an duilleag a-rithist an uairsin.
    • +
    ]]>
    + + + Tha am port seo cuingichte air sgàth adhbharan tèarainteachd + + + Shònraich an seòladh a chaidh iarraidh port (m.e. mozilla.org:80 airson port 80 air mozilla.org) a bhios ga chleachdadh a chum adhbharan eile seach seòladh an lìn. Chuir am brabhsair casg air an iarrtas airson do dhìon is do thèarainteachd.

    ]]>
    + + + Chaidh an ceangal ath-shuidheachadh + + + Bhris rudeigin a-steach air a’ cheangal ris an lìonra fhad ’s a bha sinn a’ rèiteachadh ceangail. Feuch ris a-rithist.

    +
      +
    • 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.
    • +
    • Mur urrainn dhut duilleag sam bith a luchdadh, thoir sùil air a’ cheangal dàta no WiFi aig an uidheam agad.
    • +
    ]]>
    + + + Faidhle de sheòrsa neo-thèarainte + + + +
  • Nach leig thu fios air an duilgheadas seo gun fheadhainn aig a bheil an làrach-lìn?
  • + ]]>
    + + + Mearachd air sgàth susbaint thruaillte + + + 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.

    +
      +
    • Nach leig thu fios do sheilbheadairean na làraich-lìn mun duilgheadas seo?
    • +
    ]]>
    + + + Thuislich an t-susbaint + + 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.

    +
      +
    • Nach leig thu fios do sheilbheadairean na làraich-lìn mun duilgheadas seo?
    • +
    ]]>
    + + + Mearachd le còdachadh na susbaint + + 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.

    +
      +
    • Leig fios gu muinntir na làraich mun duilgheadas seo.
    • +
    ]]>
    + + + Cha deach an seòladh a lorg + + + Cha b’ urrainn dhan bhrabhsair frithealaichte an òstair a lorg airson an t-seòlaidh a chaidh a thoirt seachad.

    +
      +
    • Thoir sùil air an t-seòladh is dèan cinnteach nach eil mearachdan cumanta ann mar + ww.example.com an àite + www.example.com.
    • +
    • Mur urrainn dhut duilleag sam bith a luchdadh, thoir sùil air a’ cheangal dàta no WiFi aig an uidheam agad.
    • +
    ]]>
    + + + Chan eil ceangal ris an eadar-lìon + + Thoir sùil air a’ cheangal agad ris an lìonra no feuch is ath-luchdaich an duilleag ann an tiotan no dhà. + + Ath-luchdaich + + + Seòladh mì-dhligheach + 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.

    ]]>
    + + Chan e seòladh dligheach a tha ann + + +
  • Bidh seòlaidhean-lìn coltach ris an fhear seo a ghnàth: http://www.example.com/
  • +
  • Dèan cinnteach gu bheil thu a’ cleachdadh slaisichean (.i. /).
  • + ]]>
    + + + Pròtacal neo-aithnichte + Tha an seòladh a’ sònrachadh pròtacal (m.e. wxyz://) nach eil am brabhsair ag aithneachadh ’s chan urrainn dhan bhrabhsair ceangal ris an làrach mar bu chòir.

    +
      +
    • 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.
    • +
    • Feumaidh cuid dhe na pròtacalan bathar-bog de threas pàrtaidh no plugain mus aithnich am brabhsair iad.
    • +
    ]]>
    + + + Cha deach am faidhle a lorg + +
  • Saoil an deach ainm eile a chur air an fhaidhle, no gun deach a thoirt air falbh no a ghluasad?
  • +
  • A bheil mearachd litreachaidh (a’ gabhail a-steach litrichean mòra/beaga) san t-seòladh?
  • +
  • A bheil cead iomchaidh agad gus an rud iarraidh?
  • + ]]>
    + + + Chaidh inntrigeadh dhan fhaidhle a dhiùltadh + + +
  • Dh’fhaoidte gun deach a thoirt air falbh no a ghluasad no gu bheil bacadh air inntrigeadh an cois ceadan an fhaidhle.
  • + ]]>
    + + + Dhiùlt am frithealaiche progsaidh an ceangal + Tha am brabhsair air a rèiteachadh gus frithealaiche progsaidh a chleachdadh ach dhiùlt a’ phrogsaidh ceangal.

    +
      +
    • A bheil rèiteachadh progsaidh a’ bhrabhsair ceart? Cuir sùil air na roghainnean is feuch ris a-rithist.
    • +
    • A bheil an t-seirbheis progsaidh seo a’ toirt cead do cheanglaichean on lìonra seo?
    • +
    • Duilgheadasan agad fhathast? Leig fios gu rianaire an lìonraidh agad no gun fhrithealaiche-lìn agad.
    • +
    ]]>
    + + + Cha deach frithealaiche progsaidh a lorg + Tha am brabhsair air a rèiteachadh gus frithealaiche progsaidh a chleachdadh ach cha deach progsaidh a lorg.

    +
      +
    • A bheil rèiteachadh progsaidh a’ bhrabhsair ceart? Cuir sùil air na roghainnean is feuch ris a-rithist.
    • +
    • A bheil an t-uidheam co-cheangailte ri lìonra beò?
    • +
    • Duilgheadasan agad fhathast? Leig fios gu rianaire an lìonraidh agad no gun fhrithealaiche-lìn agad airson cuideachadh.
    • +
    ]]>
    + + + Duilgheadas le bathar-bog droch-rùnach air an làrach + + + 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.

    ]]>
    + + + Duilgheadas le bathar-bog gun iarrtas + + 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.

    ]]>
    + + + Duilgheadas le làrach cunnartach + + 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.

    ]]>
    + + + Duilgheadas le làrach foille + + 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.

    ]]>
    + + + Chan eil làrach thèarainte ri fhaighinn + + %1$s ri fhaighinn.]]> + + Lean air adhart gun làrach-lìn HTTP +
    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 @@ + + + + + Tentar de novo + + + Non é posíbel rematar a solicitude + + Non hai información adicional dispoñíbel neste momento acerca deste problema ou erro.

    + ]]>
    + + + Fallou a conexión segura + + +
  • Non é posíbel amosar a páxina que está a tentar visualizar porque non foi posíbel comprobar a autenticidade dos datos recibidos.
  • +
  • Contacte cos propietarios do sitio web para informalos deste problema.
  • + +]]>
    + + + Fallou a conexión segura + + +
  • Isto podería ser un problema coa configuración do servidor ou pode ser alguén que está tentando facerse pasar polo servidor.
  • +
  • Se xa puido conectarse a este servidor de forma correcta, pode ser que o erro sexa temporal e pode tentalo máis tarde.
  • + + ]]>
    + + + Avanzadas… + + Alguén podería estar intentando suplantar o sitio e non debería continuar. +

    + + ]]>
    + + Retroceder (recomendado) + + Aceptar o risco e continuar + + + Este sitio web require unha conexión segura. + + + +
  • A páxina que está tentando ver non se pode mostrar porque este sitio web require unha conexión segura.
  • +
  • O máis probable é que o problema estea relacionado co sitio web e non pode facer nada para resolvelo.
  • +
  • Pode notificarlle o problema ao administrador do sitio web.
  • + + ]]>
    + + + Avanzadas… + + + %1$s ten unha política de seguranza chamada HTTP Strict Transport Security (HSTS), o que significa que %2$s só se pode conectar a ela de forma segura. Non pode engadir unha excepción para visitar este sitio. + ]]> + + Retroceder + + + Interrompeuse a conexión + + + O navegador conectouse correctamente, pero a conexión foi interrompida ao transferir información. Ténteo de novo.

    +
      +
    • O sitio podería estar dispoñible temporalmente ou estar demasiado ocupado. Téntao de novo nuns instantes.
    • +
    • Se non pode cargar ningunha páxina, comprobe os datos do dispositivo ou a conexión wifi.
    • +
    + ]]>
    + + + A conexión esgotou o tempo + + O navegador esgotou o tempo de espera de resposta porque o sitio solicitado non respondeu ao pedido de conexión.

    +
      +
    • É posíbel que o servidor tivese un alto número de visitas ou estivese fóra de servizo? Tente de novo máis tarde.
    • +
    • Non consegue navegar por outros sitios? Verifique a conexión de rede do computador.
    • +
    • Están o computador ou a rede protexidos por un firewall ou por proxy? Unha configuración incorrecta pode interferir na exploración web.
    • +
    • Continúa a ter problemas? Consulte coa adminstración da rede ou co fornecedor da Internet para obter soporte técnico.
    • +
    + ]]>
    + + + Non foi posíbel conectarse + + +
  • O sitio podería estar non dispoñíbel temporalmente ou estar demasiado saturado. Tente acceder de novo nuns minutos.
  • +
  • Se non consegue cargar algunhas páxinas, comprobe a conexión de datos ou Wi-Fi do seu dispositivo móbil.
  • + + ]]>
    + + + Resposta inesperada do servidor + + O sitio respondeu de maneira inesperada á solicitude da rede e o navegador non pode continuar.

    + ]]>
    + + + A páxina non está a redirixir correctamente + + O navegador deixou de tentar recuperar o elemento solicitado. O sitio está a redireccionar o pedido dunha forma que nunca concluirá.

    +
      +
    • Desactivou ou bloqueou cookies necesarias para este sitio?
    • +
    • 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.
    • +
    + ]]>
    + + + Modo sen conexión + + O navegador está a funcionar en modo sen conexión e non é posíbel conectar ao elemento solicitado.

    +
      +
    • Está o seu dispositivo conectado a unha rede activa?
    • +
    • Prema en «Tentar de novo» para desactivar o modo sen conexión e recargar a páxina.
    • +
    + ]]>
    + + + Porto restrinxido por motivos de seguranza + + O enderezo solicitado especificou un porto (por exemplo, mozilla.org:80 para o porto 80 de mozilla.org) que normalmente non se utiliza para a exploración web. O explorador cancelou a súa solicitude para súa protección e seguranza.

    +]]>
    + + + Reiniciouse a conexión + + Interrompeuse a ligazón de rede mentres se negociaba unha conexión. Ténteo de novo.

    +
      +
    • O sitio podería estar dispoñible temporalmente ou estar demasiado ocupado. Ténteo de novo nuns instantes.
    • +
    • Se non pode cargar ningunha páxina, comprobe os datos do dispositivo ou a conexión wifi.
    • +
    + ]]>
    + + + Tipo de ficheiro inseguro + + +
  • Contacte cos propietarios do sitio web para informalos deste problema.
  • + + ]]>
    + + + Erro de contido danado + + Non é posíbel amosar a páxina á que está a tentar acceder porque se detectou un erro na transmisión de datos.

    +
      +
    • Contacte cos propietarios do sitio web para informalos deste problema.
    • +
    + ]]>
    + + + O contido estragouse + Non é posíbel amosar a páxina a que está a tentar acceder porque se detectou un erro na transmisión de datos.

    +
      +
    • Contacte cos propietarios do sitio web para informalos deste problema.
    • +
    + ]]>
    + + + Erro de codificación do contido + Non é posíbel amosar a páxina a que está a tentar acceder porque utiliza un formulario de compresión non compatíbel.

    +
      +
    • Contacte cos propietarios do sitio web para informalos deste problema.
    • +
    + ]]>
    + + + Non se atopou o enderezo + + O navegador non atopou o servidor host para a dirección indicada.

    +
      +
    • Comprobe o enderezo para atopar erros de escritura como + ww .exemplo.gal en vez de + www .exemplo.gal.
    • +
    • Se non pode cargar ningunha páxina, comprobe os datos do dispositivo ou a conexión wifi.
    • +
    + ]]>
    + + + Non hai conexión á internet + + Comprobe a súa conexión de rede ou tente volver a cargar a páxina nuns instantes. + + Recargar + + + Enderezo incorrecto + Non se recoñece o formato do enderezo fornecido. Busque erros na barra de localización e tente de novo.

    + ]]>
    + + O enderezo é incorrecto + + +
  • Normalmente, os enderezos web escríbense como http://www.exemplo.gal/
  • +
  • Asegúrese de que está a usar barras inclinadas cara adiante (isto é /).
  • +]]>
    + + + Protocolo descoñecido + O enderezo especifica un protocolo que o navegador non recoñece (p.ex., wxyz://) e, por tanto, non consegue conectar correctamente co sitio.

    +
      +
    • Está a tentar acceder a algún tipo de servizo multimedia ou outros servizos non textuais? Busque no sitio requisitos adicionais.
    • +
    • Algúns protocolos poden requirir software ou engadidos de terceiros para que o explorador consiga recoñecelos.
    • +
    + ]]>
    + + + Non se atopou o ficheiro + +
  • É posíbel que o elemento fose renomeado, eliminado ou movido?
  • +
  • Hai algún erro ortográfico, de maiúsculas ou tipográfico no enderezo?
  • +
  • Ten vostede os permisos necesarios para acceder ao elemento solicitado?
  • + + ]]>
    + + + Denegouse o acceso ao ficheiro + +
  • Pode que fose retirado, movido ou os permisos do ficheiro impiden o acceso.
  • + + ]]>
    + + + O servidor proxy rexeitou a conexión + O navegador está configurado para utilizar un servidor proxy mais o proxy rexeitou unha conexión.

    +
      +
    • É correcta a configuración proxy do navegador? Comprobe a configuración e tente de novo.
    • +
    • Permite o servizo proxy conexións con esta rede?
    • +
    • Continúa a ter problemas? Consulte coa administración da rede ou co fornecedor da Internet para obter soporte técnico.
    • +
    + ]]>
    + + + Non se atopou o servidor proxy + O navegador está configurado para utilizar un servidor proxy mais non foi posíbel +atopalo.

    +
      +
    • É correcta a configuración proxy do navegador? Comprobe a configuración e tente de novo.
    • +
    • Está o dispositivo conectado a unha rede activa?
    • +
    • Continúa a ter problemas? Consulte coa administración da rede ou co fornecedor da Internet para obter soporte técnico.
    • +
    + ]]>
    + + + Problema de sitio con software malicioso + + O sitio %1$s ten sido denunciado como sitio atacante e foi bloqueado segundo as súas preferencias de seguranza.

    + ]]>
    + + + Problema de sitio non desexado + + O sitio %1$s ten sido denunciado como sitio que serve software non desexado e foi bloqueado segundo as súas preferencias de seguranza.

    + ]]>
    + + + Problema de sitio prexudicial + + O sitio %1$s ten sido denunciado como sitio potencialmente pernicioso e foi bloqueado segundo as súas preferencias de seguranza.

    + ]]>
    + + + Problema de sitio enganoso + + Esta páxina web en %1$s ten sido denunciada sitio enganoso e foi bloqueado segundo as súas preferencias de seguranza.

    + ]]>
    + + + Sitio seguro non dispoñíbel + + %1$s dispoñíbel.]]> + + Continuar ao sitio con HTTP +
    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 @@ + + + + + Eha’ãjey + + + Ndaikatúi ojejapopa mba’ejerure + + Ko’ag̃aite ndaipóri marandu juapypyre ko apañuãi térã jejavýpe g̃uarã.

    + ]]>
    + + + Jeikekatu ojavy + + + +
  • Ko kuatiarogue ehechaseva hína ndaikatúi ojekuaa ndojehechajeykuaái rupi mba’ekuaarã moneĩmby og̃uahẽramóva.
  • +
  • Eñe’ẽ umi ñanduti renda jára ndive emombe’u hag̃ua ko apañuãi.
  • + ]]>
    + + + Jeikekatu ojavy + + + +
  • Kóva ikatu hína oiko apañuãi mohendahavusu ñemboheko rupive, térã oĩ omohendagueséva ko mohendahavusúpe.
  • +
  • Eike memeramovoi mohendahavusúpe ko mohendavusu rovake, ko jejavy ikatu sapy’aguávante, ha eha’ãkuaajey uperire.
  • + ]]>
    + + + Jehopyre… + + Oĩvaicha omyendagueséva ko tendápe ha anive emba’apo. +

    + ]]>
    + + Guevi (jeroviaháva) + + Emoneĩ mbyaikuaa ha eku’ejey + + + Ko ñanduti renda oikotevẽ jeikekatu. + + + +
  • Pe kuatiarogue rehechaséva ndaikatúi ojehechauka ko ñanduti renda oikotevẽgui peteĩ jeikekatu.
  • +
  • Ikatu pe apañuãi ojoajuvehína ñanduti renda rehe ha ndaipóri mba’eve ikatúva ojejapo oñemyatyrõ hag̃ua.
  • +
  • Emomarandukuaa ñanduti renda ñangarekohárape ko apañuãi rehegua.
  • + +]]>
    + + + Opanungáva… + + + %1$s oguereko peteĩ ñe’ẽñemi ñeñangarekorã hérava HTTP Strict Transport Security (HSTS), he’iséva %2$s ikatuha oñembojoaju hekopete añoite. Ndaikatúi remoĩ peteĩ oĩ’ỹva reike hag̃ua ko tendápe. + ]]> + + Guevijey + + + Pe jeike ojejokóma + + + Pe kundahára oike hekopete, hákatu osẽ ñandutígui ombahasakuévo marandu. Eha’ãjey.

    +
      +
    • Pe tenda ikatu ndoikói sapy’ami térã ojeporueterei. Eha’ãjey ag̃amieve.
    • +
    • Ndaikatúiramo emyanyhẽ kuatiarogue, ehecha oikópa nde wifi térã mba’ekuaarã ne pumbyry pegua.
    • +
    ]]>
    + + + Pe jeike ndoikovéima + + + Tenda ejeruréva nombohovái pe jerure jeike rehegua ha kundahára noha’ãrõvéima ñembohovái.

    +
      +
    • ¿Ikatu mohendahavusu ojeporueterei térã peteĩ ñekytĩ ndahi’aréitava? Eha’ãjey ag̃amieve.
    • +
    • Ndaikatúi eikundaha ambue tenda rupi? Ehechajey ne mohendaha oĩpa ñandutípe.
    • +
    • Ne ñanduti térã ne mohendaha omo’ã chupe firewall térã proxy? Peteĩ ñemboheko oiko’ỹva ikatu omboykese ne ñeikundaha ñandutípe.
    • +
    • Oĩ gueteri apañuãi? Eporandu ne ñanduti ñangarekohára térã ne ñanduti me’ẽhárape nepytyvõ hag̃ua.
    • +
    ]]>
    + + + Ndaikatúi eike + + + +
  • Tenda ikatu ndoikói sapy’ami térã ojeporueterei. Eha’ãjey ag̃ave.
  • +
  • Ndaikatúiramo emyanyhẽ mavave kuatiarogue, ehechajey oĩpa wifi térã mba’ekuaarã ne mba’e’oka oku’éva rehegua.
  • + ]]>
    + + + Mbohovái eha’ãrõ’ỹva mohendahavusúgui + + + Pe tenda ombohovái ñanduti mba’ejerure oñeha’ãrõ’ỹre ha pe kundahára ndaikatuvéima oku’ejey.

    ]]>
    + + + Ko kuatiarogue ndoguerahajeýi hekoitépe + + + Kundahára opytáma orujeysehápe mba’eporu jerurepyre. Pe tenda oguerahajeýta nde jerurepyre ysaja araka’eve noĩmbamo’ãiva rehegua.

    +
      +
    • Erekópa mboguehápe térã jokohápe kookie oikotevẽva ko tenda.
    • +
    • Emboajérõ jepe kookie tenda pegua nomyatyrõi apañuãi, ikatuhína apañuãi mohendahavusu ñemboheko ha ndaha’éi ne mohendaha rehegua.
    • +
    ]]>
    + + + Jeikekatu’ỹre + + + Pe kundaha omba’apo jeikekatu’ỹre ha ndaikatúi oike pe mba’eporu jerurepyrépe.

    +
      +
    • ¿Oñembojuaju mohendaha ñanduti oikóva rehe?
    • +
    • Ejopy “Ha’ãjey” ehasa hag̃ua jeikekatúpe ha emonyhẽjey kuatiarogue.
    • +
    ]]>
    + + + Mbojuajuhaite oñemomichĩva tekorosãrã + + + Pe kundaharape ejeruréva omoha’eño mbojuajuhaite (e.g. mozilla.org:80 mbojuajuhaite 80 mozilla.org-pe) ojeporúva ojeike hag̃ua ambuéva eikundaha ñandutípe. Pe kundahára ojokóma jerurepyre ne ñemo’ã ha rekorosãrã.

    ]]>
    + + + Pe jeike oñepyrũjeýma + + + Pe juajuha ñandutigua ndoikói oñeñe’ẽ aja jeikerã rehe. Eha’ãjey ag̃ave.

    +
      +
    • Pe tenda ikatu ndoikói hína sapy’ami térã ojeporueterei. Eha’ãjey ag̃amieve.
    • +
    • Ndaikatúiramo emyanyhẽ mavave kuatiarogue, ehecha oikópa nde Wi-Fi térã mba’ekuaarã ne pumbyry pegua.
    • +
    ]]>
    + + + Marandurenda hekorosã’ỹva + + + +
  • Eñe’ẽ ñanduti renda jára ndive ha emomarandu chupekuéra ko apañuãi.
  • + ]]>
    + + + Tetepy imarãpyréva jejavy + + + Pe kuatiarogue ehechasevahína ndaikatúi ojechauka oiko rupi jejavy mba’ekuaarã ohasakuévo.

    +
      +
    • Eñe’ẽ umi ñanduti renda jára ndine ha emomarandu chupekuéra ko apañuãi.
    • +
    ]]>
    + + + Tetepy jokopyre + + Pe kuatiarogue ehechasevahína ndaikatúi ojechauka oiko rupi jejavy mba’ekuaarã ohasakuévo.

    +
      +
    • Eñe’ẽ umi ñanduti renda jára ndine ha emomarandu chupekuéra ko apañuãi.
    • +
    ]]>
    + + + Tetepy mbopapapy jejavy + + Ko kuatiarogue eñeha’ãva ehecha ndojehechaukakuaái oiporu rupi ñeikũmby oiko’ỹvaa térã ipu’aka’ỹva.

    +
      +
    • Eñe’ẽ ñanduti renda jára ndive emombe’u hag̃ua chupekuéra ko apañuãi.
    • +
    ]]>
    + + + Ndojejuhúi kundaharape + + + Kundahára ndojuhúi mohendahavusu pe kundaharapépe g̃uarã.

    +
      +
    • Ehechajey kundaharapépa ndojavýi, techapyrã, + ww.ejemplo.com. + www.ejemplo.com. rendaguépe.
    • +
    • Ndaikatúiramo emyanyhẽ mavave kuatiarogue, ehecha oikópa Wi-Fi térã pumbyry mba’ekuaarã.
    • +
    ]]>
    + + + Ndaikatúi eike ñandutípe + + Ehechajey ne ñanduti juajuha térã emyanyhẽjey pe kuatiarogue ag̃amieve. + + Myanyhẽjey + + + Kundaharape ndoikói + + Kundaharape me’ẽmbyre noĩri peteĩ ysaja ojekuaávape. Ehechajey oĩpa jejavy kundaharape rendápe ha eha’ãjey.

    ]]>
    + + Pe kundaharape ndoikói + + + +
  • Ñanduti kundaharape ojehai jepi kóicha: http://www.example.com/
  • +
  • Ema’ẽjey eiporu porãpa barra ojero’áva tenonde gotyo (i.e. /).
  • + ]]>
    + + + Taperekoite ojekuaa’ỹva + + Pe kundaharape omoha’eño peteĩ mba’ete (techapyrã wxyz://) kundahára nomoneĩri, péva rupi pe kundahára ndaikatúi oikekatu tendápe.

    +
      +
    • Ndépa eikese jeikeha hekoetáva térã ambue mba’epytyvõrã ndaha’éiva moñe’ẽrã. Ehechajey tekotevẽva tenda pegua.
    • +
    • Heta protocolo oikotevẽkuaa software térã mboguerã’i mbohapyháva pe kundahára oikuaa hag̃ua.
    • +
    ]]>
    + + + Marandurenda ndojejuhúiva + + +
  • ¿Ikatu pe mba’eporu oñemboherajey, omboguévo térã omoambuévo tape?
  • +
  • ¿Oĩ jejavy jehaípe, taiguasu jeporúpe térã oimeraẽva ambuechagua kundaharapépe?
  • +
  • ¿Ereko jeikekuaa oikoitéva pe mba’eporu jerurepyrépe?
  • + ]]>
    + + + Marandurendápe jeike noñemoneĩri + +
  • Ikatúmakuri oñemboguete térã oñemongu’e, térã marandurenda ñemoneĩ omboyke pe jeike.
  • + ]]>
    + + + Mohendahavusu proxy ombotove jeike + + Ko kundahára oñemboheko oiporu hag̃ua mohendahavusu proxy, hákatu pe proxy omboyke jeike.

    +
      +
    • Oĩporãpa proxy ñemboheko kundahárape. Ehechajey ñemboheko ha eha’ãjey.
    • +
    • ¿Omoneĩpa proxy mba’eporu jeike ko ñanduti guive?
    • +
    • Oguereko gueteri apañuãi? Eporandu ñanduti ñangarekoha térã ñanduti me’ẽhárape pytyvõrã aporekoguáva rehe.
    • +
    ]]>
    + + + Ndojejuhúi mohendahavusu proxy + + Ko kundahára oñemboheko oiporu hag̃ua mohendahavusu proxy, hákatu ndojejuhúi mohendahavusu proxy?

    +
      +
    • Oĩporã proxy kundahára ñemboheko? Ehechajey ñemboheko ha eha’ãjey.
    • +
    • ¿Pe mohendaha ojoajuhína ñanduti oikóvare?
    • +
    • Oguereko gueteri apañuãi? Eporandu ñanduti ñangarekoha térã ñanduti me’ẽhárape pytyvõrã aporekoguávare.
    • +
    ]]>
    + + + Apañuãi malware rendápe + + + Tenda %1$s-pe ojehechakuaa ha’eha tenda ivaíva ha upévare ojejokóma, ohechakuaávo ne nekorosã erohoryvéva.

    ]]>
    + + + Apañuãi tenda eipota’ỹvape + + + Pe tenda %1$s pegua oje’e hese ha’eha software ivaíva ha upévare ojejokóma, ohapypuehóvo eguerohoryvéva tekorosarã.

    ]]>
    + + + Apañuãi tenda imarãvape + + + Tenda %1$s ojehechakuaa ha’eha peteĩ tenda ivaikuaáva ha upévare ojejoko, emongu’évo erohoryvéva rekorosãrã.

    ]]>
    + + + Apañuãi tenda ijapúvape + + Ko ñanduti renda %1$s ojehechakuaa ha’eha tenda ivaíva ha upévare ojejokóma, ohechakuaávo ne nekorosã erohoryvéva.

    ]]>
    + + + Ndaipóri tenda hekorosãva + + %1$s reiporukuaáva.]]> + + Epyta ko HTTP rendápe +
    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 @@ + + + + ફરીથી પ્રયત્ન કરો + + + વિનંતી પૂર્ણ કરી શકાતી નથી + + + આ સમસ્યા અથવા ભૂલ વિશેની અતિરિક્ત માહિતી હાલમાં ઉપલબ્ધ નથી.

    +]]>
    + + + સુરક્ષિત જોડાણ નિષ્ફળ થયું + + +
  • તમે જે પૃષ્ઠને જોવા માટે પ્રયત્ન કરી રહ્યા છો તે બતાવી શકાતું નથી કારણ કે પ્રાપ્ત ડેટાની પ્રામાણિકતા ચકાસી શકાઈ નથી.
  • +
  • કૃપા કરીને વેબસાઇટ માલિકોને આ સમસ્યા વિશે જાણ કરવા માટે સંપર્ક કરો.
  • + ]]>
    + + + સુરક્ષિત જોડાણ નિષ્ફળ થયું + + + +
  • આ સર્વરના ગોઠવણીમાં સમસ્યા હોઈ શકે છે, અથવા કોઈ હોઈ શકે છે કે જે સર્વરની impersonate કરવાનો પ્રયાસ કરે છે.
  • +
  • જો તમે આ સર્વર સાથે ભૂતકાળમાં સફળતાપૂર્વક કનેક્ટ કર્યું છે, તો ભૂલ અસ્થાયી હોઈ શકે છે, અને તમે પછીથી ફરી પ્રયાસ કરી શકો છો.
  • + ]]>
    + + + અદ્યતન… + + કોઈક સાઇટની નકલ કરવાની કોશિશ કરી શકે છે અને તમારે ચાલુ રાખવું જોઈએ નહીં. +

    + + ]]>
    + + પાછા જાઓ (ભલામણ કરેલ) + + જોખમ સ્વીકારો અને ચાલુ રાખો + + + કનેક્શન વિક્ષેપિત થયું હતું + + + બ્રાઉઝર સફળતાપૂર્વક કનેક્ટ થયું, પરંતુ માહિતી સ્થાનાંતરણ કરતી વખતે કનેક્શન અવરોધિત થયું. મેહરબાની કરી ને ફરી થી પ્રયાસ કરો.

    +
      +
    • સાઇટ અસ્થાયી રૂપે અનુપલબ્ધ અથવા ખૂબ વ્યસ્ત હોઈ શકે છે. થોડીવારમાં ફરી પ્રયાસ કરો.
    • +
    • જો તમે કોઈપણ પૃષ્ઠોને લોડ કરવામાં અસમર્થ છો, તો તમારા ઉપકરણનો ડેટા અથવા Wi-Fi કનેક્શન તપાસો.
    • +
    + ]]>
    + + + જોડાણ નો સમય સમાપ્ત થઇ ગયો છે + + + વિનંતી કરેલી સાઇટએ કનેક્શન વિનંતીનો જવાબ આપ્યો નથી અને બ્રાઉઝરે જવાબની રાહ જોવી બંધ કરી દીધી છે.

    +
      +
    • શું સર્વર વધુ માંગ અથવા અસ્થાયી આઉટેજ અનુભવી શકે છે? પછીથી ફરી પ્રયાસ કરો.
    • +
    • શું તમે અન્ય સાઇટ્સ બ્રાઉઝ કરવામાં અસમર્થ છો? કમ્પ્યુટરનું નેટવર્ક કનેક્શન તપાસો.
    • +
    • શું તમારું કમ્પ્યુટર અથવા નેટવર્ક firewall અથવા પ્રોક્સી દ્વારા સુરક્ષિત છે? ખોટી સેટિંગ્સ વેબ બ્રાઉઝિંગમાં દખલ કરી શકે છે.
    • +
    • હજી મુશ્કેલી આવી રહી છે? સહાય માટે તમારા નેટવર્ક એડમિનિસ્ટ્રેટર અથવા ઇન્ટરનેટ પ્રદાતાની સલાહ લો.
    • +
    ]]>
    + + + કનેક્ટ કરવામાં અસમર્થ + + + +
  • સાઇટ અસ્થાયી રૂપે અનુપલબ્ધ અથવા ખૂબ વ્યસ્ત હોઈ શકે છે. થોડીક ક્ષણોમાં ફરી પ્રયાસ કરો.
  • +
  • જો તમે કોઈપણ પૃષ્ઠોને લોડ કરવામાં અસમર્થ છો, તો તમારા મોબાઇલ ઉપકરણનો ડેટા અથવા Wi-Fi કનેક્શન તપાસો.
  • + + ]]>
    + + + સર્વર તરફથી અનપેક્ષિત પ્રતિસાદ + + સાઇટએ નેટવર્ક વિનંતીનો અનપેક્ષિત રીતે જવાબ આપ્યો અને બ્રાઉઝર ચાલુ રાખી શકશે નહીં.

    + ]]>
    + + + પૃષ્ઠ યોગ્ય રીતે રીડાયરેક્ટ થઈ રહ્યું નથી + + + ઓફલાઈન મોડ + + + સુરક્ષા કારણોસર પોર્ટ પ્રતિબંધિત છે + + + કનેક્શન ફરીથી સેટ થયું હતું + + + અસુરક્ષિત ફાઈલ પ્રકાર + + + બગડેલ સમાવિષ્ટોની ક્ષતિ + + + સામગ્રી ક્રેશ થઈ ગઈ + + + સમાવિષ્ટ સંગ્રહપદ્ધતિ ભૂલ + + + સરનામું મળ્યું નથી + + + ઇન્ટરનેટ કનેક્શન નથી + + તમારું નેટવર્ક કનેક્શન તપાસો અથવા થોડી ક્ષણોમાં પૃષ્ઠને ફરીથી લોડ કરવાનો પ્રયાસ કરો. + + ફરીથી લોડ કરો + + + અમાન્ય સરનામું + + સરનામું માન્ય નથી + + + અજ્ઞાત પ્રોટોકોલ + + + ફાઈલ મળી નહિં + + + ફાઈલ માં પ્રવેશ નકારવામાં આવ્યો હતો + + + પ્રોક્સી સર્વરે જોડાણ તોડી નાંખ્યું + + + પ્રોક્સી સર્વર મળ્યું નથી + + + માલવેર સાઇટ ઇશ્યૂ + + + %1$s પરની સાઇટ એટેક સાઇટ તરીકેની જાણ કરવામાં આવી છે અને તમારી સુરક્ષા પસંદગીઓના આધારે અવરોધિત કરવામાં આવી છે.

    ]]>
    + + + અનિચ્છનીય સાઇટ ઇશ્યૂ + + + %1$s પરની સાઇટને અનિચ્છનીય સૉફ્ટવેરની સેવા તરીકે જાણ કરવામાં આવી છે અને તમારી સુરક્ષા પસંદગીઓનાં આધારે તેને અવરોધિત કરવામાં આવી છે.

    ]]>
    + + + નુકસાનકારક સાઇટનો ઇશ્યૂ + + + %1$s પરની સાઇટને સંભવિત હાનિકારક સાઇટ તરીકે જાણ કરવામાં આવી છે અને તમારી સુરક્ષા પસંદગીઓના આધારે અવરોધિત કરવામાં આવી છે.

    ]]>
    + + + ભ્રામક સાઇટ ઇશ્યૂ + + %1$s પરના આ વેબ પૃષ્ઠને ભ્રામક સાઇટ તરીકે જાણ કરવામાં આવ્યું છે અને તમારી સુરક્ષા પસંદગીઓના આધારે અવરોધિત કરવામાં આવ્યું છે.

    ]]>
    + +
    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 @@ + + + + + पुनः प्रयास करें + + + अनुरोध पूरा नहीं किया जा सका + + + इस समस्या या त्रुटी के बारे में अतिरिक्त जानकारी फिलहाल उपलब्ध नहीं है।

    ]]>
    + + + सुरक्षित संपर्क विफ़ल + + + +
  • जिस पृष्ठ को आप देखने की कोशिश कर रहे हैं उसे दिखाया नहीं जा सकता क्योंकि प्राप्त डेटा के प्रमाणिकता की सत्यापन नहीं की जा सकी
  • +
  • कृपया इस समस्या की जानकारी देने के लिए वेबसाइट प्रदाता से संपर्क करें।
  • + ]]>
    + + + सुरक्षित संपर्क विफ़ल + + + +
  • यह सर्वर विन्यास से जुड़ी समस्या हो सकती है, या यह कोई ऐसा हो सकता है जो सर्वर की नकल बनाने की कोशिश कर रहा हो।
  • +
  • यदि पहले कभी आप इस सर्वर से सफलतापूर्वक जुड़े हों, तो त्रुटी कुछ समय की हो सकती है, और बाद में आप पुनः प्रयास कर सकते हैं।
  • + ]]>
    + + + विस्तृत… + + + शायद कोई व्यक्ति साइट की नकल करने की कोशिश कर रहा हो और आपको आगे नहीं बढ़ना चाहिए। +

    + ]]>
    + + पीछे जाएं (सलाह) + + जोखिम स्वीकारें और आगे बढ़ें + + + संपर्क में बाधा उत्पन्न हुई + + + ब्राउज़र सफलतापूर्वक जुड़ चुका है, पर जानकारी स्थानांतरित करते समय संपर्क में कोई बाधा उत्पन्न हुई। कृपया पुनः प्रयास करें।

    +
      +
    • यह साइट अस्थाई रूप से अनुपलब्ध या अत्यंत व्यस्त हो सकता है। कुछ समय बाद पुनः प्रयास करें।
    • +
    • यदि आप कोई पृष्ठ लोड नहीं कर पा रहे हैं, तो अपने उपकरण की डेटा या वाई-फाई कनेक्शन की जांच करें।
    • +
    ]]>
    + + + संपर्क समय समाप्त + + + अनुरोधित साइट ने संपर्क की अनुरोध का जवाब नहीं दिया और ब्राउज़र ने जवाब की प्रतीक्षा करना बंद कर दिया है।

    +
      +
    • हो सकता है कि सर्वर काफी अनुरोध या अस्थाई निलंबन झेल रहा हो? कृपया बाद में पुनः प्रयास करें।
    • +
    • क्या आप अन्य साइट भी ब्राउज़ नहीं कर पा रहे? कृपया उपकरण के नेटवर्क कनेक्शन की जांच करें।
    • +
    • क्या आपका उपकरण या नेटवर्क किसी फ़ायरवॉल या प्रॉक्सी द्वारा सुरक्षित है? गलत सेटिंग से वेब ब्राउज़िंग में बाधा आ सकती है।
    • +
    • अभी भी समस्या हो रही है? सहायता के लिए अपने नेटवर्क व्यवस्थापक या इंटरनेट प्रदाता से संपर्क करें।
    • +
    ]]>
    + + + संपर्क करने में असमर्थ + + +
  • यह साइट अस्थाई रूप से अनुपलब्ध या अत्यंत व्यस्त हो सकता है। कुछ समय बाद पुनः प्रयास करें।
  • +
  • यदि आप कोई भी पृष्ठ लोड करने में असमर्थ हैं, तो अपने मोबाइल उपकरण के डेटा या वाई-फाई कनेक्शन की जांच करें।
  • + ]]>
    + + + सर्वर द्वारा अप्रत्याशित प्रतिक्रिया प्राप्त हुआ + + + इस साइट ने नेटवर्क अनुरोध की प्रतिक्रिया एक अप्रत्याशित रूप से दी है, जिस कारण ब्राउज़र आगे नहीं बढ़ सकता।

    ]]>
    + + + यह पृष्ठ सही से पुनर्निर्देशित नहीं हो पा रहा + + + ब्राउज़र ने अनुरोध की गई चीज़ को पाने का प्रयास बंद कर दिया है। यह साइट अनुरोध को इस प्रकार पुनर्निर्देशित कर रहा है कि वह कभी पूरा नहीं होगा।

    +
      +
    • क्या आपने साइट के लिए आवश्यक कूकीज़ को अक्षम या अवरुद्ध किया है?
    • +
    • यदि साइट की कूकीज़ स्वीकारने से भी यह समस्या ठीक नहीं होती, तो संभवतः यह सर्वर विन्यास की समस्या है ना कि आपके उपकरण की।
    • +
    ]]>
    + + + ऑफलाइन मोड + + + ब्राउज़र अपने ऑफलाइन मोड में चल रहा है और अनुरिधित चीज़ से नहीं जुड़ सकता।

    +
      +
    • क्या उपकरण किसी सक्रीय नेटवर्क से जुड़ा हुआ है?
    • +
    • ऑनलाइन मोड में जाने के लिए “पुनः प्रयास करें” दबाएं या पृष्ठ को पुनः लोड करें।
    • +
    ]]>
    + + + सुरक्षा कारणों से पोर्ट प्रतिबंधित है + + + अनुरोधित पते ने एक पोर्ट (उदाहरण के लिए, mozilla.org:80 पोर्ट 80 के लिए mozilla.org पर) निर्दिष्ट किया गया है, जो सामान्य रूप से वेब ब्राउजिंग के अलावा अन्य उद्देश्यों के लिए उपयोग किया जाता है। ब्राउज़र ने आपकी सुरक्षा और सुरक्षा के लिए अनुरोध को रद्द कर दिया है।]]> + + + कनेक्शन रिसेट किया गया था + + + कनैक्शन स्थापित करते समय नेटवर्क लिंक बाधित हो गयी थी। कृपया पुनः प्रयास करें। +
      + +
    • साइट अस्थायी रूप से अनुपलब्ध या बहुत व्यस्त हो सकती है। कुछ समय बाद पुनः प्रयास करें।   +
    • यदि आप कोई पेज लोड नहीं कर पा रहे हैं, तो अपने डिवाइस का डेटा या वाई-फाई कनेक्शन जांचें। + ]]> + + + असुरक्षित फाइल प्रकार + + + +
    • कृपया वेबसाइट मालिक से उन्हें इस समस्या के बारे में बताने के लिए संपर्क करें।
    • +
    ]]>
    + + + अनुपयोगी सामग्री त्रुटि + + पेज जिसे आप देखने की कोशिश कर रहे हैं नहीं दिखाया जा सकता है क्योंकि डेटा संचारण में त्रुटि पाया गया।

    +
      +
    • कृपया वेबसाइट मालिक को इस समस्या के बारे में सूचित करने के लिए संपर्क करें।
    • +
    ]]>
    + + + सामग्री क्रैश + पेज जिसे आप देखने की कोशिश कर रहे हैं नहीं दिखाया जा सकता है क्योंकि डेटा संचारण में त्रुटि पाया गया।

    +
      +
    • कृपया वेबसाइट मालिक को इस समस्या के बारे में सूचित करने के लिए संपर्क करें।
    • +
    ]]>
    + + + सामग्री एंकोडिंग त्रुटि + + पेज जिसे आप देखने की कोशिश कर रहे हैं नहीं दिखाया जा सकता है क्योंकि यह संकुचन का अवैध या असमर्थित प्रारूप का प्रयोग करता है।

    +
      +
    • कृपया वेबसाइट मालिक को इस समस्या के बारे में सूचित करने के लिए संपर्क करें।
    • +
    ]]>
    + + + पता नहीं मिला + + + इंटरनेट कनेक्शन नही है + + अपना नेटवर्क कनेक्शन जांचें या पेज कुछ ही क्षणों में पुनः लोड करने का प्रयास करें। + + फिर से लोड करें + + + अमान्य पता + + पता मान्य नहीं है + + + +
  • वेब पता प्रायः इस तरह लिखा जाता है http://www.example.com/
  • +
  • सुनिश्चित करें कि आप फॉरवर्ड स्लैश का प्रयोग कर रहे हैं (यानी /)।
  • + ]]>
    + + + अज्ञात प्रोटोकॉल + + + फ़ाइल नहीं मिला + + + फ़ाइल के ऐक्सेस को रोक दिया गया + +
  • इसे हटाया, खिसकाया जा सकता है या फाइल अनुमति पहुँच प्रतिबाधित कर सकती हैं।
  • + ]]>
    + + + प्रॉक्सी सर्वर का कनेक्शन अस्वीकार किया गया + + + प्रॉक्सी सर्वर नहीं मिला + + + मैलवेयर साइट समस्या + + %1$s पर साइट को एक हमला साइट के रूप में रिपोट किया गया है और आपकी सुरक्षा प्राथमिकताओं के आधार पर ब्लॉक किया गया हैं।

    ]]>
    + + + अवांछित साइट समस्या + + + %1$s पर साइट को एक अवांछित सॉफ्टवेयर की सेवा के रूप में रिपोट किया गया है और आपकी सुरक्षा प्राथमिकताओं के आधार पर ब्लॉक किया गया हैं।

    ]]>
    + + + हानिकारक साइट समस्या + + + %1$s पर साइट एक संभावित हानिकारक साइट के रूप में रिपोर्ट किया गया है और आपकी सुरक्षा प्राथमिकताओं के आधार पर ब्लॉक किया गया हैं।

    ]]>
    + + + धोखादायक साइट समस्या + + %1$s पर यह वेबपेज एक धोखादायक साइट के रूप में रिपोट किया गया है और आपकी सुरक्षा प्राथमिकताओं के आधार पर ब्लॉक किया गया हैं।

    ]]>
    + + + सुरक्षित साइट उपलब्ध नहीं है + + HTTP साइट को जारी रखें +
    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 @@ + + + + + Tilawan Liwat + + + Wala na kompleto nga bilin + + + Ang gindangat nga sugpon napalsu + + + Ang gindangat nga sugpon napalsu + + + Nag-abanse… + + + Pagabalikan (Ginarekomendar) + + + Nag-abanse… + + + Balikan + + + May nagsablag sa koneksyon + + + Indi makaangot + + + Offline Mode + + + Na-reset ang koneksyon + + + Unsafe File Type + + + Corrupted Content Error + + + Ang kaundan nag-crash + + + Indi Makita ang Address + + + Wala koneksyon sa internet + + + Mag-reload + + + Sala nga Address + + + Unknown Protocol + + + Indi makita ang File + + + Proxy Server Refused Connection + + + Indi makita ang Proxy Server + + + Isyu sang malware site + + + Hinali nga isyu sa site + + + Delekado na isyu sa site + + + Dulot nga kabutigan sang site + + + Ang sugpon sa HTTP Site + 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 @@ + + + + + Pokušaj ponovo + + + Nije moguće dovršiti zahtjev + + + Dodatne informacije o ovom problemu ili grešci trenutno nisu dostupne.

    ]]>
    + + + Sigurna veza nije uspjela + + +
  • Stranicu kojoj pokušavaš pristupiti nije moguće prikazati jer nije moguće provjeriti autentičnost primljenih podataka.
  • +
  • Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.
  • + ]]>
    + + + Sigurna veza nije uspjela + + +
  • Možda se radi o problemu s postavkama na poslužitelju ili možda netko pokušava oponašati ovaj poslužitelj.
  • +
  • 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.
  • + ]]>
    + + + Napredno … + + Netko možda pokušava lažno predstavljati web-stranicu, stoga je bolje da ne nastaviš. +        
    +        ]]>
    + + Idi natrag (preporučeno) + + Prihvati rizik i nastavi + + + Ova web stranica zahtjeva sigurnu vezu. + + +
  • Stranica koju pokušavate vidjeti se ne može prikazati zato što zahtjeva sigurnu vezu.
  • +
  • Problemu je najvjerojatnije uzrok web stranica i ne možete ništa napraviti što bi ga riješilo.
  • +
  • Možete obavijestiti administratora web stranice o problemu.
  • + + ]]>
    + + + Napredno… + + + %1$s provodi sigurnosnu politiku pod nazivom HTTP Strict Transport Security (HSTS), koja znači da se %2$s može na istu povezati samo sigurno. Ne možete dodati iznimku za posjete ovoj web stranici. + ]]> + + Idi natrag + + + Veza je prekinuta + + + Preglednik se uspješno povezao, ali veza je prekinuta tijekom prijenosa informacija. Pokušaj ponovo.

    +      
      +        
    • Stranica je možda privremeno nedostupna ili prezauzeta. Pokušaj ponovo za nekoliko trenutaka.
    • +        
    • Ako ne možeš učitati nijednu stranicu, provjeri podatke tvog uređaja ili Wi-Fi vezu.
    • +      
    ]]>
    + + + Vezi je isteklo vrijeme + + + Zatražena stranica nije odgovorila na zahtjev i preglednik je prestao čekati na odgovor.

    +
      +
    • Možda je poslužitelj opterećen velikom količinom zahtjeva ili je privremeno ostao bez napajanja? Pokušaj ponovo kasnije.
    • +
    • Možeš li pregledavati ostale stranice? Provjeri mrežnu vezu svog računala.
    • +
    • Jesu li tvoje računalo ili mreža zaštićeni vatrozidom ili proxyjem? Neispravne postavke mogu prouzročiti probleme prilikom pregledavanja weba.
    • +
    • Ukoliko još uvijek imaš probleme, za pomoć se obrati administratoru mreže ili pružatelju internetske usluge.
    • +
    ]]>
    + + + Povezivanje nije moguće + + + +
  • Stranica je možda privremeno nedostupna ili preopterećena. Pokušaj ponovno malo kasnije.
  • +
  • Ako ne možeš učitati niti jednu stranicu, provjeri podatke svog uređaja ili Wi-Fi vezu.
  • + ]]>
    + + + Neočekivani odgovor od poslužitelja + + + Stranica je na mrežni zahtjev odgovorila na neočekivani način, zbog čega preglednik ne može nastaviti.

    ]]>
    + + + Stranica ne preusmjerava ispravno + + + Preglednik je prestao dohvaćati zatražene stranice. Stranica preusmjerava zahtjev na takav način, da se on nikada ne može ispuniti.

    +
      +
    • Jesu li kolačići za ovu stranicu deaktivirani ili blokirani?
    • +
    • 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.
    • +
    ]]>
    + + + Izvanmrežni način rada + + + Preglednik je u izvanmrežnom načinu rada i ne može se spojiti na traženu stavku.

    +
      +
    • Je li uređaj spojen na aktivnu mrežu?
    • +
    • Klikni na „Pokušaj ponovo” za prebacivanje na mrežni način rada i ponovo učitaj stranicu.
    • +
    ]]>
    + + + Priključak je iz sigurnosnih razloga ograničen + + + Zatražena adresa ima definiran priključak (npr. mozilla.org:80 za priključak 80 na mozilla.org) koji se inače koristi za druge radnje, a ne za pregledavanje weba. Preglednik je prekinuo zahtjev radi tvoje zaštite i sigurnosti.

    ]]>
    + + + Veza je prekinuta + + + Mrežna veza je prekinuta tijekom povezivanja. Pokušaj ponovo.

    +      
      +        
    • Stranica je možda privremeno nedostupna ili prezauzeta. Pokušaj ponovo za nekoliko trenutaka.
    • +        
    • Ako ne možeš učitati nijednu stranicu, provjeri podatke tvog uređaja ili Wi-Fi vezu.
    • +      
    ]]>
    + + + Nesigurna vrsta datoteke + + + +
  • Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.
  • + ]]>
    + + + Greška oštećenog sadržaja + + Stranicu kojoj pokušavaš pristupiti nije moguće prikazati zbog greške u prijenosu podataka.

    +
      +
    • Obavijesti vlasnike web stranice o ovom problemu.
    • +
    ]]>
    + + + Greška u sadržaju + Stranicu kojoj pokušavaš pristupiti nije moguće prikazati zbog greške u prijenosu podataka.

    +
      +
    • Obavijesti vlasnike web stranice o ovom problemu.
    • +
    ]]>
    + + + Greška u kodiranju sadržaja + + Stranica koju pokušavaš vidjeti ne može biti prikazana jer koristi neispravni ili nepodržani oblik komprimiranja.

    +
      +
    • Kontaktiraj vlasnike web stranice i obavijesti ih o ovom problemu.
    • +
    ]]>
    + + + Adresa nije pronađena + + + Preglednik nije mogao pronaći poslužitelja domaćina za navedenu adresu.

    +      
      +        
    • Pazi da nemaš greške u tipkanju, kao što su +          ww.primjer.hr umjesto +          www.primjer.hr.
    • +
    • Ako ne možeš učitati nijednu stranicu, provjeri podatke svog uređaja ili Wi-Fi vezu.
    • +
    ]]>
    + + + Ne postoji veza s internetom + + Provjeri mrežnu vezu ili pokušaj ponovo učitati stranicu za nekoliko trenutaka. + + Učitaj ponovo + + + Neispravna adresa + + Upisana adresa nije u prepoznatljivom obliku. Provjeri postoji li greška u unosu u adresnoj traci.

    ]]>
    + + Adresa nije ispravna + + + +
  • Adrese web stranica se obično pišu u formatu poput http://www.example.com/
  • +
  • Pazi na način pisanja kose crte (tj. /).
  • + ]]>
    + + + Nepoznat protokol + + Adresa definira protkol (npr. xyz://) kojeg preglednik ne poznaje ili se ne može povezati sa zatraženom stranicom.

    +
      +
    • Pokušavaš li pristupiti multimedijskim ili drugim netekstualnim uslugama? Provjeri ima li stranica dodatnih zahtjeva.
    • +
    • Neki protokoli zahtijevaju program ili priključke treće strane, kako bi ih preglednik mogao prepoznati.
    • +
    ]]>
    + + + Datoteka nije pronađena + + +
  • Možda je uklonjena, premještena ili joj je ime promijenjeno?
  • +
  • Postoji li pravopisna ili nekakva tipografska greška?
  • +
  • Imaš li dovoljna prava za pristup traženoj datoteci?
  • + ]]>
    + + + Pristup datoteci je odbijen + + +
  • Možda je uklonjena, premještena ili dozvole za datoteku spriječavaju pristup.
  • + ]]>
    + + + Proxy poslužitelj odbija povezivanje + + Preglednik je postavljen da koristi proxy poslužitelj, ali proxy poslužitelj odbija povezivanje.

    +
      +
    • Jesu li proxy postavke ispravne? Provjeri postavke i pokušaj ponovo.
    • +
    • Dozvoljava li proxy usluga povezivanje iz ove mreže?
    • +
    • Ukoliko još uvijek imaš probleme, za pomoć se obrati administratoru mreže ili pružatelju internetske usluge.
    • +
    ]]>
    + + + Proxy poslužitelj nije pronađen + Preglednik je postavljen da koristi proxy poslužitelj, ali ga nije moguće naći.

    +
      +
    • Jesu li proxy postavke ispravne? Provjeri postavke i pokušaj ponovo.
    • +
    • Je li uređaj povezan na aktivnu mrežu?
    • +
    • Ukoliko još uvijek imaš probleme, za pomoć se obrati administratoru mreže ili pružatelju internet usluge.
    • +
    ]]>
    + + + Stranica sa zlonamjernim softverom + + Stranica na %1$s je prijavljena kao zloćudna stranica i blokirana je na temelju tvojih sigurnosnih postavki.

    ]]>
    + + + Neželjena stranica + + Stranica na %1$s je prijavljena zbog posluživanja nepoželjnog softvera, te je blokirana na temelju tvojih sigurnosnih postavki.

    ]]>
    + + + Štetna stranica + + Web stranica %1$s je prijavljena kao potencijalno zlonamjerna stranica i blokirana je na temelju tvojih sigurnosnih postavki.

    ]]>
    + + + Problem s obmanjujućom stranicom + + Web stranica %1$s je prijavljena kao obmanjujuća stranica i blokirana je na temelju tvojih sigurnosnih postavki.

    ]]>
    + + + Sigurna stranica nije dostupna + + %1$s
    nije dostupna.]]>
    + + Nastavi na stranicu preko HTTP-a + 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 @@ + + + + + Hišće raz spytać + + + Naprašowanje njeda so dokónčić + + Přidatne informacije wo tutym problemje abo zmylku tuchwilu k dispoziciji njesteja.

    + ]]>
    + + + Wěsty zwisk njeje so poradźił + + +
  • Strona, kotruž chceće sej wobhladać, njeda so pokazać, dokelž awtentiskosć přijatych datow njeda so přepruwować.
  • +
  • Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.
  • + + ]]>
    + + + Wěsty zwisk njeje so poradźił + + +
  • To móhło problem z konfiguraciju serwera być, abo móhło być, zo něchtó pospytuje serwer imitować.
  • +
  • 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ć.
  • + + ]]>
    + + + Rozšěrjene… + + Něchtó móhł spytać, sydło za swoje wudać, tohodla njeměł wy z tym pokročować. +

    + + ]]>
    + + Wróćo (doporučeny) + + Riziko akceptować a pokročować + + + Tute websydło sej wěsty zwisk žada. + + + +
  • Strona, kotruž sej chceće wobhladać, njeda so pokazać, dokelž tute websydło sej wěsty zwisk žada.
  • +
  • Problem so najskerje přez websydło zawinuje a tohodla ničo njeje, štož móžeće činić, zo byšće jón rozrisał.
  • +
  • Móžeće administratorej websydła problem zdźělić.
  • + + ]]>
    + + + Rozšěrjeny… + + + %1$s ma wěstotne prawidło z mjenom HTTP Strict Transport Security (HSTS), kotrež woznamjenja, zo %2$s móže so jenož wěsće zwjazać. Njemóžeće wuwzaće přidać, zo byšće tute sydło wopytał. + ]]> + + Wróćo + + + Zwisk je so přetorhnył + + + 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.

    +
      +
    • Sydło snano nachwilu k dispoziji njeje abo je přećežene. Spytajće za někotre wokomiki hišće raz.
    • +
    • Jeli njemóžeće strony začitać, přepruwujće daty swojeho grata abo WLAN-zwisk.
    • +
    + ]]>
    + + + Zwisk je čas překročił + + Požadane sydło na zwiskowe naprašowanje njewotmołwi a wobhladowak přesta na wotmołwu čakać.

    +
      +
    • Móže być, zo serwer je přećeženy abo ma nachwilne mylenje? Spytajće pozdźišo hišće raz.
    • +
    • Njemóžeće druhe sydła přehladować? Přepruwujće syćowy zwisk grata.
    • +
    • Škita so waš grat abo waša syć z wohnjowej murju abo proksy? Njekorektne nastajenja móža webpřehladowanju wadźić.
    • +
    • Maće hišće ćeže? Skonsultujće swojeho syćoweho administratora abo internetneho poskićowarja za podpěru.
    • +
    ]]>
    + + + Zwisk móžny njeje + + +
  • Sydło njesteji snano nachwilu k dispoziciji abo je přećežene. Spytajće za mało wokomikow hišće raz.
  • +
  • Jeli njemóžeće strony začitać, přepruwujće daty abo WLAN-zwisk wašeho mobilneho grata.
  • + ]]>
    + + + Njewočakowana wotmołwa ze serwera + + Sydło wotmołwi na naprašowanje syće na njewočakowane wašnje a wobhladowak njemóže pokročować.

    + ]]>
    + + + Strona njeprawje posrědkuje + + Wobhladowak přesta požadany objekt wotwołować. Sydło sposrědkuje naprašowanje na wašnje, kotrež so njekónči.

    +
      +
    • Sće placki znjemóžnił abo zablokował, kotrež su trěbne za tute sysdło?
    • +
    • Jeli akceptowanje plackow tutoho sydła problem njerozrisa, je to najskerje problem konfiguracije serwera a nic wašeho grata.
    • +
    ]]>
    + + + Offline-modus + + + Wobhladowak offline dźěła a njemóže z požadanym objektom zwjazać.

    +
      +
    • Je grat z aktiwnej syću zwjazany?
    • +
    • Klikńće na “Hišće raz”, zo byšće do online-modusa přešoł a stronu znowa začitał.
    • +
    ]]>
    + + + Port z přičinow wěstoty wobmjezowany + + Požadana adresa poda port (na př. mozilla.org:80 za port 80 on mozilla.org), kotryž so normalnje za hinaše zaměry hač webpřehladowanje wužiwa. Wobhladowak je naprašowanje za waš škit a wěstotu přetorhnył.

    + ]]>
    + + + Zwisk bu wróćo stajeny + + + Syćowy zwisk je so přetorhnył, mjeztym zo sće spytał, zwisk nawjazać. Prošu spytajće hišće raz.

    +
      +
    • Sydło snano nachwilu k dispoziji njeje abo je přećežene. Spytajće za někotre wokomiki hišće raz.
    • +
    • Jeli njemóžeće strony začitać, přepruwujće daty swojeho grata abo WLAN-zwisk.
    • +
    + ]]>
    + + + Njewěsty datajowy typ + + +
  • Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.
  • + + ]]>
    + + + Zmylk - wobškodźeny wobsah + + Strona, kotruž chceće sej wobhladać, njeda so pokazać, dokelž je so zmylk při přenošowanju datow namakał.

    +
      +
    • Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.
    • +
    + ]]>
    + + + Wobsah jo spadnył + Strona, kotruž chceće sej wobhladać, njeda so pokazać, dokelž je so zmylk při přenošowanju datow namakał.

    +
      +
    • Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.
    • +
    + ]]>
    + + + Zmylk při kodowanju wobsaha + Strona, kotruž chcéce sej wobhladać, njeda so pokazać, dokelž njepłaćiwu abo njepodpěrowanu formu kompresije wužiwa.

    +
      +
    • Prošu stajće so z wobsedźerjemi websydła do zwiska, zo byšće jich wo tutym problemje informował.
    • +
    + ]]>
    + + + Adresa njenamakana + + + Wobhladowak njemóžeše hostowy serwer za podatu adresu namakać.

    +
      +
    • Přepruwujće adresu za pisanskimi zmylkami kaž + ww.example.com město + www.example.com.
    • +
    • Jeli njemóžeće strony začitać, přepruwujće daty swojeho grata abo WLAN-zwisk.
    • +
    + ]]>
    + + + Žadyn internetny zwisk + + Přepruwujće swój syćowy zwisk abo začitajće stronu za mało wokomikow znowa. + + Znowa začitać + + + Njepłaćiwa adresa + Podata adresa w připóznatym formaće njeje. Prošu přepruwujće adresowu lajstu za zmylkami a spytajće hišće raz.

    + ]]>
    + + Adresa płaćiwa njeje + + +
  • Webadresy so z wašnjom kaž http://www.example.com/ pisaja.
  • +
  • Zawěsćće, zo wužiwaće doprědka nachilene nakósne smužki (t.j. /).
  • + +]]>
    + + + Njeznaty protokol + Adresa podawa protokol (na př. wxyz://), kotryž wobhladowak njepřipóznawa, tak zo wobhladowak njemóže prawje ze sydłom zwjazać.

    +
      +
    • Pospytujeće na multimedia abo druhe njetekstowe słužby přistup měć? Přepruwujće sydło za wosebitymi potrěbnosćemi.
    • +
    • Někotre protokole trjebaja programy třećich abo tykače, prjedy hač wobhladowak móže je připóznać.
    • +
    + ]]>
    + + + Dataja njeje so namakała + +
  • Je móžno, zo bu objekt přemjenowany, wotstronjeny abo přesunjeny?
  • +
  • Je zmylk prawopisa, wulkopisanja, abo hinaši typografiski zmylk w adresy?
  • +
  • Maće dosahace přistupne prawa za požadany objekt?
  • + + ]]>
    + + + Přistup k dataji je so wotpokazał + +
  • Snano je so wotstroniła, přesunyła, abo datajowe prawa zadźěwaju přistupej.
  • + + ]]>
    + + + Proksyserwer je zwisk wotpokazał + Wobhladowak bu konfigurowany, zo by so proksyserwer wužiwa, ale proksy zwisk wotpokaza.

    +
      +
    • Je konfiguracija proksy wobhladowaka korektna? Přepruwujće nastajenja a spytajće hišće raz.
    • +
    • Dowola słužba proksy zwiski z tuteje syće?
    • +
    • Maće hišće ćeže? Skonsultujće swojeho syćoweho administratora abo internetneho poskićowarja za podpěru.
    • +
    ]]>
    + + + Proksyserwer njenamakany + Wobhladowak bu konfigurowany, zo by so proksyserwer wužiwa, ale proksy njeda so namakać.

    +
      +
    • Je konfiguracija proksyja wobhladowaka korektna? Přepruwujće nastajenja a spytajće hišće raz.
    • +
    • Je grat z aktiwnej syću zwjazany?
    • +
    • Maće hišće ćeže? Stajće so ze swojim syćowym administratorom abo internetnym poskićowarjom za podpěru do zwiska.
    • +
    ]]>
    + + + Problem ze sydłom ze škódnej softwaru + + Sydło %1$s bu jako nadpadowace sydło zdźělene a bu na zakładźe wašich wěstotnych nastajenjow zablokowane.

    + ]]>
    + + + Problem z njewitanym sydłom + + 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.

    + ]]>
    + + + Problem z wohrožacym sydłom + + 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.

    + ]]>
    + + + Problem z wobšudnym sydłom + + 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.

    + ]]>
    + + + Wěste sydło k dispoziciji njeje + + %1$s k dispoziciji njeje.]]> + + Dale k HTTP-sydłu +
    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 @@ + + + + + Próbálja újra + + + A kérés nem teljesíthető + + Jelenleg nem áll rendelkezésre további információ a problémáról vagy hibáról.

    ]]>
    + + + A biztonságos kapcsolat sikertelen + +
  • A megtekinteni kívánt oldal nem jeleníthető meg, mert a kapott adatok hitelessége nem ellenőrizhető.
  • Lépjen kapcsolatba a webhely üzemeltetőjével, és értesítse a problémáról.
  • ]]>
    + + + A biztonságos kapcsolat sikertelen + +
  • 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.
  • 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.
  • ]]>
    + + + Speciális… + + Lehet, hogy valaki megpróbálja megszemélyesíteni az oldalt, így nem kellene folytatnia. +

    + + ]]>
    + + Visszalépés (ajánlott) + + Kockázat elfogadása és továbblépés + + + Ez a webhely biztonságos kapcsolatot igényel. + + +
  • A megtekintendő oldal nem jeleníthető meg, mert ez a webhely biztonságos kapcsolatot igényel.
  • +
  • A probléma nagy valószínűséggel a webhelyen van, és nem tud mit tenni a megoldása érdekében.
  • +
  • A problémáról értesítheti a webhely rendszergazdáját.
  • + + ]]>
    + + + Speciális… + + + A(z) %1$s oldal a HTTP Strict Transport Security (HSTS) nevű biztonsági házirendet használja, amely azt jelenti, hogy a(z) %2$s csak biztonságosan kapcsolódhat hozzá. Nem adhat hozzá kivételt, hogy felkeresse ezt az oldalt. + ]]> + + Ugrás vissza + + + A kapcsolat megszakadt + + + A böngésző sikeresen kapcsolódott, de a kapcsolat megszakadt az adatok átvitelekor. Próbálja újra.

    +
      +
    • Az oldal ideiglenes nem érhető el, vagy túl elfoglalt. Próbálja újra néhány pillanat múlva.
    • +
    • Ha egyetlen oldalt sem tud betölteni, akkor ellenőrizze az eszköz adat- vagy Wi-Fi kapcsolatát.
    • +
    + ]]>
    + + + A kapcsolat időtúllépés miatt megszakadt + + 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.

    • 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.
    • Más webhelyeket sem képes elérni? Ellenőrizze a számítógép hálózati kapcsolatát.
    • 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.
    • Továbbra is fennáll a probléma? Kérjen segítséget a rendszergazdától vagy az internetszolgáltatójától.
    ]]>
    + + + A kapcsolódás sikertelen + + + +
  • A webhely ideiglenesen nem érhető el, vagy túl elfoglalt. Próbálja újra néhány pillanat múlva.
  • +
  • Ha nem tölt be egyetlen oldal sem, akkor ellenőrizze a mobileszköz adat- vagy Wi-Fi kapcsolatát.
  • + + ]]>
    + + + Váratlan válasz a kiszolgálótól + + 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.

    ]]>
    + + + Az oldal nem megfelelően van átirányítva + + + 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.

    +
      +
    • Letiltotta a webhely által megkövetelt sütiket?
    • +
    • 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.
    • +
    + ]]>
    + + + Kapcsolat nélküli mód + + + A böngésző kapcsolat nélküli módban van, ezért nem tud csatlakozni a kért elemhez.

    +
      +
    • Csatlakoztatva van a számítógép a hálózathoz?
    • +
    • Nyomja meg a „Próbálja újra” gombot az online módba váltáshoz és az oldal újratöltéséhez.
    • +
    + ]]>
    + + + A port biztonsági okok miatt tiltva van + + A kért cím olyan portot adott meg (pl. mozilla.org:80 a mozilla.org 80-as portjához), amelyet általában nem 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.

    ]]>
    + + + A kapcsolat alaphelyzetbe állt + + + A hálózati kapcsolat megszakadt a kapcsolat újratárgyalásakor. Próbálja újra.

    +
      +
    • Az oldal ideiglenes nem érhető el, vagy túl elfoglalt. Próbálja újra néhány pillanat múlva.
    • +
    • Ha egyetlen oldalt sem tud betölteni, akkor ellenőrizze az eszköz adat- vagy Wi-Fi kapcsolatát.
    • +
    + ]]>
    + + + Nem biztonságos fájltípus + +
  • Értesítse a webhely tulajdonosait erről a problémáról.
  • ]]>
    + + + Sérült tartalom hiba + + A megtekinteni kívánt oldal nem jeleníthető meg, mert az adatátvitel közben hiba történt.

    • Lépjen kapcsolatba a webhely üzemeltetőjével, és értesítse a problémáról.
    ]]>
    + + + A tartalom összeomlott + A megtekinteni kívánt oldal nem jeleníthető meg, mert az adatátvitel közben hiba történt.

    • Lépjen kapcsolatba a webhely üzemeltetőjével, és értesítse a problémáról.
    ]]>
    + + + Tartalomkódolási hiba + + 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.

    +
      +
    • Lépjen kapcsolatba a webhely üzemeltetőjével, és értesítse a problémáról.
    • +
    + ]]>
    + + + Cím nem található + + + A böngésző nem találta a megadott címhez tartozó gazdakiszolgálót.

    +
      +
    • Ellenőrizze, hogy nincsenek elírások a címben, például + ww.example.com a + www.example.com helyett.
    • +
    • Ha egyetlen oldalt sem tud betölteni, akkor ellenőrizze az eszköz adat- vagy Wi-Fi kapcsolatát.
    • +
    + +]]>
    + + + Nincs internetkapcsolat + + Ellenőrizze a hálózati kapcsolatot, vagy próbálja meg újratölteni az oldalt néhány pillanat múlva. + + Újratöltés + + + Érvénytelen cím + 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.

    + ]]>
    + + A cím érvénytelen + +
  • A webcímek általában úgy néznek ki, mint a http://www.example.com/
  • Győződjön meg róla, hogy perjeleket használ (tehát /).
  • ]]>
    + + + Ismeretlen protokoll + + A cím meghatározott egy protokollt (például wxyz://), amelyet a böngésző nem ismer fel, ezért nem képes megfelelően csatlakozni a webhelyhez.

    +
      +
    • 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.
    • +
    • 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.
    • +
    + ]]>
    + + + A fájl nem található + + +
  • Lehet, hogy a fájlt átnevezték, eltávolították vagy áthelyezték?
  • +
  • Nincs elgépelés, kis- és nagybetű eltérés stb. a címben?
  • +
  • Rendelkezik a megfelelő hozzáférési jogokkal a kért fájlhoz?
  • + + ]]>
    + + + A fájl elérése megtagadva + + +
  • Lehet hogy törölve lett, át lett helyezve, vagy a fájljogosultságok megakadályozzák a hozzáférést.
  • + + ]]>
    + + + A proxykiszolgáló visszautasította a kapcsolatot + + A böngésző proxykiszolgáló használatára van beállítva, de a proxy visszautasította a kapcsolatot.

    +
      +
    • Jók a böngésző proxybeállításai? Ellenőrizze a beállításokat, és próbálja újra.
    • +
    • A proxyszolgáltatás engedélyezi a kapcsolatokat erről a hálózatról?
    • +
    • Továbbra is fennáll a probléma? Kérjen segítséget a rendszergazdától vagy az internetszolgáltatójától.
    • +
    + ]]>
    + + + Proxykiszolgáló nem található + + A böngésző proxykiszolgáló használatára van beállítva, de a proxy nem található.

    +
      +
    • Jók a böngésző proxybeállításai? Ellenőrizze a beállításokat, és próbálja újra.
    • +
    • Csatlakoztatva van a számítógép a hálózathoz?
    • +
    • Továbbra is fennáll a probléma? Kérjen segítséget a rendszergazdától vagy az internetszolgáltatójától.
    • +
    + ]]>
    + + + Kártékony szoftvert terjesztő webhely probléma + + 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.

    + ]]>
    + + + Nem kívánt szoftvert terjesztő webhely probléma + + 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.

    + ]]>
    + + + Káros webhely probléma + + 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.

    + ]]>
    + + + Félrevezető oldal probléma + + 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.

    ]]>
    + + + Biztonságos webhely nem érhető el + + %1$s HTTPS verziója nem érhető el.]]> + + Tovább a HTTP oldalra +
    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 @@ + + + + + Կրկին փորձել + + + Անհնար է հարցումն ավարտել + + Այս խնդրի կամ սխալի մասին հավելյալ տեղեկություններ այժմ անմատչելի է:

    ]]>
    + + + Անվտանգ կապակցումը ձախողվեց + + +
  • Այն էջը, որը փորձում եք դիտել, կարող է չերևալ, որովհետև ստացված տվյալների իսկությունը չի կարող ստուգվել:
  • +
  • Խնդրում ենք կապնվել կայքի սեփականատերերի հետ՝ նրանց տեղյակ պահելու այս խնդրի մասին:
  • + ]]>
    + + + Անվտանգ կապակցումը ձախողվեց + + + +
  • Այս խնդիրը կարող է կապված լինել սպասարկիչի կարգավորման հետ կամ, հնարավոր է, որ որևէ մեկը փորձում է որպես սպասարկիչ հանդես գալ:
  • +
  • Եթե դուք այս սպասարկիչին նախկինում հաջողությամբ եք կապակցվել, ապա սխալը կարող է ժամանակավոր բնույթ կրել և կարող եք կրկին փորձել ավելի ուշ:
  • + ]]>
    + + + Ընդլայնված… + + Ինչ-որ մեկը կարող է փորձել գաղտնալսել կայքը, և դուք չպետք է շարունակեք: +        

    +        ]]>
    + + Վերադառնալ (առաջարկվում է) + + Ընդունել վտանգը և շարունակել + + + Այս կայքը պահանջում է անվտանգ կապ: + + + +
  • Էջը, որ փորձում եք դիտել, հնարավոր չէ ցուցադրել, քանի որ կայքը պահանջում է անվտանգ կապակցում:
  • +
  • Ամենայն հավանականությամբ՝ խնդիրը կայքի հետ է և դուք ոչինչ չեք կարող անել այն ուղղելու համար:
  • +
  • Խնդրի մասին կարող եք ծանուցել կայքի վարիչին:
  • + + ]]>
    + + + Լրացուցիչ… + + + %1$s-ը ունի անվտանգության քաղաքականություն, որը կոչվում է HTTP Strict Transport Security (HSTS), ինչը նշանակում է, որ %2$s-ը կարող է դրան միմիայն անվտանգ կապակցվել: Դուք չեք կարող բացառություն ավելացնել տվյալ էջն այցելելու համար: + ]]> + + Հետ գնալ + + + Կապը խզվեց + + Դիտարկիչը հաջողությամբ կապակցվեց, բայց տեղեկատվությունը փոխանցելիս կապն ընդհատվեց: Խնդրում ենք կրկին փորձել:

    +      
      +        
    • Կայքը կարող է ժամանակավորապես անհասանելի կամ շատ զբաղված լինել: Կրկին փորձեք մի քանի րոպեից:
    • +        
    • Եթե չեք կարողանում բեռնել որևէ էջ, ստուգեք ձեր սարքի տվյալները կամ Wi-Fi կապը:
    • +      
    ]]>
    + + + Կապը ժամանակասպառվեց + + + Հարցվող կայքը չպատասխանեց կապի հարցմանը և ծրագիրը դադարեցրեց պատասխան սպասելուն

    +
      +
    • Հնարավո՞ր է կայքը խիստ ծանրաբեռնված է կամ ժամանակավորապես չի գործում: Փորձեք քիչ ուշ:
    • +
    • Այլ կայքեր ևս չի՞ լինում բացել: Ստուգեք ցանցային կապակցումը:
    • +
    • Արդյոք ձեր համակարգիչը կամ ցանցը պաշտպանվա՞ծ են Firewall­-ով կամ պրոքսիով: Դրա սխալ կազմաձևումը կարող է խանգարել վեբի զննմանը:
    • +
    • Եթե դեռ դժվարություններ ունեք, ապա օգնության համար դիմեք ցանցային վարիչին կամ համացանցի սպասարկողին:
    • +
    ]]>
    + + + Հնարավոր չէ միանալ + + + +
  • Կայքը ժամանակավոր անհասանելի կամ զբաղված է: Փորձեք մի փոքր ավելի ուշ:
  • +
  • Եթե չկարողանաք բեռնել որևէ էջ՝ ստուգեք ձեր սարքի տվյալները կամ Wi-Fi կապակցումը:
  • + ]]>
    + + + Անսպասելի պատասխան սպասարկիչից + + + Կայքը ցանցային հարցմանը պատասխանեց անսպասելի ձևով, ուստի դիտարկիչը չի կարող շարունակել:

    ]]>
    + + + Էջը ճիշտ չէ վերահասցեավորվել + + + Դիտարկիչը դադարեցրեց պահանջվող միույթը ստանալու փորձը: Կայքն այնպես է վերահասցեավորում հարցումը, որ այն երբեք չի ավարտվի:

    +
      +
    • Արդյո՞ք այս հանգույցի կողմից պահանջվող cookie-ները արգելափակել եք
    • +
    • Եթե կայքի cookie-ները ընդունելը խնդիրը չլուծի, ապա ուրեմն, ամենայն հավանականությամբ, դա սպասարկչի կազմաձևման խնդիր է և ոչ թե Ձեր սարքի:
    • +
    ]]>
    + + + Անցանց կերպ + + + Դիտարկիչը աշխատում է անցանց կերպում և չի կարող կապակցվել հարցվող միույթին:

    +
      +
    • Համակարգիչը կապակցվա՞ծ է ակտիվ ցանցի:
    • +
    • Սեղմեք “Կրկին փորձել”՝ առցանց անցնելու և էջը կրկին բեռնելու համար:
    • +
    ]]>
    + + + Պորտը սահմանափակված է անվտանգության նկատառումներից ելնելով + + + Հարցվող հասցեն հատկորոշել է պորտ (օրինակ՝ mozilla.org:80՝ 80 պորտի համար mozilla.org-ում), որը սովորաբար օգտագործվում է այլ նպատակների համար և ոչ վեբի դիտարկման: Ձեր անվտանգության և պաշտպանության նկատառումներով դիտարկիչը չեղարկել է հարցումը:

    ]]>
    + + + Կապը վերակայվեց + + + Դիտարկիչի հղումը ընդհատվել է կապակցման ընթացքում: Կրկին փորձեք:

    +
      +
    • Հնարավոր է՝ կայքը ժամանակավորապես անմատչելի կամ զբաղված է: Փոքր ինչ հետո կրկին փորձեք:
    • +
    • Եթե հնարավոր չէ բեռնել որևէ էջ, ապա ստուգեք ձեր սարքի տվյալները կամ Wi-Fi կապակցումը:
    • +
    ]]>
    + + + Ոչ անվտանգ ֆայլի տեսակ + + +
  • Խնդրեմ կապնվեք այս կայքի սեփականատերերի հետ՝ այս խնդրի մասին տեղյակ պահելու համար:
  • + ]]>
    + + + Բովանդակությունը վնասված է + + + Տվյալ էջը չի կարող ցուցադրվել, քանի որ տեղի է ունեցնել տվյալների փոխանցման սխալ։

    +
      +
    • Կապնվեք կայքի սեփականատերերի հետ՝ խնդրի մասին հաղորդելու համար։
    • +
    ]]>
    + + + Բովանդակությունը խափանվեց + Ընթացիկ էջը չի կարող ցուցադրվել, քանի որ տեղի է ունեցնել փոխանցման սխալ։

    +
      +
    • Կապնվեք վեբ կայքի հեղինակների հետ՝ նրանց այս մասին հաղորդելու համար։
    • +
    ]]>
    + + + Բովանդակության գաղտնագրման սխալ + + Էջը, որը դուք փորձում էք դիտել, չի կարող ցուցադրվել, քանի որ այն օգտագործում է սխալ կամ չսպասարկվող խտացման ձև:

    +
      +
    • Կապնվեք կայքի սեփականատերերի հետ և տեղյակ պահեք նրանց խնդրի մասին:
    • +
    ]]>
    + + + Հասցեն չի գտնվել + + + Դիտարկիչը չկարողացավ գտնել տրամադրված հասցեի հոսթի սպասարկիչը:

    +      
      +        
    • Ստուգեք հասցեն՝ մուտքագրելու սխալների համար, ինչպիսիք են +           ww .example.com +           www .example.com-ի փոխարեն
    • +        
    • Եթե որևէ էջ չի բեռնվում, ապա ստուգեք ձեր սարքի տվյալները կամ Wi-Fi կապակցումը:
    • +      
    ]]>
    + + + Չկա կապակցում համացանցին + + Ստուգեք ձեր ցանցային կապակցումը կամ փորձեք կրկին բեռնել էջը մի քանի վայրկյանից: + + Կրկին բեռնել + + + Անվավեր Հասցե + Ներկայացված հասցեն ճանաչված ձևաչափով չէ: Խնդրում ենք ստուգել տեղադրության գոտին՝ արդյոք սխալ կա, և կրկին փորձել:

    ]]>
    + + Հասցեն վավեր չէ + + + +
  • Վեբ հասցեները սովորաբար գրվում են հետևյալ կերպ` http://www.example.com/
  • +
  • Համոզվեք, որ օգտագործում եք հակադարձ սլեշներ (այսինքն` /):
  • + ]]>
    + + + Անհայտ աշխատակարգ + + Հասցեն հատկորոշում է հաղորդակարգ (օրինակ՝ wxyz://), որը դիտարկիչը չի կարողանում ճանաչել, այդ իսկ պատճառով դիտարկիչը ի վիճակի չէ պատշաճ կապակցվել կայքի հետ:

    +
      +
    • Փորձո՞ւմ եք մուլտիմեդիա կամ այլ ոչ տեքստային ծառայություններ մատչել: Ստուգե՛ք կայքի հավելյալ պահանջները:
    • +
    • Որոշ հաղորդակարգեր պահանջում են երրորդ կողմից տրամադրված ծրագրաշար կամ օժանդակ ծրագրեր, որպեսզի դիտարկիչը կարողանա դրանք ճանաչել:
    • +
    ]]>
    + + + Ֆայլը չի գտնվել + + +
  • Կարո՞ղ է նշված միույթը վերանվանվել կամ տեղափոխվել է կամ վերատեղաբաշխվել է:
  • +
  • Արդյո՞ք ուղղագրական, մեծատառ/փոքրատառ կամ այլ տպելասխալ չկա հասցեում:
  • +
  • Արդյո՞ք մատչելու բավարար թույլտվություն ունեք հարցվող միությի նկատմամբ:
  • + ]]>
    + + + Հասանելիությունը ֆայլին մերժվեց + +
  • Հնարավոր է՝ այն հեռացվել է, տեղափոխվել կամ ֆայլի թույլտվությունները կանխել են մատչումը:
  • + ]]>
    + + + Փոխանորդ սպասարկիչը մերժեց կապը + + Դիտարկիչը կազմաձևված է օգտագործելու վստահված պրոքսի սպասարկիչ, բայց պրոքսին մերժել է կապակցումը:

    +
      +        
    • Դիտարկիչի պրոքսի կազմաձևումը ճի՞շտ է: Ստուգեք կարգավորումները և կրկին փորձեք:
    • +        
    • Արդյո՞ք պրոքսի ծառայությունը թույլ է տալիս կապակցումներ այս ցանցից:
    • +        
    • Դեռ խնդիրներ ունե՞ք: Օգնության համար խորհրդակցեք ձեր ցանցի վարիչի կամ ինտերնետի մատակարարի հետ:
    • +      
    ]]>
    + + + Փոխանորդ սպասարկիչը չգտնվեց + + Դիտարկիչը կազմաձևված է օգտագործելու վստահված պրոքսի սպասարկիչ, բայց պրոքսին մերժել է կապակցումը:

    +
      +        
    • Դիտարկիչի պրոքսի կազմաձևումը ճի՞շտ է: Ստուգեք կարգավորումները և կրկին փորձեք:
    • +        
    • Սարքը կապակցված է ակտիվ ցանցի:
    • +        
    • Դեռ խնդիրներ ունե՞ք: Օգնության համար խորհրդակցեք ձեր ցանցի վարիչի կամ ինտերնետի մատակարարի հետ:
    • +      
    ]]>
    + + + Չարամիտ կայքի թողարկում + + + %1$s կայքը հաղորդվել է որպես հարձակվող կայք և արգելափակված է ըստ ձեր անվտանգության նախապատվությունների:

    ]]>
    + + + Կայքի անցանկալի խնդիր + + + %1$s կայքը հաղորդվել է որպես անցանկալի ծրագրաշար սպասարկող և արգելափակված է ըստ ձեր անվտանգության նախապատվությունների:

    ]]>
    + + + Վնասակար կայքի խնդիր + + + %1$s կայքը հաղորդվել է որպես հավանական վնասակար կայք և արգելափակված է ըստ ձեր անվտանգության նախապատվությունների:

    ]]>
    + + + Խաբուսիկ կայքի խնդիր + + %1$s վեբ էջը հաղորդվել է որպես խաբուսիկ կայք և արգելափակված է ըստ ձեր անվտանգության նախապատվությունների:

    ]]>
    + + + Անվտանգ կայքը հասանելի չէ + + %1$s-ի HTTPS տարբերակը հասանելի չէ:]]> + + Շարունակել HTTP կայքում +
    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 @@ + + + + Retentar + + + Impossibile completar le requesta + + + Nulle altere information sur iste problema o error es disponibile actualmente.

    + ]]>
    + + + Connexion secur fallite + + + +
  • Le pagina que tu tenta vider non pote esser monstrate perque le authenticitate del datos recipite non poteva esser verificate.
  • +
  • Contacta le proprietarios del sito web pro informar les de iste problema.
  • + + +]]>
    + + + Connexion secur fallite + + + +
  • Isto pote esser un problema con le configuration del servitor, o pote esser que un persona tenta de usurpar le identitate del servitor.
  • +
  • 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.
  • + + ]]>
    + + + Avantiate… + + Es possibile que alcuno tenta usurpar le identitate del sito. Tu non deberea continuar. +

    + + ]]>
    + + Retroceder (Recommendate) + + Acceptar le risco e continuar + + + Le sito web require un connexion secur. + + + +
  • Le pagina que tu tenta de vider non pote monstrar se perque iste sito web require un connexion secur.
  • +
  • Le problema es probabilemente con le sito web, e tu nihil pote facer pro resolver lo.
  • +
  • Avisa le administrator del sito web re le problema.
  • + + ]]>
    + + + Avantiate… + + + %1$s ha un politica de securitate appellate HTTP Strict Transport Security (HSTS), le qual significa que %2$s solo pote connecter se a illo de maniera secur. Tu non pote adder un exception pro visitar iste sito. + ]]> + + Retornar + + + Le connexion ha essite interrumpite + + + Le navigator se ha connectite con successo, ma le connexion ha essite interrumpite durante le transferimento de informationes. Per favor tenta lo de novo.

    +
      +
    • Le sito pote esser temporarimente indisponibile o troppo occupate. Retenta in alcun momentos.
    • +
    • Si tu non succede a cargar alcun pagina, verifica le connexion de datos o Wi-Fi de tu apparato.
    • +
    + ]]>
    + + + Le connexion ha expirate + + + Le sito requestate non ha respondite a un requesta de connexion e le navigator ha cessate de attender un responsa.

    +
      +
    • Poterea le servitor esser in supercarga o temporarimente foras de servicio? Retenta plus tarde.
    • +
    • Impossibile acceder a altere sitos? Verifica le connexion de rete del apparato.
    • +
    • Es tu apparato o rete protegite per un firewall o proxy? Configurationes incorrecte pote interferer con le navigation del web.
    • +
    • Ancora difficultates? Consulta tu administrator de rete o fornitor de Internet pro assistentia.
    • +
    + ]]>
    + + + Connexion impossibile + + + +
  • Le sito pote esser temporarimente indisponibile o troppo occupate. Retenta in alcun momentos.
  • +
  • Si tu non pote cargar alcun pagina, verifica le connexion de datos o Wi-Fi de tu apparato.
  • + + ]]>
    + + + Responsa inexpectate del servitor + + + Le sito respondeva al requesta de rete in un maniera impreviste e le navigator non pote continuar.

    + ]]>
    + + + Le pagina non redirige correctemente + + + Le navigator ha cessate de tentar recuperar le elemento requestate. Le sito redirige le requesta in un maniera que non se completara jammais.

    +
      +
    • Ha tu disactivate o blocate cookies necessari pro iste sito?
    • +
    • 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.
    • +
    + ]]>
    + + + Modo disconnectite + + + Le navigator opera in su modo sin connexion e non pote connecter se al elemento requestate.

    +
      +
    • Es le apparato connectite a un rete active?
    • +
    • Pulsa “Retentar” pro passar al modo in linea e recargar le pagina.
    • +
    + ]]>
    + + + Porta limitate pro rationes de securitate + + + Le adresse requestate specificava un porta (p.ex. mozilla.org:80 pro porta 80 sur mozilla.org) normalmente usate pro altere scopos que le navigation del web. Le navigator ha cancellate le requesta pro tu protection e securitate.

    + ]]>
    + + + Le connexion ha essite interrumpite + + + Le ligation al rete ha essite interrumpite durante le negotiation de un connexion. Per favor tenta lo de novo.

    +
      +
    • Le sito pote esser temporarimente indisponibile o troppo occupate. Retenta post alcun momentos.
    • +
    • Si tu non pote cargar alcun pagina, verifica le connexion de datos o Wi-Fi de tu apparato.
    • +
    + ]]>
    + + + Typo de file non secur + + + +
  • Contacta le proprietarios del sito web pro informar les de iste problema.
  • + +]]>
    + + + Error de contento corrumpite + + + Le pagina que tu tenta vider non pote esser monstrate perque un error in le transmission de datos ha essite detegite.

    +
      +
    • Contacta le proprietarios del sito web pro informar les de iste problema.
    • +
    + ]]>
    + + + Contento collabite + Le pagina que tu tenta vider non pote esser monstrate perque un error in le transmission de datos ha essite detegite.

    +
      +
    • Contacta le proprietarios del sito web pro informar les de iste problema.
    • +
    + ]]>
    + + + Error de codification del contento + + Le pagina que tu tenta vider non pote esser monstrate perque illo usa un forma de compression non valide o non supportate.

    +
      +
    • Contacta le proprietarios del sito web pro informar les de iste problema.
    • +
    + ]]>
    + + + Adresse non trovate + + + Le navigator non pote trovar le servitor hospite al adresse fornite.

    +
      +
    • Verifica le adresse pro errores de scriptura como + ww.example.com in loco de + www.example.com.
    • +
    • Si tu non pote cargar alcun pagina, verifica le connexion de datos o Wi-Fi de tu apparato.
    • +
    + ]]>
    + + + Nulle connexion a Internet + + Verifica tu connexion de rete o tenta recargar le pagina post alcun momentos. + + Recargar + + + Adresse non valide + Le adresse fornite non es in un formato recognoscite. Verifica si il ha errores in le barra de adresse e tenta lo de novo.

    + ]]>
    + + Le adresse non es valide + + + +
  • Adresses web se scribe normalmente in forma http://www.example.com/
  • +
  • Assecura te de usar barras oblique non inverse (i.e. /).
  • + + ]]>
    + + + Protocollo incognite + + Le adresse specifica un protocollo (p.ex. wxyz://) que le navigator non recognosce, dunque le navigator non pote connecter te correctemente al sito.

    +
      +
    • Tenta tu acceder a files multimedial o altere servicios non textual? Verifica si le sito ha requisitos additional.
    • +
    • Alcun protocollos pote requirer software o plug-ins de tertios ante que le navigator pote recognoscer los.
    • +
    + ]]>
    + + + File non trovate + + +
  • Poterea le objecto haber essite renominate, removite o displaciate?
  • +
  • Ha il un error de orthographia, de majusculas o un altere error typographic in le adresse?
  • +
  • Ha tu permissiones de accesso sufficiente pro le objecto requestate?
  • + + ]]>
    + + + Le accesso al file ha essite refusate + + +
  • Illo pote haber essite removite o displaciate, o le permissiones del file pote impedir le accesso.
  • + + ]]>
    + + + Connexion refusate per le servitor proxy + + Le navigator es configurate pro usar un servitor proxy, ma le proxy refusava un connexion.

    +
      +
    • Es le configuration del proxy del navigator correcte? Verifica le parametros e retenta.
    • +
    • Al le servicio proxy permitte al connexiones de iste rete?
    • +
    • Ancora ha tu difficultates? Consulta tu administrator de rete o fornitor de Internet pro assistentia.
    • +
    + ]]>
    + + + Servitor proxy non trovate + + Le navigator es configurate pro usar un servitor proxy, ma le proxy non pote esser trovate.

    +
      +
    • Es le configuration del proxy del navigator correcte? Verifica le parametros e retenta.
    • +
    • Es le apparato connectite a un rete active?
    • +
    • Ancora ha tu difficultates? Consulta tu administrator de rete o fornitor de Internet pro assistentia.
    • +
    + ]]>
    + + + Problema de sito malware + + + Le sito a %1$s ha essite signalate como sito attaccante e ha essite blocate in base a tu preferentias de securitate.

    + ]]>
    + + + Problema de sito indesirate + + + Le sito a %1$s ha essite signalate como servitor de software indesirate e ha essite blocate in base a tu preferentias de securitate.

    + ]]>
    + + + Problema de sito malefic + + + Le sito a %1$s ha essite signalate como potentialmente malefic e ha essite blocate in base a tu preferentias de securitate.

    + ]]>
    + + + Problema de sito fraudulente + + Iste pagina web a %1$s ha essite signalate como sito fraudulente e ha essite blocate in base a tu preferentias de securitate.

    + ]]>
    + + + Sito secur non disponibile + + %1$s.]]> + + Continuar al sito HTTP +
    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 @@ + + + + + Coba Lagi + + + Tidak Dapat Menyelesaikan Permintaan + + + Informasi tambahan terkait masalah atau kesalahan ini sedang tidak tersedia.

    + ]]>
    + + + Sambungan Aman Gagal + + + +
  • Laman yang Anda ingin lihat tidak dapat ditampilkan karena otentikasi data yang diterima tidak dapat diverifikasi
  • +
  • Mohon hubungi pemilik situs web untuk mengabarkan mereka tentang masalah ini.
  • + + ]]>
    + + + Sambungan Aman Gagal + + + +
  • Mungkin terjadi masalah dengan konfigurasi server, atau bisa saja seseorang berusaha menyamar menjadi server.
  • +
  • Jika Anda pernah tersambung dengan baik, kesalahan ini mungkin hanya sementara dan Anda dapat mencoba lagi nanti.
  • + + ]]>
    + + + Tingkat Lanjut… + + Seseorang mungkin berusaha menyamar sebagai situs dan sebaiknya Anda tidak melanjutkan. +

    + ]]>
    + + Kembali (Disarankan) + + Terima Risikonya dan Lanjutkan + + + Situs web ini membutuhkan sambungan aman. + + +
  • Halaman yang Anda coba lihat tidak dapat ditampilkan karena situs web ini memerlukan sambungan aman
  • +
  • Kemungkinan besar, masalahnya terletak pada situs web, dan tidak ada yang dapat Anda lakukan untuk menyelesaikannya.
  • +
  • Anda dapat memberi tahu admin situs web tentang masalah ini.
  • + + ]]>
    + + + Tingkat lanjut… + + + %1$s memiliki kebijakan keaman yang disebut HTTP Strict Transport Security (HSTS), yang berarti %2$s hanya dapat terhubung dengan aman. Anda tidak dapat menambahkan pengecualian untuk mengunjungi situs ini. + ]]> + + Kembali + + + Sambungan terputus + + + Peramban terhubung dengan sukses, namun koneksi terganggu saat mentransfer informasi. Silakan coba lagi.

    +
      +
    • Situs ini mungkin sementara tidak sedia atau sedang sibuk. Coba lagi dalam beberapa saat.
    • +
    • Jika Anda tidak dapat memuat laman apapun, periksa koneksi data atau Wi-Fi pada perangkat Anda.
    • +
    + ]]>
    + + + Tenggang waktu tersambung habis + + + Situs yang diminta tidak menjawab permintaan sambungan dan peramban berhenti menunggu jawaban

    +
      +
    • Mungkinkan server sedang dalam keadaan sibuk atau mati sementara? Coba lagi nanti.
    • +
    • Apakah Anda tidak dapat mengakses situs lainnya? Periksa koneksi jaringan komputer Anda.
    • +
    • Apakah jaringan atau komputer Anda dilindungi firewall atau proxy? Pengaturan yang salah dapat mengganggu penjelajahan Web.
    • +
    • Masih bermasalah? Tanyakan pada adminstrator jaringan Anda atau Penyedia Jasa Layanan Internet Anda.
    • +
    + ]]>
    + + + Tidak dapat tersambung + + + +
  • Situs mungkin tidak tersedia untuk sementara atau terlalu sibuk . Cobalah beberapa saat lagi.
  • +
  • Jika Anda tidak dapat memuat halaman apa pun, periksa data peranti atau koneksi Wi-Fi Anda.
  • + + ]]>
    + + + Respon tidak terduga dari server + + + Situs menanggapi permintaan jaringan dengan cara yang tak terduga dan peramban tidak dapat melanjutkan.

    + ]]>
    + + + Laman tidak teralihkan dengan benar + + + Peramban telah berhenti untuk menerima item yang diminta. Situs tersebut mengalihkan permintaan seakan-akan tidak pernah selesai

    +
      +
    • Apakah Anda menonaktifkan atau memblokir kuki yang diperlukan oleh situs ini?
    • +
    • Jika menerima kuki situs tidak menyelesaikan masalah, nampaknya ada masalah konfigurasi server dan bukan masalah pada komputer Anda.
    • +
    + ]]>
    + + + Mode Luring + + + Peramban beroperasi dalam mode luring dan tidak dapat terhubung pada item yang diminta.

    +
      +
    • Apakah komputer terhubung pada jaringan aktif?
    • +
    • Tekan “Coba Lagi” untuk berpindah ke mode daring dan memuat ulang laman.
    • +
    + ]]>
    + + + Port dibatasi untuk alasan keamanan + + + Alamat yang diminta menspesifikasikan sebuah port (contoh: mozilla.org:80 untuk port 80 pada mozilla.org) biasanya digunakan untuk keperluan selain dari penjelajahan Web. Peramban telah membatalkan permintaan untuk perlindungan dan keamanan Anda.

    + ]]>
    + + + Sambungan diputus + + + Tautan jaringan terganggu saat menegosiasikan koneksi. Silakan coba lagi.

    +
      +
    • Situs bisa saja sedang tidak tersedia sementara atau terlalu sibuk. Coba lagi dalam beberapa saat.
    • +
    • Jika Anda tidak dapat memuat laman apapun, cek koneksi data atau Wi-Fi perangkat Anda.
    • +
    + ]]>
    + + + Jenis Berkas Tidak Aman + + + +
  • Mohon hubungi pemilik situs web untuk mengabarkan masalah ini pada mereka.
  • + + ]]>
    + + + Galat Konten Rusak + + Laman yang akan dibuka tidak dapat ditampilkan karena ada terdeteksi galat pada pengiriman data

    +
      +
    • Silakan hubungi pemilik situs web mengenai masalah ini.
    • +
    + ]]>
    + + + Konten macet + + Laman yang Anda coba lihat tidak dapat ditampilkan karena terdapat gangguan transmisi data.

    +
      +
    • Silakan hubungi pengguna situs Web untuk mengabarkan masalah ini kepada mereka.
    • +
    + ]]>
    + + + Kesalahan Pengodean Isi (Content Encoding) + + Laman yang Anda coba lihat tidak dapat ditampilkan karena menggunakan bentuk kompresi yang tidak valid atau tidak didukung.

    +
      +
    • Silakan hubungi pengguna situs Web untuk mengabarkan masalah ini kepada mereka.
    • +
    + ]]>
    + + + Alamat Tidak Ditemukan + + + Peramban tidak dapat menemukan host server untuk alamat yang disediakan.

    +
      +
    • Periksa alamat untuk kesalahan pengetikan seperti + ww.contoh.com alih-alih + www.contoh.com.
    • +
    • Jika Anda tidak dapat memuat laman apapun, periksa koneksi data atau Wi-Fi perangkat Anda.
    • +
    + ]]>
    + + + Tidak ada sambungan Internet + + Periksa koneksi jaringan Anda atau coba muat ulang halaman dalam beberapa saat. + + Muat ulang + + + Alamat Tidak Valid + Laman yang diberikan tidak dalam format yang dikenali. Periksa ulang bilah lokasi kemudian coba kembali.

    + ]]>
    + + Alamat tidak valid + + + +
  • Alamat Web biasanya ditulis seperti http://www.example.com/
  • +
  • Pastikan Anda menggunakan garis miring depan (contoh: /).
  • + + ]]>
    + + + Protokol Tidak Dikenal + + Alamat menspesifikasi protokol (contoh: wxyz://) yang tidak dikenali peramban, sehingga peramban tidak dapat menghubungi situs dengan baik.

    +
      +
    • Apakah Anda ingin mengakses layanan multimedia atau non-teks lainnya? Periksa situs untuk persyaratan tambahan.
    • +
    • Beberapa protokol mungkin memerlukan perangkat lunak atau plugin pihak ketiga sebelum peramban dapat mengenalinya.
    • +
    + ]]>
    + + + Berkas Tidak Ditemukan + + +
  • Apakah objek telah diganti namanya, dibuang, atau dipindahkan?
  • +
  • Apa tidak ada kesalahan ejaan, huruf besar, atau kesaalhan keitk lainnya pada penulisan alamat?
  • +
  • Apakah Anda memiliki hak akses untuk mengakses objek yang diminta?
  • + + ]]>
    + + + Akses ke berkas ditolak + + +
  • Mungkin sudah dihapus, dipindahkan, atau hak akses yang ada mencegah akses terhadap berkas.
  • + + ]]>
    + + + Server Proxy Menolak Sambungan + Program peramban diatur untuk menggunakan server proksi, tetapi server proksi menolak sambungan.

    +
      +
    • Apakah pengaturan proksi peramban sudah benar? Periksa lagi pengaturan tersebut dan coba lagi.
    • +
    • Apakah layanan proksi mengizinkan sambungan dari jaringan komputer ini?
    • +
    • Masih bermasalah? Tanyakan pada administrator jaringan Anda atau Penyedia Jasa Layanan Internet (Internet Service Provider) untuk mendapatkan bantuan.
    • +
    + ]]>
    + + + Server Proksi Tidak Ditemukan + + Program peramban diatur untuk menggunakan server proxy, tetapi server proxy tidak bisa ditemukan.

    +
      +
    • Apakah pengaturan proxy program peramban sudah benar? Periksa lagi pengaturan tersebut dan coba lagi.
    • +
    • Apakah komputer tersambung pada jaringan yang berfungsi?
    • +
    • Masih bermasalah? Tanyakan pada administrator jaringan Anda atau Penyedia Jasa Layanan Internet (Internet Service Provider).
    • +
    + ]]>
    + + + Masalah situs malware + + + Situs di %1$s telah dilaporkan sebagai situs penyerang dan telah diblokir sesuai dengan pengaturan keamanan Anda.

    + ]]>
    + + + Masalah situs yang tidak diinginkan + + + Situs di %1$s telah dilaporkan melayani perangkat lunak yang tidak diinginkan dan telah diblokir sesuai dengan pengaturan keamanan Anda.

    + ]]>
    + + + Masalah situs berbahaya + + + Situs di %1$s telah dilaporkan sebagai situs yang berpotensi membahayakan dan telah diblokir sesuai dengan pengaturan keamanan Anda.

    + ]]>
    + + + Masalah situs tipuan + + Laman web di %1$s telah dilaporkan sebagai situs tipuan dan telah diblokir sesuai dengan pengaturan keamanan Anda.

    + ]]>
    + + + Situs Aman Tidak Tersedia + + %1$s tidak tersedia.]]> + + Lanjutkan ke Situs HTTP +
    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 @@ + + + + + Reyndu aftur + + + Get ekki klárað beiðni + + Viðbótarupplýsingar um þetta vandamál eru ekki til staðar.

    ]]>
    + + + Ekki tókst að koma á öruggri tengingu + + + +
  • Ekki er hægt að sýna síðuna vegna þess að ekki var hægt að auðkenna gögnin.
  • +
  • Hafið samband við vefstjóra vefsvæðisins til að láta hann vita af þessu vandamáli.
  • + ]]>
    + + + Ekki tókst að koma á öruggri tengingu + + +
  • Þ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.
  • +
  • Ef þú hefur náð að tengjast þessum netþjón áður þá gæti villan verið tímabundin og þú gætir reynt aftur síðar.
  • + ]]>
    + + + Frekari stillingar… + + Einhver gæti verið að reyna að þykjast vera þetta vefsvæði og þú ættir ekki að halda áfram. +

    + ]]>
    + + Til baka (mælt með) + + Samþykkja áhættuna og halda áfram + + + Þetta vefsvæði krefst öruggrar tengingar. + + +
  • 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.
  • +
  • Vandamálið tengist líklegast vefsvæðinu sjálfu og það er ekkert sem þú getur gert til að leysa það.
  • +
  • Þú getur látið stjórnanda vefsvæðisins vita um vandamálið.
  • + + ]]>
    + + + Ítarlegt… + + + %1$s er með öryggisstefnu sem kallast HTTP Strict Transport Security (HSTS), sem þýðir að %2$s getur aðeins tengst því á öruggan hátt. Þú getur ekki bætt við undantekningu til að heimsækja þetta vefsvæði.]]> + + Fara til baka + + + Tenging slitnaði + + Vafrinn náði að tengjast en tengingin rofnaði vði flutning upplýsinga. Endilega reyndu aftur.

    +
      +
    • Vefsvæðið gæti verið ótiltækt tímabundið eða of undir of miklu álagi. Reyndu aftur eftir smá tíma.
    • +
    • Athugaðu gagnatengingu tækisins eða þráðulausu tenginguna ef þú getur ekki hlaðið síður.
    • +
    ]]>
    + + + Tengingin svaraði ekki tímanlega + + Umbeðið vefsvæði svaraði ekki tengingu og vafrinn hætti að bíða eftir svari.

    +
      +
    • Gæti verið að svæðið sé undir of miklu álagi eða sé niðri tímabundið? Reyndu aftur seinna.
    • +
    • Getur þú ekki náð sambandi við önnur vefsvæði? Athugaðu nettenginu tækisins.
    • +
    • Er tækið þitt varið af eldvegg eða milliþjóni? Rangar stilingar gætu haft áhrif á vafra.
    • +
    • Er vandamálið enn til staðar? Hafðu samband við kerfistjóra eða netaðila eftir aðstoð.
    • +
    ]]>
    + + + Get ekki tengst + + +
  • Svæðið gæti verið ótiltækt tímabundið eða of upptekið til að svara. Reyndu aftur eftir smá tíma.
  • +
  • Skoðaðu gagnatenginguna eða þráðlausutenginguna á tækinu þínu ef þú nærð ekki að hlaða inn neinum síðum.
  • + ]]>
    + + + Rangt svar frá netþjóni + + Vefsvæðið svaraði netbeiðni á óvæntan hátt þannig að vafrinn getur ekki haldið áfram.

    ]]>
    + + + Síðan er ekki að endurbeina rétt + + 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.

    +
      +
    • Hefurðu lokað á eða gert vefkökur óvirkar frá þessu vefsvæð?
    • +
    • 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.
    • +
    ]]>
    + + + Ónettengdur hamur + + Vafrinn er að vinna án nettengingar og getur ekki tengst við umbeðna síðu.

    +
      +
    • Er tölvan tengd við virkt net?
    • +
    • Smelltu á “Reyna aftur” til að tengjast netingu og endurnýja síðuna.
    • +
    ]]>
    + + + Vegna öryggis er aðgangur að þessari gátt ekki leyfður + + Umbeðið veffang bað um ákveðna gátt (t.d. mozilla.org:80 fyrir gátt 80 á mozilla.org) sem venjulega er notað fyrir eitthvað annað en að vafra. Vafrinn hefur lokað á beiðnina þér til verndar.

    ]]>
    + + + Tenging slitnaði + + + Vafrinn náði að tengjast en tengingin rofnaði við flutning upplýsinga. Endilega reyndu aftur.

    +
      +
    • Vefsvæðið gæti verið ótiltækt tímabundið eða of undir of miklu álagi. Reyndu aftur eftir smá tíma.
    • +
    • Athugaðu gagnatengingu tækisins eða þráðulausu tenginguna ef þú getur ekki hlaðið neinar síður.
    • +
    ]]>
    + + + Óörugg skráartegund + + + +
  • Hafðu samband við vefstjóra vefsvæðisins og láttu hann vita af þessu vandamáli.
  • + ]]>
    + + + Villa vegna skemmdra gagna + + Ekki er hægt að sýna síðuna vegna villu í gagnasendingu.

    +
      +
    • Hafðu samband við vefsíðueiganda til að láta hann vita af vandamálinu.
    • +
    ]]>
    + + + Innihald olli hruni + Ekki er hægt að sýna síðuna vegna villu í gagnasendingu.

    +
      +
    • Hafðu samband við vefsíðueiganda til að láta hann vita af vandamálinu.
    • +
    ]]>
    + + + Kóðunarvilla + + Ekki er hægt að sýna síðuna því hún notar ógilda eða óstudda þjöppun.

    +
      +
    • Hafðu samband við vefstjóra vefsvæðisins og láttu hann vita af þessu vandamáli.
    • +
    ]]>
    + + + Veffang fannst ekki + + + Vafrinn gat ekki fundið hýsingarþjóninn fyrir þetta vistfang.

    +
      +
    • Athugaðu hvort það séu villu í vistfanginu, eins og t.d. + ww.example.com í staðinn fyrir + www.example.com.
    • +
    • Ef þú getur ekki hlaðið neinum vefsvæðum, athugaðu þá gagnatenginu eða þráðulausu tenginu tækis þíns.
    • +
    ]]>
    + + + Engin internet tenging + + Athugaðu nettenginguna þína eða reyndu að endurglæða síðuna eftir nokkrar mínútur. + + Endurglæða + + + Veffang ekki gilt + Innslegið veffang er ekki á viðurkenndu sniði. Skoðaðu veffangið í stikunni og reyndu aftur.

    ]]>
    + + Veffang ekki gilt + + + +
  • Vistföng eru vanalega rituð á þennan hátt http://www.example.com/
  • +
  • Gangtu úr skugga um að þú sért að nota rétt skástrik (s.s. /).
  • + ]]>
    + + + Óþekkt samskiptaregla + Umbeðið vistfang (t.d., wxyz://) skilgreinir samskiptareglu sem vafrinn kannast ekki við, þannig að vafrinn getur ekki tengst við vefsvæðið.

    +
      +
    • Ertu að reyna að tengjast myndefni eða annarskonar gagnaþjónustum? Athugaðu á vefsvæðinu hvort þú þurfir fleiri hluti.
    • +
    • Sumar samskiptareglur þarfnast forrita eða viðbóta frá þriðja aðila áður en vafrinn getur þekkt þær.
    • +
    ]]>
    + + + Skrá fannst ekki + + +
  • Gæti verið að hluturinn hafi verið endurnefndur, fjarlægður eða færður til?
  • +
  • Er stafsetningarvilla, hástafaritun, eða annarskonar stafsetningarvilla í vistfanginu?
  • +
  • Hefur þú nægar aðgangsheimildir?
  • + ]]>
    + + + Aðgangur að skránni ekki leyfður + +
  • Vera má að skráin hafi verið fjarlægð, færð til eða réttindarleyfi komi í veg fyrir aðgengi.
  • + ]]>
    + + + Milliþjónn neitar tengingum + + Vafrinn er stilltur til að nota milliþjóna, en milliþjónninn hafnaði tengingu.

    +
      +
    • Eru vafra stillingar milliþjónsins réttar? Athugaðu stillingarnar og reyndu aftur.
    • +
    • Leyfir milliþjónn tengingar frá þínu neti?
    • +
    • Hafðu samband við kerfisstjóra eða netaðila ef vandamálin eru enn til staðar.
    • +
    ]]>
    + + + Fann ekki milliþjón + + Vafrinn er stilltur til að nota milliþjóna, en milliþjónn fannst ekki.

    +
      +
    • Eru vafra stillingar milliþjóns rétt stilltar? Athugaðu stillingarnar og reyndu aftur.
    • +
    • Er tækið tengt virku neti?
    • +
    • Hafðu samband við kerfisstjóra eða netaðila ef vandamálin eru enn til staðar.
    • +
    ]]>
    + + + Vandamál með spilliforrit + + Vefsvæðið %1$s hefur verið tilkynnt sem árásarsvæði og aðgangur hefur verið hindraður vegna öryggisstillinga.

    ]]>
    + + + Vandamál með óæskilegt vefsvæði + + 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.

    ]]>
    + + + Villa vegna skaðlegs vefsvæðis + + 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..

    ]]>
    + + + Villa vegna svindlsvæðis + + Vefsvæðið %1$s hefur verið tilkynnt sem svindlsvæði og aðgangur hefur verið hindraður vegna öryggisstillinga.

    ]]>
    + + + Öruggt vefsvæði ekki tiltækt + + %1$s er ekki tiltæk.]]> + + Halda áfram á HTTP-vefsvæði +
    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 @@ + + + + + Riprovare + + + Impossibile completare la richiesta + + Attualmente non sono disponibili informazioni aggiuntive relative a questo problema o errore.

    + ]]>
    + + + Connessione sicura non riuscita + + +
  • La pagina che si sta cercando di visualizzare non può essere mostrata in quanto non è possibile verificare l’autenticità dei dati ricevuti.
  • +
  • Contattare il responsabile del sito web per informarlo del problema.
  • + + ]]>
    + + + Connessione sicura non riuscita + + +
  • Potrebbe trattarsi di un problema nella configurazione del server oppure di un tentativo da parte di qualcuno di sostituirsi al server stesso.
  • +
  • Se è stato possibile connettersi a questo server in passato, il problema potrebbe essere solo temporaneo. Si consiglia di riprovare in seguito.
  • + + ]]>
    + + + Avanzate… + + Potrebbe trattarsi di un tentativo di sostituirsi al sito originale. È sconsigliato proseguire. +

    + + ]]>
    + + Torna indietro (consigliato) + + Accetta il rischio e continua + + + Questo sito web richiede una connessione sicura. + + +
  • La pagina che si sta cercando di visualizzare non può essere mostrata in quanto questo sito web richiede una connessione sicura.
  • +
  • L’errore è probabilmente causato dal sito web e non può essere risolto.
  • +
  • È possibile segnalare il problema al gestore del sito web.
  • + +]]>
    + + + Avanzate… + + + %1$s utilizza un criterio di sicurezza chiamato HTTP Strict Transport Security (HSTS). Questo significa che %2$s può connettersi solo in modo sicuro e non è possibile aggiungere un’eccezione per visitare questo sito.]]> + + Torna indietro + + + La connessione è stata interrotta + + Il browser è stato connesso correttamente, ma la connessione è stata interrotta durante il trasferimento delle informazioni. Riprovare.

    +
      +
    • Il sito potrebbe essere temporaneamente non disponibile o troppo occupato. Riprovare tra qualche istante.
    • +
    • Se non si riesce a caricare alcuna pagina, controllare i dati del dispositivo o la connessione Wi-Fi.
    • +
    + ]]>
    + + + Tempo per la connessione esaurito + + Il sito richiesto non ha risposto a una richiesta di connessione e il browser ha smesso di attendere una risposta.

    +
      +
    • È possibile che il server sia soggetto a un’elevata richiesta o un’interruzione temporanea. Riprovare più tardi.
    • +
    • Si riesce a navigare su altri siti? Controllare la connessione di rete del dispositivo.
    • +
    • Il dispositivo o la rete sono protetti da un firewall o un proxy? Impostazioni errate possono interferire con la navigazione sul web.
    • +
    • Si riscontrano ancora problemi? Consultare l’amministratore di rete o il provider Internet per ricevere assistenza.
    • +
    ]]>
    + + + Connessione non riuscita + + +
  • Il sito potrebbe essere temporaneamente non disponibile o troppo occupato. Riprovare tra qualche istante.
  • +
  • Se non si riesce a caricare alcuna pagina, controllare i dati del dispositivo o la connessione Wi-Fi.
  • + + ]]>
    + + + Risposta inattesa del server + + Il sito ha risposto alla richiesta di rete in modo imprevisto, quindi il browser non può continuare.

    + ]]>
    + + + La pagina non reindirizza in modo corretto + + 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.

    +
      +
    • Sono stati disabilitati o bloccati i cookie richiesti da questo sito?
    • +
    • 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.
    • +
    ]]>
    + + + Non in linea + + Il browser si trova in modalità non in linea e non è possibile connettersi con l’elemento richiesto.

    +
      +
    • Il dispositivo è collegato a una rete attiva?
    • +
    • Selezionare “Riprova” per passare alla modalità in linea e ricaricare la pagina.
    • +
    ]]>
    + + + Porta bloccata per motivi di sicurezza + + L’indirizzo richiesto specificava una porta (per es. Mozilla.org:80 per la porta 80 su mozilla.org) normalmente utilizzata per scopi diversi dalla navigazione sul web. Il browser ha annullato la richiesta per garantire la protezione e la sicurezza dell’utente.

    + ]]>
    + + + La connessione è stata annullata + + Il collegamento di rete è stato interrotto durante la negoziazione di una connessione. Riprovare.

    +
      +
    • Il sito potrebbe essere temporaneamente non disponibile o troppo occupato. Riprovare tra qualche istante.
    • +
    • Se non si riesce a caricare alcuna pagina, controllare i dati del dispositivo o la connessione Wi-Fi.
    • +
    + ]]>
    + + + Tipo di file non sicuro + + +
  • Contattare il proprietario del sito web per informarlo del problema.
  • + + ]]>
    + + + Errore: contenuto danneggiato + + La pagina che si sta cercando di visualizzare non può essere mostrata a causa di un errore di trasmissione dati.

    +
      +
    • Contattare l’amministratore del sito per segnalare il problema.
    • +
    + ]]>
    + + + Arresto anomalo del contenuto + La pagina che si sta cercando di visualizzare non può essere mostrata a causa di un errore di trasmissione dati.

    +
      +
    • Contattare l’amministratore del sito per segnalare il problema.
    • +
    + ]]>
    + + + Errore di codifica del contenuto + 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.

    +
      +
    • Contattare il proprietario del sito web per informarlo del problema.
    • +
    + ]]>
    + + + Indirizzo non trovato + + Il browser non è riuscito a trovare il server host per l’indirizzo fornito.

    +
      +
    • Verificare se l’indirizzo contiene errori di battitura del tipo + ww.example.com invece di + www.example.com
    • +
    • Se non è possibile caricare alcuna pagina, controllare la connessione dati o Wi-Fi del dispositivo.
    • +
    + ]]>
    + + + Nessuna connessione a Internet + + Verifica la tua connessione di rete o prova a ricaricare la pagina tra qualche istante. + + Ricarica + + + Indirizzo non valido + L’indirizzo fornito non è in un formato riconosciuto. Controllare la barra degli indirizzi per individuare eventuali errori e riprovare.

    + ]]>
    + + L’indirizzo non è valido + + +
  • Gli indirizzi internet normalmente si scrivono nella forma http://www.example.com/
  • +
  • Verificare se si stanno utilizzando le barre corrette (ad esempio /).
  • + + ]]>
    + + + Protocollo sconosciuto + L’indirizzo richiede un protocollo (ad es. wxyz://) che il browser non riconosce, quindi non può collegarsi correttamente al sito.

    +
      +
    • Si sta accedendo a servizi multimediali o non testuali? Verificare sul sito i requisiti necessari.
    • +
    • Alcuni protocolli richiedono software esterni o plugin affinché il browser li possa riconoscere.
    • +
    ]]>
    + + + File non trovato + +
  • L’oggetto potrebbe essere stato rinominato, rimosso o spostato.
  • +
  • Potrebbe esserci un errore di ortografia nell’indirizzo.
  • +
  • Si possiedono i permessi per accedere all’oggetto specificato?
  • + + ]]>
    + + + Accesso al file non consentito + +
  • Il file potrebbe essere stato spostato o cancellato oppure i permessi sul file potrebbero impedirne l’accesso.
  • + + ]]>
    + + + Connessione rifiutata dal server proxy + Il browser è configurato per utilizzare un server proxy, ma il proxy ha rifiutato una connessione.

    +
      +
    • La configurazione proxy del browser è corretta? Controllare le impostazioni e riprovare.
    • +
    • Il servizio proxy consente le connessioni da questa rete?
    • +
    • Si riscontrano ancora problemi? Consultare l’amministratore di rete o il provider Internet per ricevere assistenza.
    • +
    + ]]>
    + + + Impossibile contattare il server proxy + Il browser è configurato per utilizzare un server proxy, ma il proxy non è stato rilevato.

    +
      +
    • La configurazione proxy del browser è corretta? Controllare le impostazioni e riprovare.
    • +
    • Il dispositivo è collegato a una rete funzionante?
    • +
    • Si riscontrano ancora problemi? Consultare l’amministratore di rete o il provider Internet per ricevere assistenza.
    • +
    ]]>
    + + + Problema di sito con malware + + Il sito web %1$s è stato segnalato come sito web malevolo ed è stato bloccato sulla base delle impostazioni di sicurezza.

    + ]]>
    + + + Problema di sito non desiderato + + Il sito web %1$s è stato segnalato come un sito contenente software indesiderato ed è stato bloccato sulla base delle impostazioni di sicurezza.

    + ]]>
    + + + Problema di sito pericoloso + + Il sito web %1$s è stato segnalato come sito web potenzialmente pericoloso ed è stato bloccato sulla base delle impostazioni di sicurezza.

    + ]]>
    + + + Problema con sito ingannevole + + Il sito web %1$s è stato segnalato come sito ingannevole ed è stato bloccato sulla base delle impostazioni di sicurezza.

    + ]]>
    + + + Versione sicura del sito non disponibile + + %1$s.]]> + + Prosegui sul sito HTTP +
    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 @@ + + + + + ניסיון חוזר + + + לא ניתן להשלים את הבקשה + + מידע נוסף על בעיה או שגיאה זו אינו זמין כעת.

    +]]>
    + + + חיבור מאובטח נכשל + + +
  • לא ניתן להציג את הדף המבוקש מכיוון שאין אפשרות לאמת את אמינות הנתונים שהתקבלו.
  • +
  • נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.
  • + +]]>
    + + + חיבור מאובטח נכשל + + + +
  • יתכן שמדובר בבעיה בתצורת השרת או שגורם כלשהו מנסה להתחזות לשרת.
  • +
  • אם התחברת לשרת זה בהצלחה בעבר, ייתכן שהשגיאה זמנית, ומומלץ לנסות שוב מאוחר יותר.
  • + + ]]>
    + + + מתקדם… + + יכול להיות שמישהו מנסה להתחזות לאתר ועדיף שלא להמשיך. +

    + ]]>
    + + חזרה (מומלץ) + + קבלת הסיכון והמשך + + + אתר זה דורש חיבור מאובטח. + + +
  • העמוד בו הינך מנסה לצפות אינו ניתן להצגה מכיוון שאתר זה דורש חיבור מאובטח.
  • +
  • כנראה שהבעיה היא באתר, ואין שום דבר שבאפשרותך לעשות כדי לפתור זאת.
  • +
  • ניתן להודיע למנהל האתר על הבעיה.
  • + + ]]>
    + + + מתקדם… + + + ל־%1$s יש מדיניות אבטחה בשם אבטחת תעבורה מחמירה של HTTP ‏(HSTS), כלומר %2$s יכול להתחבר לאתר באופן מאובטח בלבד. לא ניתן להוסיף חריגה כדי לבקר באתר זה.]]> + + חזרה אחורה + + + החיבור הופסק + + + הדפדפן התחבר בהצלחה אך החיבור הופסק במהלך העברת המידע. נא לנסות שוב.

    +
      +
    • יכול להיות שהאתר אינו זמין או עמוס מדי. יש לנסות שוב בעוד מספר רגעים.
    • +
    • אם אף דף אינו נטען, יש לוודא שחיבור הנתונים הסלולריים או רשת ה־Wi-Fi תקין.
    • +
    ]]>
    + + + תם הזמן המוקצב לחיבור + + + האתר המבוקש לא הגיב לבקשת התחברות והדפדפן הפסיק להמתין לתגובה.

    +
      +
    • האם ייתכן שהשרת חווה עומסים גבוהים או הפסקה זמנית? נא לנסות שוב מאוחר יותר.
    • +
    • האם אין ביכולתך לגלוש באתרים אחרים? נא לבדוק את הגדרות החיבור לרשת של המחשב.
    • +
    • האם המחשב או הרשת שלך מוגנים על־ידי חומת אש או שרת מתווך? הגדרות לא נכונות עלולות להפריע לגלישה באינטרנט.
    • +
    • עדיין נתקל בבעיות? היוועץ במנהל הרשת או בספק האינטרנט שלך לקבלת עזרה.
    • +
    +]]>
    + + + כישלון בהתחברות + + +
  • יכול להיות שהאתר אינו זמין או עמוס מדי. יש לנסות שוב בעוד מספר רגעים.
  • +
  • אם אף דף אינו נטען, יש לוודא שחיבור הנתונים הסלולריים או רשת ה־Wi-Fi תקין.
  • + ]]>
    + + + תגובה לא צפויה מהשרת + + האתר הגיב לבקשה מהרשת באופן בלתי צפוי, והדפדפן אינו יכול להמשיך.

    + ]]>
    + + + הדף מבצע העברה לא תקינה + + הדפדפן הפסיק לנסות למצוא את הפריט המבוקש. האתר מכוון מחדש את הבקשה בצורה שלעולם לא תושלם.

    +
      +
    • האם ביטלת או חסמת עוגיות שנדרשות על־ידי אתר זה?
    • +
    • אם קבלת עוגיות מאתר זה אינה פותרת את הבעיה, סביר שמדובר בתקלה בהגדרות השרת ולא במכשיר שלך.
    • +
    + ]]>
    + + + מצב לא־מקוון + + + הדפדפן פועל כעת במצב לא־מקוון ואינו יכול להתחבר לפריט המבוקש.

    +
      +
    • האם המכשיר מחובר לרשת פעילה?
    • +
    • יש ללחוץ על ״ניסיון חוזר״ כדי לעבור למצב מקוון ולטעון מחדש את הדף.
    • +
    + ]]>
    + + + השער נחסם מסיבות אבטחה + + + הכתובת המבוקשת ציינה שער (לדוגמה: mozilla.org:80 עבור שער 80 ב־mozilla.org) המיועד בדרך כלל לשימוש אחר מאשר גלישה באינטרנט. הדפדפן ביטל את הבקשה עבור ההגנה והאבטחה שלך.

    + ]]>
    + + + החיבור הופסק + + + קישור הרשת נקטע במהלך המשא והמתן על החיבור. נא לנסות שוב.

    +
      +
    • יכול להיות שהאתר אינו זמין או עמוס מדי. יש לנסות שוב בעוד מספר רגעים.
    • +
    • אם אף דף אינו נטען, יש לוודא שחיבור הנתונים הסלולריים או רשת ה־Wi-Fi תקין.
    • +
    ]]>
    + + + סוג קובץ מסוכן + + +
  • נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.
  • + + ]]>
    + + + שגיאת תוכן פגום + + לא ניתן להציג את הדף המבוקש מכיוון שאותרה שגיאה בתעבורת הנתונים.

    +
      +
    • נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.
    • +
    + ]]>
    + + + התוכן קרס + לא ניתן להציג את הדף המבוקש מכיוון שאותרה שגיאה בתעבורת הנתונים.

    +
      +
    • נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.
    • +
    + ]]>
    + + + שגיאה בקידוד תוכן + לא ניתן להציג את הדף המבוקש מכיוון שהוא משתמש בסוג דחיסה שאינו חוקי או שאינו נתמך.

    +
      +
    • נא ליצור קשר עם בעלי האתר כדי ליידע אותם על בעיה זו.
    • +
    + ]]>
    + + + הכתובת לא נמצאה + + + הדפדפן לא הצליח למצוא את השרת המארח לכתובת שסופקה.

    +
      +
    • יש לבדוק שהכתובת אינה מכילה שגיאות כגון + ww.example.com במקום + www.example.com.
    • +
    • אם אף דף אינו נטען, יש לוודא שחיבור הנתונים הסלולריים או רשת ה־Wi-Fi תקין.
    • +
    ]]>
    + + + אין חיבור לרשת + + נא לבדוק את החיבור שלך לרשת או לנסות לטעון את הדף מחדש בעוד מספר רגעים. + + טעינה מחדש + + + כתובת לא חוקית + הכתובת שסופקה אינה בתבנית מזוהה. נא לבדוק את שורת המיקום עבור שגיאות ולנסות שוב.

    ]]>
    + + כתובת לא חוקית + + +
  • כתובות אינטרנט לרוב נכתבות בצורה דומה לזו: http://www.example.com/
  • +
  • יש לוודא כי נעשה שימוש בלוכסנים קדמיים (כלומר /).
  • + + ]]>
    + + + פרוטוקול לא מוכר + + הכתובת מציינת פרוטוקול (לדוגמה: wxyz://) שהדפדפן אינו מזהה, ולכן הדפדפן אינו יכול להתחבר לאתר כראוי.

    +
      +
    • האם הינך מנסה לגשת לשירות מולטימדיה או לשירות אחר שאינו מבוסס טקסט? נא לבדוק אם קיימות לאתר דרישות נוספות.
    • +
    • פרוטוקולים מסויימים עשויים לדרוש שימוש ביישומי צד־שלישי או בתוספים חיצוניים לפני שהדפדפן יוכל לזהות אותם.
    • +
    ]]>
    + + + קובץ לא נמצא + + +
  • האם ייתכן שהפריט הוסר, הועבר, או שינה שם?
  • +
  • האם ישנה שגיאת איות, שגיאת רישיות, או שגיאות טופוגרפיות אחרות בכתובת?
  • +
  • האם יש לך הרשאות גישה מספיקות אל הפריט המבוקש?
  • + + ]]>
    + + + הגישה לקובץ נדחתה + +
  • ייתכן שהקובץ הוסר, הועבר או שההרשאות מונעות את הגישה אליו.
  • + + ]]>
    + + + השרת המתווך דחה את ההתחברות + + הדפדפן מוגדר להשתמש בשרת מתווך, אולם השרת המתווך דחה את ההתחברות.

    +
      +
    • האם הגדרות השרת המתווך נכונות? נא לבדוק אותן ולנסות שוב.
    • +
    • האם שירות התיווך מאפשר חיבורים מהרשת הזאת?
    • +
    • עדיין יש תקלות? כדאי להיוועץ בהנהלת הרשת או בספקית האינטרנט לקבלת סיוע.
    • +
    ]]>
    + + + שרת מתווך לא נמצא + + הדפדפן מוגדר להשתמש בשרת מתווך, אולם השרת המתווך דחה את ההתחברות.

    +
      +
    • האם הגדרות השרת המתווך נכונות? נא לבדוק אותן ולנסות שוב.
    • +
    • האם המכשיר מחובר לרשת פעילה?
    • +
    • עדיין יש תקלות? כדאי להיוועץ בהנהלת הרשת או בספקית האינטרנט לקבלת סיוע.
    • +
    ]]>
    + + + בעיה עם אתר זדוני + + האתר בכתובת %1$s דווח כאתר תקיפה ונחסם בהתאם להעדפות האבטחה שלך.

    + ]]>
    + + + בעיה עם אתר בלתי רצוי + + האתר בכתובת %1$s דווח כאתר המגיש תוכנה בלתי רצויה ונחסם בהתאם להעדפות האבטחה שלך.

    + ]]>
    + + + בעיה עם אתר מזיק + + האתר בכתובת %1$s דווח כאתר שככל הנראה מזיק ונחסם בהתאם להעדפות האבטחה שלך.

    + ]]>
    + + + בעיה עם אתר מטעה + + הדף בכתובת %1$s דווח כאתר מטעה ונחסם בהתאם להעדפות האבטחה שלך.

    + ]]>
    + + + אתר מאובטח אינו זמין + + %1$s אינה זמינה.]]> + + המשך לאתר בגרסת HTTP +
    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 @@ + + + + + 再試行 + + + リクエストを正常に完了できませんでした + + 現在のところこの問題やエラーについての詳細情報はありません。

    + ]]>
    + + + 安全な接続ができませんでした + + +
  • 受信したデータの真正性を検証できなかったため、このページは表示できませんでした。
  • +
  • ウェブサイトの所有者に連絡を取り、この問題を報告してください。
  • + + ]]>
    + + + 安全な接続ができませんでした + + +
  • サーバーの設定に問題があるか、誰かが正規のサーバーになりすましている可能性があります。
  • +
  • 以前は正常に接続できていた場合、この問題は恐らく一時的なものですので、後で再度試してみてください。
  • + + ]]>
    + + + 詳細設定… + + 誰かがサイトになりすまそうとしている可能性があります。この先へ進んではいけません。 +

    + ]]>
    + + 戻る (推奨) + + 危険性を承知の上で使用する + + + このウェブサイトには安全な接続が必要です。 + + +
  • このウェブサイトには安全な接続が必要なため、閲覧しようとしているページを表示できません。
  • +
  • この問題はウェブサイト側に原因がある可能性が高く、解決するためにあなたにできることはありません。
  • +
  • この問題について、ウェブサイトの管理者に知らせてください。
  • + +]]>
    + + + 詳細情報... + + + %1$s には HTTP Strict Transport Security (HSTS) と呼ばれるセキュリティポリシーが設定されており、%2$s は安全な接続でしか通信できません。そのため、このサイトを例外に追加することはできません。 +]]> + + 戻る + + + 接続が中断されました + + サイトに正常に接続できましたが、データの転送中にネットワーク接続が切断されました。再度試してください。

    +
      +
    • このサイトが一時的に利用できなくなっていたり、サーバーの負荷が高すぎて接続できなくなっている可能性があります。しばらくしてから再度試してください。
    • +
    • 他のサイトも表示できない場合、端末のデータ接続や Wi-Fi 接続を確認してください。
    • +
    + ]]>
    + + + 接続がタイムアウトしました + + 接続リクエストに対してリクエスト先サーバーが応答を返さなかったため、接続を中止しました。

    +
      +
    • サーバーに負荷が集中したり、一時的に停止している可能性があります。しばらく後で再度試してください。
    • +
    • 他のサイトも表示できない場合、コンピューターのネットワーク接続を確認してください。
    • +
    • ファイアウォールやプロキシでネットワークが保護されている場合、その設定に問題があると正常に表示できなくなることがあります。
    • +
    • 問題が繰り返される場合、ネットワーク管理者またはインターネットプロバイダーに問い合わせてください。
    • +
    + ]]>
    + + + 正常に接続できませんでした + + +
  • このサイトが一時的に利用できなくなっていたり、サーバーの負荷が高すぎて接続できなくなっている可能性があります。しばらくしてから再度試してください。
  • +
  • 他のサイトも表示できない場合、端末のデータ接続や Wi-Fi 接続を確認してください。
  • + + ]]>
    + + + サーバーの応答が不正です + + ネットワークリクエストに対するサイトの応答が正しくなかったため、接続を中断しました。

    + ]]>
    + + + ページの自動転送設定が正しくありません + + リクエストされたリソースの取得を中止しました。このサイトではリクエストの自動転送がループしています。

    +
      +
    • このサイトで要求されている Cookie を無効化またはブロックしていないか確認してください。
    • +
    • サイトによる Cookie の使用を許可しても解決しない場合、これはご利用のコンピューターではなくサーバーの設定に問題があると思われます。
    • +
    + ]]>
    + + + オフラインモードです + + ブラウザーは現在オフラインモードで動作しており、リクエスト対象に接続できません。

    +
      +
    • コンピューターが有効なネットワークに接続されているか確認してください。
    • +
    • “再試行” ボタンを押してブラウザーをオンラインモードに切り替え、ページを再読み込みしてください。
    • +
    + ]]>
    + + + セキュリティ上の理由によりポートの使用が制限されています + + リクエストされたアドレスのポート (例えば mozilla.jp のポート 80 であれば mozilla.jp:80) は普通ウェブサイトの表示以外の目的で使用されます。ユーザーの保護とセキュリティのため、リクエストは中止されました。

    + ]]>
    + + + 接続がリセットされました + + ネットワーク接続の確立中にリンクが切れました。再度試してください

    +
      +
    • このサイトが一時的に利用できなくなっていたり、サーバーの負荷が高すぎて接続できなくなっている可能性があります。しばらくしてから再度試してください。
    • +
    • 他のサイトも表示できない場合、端末のデータ接続や Wi-Fi 接続を確認してください。
    • +
    + ]]>
    + + + 安全でないファイルタイプ + + +
  • ウェブサイトの所有者に連絡を取り、この問題を報告してください。
  • + + ]]>
    + + + コンテンツデータ破損エラー + + このページは、データの伝送中にエラーが検出されたため表示できません。

    +
      +
    • ウェブサイトの所有者に連絡を取り、この問題を報告してください。
    • +
    + ]]>
    + + + コンテンツデータのクラッシュ + このページは、データの伝送中にエラーが検出されたため表示できません。

    +
      +
    • ウェブサイトの所有者に連絡を取り、この問題を報告してください。
    • +
    + ]]>
    + + + 内容符号化 (Content-Encoding) に問題があります + 不正または不明な形式で圧縮されているため、ページを表示できません。

    +
      +
    • ウェブサイトの所有者に連絡を取り、この問題を報告してください。
    • +
    + ]]>
    + + + アドレスが見つかりません + + 指定されたアドレスのホストサーバーが見つかりませんでした。

    +
      +
    • www.example.com を間違えて + ww.example.com と入力するなど、アドレスを間違って入力していないか確認してください。 +
    • +
    • 他のサイトも表示できない場合、端末のデータ接続や Wi-Fi 接続を確認してください。
    • +
    + ]]>
    + + + インターネットに接続されていません + + ネットワーク接続を確認するか、しばらくしてからページを再読み込みしてください。 + + 再読み込み + + + アドレスが無効です + 指定されたアドレスの書式が正しくありません。ミスがないかロケーションバーを確認してください。

    + ]]>
    + + アドレスの書式が正しくありません + + +
  • ウェブのアドレスは通常 http://www.example.com/
  • のようなものになります。 +
  • スラッシュ (/)
  • が使われているか確認してください。 + + ]]>
    + + + プロトコルが不明です + 指定されたプロトコル (例えば wxyz://) を認識できないため、サイトに正常に接続できませんでした。

    +
      +
    • マルチメディアファイルなど非テキストデータにアクセスしようとしている場合、サイトによる特別な動作要件がないか確認してください。
    • +
    • 一部のプロトコルを使用するにはサードパーティのソフトウェアやプラグインが必要になります。
    • +
    + ]]>
    + + + ファイルが見つかりません + +
  • ファイルの名前が変更、削除、または移動されている可能性があります。
  • +
  • アドレスに入力ミス、大文字/小文字の違い、その他の間違いがないか確認してください。
  • +
  • リソースへのアクセス権限があるか確認してください。
  • + + ]]>
    + + + ファイルへのアクセスが拒否されました + +
  • ファイルが削除または移動されているかファイルの許可属性によりアクセスが拒否された可能性があります。
  • + + ]]>
    + + + プロキシサーバーが接続を拒否しました + プロキシサーバーを使用する設定になっていますが、プロキシサーバーは接続を拒否しました。

    +
      +
    • ブラウザーのプロキシ設定が正しいか確認してください。
    • +
    • このネットワークからプロキシサービスへの接続が許可されているか確認してください。
    • +
    • 問題が繰り返される場合、ネットワーク管理者またはインターネットプロバイダーに問い合わせてください。
    • +
    + ]]>
    + + + プロキシサーバーが見つかりませんでした + プロキシサーバーを使用する設定になっていますが、プロキシサーバーが見つかりませんでした。

    +
      +
    • ブラウザーのプロキシ設定が正しいか確認してください。
    • +
    • コンピューターが有効なネットワークに接続されているか確認してください。
    • +
    • 問題が繰り返される場合、ネットワーク管理者またはインターネットプロバイダーに問い合わせてください。
    • +
    + ]]>
    + + + マルウェアサイトの問題 + + %1$s のウェブサイトは攻撃サイトとして報告されており、セキュリティ設定に従いブロックされています。

    + ]]>
    + + + 望ましくないサイトの問題 + + %1$s のウェブサイトは望ましくないソフトウェアを配布しているサイトとして報告されており、セキュリティ設定に従いブロックされています。

    + ]]>
    + + + 有害なサイトの問題 + + %1$s のウェブサイトは有害サイトの可能性があると報告されており、セキュリティ設定に従いブロックされています。

    + ]]>
    + + + 詐欺サイトの問題 + + %1$s のウェブページは詐欺サイトとして報告されており、セキュリティ設定に従いブロックされています。

    + ]]>
    + + + 安全なサイトが利用できません + + %1$s の HTTPS バージョンは利用できません。]]> + + HTTP サイトを開く +
    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 @@ + + + + + ხელახლა ცდა + + + მოთხოვნის შესრულება ვერ მოხერხდა + + დამატებითი მონაცემები, ამ ხარვეზის ან შეცდომის შესახებ ამჟამად მიუწვდომელია.

    ]]>
    + + + უსაფრთხო დაკავშირება ვერ მოხერხდა + + +
  • თქვენ მიერ მოთხოვნილი გვერდის ნახვა შეუძლებელია, ვინაიდან მიღებული მონაცემების ნამდვილობა ვერ დასტურდება.
  • +
  • გთხოვთ, დაუკავშირდეთ ვებსაიტის მფლობელებს და შეატყობინოთ ეს პრობლემა.
  • + ]]>
    + + + უსაფრთხო დაკავშირება ვერ მოხერხდა + + +
  • ამას შესაძლოა, საიტის გაუმართაობა ან სხვა საიტად გასაღების მცდელობა იწვევდეს.
  • +
  • თუ აღნიშნულ საიტს მანამდე წარმატებით უკავშირდებოდით, ხარვეზი შეიძლება დროებითია და მოგვიანებით შეგიძლიათ, კვლავ სცადოთ.
  • + ]]>
    + + + დამატებით… + + შესაძლოა, ვიღაც ამ საიტის სხვა საიტად გასაღებას ცდილობდეს და ჯობია, აღარ განაგრძოთ. +

    + ]]>
    + + უკან დაბრუნება (სასურველია) + + საფრთხის გაცნობიერება და გაგრძელება + + + ვებსაიტი ითხოვს დაცულ კავშირს. + + +
  • გვერდი, რომლის ნახვაც გსურთ, ვერ გამოჩნდება, ვინაიდან საჭიროებს დაცულ კავშირს.
  • +
  • ეს ხარვეზი უმეტესად დაკავშირებულია თავად ვებსაიტთან და თქვენ ვერ მოახერხებთ მის გამოსწორებას.
  • +
  • მხოლოდ შგიძლიათ აცნობოთ საიტის მფლობელებს ამ ხარვეზის შესახებ.
  • + + ]]>
    + + + დამატებით… + + + %1$s იყენებს უსაფრთხოების დებულებას სახელწოდებით HTTP Strict Transport Security (HSTS), ეს კი ნიშნავს, რომ %2$s მას მხოლოდ უსაფრთხო შეერთებით შეიძლება დაუკავშირდეს. გამონაკლისს ვერ დაამატებთ ამ საიტისთვის. + ]]> + + უკან დაბრუნება + + + კავშირი გაწყდა + + + დაკავშირება წარმატებული იყო, მაგრამ მოულოდნელად გაწყდა მონაცემთა გადმოტანისას. გთხოვთ, კვლავ სცადოთ.

    +
      +
    • საიტი დროებით მიუწვდომელი ან გადატვირთულია. სცადეთ ხელახლა ცოტა ხანში.
    • +
    • თუ სხვა გვერდების გახსნასაც ვერ ახერხებთ, შეამოწმეთ მოწყობილობის ფიჭური ან WiFi-კავშირი.
    • +
    ]]>
    + + + კავშირის დრო ამოიწურა + + + საიტი კავშირის მოთხოვნას არ პასუხობს და ბრაუზერმა შეწყვიტა პასუხის ლოდინი.

    +
      +
    • შესაძლოა სერვერი გადატვირთული ან დროებით გათიშული იყოს? სცადეთ მოგვიანებით.
    • +
    • ვერც სხვა საიტებს ხსნით? შეამოწმეთ მოწყობილობის ქსელთან კავშირი.
    • +
    • თქვენი მოწყობილობა ან ქსელი ფარით ან პროქსითაა დაცული? გაუმართავი პარამეტრები შესაძლოა აფერხებდეს ინტერნეტკავშირს.
    • +
    • კვლავ ხარვეზებია? მაშინ, დახმარებისთვის მიმართეთ თქვენი ქსელის მმართველს ან ინტერნეტმომსახურების მომწოდებელს.
    • +
    ]]>
    + + + დაკავშირება ვერ ხერხდება + + + +
  • საიტი დროებით მიუწვდომელი ან გადატვირთულია. სცადეთ ხელახლა ცოტა ხანში.
  • +
  • თუ სხვა გვერდების გახსნასაც ვერ ახერხებთ, შეამოწმეთ მოწყობილობის ფიჭური ან WiFi-კავშირი.
  • + ]]>
    + + + გაუთვალისწინებელი პასუხი სერვერიდან + + + საიტის პასუხი მოთხოვნაზე გაურკვეველი სახისაა და ბრაუზერი ვერ ახერხებს მის დამუშავებას.

    ]]>
    + + + ეს გვერდი არამართებულად გადამისამართდა + + + ბრაუზერმა შეწყვიტა მოთხოვნილი გვერდის ჩატვირთვის მცდელობა. საიტის გადამისამართება მოთხოვნის დასრულების შესაძლებლობას გამორიცხავს.

    +
      +
    • ამორთული ან შეზღუდული ხომ არ გაქვთ ფუნთუშები ამ საიტიდან?
    • +
    • თუ საიტის ფუნთუშების დაშვებამაც არ გამოასწორა, მაშინ ხარვეზი საიტის გაუმართაობას უკავშირდება და არა თქვენს მოწყობილობას.
    • +
    ]]>
    + + + კავშირგარეშე რეჟიმი + + + ბრაუზერი მუშაობს კავშირგარეშედ და მოთხოვნილ მისამართს ვერ დაუკავშირდება.

    +
      +
    • ჩართულია მოწყობილობა მოქმედ ქსელში?
    • +
    • გამოიყენეთ „სცადეთ ხელახლა“ ღილაკი კავშირზე დასაბრუნებლად და გვერდის ხელახლა ჩასატვირთად.
    • +
    ]]>
    + + + პორტი შეზღუდულია უსაფრთხოების მიზნით + + + მოთხოვნილ მისამართში მითითებულია პორტი (მაგ., mozilla.org:80 ანუ პორტი 80 mozilla.org საიტზე) რომელიც, ჩვეულებრივ სხვა მიზნით გამოიყენება ხოლმე და არა ვებგვერდების მოსანახულებლად. თქვენი უსაფრთხოებისთვის ბრაუზერმა ეს მოთხოვნა გააუქმა.

    ]]>
    + + + კავშირი განულდა + + + ქსელური ბმა გაწყდა კავშირის შეთანხმებისას. გთხოვთ კვლავ სცადოთ.

    +
      +
    • საიტი დროებით მიუწვდომელი ან გადატვირთულია. სცადეთ ხელახლა ცოტა ხანში.
    • +
    • თუ სხვა გვერდების გახსნასაც ვერ ახერხებთ, შეამოწმეთ მოწყობილობის ფიჭური ან WiFi-კავშირი.
    • +
    ]]>
    + + + სახიფათო სახის ფაილი + + + +
  • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
  • + ]]>
    + + + დაზიანებული შიგთავსის შეცდომა + + + გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო

    +
      +
    • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
    • +
    ]]>
    + + + შიგთავსი დაზიანდა + + გვერდის ჩვენება, რომლის ნახვასაც ცდილობთ, ვერ ხერხდება მონაცემთა გადაცემისას აღმოჩენილი შეცდომის გამო

    +
      +
    • გთხოვთ, დაუკავშირდეთ საიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
    • +
    ]]>
    + + + შიგთავსის დაშიფვრის შეცდომა + + გვერდის ჩვენება, რომლის ნახვაც გსურთ, შეუძლებელია, რადგან იყენებს შეკუმშვის გაუგებარ ან არამართებულ საშუალებას.

    +
      +
    • გთხოვთ, დაუკავშირდეთ ვებსაიტის მფლობელებს და აცნობოთ ამ ხარვეზის შესახებ.
    • +
    ]]>
    + + + მისამართი ვერ მოიძებნა + + + მითითებულ მისამართზე, ბრაუზერმა სერვერი ვერ მოიძია.

    +
      +
    • შეცდომა ხომ არ დაუშვით აკრეფისას, მაგ. + ww.example.com კი არა, უნდა იყოს + www.example.com.
    • +
    • თუ სხვა გვერდების გახსნასაც ვერ ახერხებთ, შეამოწმეთ მოწყობილობის ფიჭური ან WiFi-კავშირი.
    • +
    ]]>
    + + + ინტერნეტთან კავშირი არაა + + შეამოწმეთ ქსელთან თქვენი კავშირი ან სცადეთ გვერდის ხელახლა გახსნა რამდენიმე წუთში. + + ხელახლა გახსნა + + + არამართებული მისამართი + მითითებული მისამართი უცნობი სახისაა. გთხოვთ, გადაამოწმოთ აკრეფის შეცდომები და სცადოთ ხელახლა.

    ]]>
    + + მისამართი უმართებულოა + + + +
  • ვებ მისამართი ჩვეულებრივ ამგვარად მიეთითება http://www.example.com/
  • +
  • გადაამოწმეთ, რომ წინ გადახრილ ხაზს იყენებთ (მაგ. /).
  • + ]]>
    + + + უცნობი ოქმი + + მისამართში გამოყენებული ოქმი (მაგ., wxyz://) ბრაუზერმა ვერ ამოიცნო და შესაბამისად, საიტს მართებულად ვერ დაუკავშირდა.

    +
      +
    • მულტიმედიურ, ან სხვა არატექსტური შიგთავსის მქონე საიტთან დაკავშირებას ცდილობთ? გადაამოწმეთ საიტის დამატებითი მოთხოვნები.
    • +
    • ზოგი ოქმი, ბრაუზერთან სამუშაოდ, შეიძლება სხვა პროგრამას, ან მოდულებს საჭიროებდეს.
    • +
    ]]>
    + + + ფაილი ვერ მოიძებნა + + +
  • იქნებ გადარქმეული, წაშლილი ან გადაადგილებულია?
  • +
  • ხომ არაა მართლწერის შეცდომა, მთავრული ან სხვა სახის მცდარბეჭდილი მისამართში?
  • +
  • გაქვთ შესაბამისი ნებართვა მოთხოვნილ ფაილთან წვდომისთვის?
  • + ]]>
    + + + ფაილთან წვდომა უარყოფილია + + +
  • შესაძლოა წაშლილია, გადატანილია ან ფაილთან წვდომის უფლებები შეზღუდულია.
  • + ]]>
    + + + პროქსი-სერვერმა კავშირი უარყო + + ბრაუზერი გამართულია პროქსი-სერვერის გამოსაყენებლად, მაგრამ პროქსი-სერვერმა კავშირი უარყო.

    +
      +
    • ბრაუზერის პროქსი, სწორადაა გამართული? შეამოწმეთ პარამეტრები და სცადეთ ხელახლა.
    • +
    • იძლევა პროქსი-სერვერი ამ ქსელიდან დაკავშირების უფლებას?
    • +
    • კვლავ ხარვეზებია? მაშინ, დახმარებისთვის მიმართეთ თქვენი ქსელის მმართველს ან ინტერნეტმომსახურების მომწოდებელს.
    • +
    ]]>
    + + + პროქსი-სერვერი ვერ მოიძებნა + + ბრაუზერი გამართულია პროქსი-სერვერის გამოსაყენებლად, მაგრამ პროქსი-სერვერი ვერ მოიძებნა.

    +
      +
    • ბრაუზერის პროქსი სწორადაა გამართული? შეამოწმეთ პარამეტრები და სცადეთ ხელახლა.
    • +
    • მოწყობილობა მოქმედ ქსელთანაა მიერთებული?
    • +
    • კვლავ ხარვეზებია? მაშინ, დახმარებისთვის მიმართეთ თქვენი ქსელის მმართველს ან ინტერნეტმომსახურების მომწოდებელს.
    • +
    ]]>
    + + + მავნე საიტი + + + ვებგვერდი %1$s მიჩნეულია მოიერიშე საიტად და შეზღუდულია უსაფრთხოების მიზნით.

    ]]>
    + + + არასასურველი საიტი + + + ვებგვერდი %1$s შემჩნეულია არასასურველი პროგრამების შემოთავაზებაში და შეზღუდულია უსაფრთხოების მიზნით

    ]]>
    + + + სახიფათო საიტი + + + ვებგვერდი %1$s მიჩნეულია მავნე საიტად და შეზღუდულია უსაფრთხოების მიზნით.

    ]]>
    + + + თაღლითური საიტი + + ვებგვერდი %1$s მიჩნეულია თაღლითურ საიტად და შეზღუდულია უსაფრთხოების მიზნით.

    ]]>
    + + + უსაფრთხო საიტი მიუწვდომელია + + %1$s არაა წარმოდგენილი დაცული HTTPS-ვერსიით.]]> + + გადასვლა HTTP-საიტზე +
    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 @@ + + + + + Qayta urınıp kóriń + + + Sorawıńızdı qanaatlandırıw múmkinshiligi joq + + + Bul mashqala hám qáte haqqında qosımsha maǵlıwmat házirshe joq.

    ]]>
    + + + Qáwipsiz qosılıw ámelge aspadı + + + +
  • Siz kórmekshi bolǵan betti kórsetip bolmaydı, sebebi alınǵan maǵlıwmatlardıń isenimli ekenligin tekseriw ámelge aspadı.
  • +
  • Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.
  • + ]]>
    + + + Qáwipsiz jalǵanıw ámelge aspadı + + + +
  • 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.
  • +
  • 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ń.
  • + + ]]>
    + + + Qosımsha… + + + 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. +

    + + ]]>
    + + Artqa qaytıw (Usınıs etiledi) + + + Qáwipti qabıllaw hám dawam etiw + + + Bul sayt qáwipsiz jalǵanıwdı talap etedi. + + + +
  • Siz kórmekshi bolǵan betti kórsetip bolmaydı, sebebi bul sayt qáwipsiz jalǵanıwdı talap etedi.
  • +
  • Bul mashqala sayttıń ózi menen baylanıslı bolıwı múmkin hám siz bul jaǵdayda heshnárse isley almaysız.
  • +
  • Siz bul mashqala haqqında sayttıń administratorın xabarlandırıwıńız múmkin.
  • + + ]]>
    + + + Qosımsha… + + + %1$s HTTP qatań transport qáwipsizligi (HSTS) dep atalǵan qáwipsizlik siyasatına iye, yaǵnıy %2$s oǵan tek ǵana qáwipsiz jalǵanıwı múmkin. Bul saytqa kiriw ushın esaptan tısqarılardı qosa almaysız.]]> + + Artqa qaytıw + + + Jalǵanıw úzildi + + + Brauzer tabıslı jalǵandı, biraq maǵlıwmatlardı jetkeriw waqtında baylanısta úzilis boldı. Ótinish, qayta urınıp kóriń.

    +
      +
    • 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ń.
    • +
    • Eger siz birde-bir betti júkley almasańız, qurılmańız mobil internetin yamasa Wi-Fi jalǵanıwın tekseriń.
    • +
    + ]]>
    + + + Jalǵanıw waqtı tawsıldı + + + Sayt jalǵanıw sorawına juwap bermedi hám brauzer kútiwdi toqtattı

    +
      +
    • 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ń.
    • +
    • Eger de siz basqa saytlardı asha almay atırǵan bolsańız, qurılmańızdıń tarmaq penen baylanısın tekseriń.
    • +
    • 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.
    • +
    • Mashqala ele saplastırılmadı ma? Járdem ushın tarmaq administratorı yamasa internet provayderińiz benen másláhátlesiń.
    • +
    + ]]>
    + + + Jalǵanıw ámelge aspadı + + +
  • Sayt waqıtsha islemey qalǵan yamasa júdá bánt bolıwı múmkin. Keyinirek jáne urınıp kórıń.
  • +
  • Eger siz birde-bir betti júkley almasańız, qurılmańız mobil internetin yamasa Wi-Fi jalǵanıwın tekseriń.
  • + + ]]>
    + + + Serverdan kútilmegen juwap + + + Sayt tarmaq sorawına kútilgen kóriniste juwap berdi hám brauzer óz jumısın dawam ete almaydı.

    + ]]>
    + + + Bet durıs jóneltirilmey atır + + + Brauzer betti júklew háreketin toqtattı, sebebi sayt sorawdı heshqashan orınlanbaytuǵınday etip jóneltirip atır.

    +
      +
    • Siz bul sayttıń islewi ushın kerek bolǵan cookiedi óshirip yamasa bloklap qoyǵan bolıwıńız múmkin.
    • +
    • 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.
    • +
    + ]]>
    + + + Oflayn rejim + + + Brauzer oflayn rejimde islemekte hám soralǵan sayt penen baylanıs ornata almaydı.

    +
      +
    • Qurılmańız islep turǵan tarmaqqa jalǵanǵanına isenim kámil etiń.
    • +
    • Online rejimǵe qaytıw hám betti jańalaw ushın «Qayta urınıw» túymesine basıń.
    • +
    + ]]>
    + + + Port qáwipsizlik sebepli sheklengen + + + Soralǵan mánzil ushın ádette veb-saytlar menen islew ushın qollanılmaytuǵın port kórsetilgen (mısalı: mozilla.org:80 - bul mozilla.orgdaǵı 80-port). Qáwipsizligińiz ushın brauzer bul sorawdı biykar etdi.

    + ]]>
    + + + Jalǵanıw qayta ornatıldı + + + Sayt penen jalǵanıwdı ornatıw waqtında baylanıs úzildi. +Ótinish, qayta urınıp kóriń.

    +
      +
    • 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ń.
    • +
    • Eger siz birde-bir betti júkley almasańız, qurılmańız mobil internetin yamasa Wi-Fi jalǵanıwın tekseriń.
    • +
    + ]]>
    + + + Qáwipli fayl túri + + + +
  • Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.
  • + ]]>
    + + + Buzılǵan kontent qátesi + + + Siz kórmekshi bolǵan betti kórsetip bolmaydı, sebebi maǵlıwmatlardı jetkerip beriwde qátelik anıqlandı.

    +
      +
    • Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.
    • +
    + ]]>
    + + + Kontent buzıldı + + Siz kórmekshi bolǵan betti kórsetip bolmaydı, sebebi maǵlıwmatlardı jetkerip beriwde qátelik anıqlandı.

    +
      +
    • Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.
    • +
    + ]]>
    + + + Kontentti sıǵıw qátesi + + 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ı.

    +
      +
    • Ótinish, sayt iyeleri menen bul mashqala haqqında xabar beriw ushın baylanısıń.
    • +
    + ]]>
    + + + Mánzil tawılmadı + + + Brauzer kórsetilgen mánzil boyınsha serverdi taba almadı.

    +
      +
    • Mánzilde qátelik joqlıǵın tekserıń, Mısalı: + www.example.com ornına + ww.example.com.
    • +
    • Eger siz birde-bir betti júkley almasańız, qurılmańız mobil internetin yamasa Wi-Fi jalǵanıwın tekseriń.
    • +
    + ]]>
    + + + Internet penen baylanıs joq + + Tarmaq jalǵanǵanın tekseriń yamasa bir neshe minutlardan keyin qayta júklewge urınıp kóriń. + + Qayta júklew + + + Jaramsız mánzil + 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ıń.

    + ]]>
    + + Mánzil jaramsız + + + +
  • Sayt mánzilleri ádette usınday kóriniste jazıladi http://www.example.com/
  • +
  • Qıya sızıqtan (/) paydalanıp atırǵanıńızǵa isenim kámil etıń (Mısalı: /).
  • + ]]>
    + + + Belgisiz Protokol + + Mánzil (wxyz:// uqsaǵan) belgisiz protokoldı óz ishine aladı, sonıń ushın brauzer sayt penen durıs baylanıs ornata almaydı.

    +
      +
    • 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ń.
    • +
    • Brauzer anıqlawı ushın ayırım protokollar sırtqı baǵdarlamalardı yamasa plaginlerdi ornatıwdı talap etiwi múmkin.
    • +
    + ]]>
    + + + Fayl tawılmadı + + +
  • Fayl ózgertilgen, óshirilgen yamasa kóshirilgen bolıwı múmkin.
  • +
  • Mánzildi kiritgende qátelik ketpegenin tekseriń.
  • +
  • Soralǵan fayldı kóriwge jeterli dárejede huqıqqa iyeligińizge isenim kámil etiń.
  • + + ]]>
    + + + Faylǵa kiriw biykar etilgen + + +
  • Fayl óshirilgen, kóshirilgen yamasa faylǵa ruqsatlar kóriwge tosqınlıq etip atırǵan bolıwı múmkin.
  • + + ]]>
    + + + Proksi server qosılıwdı biykarladı + + Brauzer isenimli serverden paydalanıwǵa sazlanǵan, biraq isenimli server baylanıstı biykarladı.

    +
      +
    • Brauzerdiń isenimli serveri sazlawların tekseriń hám qayta urınıp kóriń.
    • +
    • Isenimli server bul tarmaqtan baylanısıwǵa ruqsat bere me?
    • +
    • Mashqala ele saplastırılmadı ma? Járdem ushın tarmaq administratorı yamasa internet provayderińiz benen másláhátlesiń.
    • +
    + ]]>
    + + + Proksi server tawılmadı + + Brauzer isenimli serverden paydalanıwǵa sazlanǵan, biraq isenimli server tabılmadı.

    +
      +
    • Brauzerdiń isenimli serveri sazlawların tekseriń hám qayta urınıp kóriń.
    • +
    • Qurılmańız islep turǵan tarmaqqa jalǵanǵanına isenim kámil etiń.
    • +
    • Mashqala ele saplastırılmadı ma? Járdem ushın tarmaq administratorı yamasa internet provayderińiz benen másláhátlesiń.
    • +
    + ]]>
    + + + Zıyanlı sayt mashqalası + + + %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.

    ]]>
    + + + Kútilmegen sayt mashqalası + + + %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.

    ]]>
    + + + Zıyanlı sayt mashqalası + + + %1$s saytı itimallı zıyanlı ekenligi haqqında maǵlıwmat bar hám siziń qawipsizlik sazlawlarıńız tiykarında bloklanǵan.

    + ]]>
    + + + Aldamshı sayt mashqalası + + + %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.

    ]]>
    + + + Qáwipsiz sayt joq + + %1$s saytında HTTPS versiyası qollap-quwatlanbaydı.]]> + + HTTP saytında dawam etiw +
    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 @@ + + + + + Ɛreḍ tikkelt-nniḍen + + + Tuttra ur tezmir ad temmed + + Ulac talɣut-nniḍen ɣef wugur-a neɣ tuccḍa-a ulac-itt akka tura.

    + ]]>
    + + + Tuqqna taɣelsant ur teddi ara + + +
  • Asebter-agi i tebɣiḍ ad twaliḍ ur yizmir ara ad d-yettwasken, acku ur nemzir ara ad nessenqed tilawt n yisefka d-ittwaremsen
  • +
  • Ma ulac aɣilif nermes imawlan n usmel web ɣef wugur-agi.
  • +]]>
    + + + Tuqqna taɣelsant ur teddi ara + + +
  • Ayagi yezmer ad yili d ugur n twila n uqeddac neɣ d albaɛḍ i yeɛarḍen ad yakwer tamagit n uqeddac-agi.
  • +
  • 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.
  • + ]]>
    + + + Talqayt… + + Illa ahat amdan i yebɣan ad yakker tamagit n usmel, ur ilaq ara ad tkemleḍ. +

    + + ]]>
    + + Uɣal ɣer deffir (Yelha) + + Qbel ugur u kemmel + + + Asmel-a web yesra tuqqna taɣelsant. + + + +
  • Asebter i tettaɛraḍeḍ ad t-twaliḍ ur yezmir ara ad d-yettwaskan acku asmel-a yesra tuqqna taɣelsant.
  • +
  • Ugur ad yili akked usmel-a web, ur yelli wayen ara t-tgeḍ i wakken ad yefru.
  • +
  • You can notify the website’s administrator about the problem.
  • + + ]]>
    + + + Talqayt… + + + %1$s ɣur-s tasertit n tɣellist HTTP Strict Transport Security (HSTS), ay-agi yemmal-d d akken %2$s izmer kan ad iqqen ɣur-s s tɣellist. Ur tezmireḍ ara ad ternuḍ tasureft akken ad twaliḍ asmel-agi.]]> + + Uɣal ɣer deffir + + + Tuqqna teḥbes + + + Iminig yeqqen akken iwata, maca tuqqna teḥbes deg usiweḍ n telɣut.Ma ulac aɣilif ɛreḍ tikkelt-nniḍen.

    +
      +
    • Asmel yezmer yeḥbes kra n wakud neɣ ahat iɛebba taɛekkemt. Ɛreḍ tikkelt-nniden ticki.
    • +
    • Ma yella ur tezmireḍ ara ad tinigeḍ deg yismal meṛṛa, sefqed tuqqna-ik n yisefka neɣ Wi-Fi n yibenk-ik.
    • +
    + ]]>
    + + + Tanzagt n uraǧu tezri + + + Asmel i tessutreḍ ur d-yerri ara tiririt i usuter n tuqqna, ihi iminig iseḥbes araǧu n tririt.

    +
      +
    • Ahat aqeddac iɛebba ayen ur yezmir neɣ yeḥbes kra n wakud?Ɛreḍ tikkelt-nniḍen ticki.
    • +
    • Ur tezmireḍ ara ad tinigeḍ deg yismal-nniḍen? Sefqed tuqqna n uẓeṭṭa n uselkim-ik.
    • +
    • Aselkim-ik yettummesten s uɣrab n tmes neɣ apṛuksi? Yir iɣewwaṛen zemren ad sḥebsen tunigin deg web.
    • +
    • Mazal uguren? Suter anedbal-ik n uẓeṭṭa neɣ asaǧǧaw-ik nternet ɣef wugar n tallelt.
    • +
    + ]]>
    + + + Igguma ad iqqen + + + +
  • Ahat asmel ulac-it akka tura neγ iɛebba kra. Ɛreḍ tikkelt-nniḍen ticki.
  • +
  • Ma yella ur tezmireḍ ara ad tinigeḍ ɣer usmel, senqed tuqqna-ik n yisefka neɣ Wi-Fi n yibenk-ik.
  • + ]]>
    + + + Aqeddac yerra-d yir tiririt ur nettwaṛǧu ara + + + Asmel yerra-d i tririt n uẓeṭṭa s tarrayt n nettwarǧi ara, ihi iminig ur yezmir ara ad ikemmel.

    + ]]>
    + + + Asebter ur yettuwelleh ara akken iwata + + Iminig-ik yeḥbes araǧu n tririt n usmel. Asmel iwehha tuttra s tarrayt ara tt-yeǧǧen ur tettaweḍ ara.

    +
      +
    • Ahat tsenseḍ neɣ tesweḥleḍ inagan n tuqqna i ilaqen deg usmel-agi?
    • +
    • Ma yella ugur ur yefri ara ticki tremdeḍ inagan n tuqqna, ahat ugur ad yili deg twila n uqeddac mačči deg uselkim-ik.
    • +
    + ]]>
    + + + Askar war tuqqna + + + Iminig iteddu deg uskar war tuqqna ihi ur yezmir ara ad yeqqen ɣer tansa i d-yettwammlen.

    +
      +
    • Aselkim-ik iqqen ɣer uẓeṭṭa urmid?
    • +
    • Sit ɣef "Ɛreḍ tikkelt-nniḍen" akken ad tuɣaleḍ ɣer uskar usrid sakin sali-d asebter-inek tikkelt-nniḍen.
    • +
    + ]]>
    + + + Tabburt tezzem ɣef sebba n tɣellist + + + Tansa d-ittusutren temmal-d tabburt (amedya, mozilla.org:80 i tebburt 80 ɣef mozilla.org) i yettuseqdacen degumnaḍ-nniḍen war tunigin deg web. Iminig isefsex tuttra n ummesten-ik d tɣellist-ik.

    + ]]>
    + + + Tuqqna tettuwennez + + + Asiweḍ n telɣut ɣer uẓeṭṭa yeḥbes deg uɛraḍ n tuqqna. Ma ulac aɣilif ɛreḍ tikkelt-nniḍen.

    +
      +
    • Asmel yezmer yeḥbes kra n wakud neɣ ahat iɛebba taɛekkemt. Ɛreḍ tikkelt-nniden ticki.
    • +
    • Ma yella ur tezmireḍ ara ad tinigeḍ deg yismal meṛṛa, sefqed tuqqna-ik n yisefka neɣ Wi-Fi n yibenk-ik.
    • +
    + ]]>
    + + + Tawsit n ufaylu mačči d taɣelsant + + + +
  • Ma ulac aɣilif, nermes imawlan n usmel web ɣef wugur-agi.
  • + + ]]>
    + + + Tuccḍa d yekkan seg ugbur yerẓen + + + Asebter-agi i tebɣiḍ ad twaliḍ ur yizmir ara ad ittwasken, acku tella tuccḍa di tuzzna n isefka.

    +
      +
    • Ma ulac aɣilif nermes imawlan n usmel web ɣef ugur-agi.
    • +
    + ]]>
    + + + Yeɣli ugbur + + Asebter-agi i tebɣiḍ ad twaliḍ ur yizmir ara ad ittwasken, acku tella tuccḍa di tuzzna n isefka.

    +
      +
    • Ma ulac aɣilif nermes imawlan n usmel web ɣef ugur-agi.
    • +
    + ]]>
    + + + Tuccḍa n usettengel n ugbur + + Asebter-agi i tebɣiḍ ad twaliḍ ur yizmir ara ad ittwasken, acku iseqdac tawsit usekkussem tarameɣtut neɣ ur yettusefraken ara.

    +
      +
    • Ma ulac aɣilif nermes imawlan n usmel web ɣef ugur-agi.
    • +
    + ]]>
    + + + Ulac tansa + + + Iminig ur izmir ara ad d-yaf aqeddac asenneftaɣ i tansa i d-ttunefken.

    +
      +
    • Senqed tansa ɣef tuccḍiwin n tira am + ww.amedya.com deg umḍiq n + www.amedya.com.
    • +
    • Ma tzemreḍ ad tessaliḍ yal asebter, wali isefka n yibenk-ik neɣ tuqqna Wi-Fi.
    • +
    + ]]>
    + + + Ulac tuqqna Internet + + Senqed tuqqna n uẓeṭṭa-ik neɣ ɛreḍ asali n usebter deg kra n wakud. + + Smiren + + + Tansa tarameɣtut + 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.

    + ]]>
    + + Tansa-yagi d tarameɣtut + + + +
  • Tansiwin web ttwarunt am http://www.example.com/
  • +
  • Wali ma yella tseqdaceḍ amgal islacen (i.e. /).
  • + + ]]>
    + + + Aneggaf arussin + + Tansa temmaled aneggaf (amedya wxyz://) arussin n iminig, ihi iminig ur yezmir ara ad iqqen ɣer usmel.

    +
      +
    • Tettaɛraḍeḍ ad tkecmeḍ ɣer tanfiwin n wallalen n teywalt neɣ araḍris? Senqed asmel ma yesra ayen nniḍen.
    • +
    • Kra n ineggafen sran aseɣẓan neɣ azegrir wis kraḍ send ad ten-yissin iminig.
    • +
    + ]]>
    + + + Ulac afaylu + + +
  • Ahat afaylu ittusnifel, ittwakkes neɣ ittusenkez?
  • +
  • Llant tuccḍiwin n teɣdira, isekkilen imeqqṛanen, neɣ tuccḍiwin nniḍen?
  • +
  • Ɣur-k tasiregt ara iqadden ɣef ufaylu agi?
  • + + ]]>
    + + + Anekcum ɣer ufaylu yegdel + + +
  • Ahat yettwakkes, yettusenkez, neɣ tisirag uggint anekcum.
  • + + ]]>
    + + + Tuqqna yugi-tt uqeddac apṛuksi + + Iminig-inek ittuswel akken ad iseqdec aqeddac apṛuksi, acu kan apṛuksi yuggi tuqqna.

    +
      +
    • Tawila n uminig inek d tameɣtut? Senqed iɣewwaṛen u ɛreḍ tikelt nniḍen.
    • +
    • Aseɣẓan agrawan isirig tuqqniwin seg uẓeṭṭa-yagi?
    • +
    • Zgan ɣur-k wuguren? Nermes anedbal-ik n unagraw neɣ aseǧǧaw-ik n Internet i tallalt.
    • +
    +]]>
    + + + Ulac aqeddac apṛuksi + + Iminig-inek ittuswel akken ad iseqdec aqeddac apṛuksi, acukan apṛksi ulac-it.

    +
      +
    • Tawila n uminig-inek d tameɣtut? Senqed iɣewwaṛen sakin ɛreḍ tikkelt-nniḍen.
    • +
    • Aselkim-inek iqqen ɣer uẓeṭṭa urmid?
    • +
    • Zgan ɣur-k wuguren? Nermes anedbal-ik n unagraw neɣ aseǧǧaw-ik n Internet i tallelt.
    • +
    + ]]>
    + + + Ugur n yir asmel + + + 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.

    + ]]>
    + + + Ugur n usmel ur nerǧi ara + + + 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.

    + ]]>
    + + + Ugur n usmel aqesḥan + + + Asmel web %1$s ittwammel d akken d asmel diri-t i teγlist. Ihi ittusewḥel akken llan ismenyifen-ik n tɣellist.

    +]]>
    + + + Ugur n usmel n ukellex + + Asmel web illan deg tansa %1$s ittwammel d akken d asmel n ukellex. Ihi ittusewḥel akken llan ismenyifen-inek n tɣellist.

    + ]]>
    + + + Ulac taɣellist n usmel + + %1$s.]]> + + Kemmel ɣer usmel HTTP +
    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 @@ + + + + + Қайталап көру + + + Сұранымды аяқтау мүмкін емес + + + Бұл қате жөнінде қосымша ақпарат қазір қолжетерсіз.

    ]]>
    + + + Қорғалған байланысты орнату сәтсіз аяқталды + + +
  • Сіз сұраған парақ көрсетілмейді, өйткені алынған мәліметтер шынайылығын тексеру мүмкін емес.
  • +
  • Сайт иесіне осы мәселе жөнінде хабарлаңыз.
  • + ]]>
    + + + Қорғалған байланысты орнату сәтсіз аяқталды + + + +
  • Бұл сервердің қатесі болуы мүмкін, немесе біреу сізге керек серверді басқасымен ауыстырғысы келеді.
  • +
  • Осыған дейін осы серверге сәтті қосылған болсаңыз, осы қате уақытша болуы мүмкін. Біраздан кейін қайталап көріңіз.
  • + ]]>
    + + + Кеңейтілген… + + Біреу бұл сайтты еліктеу талабын жасап жатқаны мүмкін, сондықтан жалғастырмауыңыз керек. +

    + ]]>
    + + Артқа оралу (ұсынылады) + + Тәуекелді қабылдап, жалғастыру + + + Бұл веб-сайт қауіпсіз байланысты талап етеді. + + +
  • Сіз көргіңіз келетін бетті көрсету мүмкін емес, себебі бұл веб-сайт қауіпсіз байланысты талап етеді.
  • +
  • Мәселе веб-сайтта болуы мүмкін және оны шешу үшін ештеңе істей алмайсыз.
  • +
  • Веб-сайттың әкімшісіне мәселе туралы хабарлауға болады.
  • + + ]]>
    + + + Қосымша… + + + %1$s сайтының HTTP Strict Transport Security (HSTS) деп аталатын қауіпсіздік саясаты бар, бұл дегеніміз, %2$s оған тек қауіпсіз түрде байланыса алады. Бұл веб-сайт үшін ережеден тыс жағдайды қоса алмайсыз. + ]]> + + Артқа + + + Байланыс үзілген + + + Браузер сәтті байланысқан, бірақ, байланыс ақпаратты тасымалдау кезінде үзілген. Қайталап көріңіз.

    +
      +
    • Сайт уақытша қолжетімсіз немесе сұранымдарға толы шығар. Біраздан кейін қайталап көріңіз.
    • +
    • Егер бірде-бір парақ жүктелмесе, құрылғының деректер немесе Wi-Fi байланысын тексеріңіз.
    • +
    ]]>
    + + + Байланысты күту уақыты аяқталды + + + Көрсетілген сайтпен байланыс орнату сұранымына жауап бермеді және браузердің күту уақыты бітті.

    +
      +
    • Сайт сервері тым жүктелген немесе уақытша жұмыстан тыс болуы мүмкін бе? Біраз уақыт күтіп, қайталап көріңіз.
    • +
    • Егер сіз басқа да сайттарды аша алмасаңыз, компьютеріңіздің желімен байланысын тексеріңіз.
    • +
    • Егер құрылғыңыз желіаралық экран немесе прокси-сервермен қорғалса, олардың баптауларын тексеріңіз.
    • +
    • Егер басқа да мәселелер пайда болса, жүйелік администраторыңызға не Интернет-провайдеріңізге хабарласыңыз.
    • +
    ]]>
    + + + Байланысты орнату мүмкін емес + + +
  • Сайт уақытша қолжетімсіз, немесе сұранымдарға толы шығар. Кейінірек қайталап көріңіз.
  • +
  • Бірде-бір сайт ашылмаса, мобильді құрылғыңыздың деректер не Wi-Fi байланысын тексеріңіз.
  • + ]]>
    + + + Сервердің жауабы күтпеген түрде + + + Сайт сұранымға күтпеген түрде жауап берді, браузер өз жұмысын жалғастыра алмайды.

    ]]>
    + + + Парақтағы қайта бағдарлау дұрыс емес + + Браузер парақтың жүктелуін тоқтатты, өйткені сайт сұранымды ешқашан аяқталмайтындықтай бағдарлайтыны анықталды.

    +
      +
    • Бұл сайт сұраған cookies өшірген немесе бұғаттаған жоқсыз ба?
    • +
    • Егер сайттан cookies қабылдау мүмкіндігін қосу арқылы мәселе шешілмесе, онда бұл қате серверден кеткен шығар.
    • +
    ]]>
    + + + Дербес жұмыс режимі + + Браузер қазір дербес жұмыс істеу режимінде, сол үшін сұранған нәрсеге байланыс орната алмайды.

    +
      +
    • Бұл құрылғы белсенді желіге қосулы тұр ма?
    • +
    • Онлайн режиміне ауысып, парақты жаңарту үшін, "Қайталап көру" басыңыз.
    • +
    ]]>
    + + + Порт қауіпсіздік мақсатында шектелген + + Сұранған байланыс үшін көрсетілген порт (мысалы: mozilla.org:80 бұл mozilla.org сайтындағы 80 порт) әдетте веб-сайттармен байланысу үшін қолданылмайды. Қауіпсіздік мақсатында браузер бұл байланысты үзді.

    ]]>
    + + + Байланыс үзілген + + + Байланысты орнату кезінде желілік байланыс үзілді. Қайталап көріңіз.

    +
      +
    • Сайт уақытша қолжетімсіз немесе сұранымдарға толы шығар. Біраздан кейін қайталап көріңіз.
    • +
    • Егер бірде-бір парақ жүктелмесе, құрылғының деректер немесе Wi-Fi байланысын тексеріңіз.
    • +
    ]]>
    + + + Қауіпсіз емес файл түрі + + + +
  • Веб сайт иелеріне осы мәселе жөнінде хабарлаңыз.
  • + ]]>
    + + + Зақымдалған құрама қатесі + + + Сіз қараймын деген парақ көрсетілмейді, өйткені мәліметтер тасымалданған кезде қате анықталды.

    +
      +
    • Веб сайт иелеріне осы мәселе жөнінде хабарлаңыз.
    • +
    ]]>
    + + + Құрамасы бұзылды + Сіз қараймын деген парақ көрсетілмейді, өйткені мәліметтер тасымалданған кезде қате анықталды.

    +
      +
    • Веб сайт иелеріне осы мәселе жөнінде хабарлаңыз.
    • +
    ]]>
    + + + Құраманы декодтау кезінде қате кетті + Сіз сұраған бет көрсетілмейді, өйткені оның сығуы қате немесе браузер оны қолдамайды.

    +
      +
    • Веб-сайттың иесімен осы мәселе жайында хабарласыңыз.
    • +
    ]]>
    + + + Адрес табылмады + + + Браузер көрсетілген адрес үшін хост атын таба алмады.

    +
      +
    • Адресті теру қателеріне тексеріңіз, мысалы + www.example.com орнына + ww.example.com.
    • +
    • Бірде-бір парақ жүктелмесе, құрылғының деректер немесе Wi-Fi байланысын тексеріңіз.
    • +
    ]]>
    + + + Интернетпен байланыс жоқ + + Желілік байланысты тексеріңіз немесе біраздан кейін бетті қайта жүктеп көріңіз. + + Қайта жүктеу + + + Адрес пішімі қате + + Енгізілген адрестің пішімі қате. Оның енгізілуін тексеріп, қайта көріңіз.

    ]]>
    + + Адрес пішімі қате + + +
  • Веб адрестері әдетте келесідей жазылады - http://www.example.com/
  • +
  • Қалыпты слэштер қолданатыңызға көз жеткізіңіз (мыс. /).
  • + ]]>
    + + + Белгісіз хаттама + + Көрсетілген адрес браузерге белгсіз хаттамадан (wxyz:// сияқты) басталады, сондықтан браузер сайтпен байланыс орната алмайды.

    +
      +
    • Егер сіз мультимедиа немесе басқа да мәтіндік емес сервистері бар сайтпен байланыс орнатсаңыз, сайттың бағдарламалық қамтамаға қойылатын талаптарын тексеріңіз.
    • +
    • Браузер кейбір хаттамаларды қолдана алу үшін сыртқы бағдарламалық қамтаманы немесе плагиндерді орнату керек.
    • +
    ]]>
    + + + Файл табылмады + + +
  • Ол нәрсенің аты ауысқан, ол өшірілген не орны ауысқан болуы мүмкін бе?
  • +
  • Адрес ішінде айтылу не жазылу қателері не басқа қателер жоқ па?
  • +
  • Сұранған нәрсеге сізде керек рұқсаттар бар ма?
  • + ]]>
    + + + Файлға қатынау құқығы жоқ + +
  • Ол өшірілген, жылжытылған немесе файл рұқсаттары қатынауға жол бермеуі мүмкін.
  • + ]]>
    + + + Прокси-сервер сұранымды үзген + + Браузер прокси-сервер қолдануға бапталған, бірақ прокси-сервер байланыс орнатуға рұқсат бермейді.

    +
      +
    • Браузердегі прокси-сервер баптаулары дұрыс па? Оларды тексеріп, қайталап көріңіз.
    • +
    • Прокси-сервер осы желіден байланыс орнатуға рұқсат бере ме?
    • +
    • Әлі де мәселелер бар ма? Жүйелік администраторыңызбен, не Интернет-провайдеріңізбен хабарласыңыз.
    • +
    ]]>
    + + + Прокси-сервер табылмады + + Браузер прокси-серверді қолдануға бапталған, бірақ прокси-серверді табу мүмкін емес.

    +
      +
    • Браузердің прокси-сервер баптаулары дұрыс па? Оларды тексеріп, қайталап көріңіз.
    • +
    • Құрылғы белсенді желіге байланысып тұр ма?
    • +
    • Әлі де мәселелер бар ма? Жүйелік администраторыңызбен, не Интернет-провайдеріңізбен хабарласыңыз.
    • +
    ]]>
    + + + Зиянкес бағдарлама сайты + + + %1$s сайты пайдаланушыларға шабуыл жасау үшін қолданылытыны туралы ақпарат бар, сондықтан қауіпсіздік баптауларыңызға сәйкес ол бұғатталды.

    ]]>
    + + + Зиянкес бағдарлама сайты + + %1$s сайты ұнамсыз бағдарламалық қамтаманы тарататыны туралы ақпарат бар, сондықтан қауіпсіздік баптауларыңызға сәйкес ол бұғатталды.

    ]]>
    + + + Зиянкес сайт мәселесі + + %1$s сайты зиянкес болу мүмкіншілігі туралы ақпарат бар, сондықтан қауіпсіздік баптауларыңызға сәйкес ол бұғатталды..

    ]]>
    + + + Жалған сайт мәселесі + + %1$s сайты жалған сайт екені туралы ақпарат бар, сондықтан қауіпсіздік баптауларыңызға сәйкес ол бұғатталды.

    ]]>
    + + + Қауіпсіз сайт қолжетімді емес + + %1$s адресінің HTTPS нұсқасы қолжетімсіз.]]> + + HTTP сайтына өту +
    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 @@ + + + + Dîsa biceribîne + + + Daxwaz nehate temamkirin + + Derbarê vê çewtiyê an jî pirsgirêkê de agahiya ekstra niha ne mewcûd e.

    + ]]>
    + + + Girêdana ewle pêk nehat + + + +
  • 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.
  • +
  • Ji kerema xwe bi xwediyên malperê re têkeve têkiliyê û hayê wan ji vê pirsgirêkê çêke.
  • + + ]]>
    + + + Girêdana ewle pêk nehat + + + +
  • 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.
  • +
  • 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î.
  • + + ]]>
    + + + Pêşketî… + + Dibe ku hinek pêşkêşkarê teqlîd dikin û divê tu dewam nekî. +

    + + ]]>
    + + Vegere (tê pêşniyarkirin) + + + Rîskê qebûl bike û bidomîne + + + Pêdiviya vê malperê bi peywendiyeke ewle heye. + + +
  • Malpera tu hewlê didî ku vekî nayê nîşandan, lewre pêdiviya wê bi peywendiyeke ewle heye..
  • +
  • Ev bi piranî têkildarî malperê ye û ji bo çareserkirinê tiştek ji destê te nayê.
  • +
  • Tu dikarî rêveberiya malperê derbarê problemê de agahdar bikî.
  • + + ]]>
    + + + Pêşketî… + + + %1$s rêgezeke ewlekariyê ya bi navê Ewlekariya Jidandî ya Guheztinê ya HTTPê (HSTS) heye û loma %2$s dikare tenê di rewşeke ewle de bi malperê re têkiliyê dayne. Tu nikarî zêdekariyan bidiyê û têkeviyê. + ]]> + + Vegere + + + Girêdan qut bû + + + Gerokê bi serfirazî girêdan çêkir, lê di şandina agahiyan de girêdan qut bû. Tika ye careke din biceribîne.

    +
      +
    • Dibe ku malper demekê ne berdest be yan mijûl be. Piştî çend xulekan dîsa biceribîne.
    • +
    • Heke tu nikarî ti rûpelan bar bikî, girêdana daneyî û Wi-Fi’ya cîhaza xwe kontrol bike.
    • +
    + ]]>
    + + + Girêdan derdem bû + + + Malpera ku hat xwestin bersiva daxwaza pêwendiye neda û gerokê jî li bendemayîna ji bo bersivê rawestand.

    +
      +
    • Gelo dibe ku ji pêşkêşkarê re gelek daxwaz hene yan jî qutbûneke demkî hebe.
    • +
    • Gelo tu nikarî malperên din jî vekî? Girêdana înternetê ya cîhazê xwe kontrol bike.
    • +
    • 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.
    • +
    • Hê jî pirsgirêk heye? Ji bo alîkariyê bi rêveberê xwe yê torê an jî peydakera înternetê re bişêwirin.
    • +
    + ]]>
    + + + Nehate girêdan + + + +
  • Dibe ku malper bi awayekî demkî ne berdest be yan jî pir mijûl be. Piştî çend xulekan dîsa biceribîne
  • +
  • Heke tu nikarî rûpelên din jî bar bikî, girêdana daneyî yan jî Wi-Fi’ya cîhaza xwe kontrol bike.
  • + + ]]>
    + + + Bersiva nebende ji serverê + + + Malperê bi awayekî nebende bersiva daxwaza torê da û gerok nikare berdewam bike.

    + ]]>
    + + + Rûpel bi awayekî rast nayê berhêlkirin + + + Gerokê dest ji hewla vegerandina hêmana daxwazkirî berda. Malper daxwazê bi rengekî ku ew ê ti carî temam nebe, dibersivîne

    +
      +
    • Te çerezên ku ji hêla vê malperê ve hatine xwestin neçalak an jî asteng kirine?
    • +
    • 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.
    • +
    + ]]>
    + + + Moda derhêl + + + Gerok di moda xwe ya negirêdayî de dixebite û nikare xwe bigihîne hêmana daxwazkirî.

    +
      +
    • Gelo amûr bi toreke çalak ve girêdayî ye?
    • +
    • Li ser "Dîsa Biceribîne"yê bitikîne ku derbasî moda girêdayî bibî û rûpelê bar bikî.
    • +
    + ]]>
    + + + Port ji ber sedemên ewlehiyê hatiye sînordarkirin + + + Navnîşana hatî xwestin porteke diyarkirî ye (mînak, mozilla.org:80 ji bo potra 80 li ser mozilla.orgê) ku di rewşa asayî de ji bilî gera webê ji bo armancên din tê bikaranîn. Gerokê daxwaz ji bo parastin û ewlekariya te betal kir..

    + ]]>
    + + + Girêdan hate resetkirin + + + Girêdana torê di dema bazariya girêdanê de qut bû. Tika ye dîsa biceribîne.

    +
      +
    • Dibe ku malper demkî nayê bikaranîn yan jî gelekî mijêl e. Piştî çend xulekan dîsa biceribîne.
    • +
    • Heke ti malperek venabe, girêdana înterneta cîhaza xwe yan jî girêdana Wi-Fiyê kontrol bike.
    • +
    + ]]>
    + + + Cureya dosyeyê ya neewle + + + +
  • Tika ye bi xwediyên malperê re peywendiyê dayne ku wan derbarê vê problemê de agahdar bikî.
  • + + ]]>
    + + + Çewtiya Naveroka Xerabe + + + Ji ber ku di guhestina daneyan de çewtiyek hatiye tespîtkirin, rûpela dixwazî vekî nayê nîşandan.

    +
      +
    • Tika ye bi xwediyên malperê re peywendiyê çêke û wan agahdar bike.
    • +
    + ]]>
    + + + Naverok têk çû + + Ji ber ku di guhestina daneyan de çewtiyek hatiye tespîtkirin, rûpela dixwazî vekî nayê nîşandan.

    +
      +
    • Tika ye bi xwediyên malperê re peywendiyê çêke û wan agahdar bike.
    • +
    + ]]>
    + + + Çewtiya kodkirina naverokê + + 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.

    +
      +
    • Tika ye bi xwediyên malperê re peywendiyê çêke û wan agahdar bike.
    • +
    + ]]>
    + + + Navnîşan nehate dîtin + + + Gerokê nekarî ji bo navnîşana hatî xwestin pêşkêşkata host peyda bike.

    +
      +
    • Navnîşanê ji bo şaşiyên nivîsandinê kontrol bike: wekî + ww.example.com ji dêvla + www.example.com.
    • +
    • Heke tu nkarî ti rûpelan bar bikî, înterneta amûra xwe yan jî girêdana Wi-Fiyê kontrol bike.
    • +
    + ]]>
    + + + Girêdana înternetê tune + + Girêdana înterneta xwe kontrol bike an jî bîstek din rûpelê ji nû ve bar bike. + + Ji nû ve bar bike + + + Navnîşana nederbasdar + 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.

    + ]]>
    + + Navnîşan ne derbasdar e + + + +
  • Malperên înternetê bi piranî wisa tên nivîsandin: http://www.example.com/
  • +
  • Bala xwe bidê ka te xêza paldayî nivîsandiye: /).
  • + + ]]>
    + + + Protokola Nenas + + Navnîşan protokolekê destnîşan dike (mînak, wxyz://) ku gerok wê nas nake, loma gerok nikare bi asayî bi malperê ve bê girîdan.

    +
      +
    • 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.
    • +
    • Dibe ku ji bo hin protokolan beriya gerok wan nas bike, nermalav yan jî pêvekên partiya sêyemîn bivên.
    • +
    + ]]>
    + + + Rûpel nehate dîtin + + +
  • Dibe ku pel hatibe guheztin, jêbirin yan jî navê wê hatibe guhertin?
  • +
  • Dibe ku navnîşan çewt hatibe nivîsandin?
  • +
  • Dibe ku ji bo xwe bigihînî vê hêmanê destûrs te tune be?
  • + + ]]>
    + + + Gihîna li dosyeyê hate redkirin + + +
  • Dibe ku hatibe jêbirin, ciyê wê hatibe guhertin yan jî ji ber destûrên pelê nikarî xwe bigihîniyê.
  • + + ]]>
    + + + Proxy Serverê girêdan red kir + + Gerok ji bo bikaranîna pêşkêşkara proksy hatiye eyarkirin, lê proksyê daxwaza têkiliyê red kir.

    +
      +
    • DIbe ku eyarên proksyê ne rast hatibin çêkirin? Eyaran kontrol bike û dîsa biceribîne.
    • +
    • DIbe ku ev proksy destûrê nade têkiliyên di ser wê torê re?
    • +
    • Heke hê jî problem hebin, serî li rêveberiya torê yan jî dabînkera xizmeta înternetê bide.
    • +
    + ]]>
    + + + Proxy server nehate dîtin + + Gerok li gorî bikarînana rajekara proxyê hatiye vesazkirin, lê rajekara proxyê nehat dîtin

    +
      +
    • Vesazkirina proxyê rast e? Sazkariyan kontrol bike û dîsa biceribîne.
    • +
    • Cîhaza te bi toreke çalak ve girêdayî ye?
    • +
    • 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.
    • +
    + ]]>
    + + + Pirsgirêka malpera nebaş (malware) + + + Malpera %1$sê wekî malpereke erîşkar hate ragihandin û ew li gorî tercîhên te yên ewlehiyê hate astengkirin.

    + ]]>
    + + + Pirsgirêka malpera nexwestî + + + Malpera %1$sê wekî nermalava ku nayê xwestin hate ragihandin û ew li gorî tercîhên te yên ewlehiyê hate astengkirin.

    + ]]>
    + + + Pirsgirêka malpera ziyandar + + + Malpera %1$sê wekî malpereke ziyandar hate ragihandin û li gorî tercîhên te yên ewlehiyê malper hate astengkirin.

    + ]]>
    + + + Pirsgirêka malpera xapîner + + + Malpera webê ya %1$sê wekî malpereke xapînok hate ragihandin û ev malper li gorî tercîhên te yên ewlehiyê hate astengkirin.

    + ]]>
    + + + Malpera ewle ne berdest e + + %1$sê ne berdest e.]]> + + Ji malpera HTTP dewam bike +
    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 @@ + + + + + ಮತ್ತೆ ಪ್ರಯತ್ನಿಸು + + + ಮನವಿಯನ್ನು ಪೂರ್ಣಗೊಳಿಸಲಾಗಿಲ್ಲ + + ಈ ತೊಂದರೆ ಅಥವ ದೋಷಕ್ಕಾಗಿನ ಹೆಚ್ಚುವರಿ ಮಾಹಿತಿ ಪ್ರಸ್ತುತ ಲಭ್ಯವಿಲ್ಲ.

    ]]>
    + + + ಸುರಕ್ಷಿತ ಸಂಪರ್ಕವು ವಿಫಲಗೊಂಡಿದೆ + + +
  • ನೀವು ನೋಡಲು ಪ್ರಯತ್ನಿಸುತ್ತಿರುವ ಪುಟವನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ ಪಡೆಯಲಾದ ಮಾಹಿತಿಯ ವಿಶ್ವಾಸಾರ್ಹತೆಯನ್ನು ಪರಿಶೀಲಿಸಲಾಗಿಲ್ಲ.
  • +
  • ದಯವಿಟ್ಟು ಈ ತೊಂದರೆಯ ಬಗ್ಗೆ ಜಾಲತಾಣದ ಮಾಲಿಕರಿಗೆ ತಿಳಿಸಿ.
  • + ]]>
    + + + ಸುರಕ್ಷಿತ ಸಂಪರ್ಕವು ವಿಫಲಗೊಂಡಿದೆ + + + +
  • ಇದು ಸರ್ವರ್ ಸಂರಚನೆಯಲ್ಲಿನ ಒಂದು ತೊಂದರೆ ಇರಬಹುದು, ಅಥವ ಯಾರಾದರೂ ಸರ್ವರ್‍‍ನಂತೆ ವರ್ತಿಸಲು ಪ್ರಯತ್ನಿಸುತ್ತಿರಬಹುದು.
  • +
  • ಈ ಮೊದಲು ನೀವು ಸರ್ವರ್‍‍ಗೆ ಯಶಸ್ವಿಯಾಗಿ ಸಂಪರ್ಕ ಹೊಂದಲು ಸಾಧ್ಯವಾಗಿದ್ದಲ್ಲಿ, ಈಗಿನ ದೋಷವು ತಾತ್ಕಾಲಿಕವಾಗಿರಬಹುದು, ಹಾಗು ನೀವು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.
  • + ]]>
    + + + ಮುಂದುವರೆದ… + + ಹಿಂತಿರುಗಿ (ಶಿಫಾರಸು ಮಾಡಲಾಗಿದೆ) + + + ಅಪಾಯವನ್ನು ಸ್ವೀಕರಿಸಿ ಮತ್ತು ಮುಂದುವರಿಸಿ + + + ಸಂಪರ್ಕಕ್ಕೆ ಅಡಚಣೆಯಾಗಿದೆ + + + ಸಂಪರ್ಕದ ಕಾಲಾವಧಿ ಮುಗಿದಿದೆ + + + ಮನವಿ ಸಲ್ಲಿಸಲಾದ ತಾಣವು ಒಂದು ಸಂಪರ್ಕ ಮನವಿಗೆ ಪ್ರತಿಸ್ಪಂದಿಸುತ್ತಿಲ್ಲ ಹಾಗು ವೀಕ್ಷಕವು ಒಂದು ಪ್ರತ್ಯುತ್ತರಕ್ಕೆ ಕಾಯುವುದನ್ನು ನಿಲ್ಲಿಸಿದೆ.

    +
      +
    • ಪರಿಚಾರಕವು ಅತಿಯಾದ ಬೇಡಿಕೆಗೆ ಒಳಪಟ್ಟಿರಬಹುದೆ ಅಥವ ಒಂದು ತಾತ್ಕಾಲಿಕ ಸಂಪರ್ಕ ಕಡಿತಕ್ಕೆ ಒಳಗಾಗಿರಬಹುದೆ? ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.
    • +
    • ನೀವು ಬೇರೆ ತಾಣಗಳನ್ನು ವೀಕ್ಷಿಸಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲವೆ? ಗಣಕದ ಜಾಲ ಸಂಪರ್ಕವನ್ನು ಪರೀಕ್ಷಿಸಿ.
    • +
    • ನಿಮ್ಮ ಗಣಕ ಅಥವ ಜಾಲ ಸಂಪರ್ಕವು ಫೈರ್ವಾಲ್ ಅಥವ ಪ್ರಾಕ್ಸಿಯಿಂದ ಸಂರಕ್ಷಿತಗೊಂಡಿದೆಯೆ? ಸರಿಯಲ್ಲದ ಸಿದ್ಧತೆಗಳು ಜಾಲ ವೀಕ್ಷಣೆಯಲ್ಲಿ ಹಸ್ತಕ್ಷೇಪ ಮಾಡಬಹುದು.
    • +
    • ಇನ್ನೂ ಸಹ ತೊಂದರೆ ಇದೆಯೆ?ನೆರವಿಗಾಗಿ ನಿಮ್ಮ ಗಣಕ ವ್ಯವಸ್ಥಾಪಕರನ್ನು ಅಥವ ಜಾಲ ಸಂಪರ್ಕ ಒದಗಿಸುವವರನ್ನು ಸಂಪರ್ಕಿಸಿ
    ]]>
    + + + ಸಂಪರ್ಕ ಹೊಂದಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ + + + +
  • ಈ ತಾಣವು ತಾತ್ಕಾಲಿಕವಾಗಿ ಲಭ್ಯವಿಲ್ಲದಿರಬಹುದು ಅಥವಾ ಕಾರ್ಯದ ಒತ್ತಡದಲ್ಲಿರಬಹುದು. ಕೆಲವು ಕ್ಷಣಗಳ ನಂತರ ಮತ್ತೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸಿ.
  • +
  • ಯಾವುದೇ ಪುಟವೂ ತೆರೆಯದಿದ್ದಲ್ಲಿ, ನಿಮ್ಮ ಮೊಬೈಲಿನ ಡೇಟಾ ಅಥವಾ ವೈ-ಫೈ ಪರಿಶೀಲಿಸಿ.
  • + ]]>
    + + + ಪರಿಚಾರಕದಿಂದ ಅನಿರೀಕ್ಷಿತ ಪ್ರತಿಕ್ರಿಯೆ + + + ತಾಣವು ಒಂದು ಅನಿರೀಕ್ಷಿತ ರೀತಿಯಲ್ಲಿ ಜಾಲ ಮನವಿಗಳಿಗೆ ಪ್ರತ್ಯುತ್ತರಿಸಿದೆ ಹಾಗು ವೀಕ್ಷಕವು ಮುಂದುವರೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ.

    ]]>
    + + + ಪುಟವು ಸರಿಯಾಗಿ ಮರಳಿ ನಿರ್ದೇಶನಗೊಳ್ಳುತ್ತಿಲ್ಲ + + + ವೀಕ್ಷಕವು ಮನವಿ ಸಲ್ಲಿಸಿದ ವಿಷಯವನ್ನು ಮರಳಿ ಪಡೆಯುವುದನ್ನು ನಿಲ್ಲಿಸಿದೆ. ತಾಣವು ಎಂದೆಂದಿಗೂ ಮುಗಿಯದ ರೀತಿಯಲ್ಲಿ ಮನವಿಯನ್ನು ಮರಳಿ ನಿರ್ದೇಶಿಸುತ್ತಿದೆ.

    +
      +
    • ಈ ತಾಣಕ್ಕೆ ಅಗತ್ಯವಿರುವ ಕುಕಿಗಳನ್ನು ನೀವು ಅಶಕ್ತ ಅಥವ ನಿರ್ಬಂಧಿಸಿದ್ದೀರೆ?
    • +
    • ಈ ತಾಣದ ಕುಕಿಗಳನ್ನು ಅನುಮತಿಸಿದರೂ ಸಹ ತೊಂದರೆ ಪರಿಹಾರವಾಗದಿದ್ದರೆ, ಅದು ಒಂದು ಪರಿಚಾರಕದ ಸಂರಚನೆಗೆ ಸಂಬಂಧಿತ ವಿಷಯವಾಗಿದ್ದು ನಿಮ್ಮ ಗಣಕಕ್ಕೆ ಸಂಬಂಧ ಪಟ್ಟಿದ್ದಲ್ಲಎಂದರ್ಥ.
    • +
    ]]>
    + + + ಆಫ್‍ಲೈನ್ ವಿಧಾನ + + + ಜಾಲವೀಕ್ಷಕವು ತನ್ನ ಆಫ್‌ಲೈನ್ ಕ್ರಮದಲ್ಲಿದೆ ಮತ್ತು ಮನವಿ ಮಾಡಲಾದ ಅಂಶದೊಂದಿಗೆ ಸಂಪರ್ಕವನ್ನು ಸಾಧಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ.

    +
      +
    • ಇದು ಗಣಕವು ಸಕ್ರಿಯ ಜಾಲಬಂಧದೊಂದಿಗೆ ಸಂಪರ್ಕತಗೊಂಡಿದೆಯೆ?
    • +
    • ಆನ್‌ಲೈನ್ ಕ್ರಮಕ್ಕೆ ಬದಲಾಯಿಸಿ ನಂತರ ಪುಟವನ್ನು ಮರಳಿ ಲೋಡ್ ಮಾಡಲು “ಇನ್ನೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸು” ಅನ್ನು ಒತ್ತಿ.
    • +
    ]]>
    + + + ಮನವಿ ಸಲ್ಲಿಸಲಾದ ವಿಳಾಸವು ಒಂದು ಸಂಪರ್ಕ ಸ್ಥಾನವನ್ನು (e.g. mozilla.org:80 ಪೋರ್ಟ್ 80 ಕ್ಕೆ mozilla.org ನಲ್ಲಿ) ಸೂಚಿಸಿದೆ ಸಾಮಾನ್ಯವಾಗಿ ಜಾಲ ವೀಕ್ಷಣೆಯಲ್ಲದೆ ಇತರೆ ಕಾರ್ಯಗಳಿಗೂ ಬಳಸಲ್ಪಡುತ್ತದೆ. ವೀಕ್ಷಕವು ನಿಮ್ಮ ಸುರಕ್ಷತೆ ಹಾಗು ಸಂರಕ್ಷಣೆಯ ಸಲುವಾಗಿ ಈ ಮನವಿಯನ್ನು ರದ್ದು ಮಾಡಿದೆ.

    ]]>
    + + + ಸಂಪರ್ಕವು ಮರಳಿ ಹೊಂದಿಸಲ್ಪಟ್ಟಿತು + + + ಅಸುರಕ್ಷಿತ ಕಡತದ ಬಗೆ + + + +
  • ದಯವಿಟ್ಟು ಈ ತೊಂದರೆಯನ್ನು ತಿಳಿಸಲು ಜಾಲತಾಣದ ಮಾಲಿಕರನ್ನು ಸಂಪರ್ಕಿಸಿ.
  • + ]]>
    + + + ವಿಷಯ ಹಾಳಾದ ದೋಷ + + + ನೀವು ನೋಡಲು ಬಯಸುವ ಪುಟವನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ ಮಾಹಿತಿ ವರ್ಗಾವಣೆಯಲ್ಲಿ ಒಂದು ದೋಷವು ಕಂಡು ಬಂದಿದೆ.

    +
      +
    • ಈ ತೊಂದರೆಯನ್ನು ವರದಿ ಮಾಡಲು ಜಾಲತಾಣದ ಮಾಲಿಕರನ್ನು ಸಂಪರ್ಕಿಸಿ.
    • +
    ]]>
    + + + ವಿಷಯ ಕ್ರ್ಯಾಶ್ ಆಗಿದೆ + + ನೀವು ನೋಡಲು ಬಯಸುವ ಪುಟವನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ ಮಾಹಿತಿ ವರ್ಗಾವಣೆಯಲ್ಲಿ ಒಂದು ದೋಷವು ಕಂಡು ಬಂದಿದೆ.

    +
      +
    • ಈ ತೊಂದರೆಯನ್ನು ವರದಿ ಮಾಡಲು ಜಾಲತಾಣದ ಮಾಲಿಕರನ್ನು ಸಂಪರ್ಕಿಸಿ.
    • +
    ]]>
    + + + ವಿಷಯದ ಎನ್ಕೋಡಿಂಗ್‌ ದೋಷ + + ನೀವು ನೋಡಲು ಬಯಸುತ್ತಿರುವ ಪುಟವನ್ನು ತೋರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ ಅದು ಅಮಾನ್ಯವಾದ ಅಥವ ಬೆಂಬಲವಿಲ್ಲದ ಬಗೆಯ ಒಂದು ಸಂಕುಚನ(ಕಂಪ್ರೆಶನ್) ಅನ್ನು ಬಳಸುತ್ತದೆ.

    +
      +
    • ದಯವಿಟ್ಟು ಈ ತೊಂದರೆಯನ್ನು ತಿಳಿಸಲು ಜಾಲತಾಣದ ಮಾಲಿಕರನ್ನು ಸಂಪರ್ಕಿಸಿ.
    • +
    ]]>
    + + + ವಿಳಾಸವು ಕಂಡುಬಂದಿಲ್ಲ + + + ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕ ಇಲ್ಲ + + + ನಿಮ್ಮ ನೆಟ್‌ವರ್ಕ್ ಸಂಪರ್ಕವನ್ನು ಪರಿಶೀಲಿಸಿ ಅಥವಾ ಕೆಲವು ಕ್ಷಣಗಳಲ್ಲಿ ಪುಟವನ್ನು ಮರುಲೋಡ್ ಮಾಡಲು ಪ್ರಯತ್ನಿಸಿ. + + ಪುನಃ ಲೋಡ್ ಮಾಡು + + + ಅಸಿಂಧುವಾದ ವಿಳಾಸ + ಒದಗಿಸಲಾದ ವಿಳಾಸವು ಒಂದು ಗುರುತಿಸಬಲ್ಲ ಮಾದರಿಯಲ್ಲಿ ಇಲ್ಲ. ದಯವಿಟ್ಟು ತಪ್ಪುಗಳಿಗಾಗಿ ಸ್ಥಳ ಪಟ್ಟಿಯಲ್ಲಿ ನೋಡಿ ನಂತರ ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.

    ]]>
    + + ವಿಳಾಸವು ಸಮಂಜಸವಾಗಿಲ್ಲ + + + +
  • ಜಾಲ ವಿಳಾಸವು ಸಾಮಾನ್ಯವಾಗಿ http://www.example.com/ ರೀತಿಯಲ್ಲಿ ಬರೆಯಲ್ಪಡುತ್ತದೆ
  • +
  • ಮುಮ್ಮುಖವಾಗಿರುವ ಅಡ್ಡಗೆರಗಳನ್ನು ನೀವು ಬಳಸಿದ್ದೀರ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ (ಉದಾ. /).
  • + ]]>
    + + + ಅಜ್ಞಾತ ಪ್ರೊಟೊಕಾಲ್ + + ವಿಳಾಸ ಸೂಚಿಸುವ ಒಂದು ಪ್ರೊಟೊಕಾಲನ್ನು (e.g. wxyz://) ವೀಕ್ಷಕವು ಗುರುತಿಸಲಾಗಿಲ್ಲ, ಆದ್ದರಿಂದ ವೀಕ್ಷಕವು ತಾಣಕ್ಕೆ ಸರಿಯಾಗಿ ಸಂಪರ್ಕ ಹೊಂದಲಾಗುತ್ತಿಲ್ಲ.

    +
      +
    • ನೀವು ಮಲ್ಟಿಮೀಡಿಯಾ ಅಥವ ಪಠ್ಯವಲ್ಲದ ಇತರೆ ಸೇವೆಗಳನ್ನು ನಿಲುಕಿಸಿಕೊಳ್ಳಲು ಪ್ರಯತ್ನಿಸುತ್ತಿದ್ದೀರಾ?? ತಾಣದ ಹೆಚ್ಚುವರಿ ಅಗತ್ಯತೆಗಳನ್ನು ಪರೀಕ್ಷಿಸಿ.
    • +
    • ಕೆಲವೊಂದು ಪ್ರೋಟೊಕಾಲ್‍ಗಳನ್ನು ವೀಕ್ಷಕವು ಗುರುತಿಸಲು ಕೆಲವೊಂದು ಮೂರನೆ ಪಕ್ಷದ(third party) ತಂತ್ರಾಂಶ ಅಥವ ಪ್ಲಗ್‍ಇನ್‍ಗಳ ಅಗತ್ಯವಿರುತ್ತದೆ.
    ]]>
    + + + ಕಡತವು ಕಂಡು ಬಂದಿಲ್ಲ + + +
  • ಅದರ ಹೆಸರು ಬದಲಾಯಿಸರಬಹುದು, ತೆಗೆದುಹಾಕಲ್ಪಟ್ಟಿರಬಹುದು, ಅಥವ ಸ್ಥಳಾಂತರಗೊಂಡಿರಬಹುದು
  • +
  • ವಿಳಾಸದಲ್ಲಿ ಕಾಗುಣಿತ, ಕ್ಯಾಪಿಟಲೈಸೇಶನ್, ಅಥವ ಬೆರಳಚ್ಚುದೋಷ ಇದ್ದಿರಬಹುದೆ?
  • +
  • ನೀವು ಮನವಿ ಸಲ್ಲಿಸಿದ ವಿಷಯವನ್ನು ನಿಲುಕಿಸಿಕೊಳ್ಳಲು ನಿಮಗೆ ಸಾಕಷ್ಟು ಅನುಮತಿ ಇದೆಯೆ?
  • + ]]>
    + + + ಕಡತ ಪ್ರವೇಶವನ್ನು ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ + + +
  • ಅದನ್ನು ತೆಗೆದುಹಾಕಿರಬಹುದು, ಜರುಗಿಸಿರಬಹುದು, ಅಥವಾ ಕಡತದ ಅನುಮತಿಗಳು ಪ್ರವೇಶವನ್ನು ತಡೆಹಿಡಿಯುತ್ತಿರವಬಹುದು.
  • + ]]>
    + + + ಪ್ರಾಕ್ಸಿ ಪರಿಚಾರಕವು ಸಂಪರ್ಕವನ್ನು ನಿರಾಕರಿಸಿದೆ + + ವೀಕ್ಷಕವು ಒಂದು ಪ್ರಾಕ್ಸಿ ಪರಿಚಾರಕವನ್ನು ಬಳಸಲು ಸಂರಚಿತಗೊಂಡಿದೆ, ಆದರೆ ಪ್ರಾಕ್ಸಿಯು ಒಂದು ಸಂಪರ್ಕವನ್ನು ನಿರಾಕರಿಸಿದೆ.

    +
      +
    • ವೀಕ್ಷಕದ ಪ್ರಾಕ್ಸಿ ಸಂರಚನೆಯು ಸರಿಯಾಗಿದೆಯೆ? ಸಂರಚನೆಯನ್ನು ಪರೀಕ್ಷಿಸಿ ಹಾಗು ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.
    • +
    • ಪ್ರಾಕ್ಸಿ ಸೇವೆಯು ಈ ಜಾಲದಿಂದ ಸಂಪರ್ಕವನ್ನು ಅನುಮತಿಸುತ್ತದೆಯೆ?
    • +
    • ಇನ್ನೂ ಸಹ ತೊಂದರೆ ಇದೆಯೆ?ನೆರವಿಗಾಗಿ ನಿಮ್ಮ ಗಣಕ ವ್ಯವಸ್ಥಾಪಕರನ್ನು ಅಥವ ಜಾಲ ಸಂಪರ್ಕ ಒದಗಿಸುವವರನ್ನು ಸಂಪರ್ಕಿಸಿ.
    • +
    ]]>
    + + + ಪ್ರಾಕ್ಸಿ ಪರಿಚಾರಕವು ಕಂಡು ಬಂದಿಲ್ಲ + + ವೀಕ್ಷಕವು ಒಂದು ಪ್ರಾಕ್ಸಿ ಪರಿಚಾರಕವನ್ನು ಬಳಸಲು ಸಂರಚಿತಗೊಂಡಿದೆ, ಆದರೆ ಪ್ರಾಕ್ಸಿಯು ಒಂದು ಸಂಪರ್ಕವನ್ನು ನಿರಾಕರಿಸಿದೆ.

    +
      +
    • ವೀಕ್ಷಕದ ಪ್ರಾಕ್ಸಿ ಸಂರಚನೆಯು ಸರಿಯಾಗಿದೆಯೆ? ಸಂರಚನೆಯನ್ನು ಪರೀಕ್ಷಿಸಿ ಹಾಗು ಮರಳಿ ಪ್ರಯತ್ನಿಸಿ.
    • +
    • ಪ್ರಾಕ್ಸಿ ಸೇವೆಯು ಈ ಜಾಲದಿಂದ ಸಂಪರ್ಕವನ್ನು ಅನುಮತಿಸುತ್ತದೆಯೆ?
    • +
    • ಇನ್ನೂ ಸಹ ತೊಂದರೆ ಇದೆಯೆ?ನೆರವಿಗಾಗಿ ನಿಮ್ಮ ಗಣಕ ವ್ಯವಸ್ಥಾಪಕರನ್ನು ಅಥವ ಜಾಲ ಸಂಪರ್ಕ ಒದಗಿಸುವವರನ್ನು ಸಂಪರ್ಕಿಸಿ.
    • +
    ]]>
    + + + ಮಾಲ್ವೇರ್ ಸೈಟ್ ಸಮಸ್ಯೆ + + + %1$s ಒಂದು ಧಾಳಿಕಾರಕ ತಾಣವೆಂದು ವರದಿ ಮಾಡಲ್ಪಟ್ಟಿದೆ ಹಾಗು ನಿಮ್ಮ ಸುರಕ್ಷತಾ ಆದ್ಯತೆಗಳಿಗನುಸಾರವಾಗಿಅದು ನಿರ್ಬಂಧಿಸಲ್ಪಟ್ಟಿದೆ.

    ]]>
    + + + ಅನಗತ್ಯ ಸೈಟ್ ಸಮಸ್ಯೆ + + + %1$s ಅನವಶ್ಯ ತಂತ್ರಾಂಶಗಳನ್ನು ಒದಗಿಸುವ ತಾಣವೆಂದು ವರದಿ ಮಾಡಲ್ಪಟ್ಟಿದೆ ಹಾಗು ನಿಮ್ಮ ಸುರಕ್ಷತಾ ಆದ್ಯತೆಗಳಿಗನುಸಾರವಾಗಿ ಅದು ನಿರ್ಬಂಧಿಸಲ್ಪಟ್ಟಿದೆ.

    ]]>
    + + + ಹಾನಿಕಾರಕ ಸೈಟ್ ಸಮಸ್ಯೆ + + + %1$s ಒಂದು ಧಾಳಿಕಾರಕ ತಾಣವೆಂದು ವರದಿ ಮಾಡಲ್ಪಟ್ಟಿದೆ ಹಾಗು ನಿಮ್ಮ ಸುರಕ್ಷತಾ ಆದ್ಯತೆಗಳಿಗನುಸಾರವಾಗಿಅದು ನಿರ್ಬಂಧಿಸಲ್ಪಟ್ಟಿದೆ.

    ]]>
    + + + ವಂಚಕ ತಾಣ ವರದಿ ಮಾಡಿ + + %1$s ಒಂದು ಧಾಳಿಕಾರಕ ತಾಣವೆಂದು ವರದಿ ಮಾಡಲ್ಪಟ್ಟಿದೆ ಹಾಗು ನಿಮ್ಮ ಸುರಕ್ಷತಾ ಆದ್ಯತೆಗಳಿಗನುಸಾರವಾಗಿಅದು ನಿರ್ಬಂಧಿಸಲ್ಪಟ್ಟಿದೆ.

    ]]>
    + +
    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 @@ + + + + + 다시 시도 + + + 요청을 완료할 수 없습니다 + + 현재 이 문제 또는 오류에 대한 추가적 정보가 없습니다.

    + ]]>
    + + + 보안 연결 실패 + + +
  • 현재 보시려는 페이지는 수신된 데이터의 진위를 확인할 수 없기 때문에 보여줄 수 없습니다.
  • +
  • 웹 사이트 소유자에게 이 문제를 알리십시오.
  • + ]]>
    + + + 보안 연결 실패 + + +
  • 이는 서버의 설정에 문제가 있거나, 누군가가 서버를 위장하려 할 때 발생할 수 있습니다.
  • +
  • 이전에는 서버에 연결할 때 아무 문제도 없었다면 일시적인 오류일 수 있으므로 나중에 다시 시도해보십시오.
  • + + ]]>
    + + + 고급… + + 누군가 사이트를 가장하려고 할 수 있으므로 계속해서는 안됩니다. +

    + + ]]>
    + + 뒤로 (권장) + + 위험을 감수하고 계속 + + + 이 웹사이트는 보안 연결이 필요합니다. + + +
  • 이 웹 사이트는 보안 연결이 필요하므로, 페이지를 표시할 수 없습니다.
  • +
  • 이러한 문제는 대부분 웹 사이트와 관련이 있고 사용자가 할 수 있는 일은 없습니다.
  • +
  • 웹 사이트의 관리자에게 문제에 대해 알려주실 수 있습니다.
  • + + ]]>
    + + + 고급… + + + %1$s 사이트는 HTTP Strict Transport Security (HSTS)라는 보안 정책을 가지고 있어서 %2$s가 보안 연결만 할 수 있습니다. 이 사이트를 방문하기 위해 예외를 추가 할 수 없습니다. + ]]> + + 뒤로 + + + 연결이 끊어짐 + + 브라우저가 성공적으로 연결되었지만 정보 전송 중에 연결이 끊어졌습니다. 다시 시도해보십시오.

    +
      +
    • 서버가 일시적으로 사용할 수 없거나 사용자가 너무 많은 상태일 수 있습니다. 잠시 후 다시 시도해보십시오.
    • +
    • 페이지가 전혀 로딩되지 않는다면 기기의 데이터 또는 와이파이 연결 상태를 확인해보십시오.
    • +
    + ]]>
    + + + 연결 시간 초과 + + 요청하신 사이트가 연결 요청에 응답하지 않아 브라우저가 응답 대기를 중단했습니다.

    +
      +
    • 서버의 사용량이 많거나, 일시적 가동 중지일 수 있습니다. 나중에 다시 시도해 보십시오.
    • +
    • 다른 사이트도 열리지 않습니까? 컴퓨터의 네트워크 연결을 점검해 보십시오.
    • +
    • 사용자의 컴퓨터 또는 네트워크가 방화벽 또는 프록시에 의해 보호되고 있습니까? 설정이 잘못되면 웹 탐색에 방해가 될 수 있습니다.
    • +
    • 여전히 문제가 있습니까? 네트워크 관리자 또는 인터넷 서비스 제공자에게 지원을 요청하세요.
    • +
    ]]>
    + + + 연결할 수 없음 + + +
  • 서버가 일시적으로 사용할 수 없거나 사용자가 너무 많은 상태일 수 있습니다. 잠시 후 다시 시도해보십시오.
  • +
  • 페이지가 전혀 로딩되지 않는다면 기기의 데이터 또는 와이파이 연결 상태를 확인해보십시오.
  • + + ]]>
    + + + 서버에서 예기치 않은 응답 + + 네트워크 요청에 대해 사이트가 예기치 않은 방식으로 반응했기 때문에 브라우저가 계속할 수 없습니다.

    + ]]>
    + + + 페이지 리다이렉션 오류 + + 브라우저가 요청하신 항목을 불러오려는 시도를 중단했습니다. 사이트가 해당 요청을 결코 끝날 수 없는 방식으로 리다이렉트하고 있습니다.

    +
      +
    • 이 사이트가 요구하는 쿠키를 비활성화하거나 차단했습니까?
    • +
    • 사이트의 쿠키를 승인해도 문제가 해결되지 않는다면 컴퓨터의 문제가 아니라 서버 설정 문제일 가능성이 큽니다.
    • +
    + ]]>
    + + + 오프라인 모드 + + 브라우저가 오프라인 모드로 작동하고 있고 요청하신 항목에 연결할 수 없습니다.

    +
      +
    • 활성화된 상태의 네트워크에 컴퓨터가 연결되어 있습니까?
    • +
    • ‘다시 시도하기’를 눌러 온라인 모드로 전환한 다음 페이지를 다시 로딩하십시오.
    • +
    + ]]>
    + + + 보안상 이유로 제한된 포트 + + 요청하신 주소는 일반적으로 웹 탐색 이외의 목적으로 사용되는 포트를 특정하고 있습니다(예: mozilla.org에서 포트 80에 대한 mozilla.org:80). 안전과 보안을 위해 브라우저가 해당 요청을 취소했습니다.

    ]]>
    + + + 연결이 초기화되었습니다 + + 연결 시도 중에 네트워크 연결이 끊어졌습니다. 다시 시도해보십시오.

    +
      +
    • 서버가 일시적으로 사용할 수 없거나 사용자가 너무 많은 상태일 수 있습니다. 잠시 후 다시 시도해보십시오.
    • +
    • 어느 페이지도 로딩되지 않는다면 기기의 데이터 또는 와이파이 연결 상태를 확인해보십시오.
    • +
    + ]]>
    + + + 안전하지 않은 파일 형식 + + +
  • 웹 사이트 소유자에게 이 문제를 알리십시오.
  • + ]]>
    + + + 손상된 콘텐츠 오류 + + 현재 보시려는 페이지는 데이터 전송 오류가 탐지되었기 때문에 보여줄 수 없습니다.

    +
      +
    • 웹 사이트 소유자에게 이 문제를 알리십시오.
    • +
    ]]>
    + + + 콘텐츠 충돌됨 + 현재 보시려는 페이지는 데이터 전송 오류가 탐지되었기 때문에 보여줄 수 없습니다.

    +
      +
    • 웹 사이트 소유자에게 이 문제를 알리십시오.
    • +
    ]]>
    + + + 콘텐츠 인코딩 오류 + 현재 보시려는 페이지는 유효하지 않거나 지원되지 않는 압축 형식을 사용하고 있기 때문에 보여드릴 수 없습니다.

    +
      +
    • 웹 사이트 소유자에게 이 문제를 알리십시오.
    • +
    ]]>
    + + + 서버를 찾을 수 없음 + + 브라우저가 제시된 주소에 대한 호스트 서버를 찾지 못했습니다.

    +
      +
    • www.example.com이 아니라 ww.example.com이라고 쓰는 등의 오타가 있지 않은지 확인해보십시오.
    • +
    • 어느 페이지도 로딩되지 않는다면 기기의 데이터 또는 와이파이 연결 상태를 확인해보십시오.
    • +
    + ]]>
    + + + 인터넷 연결 안 됨 + + + 네트워크 연결을 확인하시거나 잠시 후 페이지를 다시 로드하세요. + + 새로 고침 + + + 잘못된 주소 + 제시된 주소는 인정된 형식이 아닙니다. 주소 표시줄에 오류가 있지 않은지 확인한 다음 다시 시도해보십시오.

    ]]>
    + + + 유효하지 않은 주소입니다 + + +
  • 웹 주소는 일반적으로 다음과 같이 씁니다:http://www.example.com/
  • +
  • 반드시 슬래시(/)를 사용하십시오.
  • + + ]]>
    + + + 알 수 없는 프로토콜 + 이 주소는 브라우저가 인식하지 못하는 프로토콜(예: wxyz://)을 특정하고 있기 때문에 브라우저가 사이트와 연결할 수 없습니다.

    +
      +
    • 멀티미디어 또는 기타 비문자 서비스에 접근하려는 것입니까? 사이트에서 추가적 요건을 확인해보십시오.
    • +
    • 어떤 프로토콜은 브라우저가 인식할 수 없는 제3자 소프트웨어 또는 플러그인을 요할 수 있습니다.
    • +
    + + ]]>
    + + + 파일을 찾을 수 없음 + +
  • 해당 항목이 명칭이 바뀌거나, 제거되거나, 이동되지 않았습니까?
  • +
  • 주소에 철자, 대소문자 구분 또는 기타 오타가 있지 않습니까?
  • +
  • 요청하신 항목에 대한 충분한 접근권을 가지고 있습니까?
  • + + ]]>
    + + + 파일 접근이 거부되었습니다 + +
  • 파일이 제거 또는 이동되었거나, 파일 권한 문제로 접근이 금지될 수 있습니다.
  • + + ]]>
    + + + 프록시 서버가 연결을 거부했습니다 + 브라우저가 프록시 서버를 사용하도록 설정되어 있지만 프록시가 연결을 거부했습니다.

    +
      +
    • 브라우저의 프록시 설정이 올바르게 되어 있습니까? 설정을 확인한 다음 다시 시도해보십시오.
    • +
    • 프록시 서비스가 이 네트워크를 통한 연결을 허용합니까?
    • +
    • 여전히 문제가 있습니까? 네트워크 관리자 또는 인터넷 서비스 제공자에게 지원을 요청하십시오.
    • +
    + ]]>
    + + + 프록시 서버를 찾을 수 없습니다 + 브라우저가 프록시 서버를 사용하도록 설정되어 있지만 프록시를 찾을 수 없습니다.

    +
      +
    • 브라우저의 프록시 설정이 올바르게 되어 있습니까? 설정을 확인한 다음 다시 시도해보십시오.
    • +
    • 컴퓨터가 활성화된 네트워크에 연결되어 있습니까?
    • +
    • 여전히 문제가 있습니까? 네트워크 관리자 또는 인터넷 서비스 제공자에게 지원을 요청하십시오.
    • +
    + ]]>
    + + + 악성 코드 사이트 문제 + + %1$s의 사이트는 공격 사이트로 보고되었으며, 사용자의 보안 설정에 따라 차단되었습니다.

    + ]]>
    + + + 원하지 않은 사이트 문제 + + %1$s의 사이트는 원하지 않은 소프트웨어를 제공하는 것으로 보고되었으며, 사용자의 보안 설정에 따라 차단되었습니다.

    + ]]>
    + + + 유해 사이트 문제 + + %1$s의 사이트는 잠재적 유해 사이트로 보고되었으며, 사용자의 보안 설정에 따라 차단되었습니다.

    + ]]>
    + + + 사기 사이트 문제 + + %1$s의 이 웹페이지는 사기 사이트로 보고되었으며, 사용자의 보안 설정에 따라 차단되었습니다.

    + ]]>
    + + + 보안 사이트를 사용할 수 없음 + + %1$s의 HTTPS 버전을 사용할 수 없습니다.]]> + + HTTP 사이트로 계속 +
    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 @@ + + + + Assaya arta + + + Ny Yllir Kowlwul Govyn + + Nyns yw kavadow kedhlow ynwedhek war an kudyn po gwall ma.

    + ]]>
    + + + Fyllis Junyans Servyer + + +
  • Ny yllir diskwedhes dhis an folen may fynn\'ta drehedhes drefen na allas gwirhe gwiryonder an deur resevys.
  • +
  • Mar pleg kestav orth perghenogyon an wiasva dhe dherivas orta an kudyn ma.
  • + + ]]>
    + + + Junyans Diogel a Fyllis + + + +
  • Martesen yth yw hemma kudyn gans kefurvyans an servyer, po y hyll bos nebonan owth assaya omwul an servyer.
  • +
  • Mar junsys yn sewen orth an servyer ma kyns, y hyll bos anparghus an gwall ha ty a yll assaya arta diwettha.
  • + + ]]>
    + + + Avonsys… + + Possybyl yw bos nebonan owth assaya omwul an wiva ha gwell vydh sevel orth pesya. +

    + + ]]>
    + + Mos War-gamm (Avisys) + + Degemeres an Peryl ha Pesya + + + Res yw junyans diogel dhe\'n wiasva ma. + + +
  • Ny yllir diskwedhes an folen mayth esowgh owth assaya mires drefen bos res junyans diogel dhe\'n wiasva ma.
  • +
  • Dres lycklod yma an kudyn gans an wiasva, ha ny yllydh gul travyth dh\'y ewna.
  • +
  • Ty a yll gwarnya menystrer an wiasva a\'n kudyn.
  • + + ]]>
    + + + Avonsys… + + + %1$s HTTP Strict Transport Security (HSTS) y hanow, ha rag henna ny yll %2$s mes junya orti yn tiogel. Ny yll\'ta keworra namm dhe vysytya orth an wiasva ma. + ]]> + + + Mos War-gamm + + + Goderrys veu an junyans + + An beurell a junyas yn sewen, mes goderrys veu an junyans hag ow treuskorra kedhlow. Mar pleg assay arta.

    +
      +
    • Possybyl yw bos ankavadow yn anparghus po re vysi.Assay arta yn tro.
    • +
    • Mar ny yll\'ta karga py folennow pynag, check kedhlow po junyans Diwi dha dhevis.
    • +
    + ]]>
    + + + Termyn an junyans re dhiwedhas + + Ny worthebis an wiasva a vynnsys orth govyn junyans hag an beurell a worfennas gortos gorthyp.

    +
      +
    • A alsa bos an servyer yn-dann veur dhemond po torr servis? Assay arta diwettha.
    • +
    • A ny yll\'ta peuri gwiasvaow erel? Check junyans rosweyth an devis.
    • +
    • Yw difresys dha rosweyth gans tanfos po kanasek? Y hyll dewisyow kamm mellya orth peuri an Wi.
    • +
    • Trobel hwath? Kussul orth menystrer dha rosweyth po dha brovier kesrosweyth rag gweres.
    • +
    + ]]>
    + + + Ny allas junya + + +
  • An wiasva a alsa bos ankavadow rag tro po re vysi. Assay arta wosa pols.
  • +
  • Mar ny yll\'ta karga folen vyth, check deur po junyans Diwi dha dhevis.
  • + + ]]>
    + + + Gorthyp anwaytys a\'n servyer + + An wiasva a worthebis orth an govyn rosweyth yn fordh anwaytys ha nyns yw possybyl an beurell dhe besya.

    + ]]>
    + + + Nyns usi an folen ow tasprennya yn ewn + + An beurell a hedhas assaya hwilas an daklen a veu bysys. Yma an wiva ow tasprennya an govyn ma na yll nevra bos kowlwrys.

    +
      +
    • A dhisweythressys po lettya an pastiow yw res dhe\'n wiva ma?
    • +
    • Mar ny weres degemeres pastiow an wiva owth ewna an kudyn, possybyl yw bos kudyn kefurvyans an servyer a-der dha dhevis tejy.
    • +
    + ]]>
    + + + Modh Dhywarlinen + +
    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 @@ + + + + + Preuva torna + + + No riescio a finî a domanda + + No gh\'é son atre informaçioin in sciô problema.

    ]]>
    + + + Conescion segua no riescia + + +
  • A pagina che ti veu vedde a no peu ese mostrâ perché no l\'é poscibile verificâ l’aotenticitæ di dæti riçevui.
  • +
  • Ciamma o responsabile do scito web pe informalo do problema.
  • + ]]>
    + + + Conescion segua no riescia + + + +
  • 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.
  • +
  • Se ti ê conesso sensa problemi a sto server into passou, aloa l\'erô o peu esê tenporaneo e ti peu provâ torna dòppo.
  • + ]]>
    + + + Avansæ… + + Porieva ese che quardun o preuva a fâ finta d\'ese o scito e sarieiva megio se no ti væ avanti. +

    + ]]>
    + + Vanni inderê (Racomandou) + + Acetta o reizego e vanni + + + Avansæ… + + + Vanni inderê + + + A conescion a l\'é stæta scancelâ + + + O navegatô o s\'é conesso ben, ma a conescion a l\'é è stæta interotta into trasferimento de informaçioin. Preuva torna.

    +
      +
    • O scito o porieiva ese tenporaneamente no disponibile ò tròppo indafarou. Preuva torna tra quarche momento.
    • +
    • Se no ti riesci a caregâ nisciunn-a pagina, contròlla i dæti do dispoxitivo ò a conescion Wi-Fi.
    • +
    ]]>
    + + + A conescion a gh\'à misso tròppo tenpo + + + O scito che t\'æ domandou o no risponde e o navegatô o no l\'aspetâ ciù a risposta.

    +
      +
    • Fòscia o server o gh\'à \'n\'erta domanda ò \'n problema tenporaneo? Preuva torna dòppo.
    • +
    • No ti riesci a navegâ inti atri sciti? Contròlla a conescion.
    • +
    • 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.
    • +
    • i gh\'æ torna di problemi? Ciamma o teu aministratô da ræ ò fornitô de serviççi internet pe ascistensa.
    • +
    ]]>
    + + + No riescio a conetime + + + +
  • O scito o porieiva ese tenporaneamente inacesibile ò tròppo traficou. Preuva torna tra quarche momento.
  • +
  • Se no ti riesci a caregâ nisciunn-a pagina, preuva a controlâ a conescion do teu computer.
  • + ]]>
    + + + Risposta sbaliâ da-o server + + + O scito risponde in \'n mòddo ch\'o no capiscio e-o navegatô o no peu continoâ.

    ]]>
    + + + A pagina no redireçionn-a ben + + O navegatô o no preuvâ ciù a riçeive l\'ògetto domandou. O scito redireçionn-a sensa fin.

    +
      +
    • T\'æ dizabilitou ò blocou i cookie domandæ da sto scito?
    • +
    • 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.
    • +
    ]]>
    + + + Feua linia + + + O navegatô o l\'é into mòddo feua linia e o no peu colegase a l\'ògetto çernuo.

    +
      +
    • O dispoxitivo o l\'é conesso a \'na ræ ativa?
    • +
    • Sciacca “Preuva torna” pe pasâ a-o mòddo in linia e caregâ torna a pagina.
    • +
    ]]>
    + + + Acesso a-a pòrta dizabilitou pe raxoin de seguessa + + + A pòrta domandâ (ez. mozilla.org:80 pa-a pòrta 80 in sciô mozilla.org) l\'é uzâ pe atri fin che no seggian a navegaçion. O navegatô l\'à scancelou a domanda pe a teu proteçion e seguessa.

    ]]>
    + + + Conescion scancelâ + + + O navegatô o s\'é conesso ben, ma a conescion a l\'é è stæta interotta into trasferimento de informaçioin. Preuva torna.

    +
      +
    • O scito o porieiva ese tenporaneamente no disponibile ò tròppo indafarou. Preuva torna tra quarche momento.
    • +
    • Se no ti riesci a caregâ nisciunn-a pagina, contròlla i dæti do dispoxitivo ò a conescion Wi-Fi.
    • +
    ]]>
    + + + Tipo de file no seguo + + +
  • Pe piaxei, contatta i padroin do scito pe faghe savei de questo problema.
  • + ]]>
    + + + Erô contegnuo andæto a mâ + + + A pagina che ti veu vedde a no peu ese mostrâ perché gh\'é \'n erô inta trasmiscion di dæti.

    +
      +
    • Pe piaxei, ciamma o responsabile do scito pe informalo do problema.
    • +
    ]]>
    + + + Contegnuo ciantou + + A pagina che ti veu vedde a no peu ese mostrâ perché gh\'é \'n erô inta trasmiscion di dæti.

    +
      +
    • Pe piaxei, ciamma o responsabile do scito pe informalo do problema.
    • +
    ]]>
    + + + Erô còdifica do contegnuo + + A pagina che t\'eu vedde a no se peu mostrâ perché a gh\'à \'na comprescion ch\'a no l\'é soportâ.

    +
      +
    • Pe piaxei, contatta i padroin do scito pe faghe savei de questo problema.
    • +
    ]]>
    + + + Indirisso no trovou + + + O navegatô o no riesce a trovâ o server host pe l\'indirisso fornio.

    +
      +
    • Contròlla che no ghe segge di eroî comme: + ww.ezenpio.com in cangio de + www.ezenpio.com.
    • +
    • Se ti no riesci a caregâ nisciunn-a pagina, contròla a conescion dæti o Wi-Fi do teu dispoxitivo.
    • +
    ]]>
    + + + Niscinn-a conescion Internet + + ontròlla a tue conescion ò preuva a caregâ a pagina fra quarche momento. + + Recarega + + + Indirisso no valido + 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.

    ]]>
    + + L\'indirisso o no l\'é valido + + + +
  • Generalmente i indirissi web en scriti coscì http://www.ezenpio.com/
  • +
  • Amia ben se tu deuvi e bare giuste (prezenpio /).
  • + ]]>
    + + + Protocòllo sconosciuo + + L\’indirisso o domanda \'n protocòllo (prez. wxyz://) ch\'o navegatô o no conosce, quindi o no peu colegase a-o scito.

    +
      +
    • Ti ê derê a acede a serviççi moltimediali ò no de testo? Verificâ in sciô scito i requixiti necesai
    • +
    • Quarche protocòllo o domanda software esterni ò plugin pe poei ese riconosciui da o navegatô.
    • +
    ]]>
    + + + File no trovou + + +
  • Peu miga ese che l\'ògetto o segge stæto rinominou, scancelou ò mesciou?
  • +
  • Gh\'é quarche erô de òrtografia inte l\'indirisso?
  • +
  • Ti ghe l\'æ abasta permissi pe acede a l\'ògetto?
  • + ]]>
    + + + Acesso a-o file negou + +
  • O peu ese stæto scancelou,mesciou, ò i permissi de acesso a-i file peuan proibine l\'acesso.
  • +]]>
    + + + Conescion refuâ do-u server proxy + + O navegatô o l\'é inpostou pe uzâ un server proxy ma o proxy o refua e conescioin.

    +
      +
    • E inpostaçioin proxy do navegatô en giuste? Contròlla e preuva torna.
    • +
    • O proxy permette e conescioin da sta ræ?
    • +
    • Ti gh\'æ torna di problemi? Ciamma o teu aministratô da ræ ò fornitô de serviççi internet pe ascistensa.
    • +
    ]]>
    + + + No treuvo o proxy + + O navegatô o l\'é inpostou pe uzâ un server proxy ma o proxy o refua e conescioin.

    +
      +
    • E inpostaçioin proxy do navegatô en giuste? Contròlla e preuva torna.
    • +
    • Ti ê conesso a \'na ræ ativa?
    • +
    • Ti gh\'æ torna di problemi? Ciamma o teu aministratô da ræ ò fornitô de serviççi internet pe ascistensa.
    • +
    ]]>
    + + + Problema de scito con malware + + 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.

    ]]>
    + + + Problema de scito no no deziderou + + + 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.

    ]]>
    + + + Scito pericolozo + + + 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.

    ]]>
    + + + Scito inganevole + + A pagina %1$s a l\'é segnalâ comme \'n scito inganevole a l\'é stæta blocâ da-e teu preferense de seguessa.

    ]]>
    + + + Verscion segua do scito no disponibile +
    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 @@ + + + + + ລອງ​ອີກ​ຄັ້ງ + + + ບໍ່ສາມາດຕອບສະຫນອງຕາມຄຳຮ້ອງຂໍໄດ້ + + + ຂໍ້ມູນເພີ່ມເຕີ່ມກ່ຽວກັບບັນຫານີ້ແມ່ນຫຍັງບໍ່ທັນມີເທື່ອໃນຕອນນີ້.

    + ]]>
    + + + ການເຊື່ອມຕໍ່ທີປອດໄພຫລົ້ມເຫລວ + + + +
  • ບໍ່ສາມາດສະແດງໜ້າທີ່ທ່ານກຳລັງພະຍາຍາມເຂົ້າໄປເບິ່ງໃຫ້ເຫັນໄດ້ເພາະວ່າຄວາມຖືກຕ້ອງຂອງຂໍ້ມູນທີ່ໄດ້ຮັບບໍ່ສາມາດຢັ້ງຢືນໄດ້.
  • +
  • ກະລຸນາຕິດຕໍ່ຫາເຈົ້າຂອງເວັບໄຊທເພື່ອແຈ້ງໃຫ້ພວກເຂົາຮູ້ກ່ຽວກັບບັນຫານີ້.
  • + + ]]>
    + + + ການເຊື່ອມຕໍ່ທີປອດໄພຫລົ້ມເຫລວ + + + +
  • ປັນຫານີ້ອາດຈະເກີດມາຈາກການຕັ້ງຄ່າຂອງເຊີບເວີ ຫລື ອາດຈະມີຄົນພະຍາຍາມປອມແປງເຊີບເວີ.
  • +
  • ຖ້າທ່ານຫາກເຄີຍເຊື່ອມຕໍ່ກັບເຊີບເວີນີ້ໄດ້ມາກ່ອນ, ຂໍ້ຜິດພາດນີ້ອາດຈະເກີດຂື້ນຊົ່ວຄາວ ແລະ ທ່ານສາມາດລອງໃໝ່ອີກຄັ້ງໃນພາຍຫຼັງ.
  • + + ]]>
    + + + ຂັ້ນສູງ… + + ມີບາງຄົນອາດຈະກຳລັງປອມແປງເວັບໄຊທ ແລະ ທ່ານບໍ່ຄວນຈະເຂົ້າໄປ. +

    + + ]]>
    + + ກັບຄືນ (ແນະນຳ) + + ຍອມຮັບຄວາມສ່ຽງ ແລະ ດຳເນີນການຕໍ່ + + + ເວັບໄຊທນີ້ຕ້ອງການການເຊື່ອມຕໍ່ທີ່ປອດໄພ. + + + +
  • ໜ້າ​ທີ່​ເຈົ້າ​ພະ​ຍາ​ຍາມ​ເບິ່ງ​ບໍ່​ສາ​ມາດ​ສະ​ແດງ​ໄດ້​ເນື່ອງ​ຈາກ​ວ່າ​ເວັບ​ໄຊ​ທ​໌​ນີ້​ຕ້ອງ​ການ​ການ​ເຊື່ອມ​ຕໍ່​ທີ່​ປອດ​ໄພ.
  • +
  • ບັນຫາແມ່ນເປັນໄປໄດ້ຫຼາຍທີ່ສຸດກັບເວັບໄຊທ໌, ແລະບໍ່ມີຫຍັງທີ່ເຈົ້າສາມາດແກ້ໄຂມັນໄດ້.
  • +
  • ທ່ານ​ສາ​ມາດ​ແຈ້ງ​ໃຫ້​ຜູ້​ຄວບ​ຄຸມ​ຂອງ​ເວັບ​ໄຊ​ທ​໌​ກ່ຽວ​ກັບ​ບັນ​ຫາ​ໄດ້.
  • + + ]]>
    + + + ຂັ້ນສູງ… + + + %1$s ມີນະໂຍບາຍຄວາມປອດໄພທີ່ເອີ້ນວ່າ HTTP Strict Transport Security (HSTS), ຊຶ່ງຫມາຍຄວາມວ່າ %2$s ສາມາດເຊື່ອມຕໍ່ກັບມັນໄດ້ຢ່າງປອດໄພເທົ່ານັ້ນ. ທ່ານບໍ່ສາມາດເພີ່ມຂໍ້ຍົກເວັ້ນເພື່ອເຂົ້າເບິ່ງເວັບໄຊນີ້ໄດ້. + ]]> + + ກັບຄືນ + + + ການເຊື່ອມຕໍ່ຖືກລົບກວນ + + + ບຣາວເຊີໄດ້ສຳເລັດການເຊື່ອຕໍ່ແລ້ວ, ແຕ່ວ່າການເຊື່ອມຕໍ່ໄດ້ຖືກຕັດລະຫວ່າງການສົ່ງຂໍ້ມູນ. ກະລຸນາລອງຫໃ່ອີກຄັ້ງຫນຶ່ງ.

    +
      +
    • ເວັບໄຊທອາດຈະໃຊ້ງານບໍ່ໄດ້ຊົ່ວຄາວ ຫລື ອາດຈະມີຄົນເຂົ້າຫລາຍເກີນໄປ. ລອງໃຫມ່ອີກຄັ້ງໃນອີກສອງສາມນາທີຂ້າງຫນ້າ.
    • +
    • ຖ້າຫາກວ່າທ່ານບໍ່ສາມາດໂຫລດຫນ້າເວັບໃດໆໄດ້ເລີຍໃຫ້ທ່ານກວດປະລິມານການນຳໃຊ້ຂໍ້ມູນ ຫລື ການເຊື່ອມຕໍ່ກັບ Wi-Fi ໃນອຸປະກອນຂອງທ່ານຄືນໃຫມ່.
    • +
    + ]]>
    + + + ການເຊື່ອມຕໍ່ໄດ້ຫມົດເວລາແລ້ວ + + + ເວັບໄຊທທີ່ທ່ານຮ້ອງຂໍບໍ່ສາມາດດຳເນີນການໄດ້ ແລະ ບຣາວເຊີໄດ້ຍຸດການລໍຖ້າຄຳຮ້ອງຂໍດັ່ງກ່າວແລ້ວ.

    +
      +
    • ອາດຈະເປັນນຳເຊີເວີອາດປະສົບກັບບັນຫາທີ່ມີຜູ້ເຂົ້ານຳໃຊ້ສຸງ ຫລື ໄຟຟ້າດັບຊົ່ວຄາວ. ໃຫ້ລອງເຂົ້າໃຫມ່ອີກຄັ້ງໃນພາຍຫລັງ.
    • +
    • ທ່ານສາມາດເຂົ້າເວັບໄຊທອື່ນໄດ້ບໍ່? ໃຫ້ກວດເບິງການເຊື່ອມຕໍ່ເນັດເວີກຂອງຄອມພິວເຕີຄືນ.
    • +
    • ເນັດເວີກຂອງທ່ານມີໄຟວໍ ຫລື ພັອກຊີຄັ້ນໄວ້ບໍ່? ການຕັ້ງຄ່າທີ່ຜິດພາດອາດຈະເຮັດໃຫ້ລົບກວນການເຂົ້າເວັບໄດ້.
    • +
    • ຫຍັງຄົງມີບັນຫາຢູ່ບໍ່? ໃຫ້ປຶກສາກັບຜູ້ຄຸ້ມຄອງລະບົບເນັດເວີກຂອງທ່ານ ຫລື ຜູ້ໃຫ້ບໍລິການອິນເຕີເນັດສຳລັບການຊ່ວຍເຫລືອ.
    • +
    + ]]>
    + + + ບໍ່​ສາ​ມາດ​ທີ່​ຈະ​ເຊື່ອມ​ຕໍ່ + + + +
  • ເວັບໄຊທອາດຈະໃຊ້ງານບໍ່ໄດ້ຊົ່ວຄາວ ຫລື ອາດຈະມີຄົນເຂົ້າຫລາຍເກີນໄປ. ລອງໃຫມ່ອີກຄັ້ງໃນອີກສອງສາມນາທີຂ້າງຫນ້າ.
  • +
  • ຖ້າຫາກວ່າທ່ານບໍ່ສາມາດໂຫລດຫນ້າເວັບໃດໆໄດ້ເລີຍໃຫ້ທ່ານກວດປະລິມານການນຳໃຊ້ຂໍ້ມູນ ຫລື ການເຊື່ອມຕໍ່ກັບ Wi-Fi ໃນອຸປະກອນຂອງທ່ານຄືນໃຫມ່.
  • + + ]]>
    + + + ການຕອບສະຫນອງທີ່ບໍ່ຄາດຄິດຈາກເຊີເວີ + + ເວັບໄຊທຕອບສະຫນອງຕໍຄຳຮ້ອງຂໍຂອງເນັດເວີກດ້ວຍວິທີທີ່ບໍຖືກຕ້ອງ ແລະ ບຣາວເຊີບໍ່ສາມາດເຂົ້າໄປຕໍໄດ້.

    + ]]>
    + + + ຫນ້າເວັບນີ້ມີການປ່ຽນເສັ້ນທາງທີ່ບໍ່ຖືກຕ້ອງ + + + ບຣາວເຊີໄດ້ຢຸດພະຍາຍາມໃນການຮ້ອງຂໍລາຍການ. ເວັບໄຊທໄດ້ປ່ຽນເສັ້ນທາງຄຳຮ້ອງຂໍໄປໃນທາງທີ່ບໍມີການສີ້ນສຸດ.

    +
      +
    • ທ່ານໄດ້ປິດ ຫລື ບັອກຄຸກກີ້ທີ່ຕ້ອງການໂດຍເວັບໄຊທນີ້ບໍ?
    • +
    • ຖ້າວ່າການຍອມຮັບຄຸກກີ້ຂອງເວັບໄຊທບໍ່ໄດ້ແກ້ໄຂບັນຫາ ມັນອາດຈະເປັນນຳບັນຫາໃນການຕັ້ງຄ່າເຊີເວີ ແລະ ບໍ່ໄດ້ເປັນນຳຄອມພິວເຕີຂອງທ່ານ.
    • +
    + ]]>
    + + + ໂໝດອັອບໄລ + + + ບຣາວເຊີກຳລັງເຮັດວຽກໃນໂຫມດອັອບລາຍ ແລະ ບໍ່ສາມາດເຊື່ອມຕໍ່ໄປຫາລາຍການທີ່ຮ້ອງຂໍໄດ້.

    +
      +
    • ຄອມພິວເຕີໄດ້ເຊື່ອມຕໍ່ກັບເນັດເວີກຢູ່ບໍ?
    • +
    • ກົດ “ລອງໃຫມ່ອີກຄັ້ງ” ເພື່ອປ່ຽນໄປໂຫມດອອນລາຍ ແລະ ໂຫລດຫນ້າໃຫມ່ອີກຄັ້ງ.
    • +
    + ]]>
    + + + ພອດຖືກຈຳກັດດ້ວຍເຫດຜົນດ້ານຄວາມປອດໄພ + + + ທີ່ຢູ່ນີ້ຮ້ອງຂໍພອດທີ່ລະບຸຊັດເຈນ (ເຊັ່ນ: mozilla.org:80 ສຳລັບພອດ 80 ໃນ mozilla.org) ປົກກະຕິແລ້ວໃຊ້ສຳລັບ ອື່ນໆ ຫຼາຍກ່ອນການທ່ອງເວັບ. ບຣາວເຊີໄດ້ຍົກເລີກຄຳຮ້ອງຂໍດັ່ງກ່າວເພື່ອປົກປ້ອງທ່ານ ແລະ ຄວາມປອດໄພ.

    + ]]>
    + + + ການເຊື່ອມຕໍ່ຖືກຕັດ + + + ເນັດເວີກລີ້ງໄດ້ຮັບການຂັດຈັງຫວະໃນຂະນະທີ່ແລກປ່ຽນຂໍ້ມູນການເຊື່ອມຕໍ. ກະລຸນາລອງໃຫມ່ອີກຄັ້ງ.

    +
      +
    • ເວັບໄຊທອາດຈະບໍ່ສາມາດໃຊ້ງານໄດ້ຊົ່ວຄາວ ຫລື ເຂົ້າບໍ່ໄດ້. ໃຫ້ລອງເຂົ້າໃຫມ່ໃນອີກ 2-3 ນາທີ.
    • +
    • ຖ້າຫາກວ່າທ່ານຍັງບໍສາມາດໂຫລດຫນ້າເວັບໄດ້ ໃຫ້ລອງກວດເບິງອິນເຕີເນັດ ຫລື ການເຊື່ອມຕໍ Wi-Fi ໃນອຸປະກອນຂອງທ່ານ.
    • +
    + ]]>
    + + + ປະເພດເອກະສານທີ່ບໍ່ປອດໄພ + + +
  • ກະລູນາຕິດຕໍ່ຫາເຈົ້າຂອງເວັບໄຊທເພື່ອແຈ້ງບັນຫານີ້ໃຫ້ພວກເຂົາຮູ້.
  • + ]]>
    + + + ຂໍ້ຜິດພາດເນື້ອຫາທີ່ເສີຍຫາຍ + + + ຫນ້າເວັບທີ່ທ່ານກຳລັງເຂົ້າໄປເບິງບໍ່ສາມາດສະແດງໄດ້ເພາະວ່າມັນເກີດມີຂໍ້ຜິດພາດໃນການສົ່ງຂໍ້ມູນ.

    +
      +
    • ກະລຸນາແຈ້ງບັນຫານີ້ໃຫ້ເຈົ້າຂອງເວັບໄຊທຮູ້.
    • +
    + ]]>
    + + + ເນື້ອຫາຂັດຂ້ອງ + + ບໍ່ສາມາດສະແດງຫນ້າເວັບທີ່ທ່ານກຳລັງພະຍາຍາມເຂົ້າໄປເບິງໄດ້ເນື່ອງຈາກກວດພົບຂໍ້ຜິດພາດໃນການສົ່ງຂໍ້ມູນ.

    +
      +
    • ກະລຸນາຕິດຕໍ່ຫາເຈົ້າຂອງເວັບໄຊທເພື່ອແຈ້ງບັນຫານີ້.
    • +
    ]]>
    + + + ຂໍ້ຜິດພາດການເຂົ້າລະຫັດເນື້ອຫາ + + ຫນ້າເວັບທີ່ທ່ານກຳລັງເຂົ້າໄປເບິງບໍ່ສາມາດສະແດງໄດ້ເພາະວ່າມັນໃຊ້ຮູບແບບການບີບອັດຂໍ້ມູນທີ່ບໍ່ຖືກຕ້ອງ ຫລື ບໍ່ໄດ້ຮັບການຊັບພອດ.

    +
      +
    • ກະລຸນາແຈ້ງບັນຫານີ້ໃຫ້ເຈົ້າຂອງເວັບໄຊທຮູ້.
    • +
    + ]]>
    + + + ບໍ່ພົບທີ່ຢູ່ + + + ບຣາວເຊີອາດຈະບໍ່ສາມາດຊອກຫາທີ່ຢູ່ຂອງໂຮສທີ່ລະບຸມາ.

    +
      +
    • ກວດຄືນເບິງວ່າຂຽນຊື່ທີ່ຢູ່ຖືກຕ້ອງແລ້ວບໍເຊັ່ນວ່າ: + ww.example.com ແທນທີ່ຈະເປັນ + www.example.com.
    • +
    • ຖ້າຫາກວ່າທ່ານຍັງບໍສາມາດໂຫລດຫນ້າເວັບໄດ້ ໃຫ້ລອງກວດເບິງອິນເຕີເນັດ ຫລື ການເຊື່ອມຕໍ Wi-Fi ໃນອຸປະກອນຂອງທ່ານ.
    • +
    + ]]>
    + + + ບໍ່ມີການເຊື່ອມຕໍ່ກັບອິນເຕີເນັດ + + ກວດເບິງເນັດເວີກຂອງທ່ານຄືນ ຫລື ລອງໂຫລດຫນ້າເວັບຄືນໃຫມ່ໃນອີກ 2-3 ນາທີ. + + ໂຫລດຄືນໃຫມ່ + + + ທີ່ຢູ່ບໍ່ຖືກຕ້ອງ + ຮູບແບບຂອງທີ່ຢູ່ທີໃຫ້ມາແມ່ນບໍ່ຖືກຕ້ອງ. ກະລຸນາກວດເບິງທີ່ຢູ່ໃນແຖບທີ່ຕັ້ງເພື່ອຊອກຫາຂໍ້ຜິດພາດ ແລະ ລອງໃຫມ່ອີກຄັ້ງ.

    + ]]>
    + + ທີ່ຢູ່ບໍ່ຖືກຕ້ອງ + + + +
  • ທີ່ຢູ່ຂອງເວັບມັກຈະຂຽນເປັນ http://www.example.com/
  • +
  • ກວດເບິງໃຫ້ຫມັ້ນໃຈວ່າທ່ານໄດ້ໃຊ້ເຄືອງຫມາຍສະແລັດຖືກຕ້ອງແລ້ວ ( /).
  • + + ]]>
    + + + ໂປຼໂຕຄໍທີ່ບໍ່ຮູ້ຈັກ + + ທີ່ຢູ່ລະບຸໂປຣໂຕຄໍ (ເຊັ່ນ: wxyz://) ບຣາວເຊີບໍ່ຮັບຮູ້, ສະນັ້ນ ບຣາວເຊີບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເວັບໄຊທ໌ໄດ້ຢ່າງຖືກຕ້ອງ.

    +
      +
    • ທ່ານກຳລັງພະຍາຍາມເຂົ້າເຖິງມັນຕິມີເດຍ ຫຼືບໍລິການອື່ນໆທີ່ບໍ່ແມ່ນຂໍ້ຄວາມບໍ? ກວດເບິ່ງເວັບໄຊສຳລັບຄວາມຕ້ອງການເພີ່ມເຕີມ.
    • +
    • ບາງໂປຣໂຕຄໍອາດຈະຕ້ອງການຊອບແວພາກສ່ວນທີສາມ ຫຼື plugins ກ່ອນທີ່ຕົວທ່ອງເວັບສາມາດຮັບຮູ້ພວກມັນໄດ້.
    • +
    + ]]>
    + + + ບໍ່ພົບໄຟລ໌ນີ້ + + +
  • ລາຍການດັ່ງກ່າວຖືກປ່ຽນຊື່, ລຶບອອກ ຫຼື ຍ້າຍອອກບໍ?
  • +
  • ມີການສະກົດຄໍາ, ຕົວພິມໃຫຍ່, ຫຼືການພິມຜິດອື່ນໆຢູ່ໃນນີ້ບໍ?
  • +
  • ສິດອະນຸຍາດຂອງທ່ານພຽງພໍກັບລາຍການທີ່ຮ້ອງຂໍບໍ?
  • + + ]]>
    + + + ການເຂົ້າເຖິງໄຟລ໌ຂໍ້ມູນໄດ້ຖືກປະຕິເສດ + +
  • ມັນອາດຈະຖືກລຶບອອກໄປແລ້ວ, ຍ້າຍໄປໄວ້ຢູ່ບ່ອນອື່ນແລ້ວ ຫລື ທ່ານອາດບໍ່ມີສິດເຂົ້າເຖິງຟາຍຂໍ້ມູນນີ້.
  • ]]>
    + + + ພັອກຊີເຊີເວີໄດ້ປະຕິເສດການເຊື່ອມຕໍ່ + + ຕົວ​ທ່ອງ​ເວັບ​ໄດ້​ຖືກ​ຕັ້ງ​ຄ່າ​ໃຫ້​ໃຊ້​ເຊີບ​ເວີ​ພຣັອກຊີ, ແຕ່​ພຣັອກຊີ​ໄດ້​ປະ​ຕິ​ເສດ​ການ​ເຊື່ອມ​ຕໍ່.

    +
      +
    • ການກຳນົດຄ່າພຣັອກຊີຂອງບຣາວເຊີຖືກຕ້ອງບໍ? ກວດເບິ່ງການຕັ້ງຄ່າແລ້ວລອງໃໝ່ອີກຄັ້ງ.
    • +
    • ບໍລິການພຣັອກຊີອະນຸຍາດການເຊື່ອມຕໍ່ຈາກເຄືອຂ່າຍນີ້ບໍ?
    • +
    • ຍັງມີບັນຫາຢູ່ບໍ? ປຶກສາຜູ້ເບິ່ງແຍງລະບົບເຄືອຂ່າຍ ຫຼືຜູ້ໃຫ້ບໍລິການອິນເຕີເນັດຂອງທ່ານເພື່ອຂໍຄວາມຊ່ວຍເຫຼືອ.
    • +
    + ]]>
    + + + ບໍ່ພົບພລັອກຊີເຊີເວີ + + ຕົວ​ທ່ອງ​ເວັບ​ໄດ້​ຖືກ​ຕັ້ງ​ຄ່າ​ໃຫ້​ໃຊ້​ເຊີບ​ເວີ​ພຣັອກ​ຊີ, ແຕ່​ບໍ່​ສາ​ມາດ​ຊອກ​ຫາ​ຕົວ​ແທນ​ໄດ້.

    +
      +
    • ການກຳນົດຄ່າພຣັອກຊີຂອງບຣາວເຊີຖືກຕ້ອງບໍ? ກວດເບິ່ງການຕັ້ງຄ່າແລ້ວລອງໃໝ່ອີກຄັ້ງ.
    • +
    • ອຸປະກອນເຊື່ອມຕໍ່ກັບເຄືອຂ່າຍທີ່ໃຊ້ຢູ່ບໍ?
    • +
    • ຍັງມີບັນຫາຢູ່ບໍ? ປຶກສາຜູ້ເບິ່ງແຍງລະບົບເຄືອຂ່າຍ ຫຼືຜູ້ໃຫ້ບໍລິການອິນເຕີເນັດຂອງທ່ານເພື່ອຂໍຄວາມຊ່ວຍເຫຼືອ.
    • +
    + ]]>
    + + + ປັນຫາເວັບໄຊທ Malware + + + ເວັບໄຊທທີ່ %1$s ໄດ້ຖືກລາຍງານວ່າເປັນເວັບອັນຕະລາຍ ແລະ ໄດ້ຖືກບັອກໂດຍອີງຈາກການຕັ້ງຄ່າຄວາມປອດໄພຂອງທ່ານ.

    + ]]>
    + + + ບັນຫາໃນເວັບໄຊທທີ່ບໍ່ເພິງປະສົງ + + + ເວັບໄຊທທີ່ %1$s ໄດ້ຖືກລາຍງານວ່າເປັນເວັບທີ່ໃຫ້ບໍລິການຊັອບແວທີ່ອັນຕະລາຍ ແລະ ໄດ້ຖືກບັອກໂດຍອີງຈາກການຕັ້ງຄ່າຄວາມປອດໄພຂອງທ່ານ.

    + ]]>
    + + + ເວບໄຊທທີ່ເປັນອັນຕະລາຍ + + + ເວັບໄຊທທີ່ %1$s ໄດ້ຖືກລາຍງານວ່າເປັນເວັບທີ່ອາດຈະເປັນອັນຕະລາຍ ແລະ ໄດ້ຖືກບັອກໂດຍອີງຈາກການຕັ້ງຄ່າຄວາມປອດໄພຂອງທ່ານ.

    + ]]>
    + + + ບັນຫາເວັບໄຊທປອມແປງ + + ຫນ້າເວັບນີ້ທີ່ %1$s ໄດ້ຖືກລາຍງານວ່າເປັນເວັບປອມແປງ ແລະ ໄດ້ຖືກບັອກໂດຍອີງຈາກການຕັ້ງຄ່າຄວາມປອດໄພຂອງທ່ານ.

    + ]]>
    + + + ເວັບໄຊທທີ່ປອດໄພຍັງບໍ່ພ້ອມໃຫ້ໃຊ້ງານ + + %1$s ຍັງບໍພ້ອມໃຫ້ໃຊ້ງານ.]]> + + ສືບຕໍ່ໄປຫາເວັບໄຊທທີ່ນຳໃຊ້ HTTP +
    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 @@ + + + + + Bandyti dar kartą + + + Nepavyko atlikti užklausos + + + Šiuo metu apie šią problemą ar klaidą daugiau informacijos nėra.

    ]]>
    + + + Saugaus ryšio užmegzti nepavyko + + + +
  • Svetainė, kurią bandote atverti, negali būti įkelta, nes nepavyko patikrinti gaunamų duomenų autentiškumo.
  • +
  • Prašome informuoti svetainės kūrėjus apie šią problemą.
  • + ]]>
    + + + Saugaus ryšio užmegzti nepavyko + + + +
  • Tai gali būti serverio konfigūracijos problema, arba kažkieno bandymas apsimesti serveriu.
  • +
  • Jei anksčiau esate sėkmingai prisijungę prie šio serverio, ši klaida gali būti laikina, tad pabandykite vėliau.
  • + ]]>
    + + + Papildomai… + + Kažkas gali bandyti apsimesti svetaine, tad patartume jos neatverti. +

    + ]]>
    + + Grįžti (rekomenduojama) + + Priimti riziką ir tęsti + + + Šiai svetainei reikalingas saugus ryšys. + + + +
  • Tinklalapis, kurį bandote peržiūrėti, negali būti parodytas, nes šiai svetainei reikalingas saugus ryšys.
  • +
  • Greičiausiai problema kyla dėl svetainės, ir jūs nieko negalite padaryti, kad ją išspręstumėte.
  • +
  • Apie problemą galite pranešti svetainės prižiūrėtojui.
  • + + ]]>
    + + + Papildomai… + + + %1$s turi saugos nuostatą, vadinamą „HTTP Strict Transport Security“ (HSTS), ir reiškia, kad „%2$s“ gali jungtis tik saugiu ryšiu. Jūs negalite sukurti išimties, kad aplankytumėte šią svetainę. + ]]> + + Eiti atgal + + + Ryšys buvo nutrauktas + + + Naršyklė sėkmingai užmezgė ryšį, tačiau prisijungimas buvo nutrauktas perduodant duomenis. Bandykite dar kartą.

    +
      +
    • Svetainė gali būti laikinai nepasiekiama arba turi daug lankytojų. Pabandykite dar kartą šiek tiek vėliau.
    • +
    • Jeigu negalite įkelti jokio tinklalapio, patikrinkite savo įrenginio mobilųjį arba belaidį ryšį.
    • +
    ]]>
    + + + Ryšiui skirtas laikas baigėsi + + + Svetainė neatsakė į bandymą užmegzti ryšį, o naršyklės laukimo limitas baigėsi.

    +
      +
    • Svetainė gali turėti didelį lankytojų srautą, arba laikinų nesklandumų. Prašome bandyti vėliau.
    • +
    • Jei nepavyksta įkelti ir kitų tinklalapių, patikrinkite įrenginio ryšį su tinklu.
    • +
    • Jei jūsų įrenginys ar tinklas yra apsaugotas užkarda, arba jungiamasi per įgaliotąjį serverį, tai įsitikinkite, kad parinktos tinkamos nuostatos.
    • +
    • Jei nesklandumo pašalinti nepavyksta, kreipkitės į tinklo administratorių ar interneto paslaugų teikėją.
    • +
    ]]>
    + + + Nepavyko užmegzti ryšio + + + +
  • Svetainė gali būti laikinai nepasiekiama. Pabandykite šiek tiek vėliau.
  • +
  • Jeigu nepavyksta įkelti jokio tinklalapio, patikrinkite savo įrenginio duomenų arba belaidį ryšį.
  • + ]]>
    + + + Netikėtas serverio atsakas + + + Svetainė netikėtu būdu atsakė į tinklo užklausą, tad naršyklė negali tęsti darbo.

    ]]>
    + + + Netinkamas tinklalapio peradresavimas + + + Naršyklė nutraukė ryšį, kadangi svetainė cikliškai peradresuoja užklausas sau pačiai.

    +
      +
    • Ši klaida galėjo įvykti dėl to, kad naršyklė nepriima svetainės slapukų.
    • +
    • Jei leidus priimti svetainės slapukus nesklandumas išlieka, tai jo priežastis gali būti ne jūsų kompiuteryje, o serverio sąrankoje.
    • +
    ]]>
    + + + Dirbama neprisijungus prie tinklo + + + Naršyklė yra atjungta nuo tinklo ir negali užmegzti ryšio su užklaustu objektu.

    +
      +
    • Ar jūsų įrenginys prijungtas prie veikiančio tinklo?
    • +
    • Spustelėkite „Bandyti dar kartą“, kad būtų prisijungta prie tinklo ir pabandyta įkelti tinklalapį iš naujo.
    • +
    ]]>
    + + + Saugumo tikslais apribota prieiga prie šio prievado + + + Adreso, prie kurio bandoma prisijungti, nurodytas numeris prievado (pvz., mozilla.org:80 – mozilla.org su prievadu 80), paprastai naudojamo ne naršymui saityne, o kitiems tikslams. Kad užtikrintų jūsų saugumą, naršyklė atmetė šią užklausą.

    ]]>
    + + + Ryšys nutrūko + + + Prisijungimas buvo nutrauktas bandant užmegzti ryšį. Bandykite dar kartą.

    +
      +
    • Svetainė gali būti laikinai nepasiekiama, arba turi daug lankytojų. Pabandykite dar kartą šiek tiek vėliau.
    • +
    • Jeigu negalite įkelti jokio tinklalapio, patikrinkite savo įrenginio mobilųjį arba belaidį ryšį.
    • +
    ]]>
    + + + Nesaugus failo tipas + + + +
  • Praneškite apie šią problemą svetainės prižiūrėtojams.
  • + ]]>
    + + + Klaida: duomenys pažeisti + + + Tinklalapio, kurį bandote atverti, parodyti negalima, nes perduodant duomenis įvyko klaida.

    +
      +
    • Praneškite apie šią problemą svetainės prižiūrėtojams.
    • +
    ]]>
    + + + Turinio problema + + Tinklalapio, kurį bandote atverti, parodyti negalima, nes perduodant duomenis įvyko klaida,

    +
      +
    • Praneškite apie šią problemą svetainės prižiūrėtojams.
    • +
    ]]>
    + + + Kodavimo klaida + + Tinklalapis, kurį bandote atverti, negali būti parodytas, nes naudoja nežinomą arba nepalaikomą suspaudimo būdą.

    +
      +
    • Praneškite apie šią problemą svetainės prižiūrėtojams.
    • +
    ]]>
    + + + Nerastas serveris + + + Naršyklei nepavyko rasti serverio nurodytu adresu.

    +
      +
    • Patikrinkite, ar rinkdami adresą nepadarėte klaidų, pavyzdžiui, + ww.example.com vietoje + www.example.com.
    • +
    • Jei nepavyksta įkelti ir kitų tinklalapių, patikrinkite savo įrenginio duomenų ar belaidį ryšį.
    • +
    ]]>
    + + + Nėra ryšio su internetu + + Patikrinkite savo tinklo ryšį, arba pabandykite įkelti tinklalapį iš naujo. + + Įkelti iš naujo + + + Neteisingas adresas + Neteisingas adreso formatas. Patikrinkite adresą adreso lauke ir bandykite dar kartą.

    ]]>
    + + Neteisingas adresas + + + +
  • Svetainių adresai dažniausiai rašomi taip: http://www.example.com/
  • +
  • Įsitikinkite, kad naudojate pasviruosius brūkšnius (į priekį): /.
  • + ]]>
    + + + Nežinomas protokolas + + Adrese nurodytas protokolas (pvz., wxyz://) naršyklei nežinomas, todėl svetainės įkėlimas nutrauktas.

    +
      +
    • Jei bandote prisijungti prie įvairialypės terpės išteklių, patikrinkite, ar nėra tam specialių reikalavimų.
    • +
    • Kai kurių protokolų atvėrimui gali būti reikalinga papildoma programinė įranga arba naršyklės papildiniai.
    • +
    ]]>
    + + + Nerastas failas + + +
  • Patikrinkite, ar failas nebuvo pervadintas, perkeltas, ar pašalintas.
  • +
  • Patikrinkite, ar failo pavadinime nėra rinkimo klaidų, pvz., didžiosios raidės pakeistos mažosiomis.
  • +
  • Patikrinkite, ar turite leidimą šį failą atverti.
  • + ]]>
    + + + Priėjimas prie failo uždraustas + + +
  • Jis galėjo būti pašalintas, perkeltas, arba priėjimą riboja failo leidimai.
  • + ]]>
    + + + Įgaliotasis serveris atmetė ryšį + + Naršyklės nuostatose nurodytas įgaliotasis serveris, bet jis atmetė ryšį.

    +
      +
    • Patikrinkite įgaliotojo serverio nuostatas ir bandykite vėl.
    • +
    • Įsitikinkite, ar įgaliotasis serveris leidžia prisijungimus iš jūsų tinklo.
    • +
    • Jei nesklandumo pašalinti nepavyksta, kreipkitės į tinklo prižiūrėtoją ar interneto paslaugų teikėją.
    • +
    ]]>
    + + + Nerastas įgaliotasis serveris + + Naršyklės nuostatose nurodytas įgaliotasis serveris, tačiau jo nepavyko rasti.

    +
      +
    • Patikrinkite įgaliotojo serverio nuostatas ir bandykite vėl.
    • +
    • Įsitikinkite, ar įrenginys yra prijungtas prie veikiančio tinklo.
    • +
    • Jei nesklandumo pašalinti nepavyksta, kreipkitės į tinklo prižiūrėtoją ar interneto paslaugų teikėją.
    • +
    ]]>
    + + + Problema dėl kenkėjiškos svetainės + + + Yra pranešta, jog svetainė adresu %1$s yra kenkėjiška, tad ji užblokuota remiantis jūsų saugumo nuostatomis.

    ]]>
    + + + Problema dėl nepageidaujamos svetainės + + + Yra pranešta, jog svetainė adresu %1$s yra kenkėjiška, tad ji užblokuota remiantis jūsų saugumo nuostatomis.

    ]]>
    + + + Problema dėl žalingos svetainės + + + Yra pranešta, jog svetainė adresu %1$s yra kenkėjiška, tad ji užblokuota remiantis jūsų saugumo nuostatomis.

    ]]>
    + + + Problema dėl apgaulingos svetainės + + Yra pranešta, jog svetainė adresu %1$s yra apgaulinga, tad ji užblokuota remiantis jūsų saugumo nuostatomis.

    ]]>
    + + + Saugi svetainė nepasiekiama + + %1$s HTTPS versija nėra pasiekiama.]]> + + Eiti į HTTP svetainę +
    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 @@ + + + + + Ki’tsàa tuku + + + Ma ku tsinuña + + Koo tu"un tsa la vaa yo.

    + ]]>
    + + + Mani^ku tyiitai^ña + + + +
  • Kue ku snai página kunu kuntyeu tsiniñu kuntyeu la ntyau.
  • +
  • Katu"un nu sto"o web takua na kuntyii ntyi vas yee.
  • + + ]]>
    + + + Conexión segura fallida + + + Ntyityí + + + Alguien podría estar intentando imitar el sitio y no deberías continuar. +        

    +         +    ]]>
    + + Ntyiko +(recomendado) + + Vaa kitsa + + + Stini ñu´u iin conexión vaa. + + + Satà + + + Ma ku kitsau + + + El navegador se conectó exitosamente, pero se interrumpió la conexión mientras se transfería la información. Vuelva a intentarlo.

    +
      +
    • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelva a intentarlo en unos minutos.
    • +
    • Si no puede cargar ninguna página, revise la conexión wifi o de datos de su dispositivo móvil.
    • +
    ]]>
    + + + Koo ña kunu kuntyeu + + + El sitio solicitado no respondió a una petición de conexión y el navegador ha dejado de esperar una respuesta.

    +
      +
    • ¿Podría estar experimentando el servidor una alta demanda o un corte temporal? Vuelva a intentarlo más tarde.
    • +
    • ¿No puede navegar por otros sitios? Compruebe la conexión de red del equipo.
    • +
    • ¿Su red o equipo está protegido por un firewall o un proxy? Una configuración incorrecta podría interferir con la navegación web.
    • +
    • ¿Aun con problemas? Consulte con su administrador de red o proveedor de Internet para obtener asistencia técnica.
    • +
    ]]>
    + + + Kue ku tyitaiña + + + +
  • El sitio podría no estar disponible temporalmente o estar demasiado ocupado. Vuelva a intentarlo en unos minutos.
  • +
  • Si no puede cargar ninguna página, revise la conexión wifi o de datos de su dispositivo móvil.
  • + + ]]>
    + + + Respuesta inesperada del servidor, y el navegador no puede continuar + + El sitio respondió a la solicitud de red de una forma inesperada y el navegador no puede continuar.

    + ]]>
    + + + Modo koo conexión + + + Puerto restringido, por razones de seguridad + + + Ndu tsaa conexión + + + Tutu vaá + + + La página que está intentando ver no puede mostrarse porque se detectó un error en la transmisión de los datos.

    +
      +
    • Póngase en contacto con los propietarios del sitio web para informarles de este problema.
    • +
    + ]]>
    + + + Maku kuntyeu + + + Kue vaa yee la kunu kuntyeu + + + Kue ni ndanii dirección + + + Ma ku kivu nu Internet + + Kitsa tuku + + + Va^a dirección + + + Kue vaa yee ña ntyau + + + Kòo tutu ndukuku + + + Kue ku kunaku tutu ma ku kuntyeu ña + + + Pàgina vaa + +
    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 @@ + + + + + വീണ്ടും ശ്രമിക്കുക + + + അഭ്യർത്ഥന പൂർത്തിയാക്കാൻ കഴിയില്ല + + ഈ പിശകിനെയോ അല്ലെങ്കില്‍ പ്രശ്നത്തെയോ സംബന്ധിച്ചുള്ള കൂടുതല്‍ വിവരങ്ങള്‍ നിലവില്‍ ലഭ്യമല്ല.

    ]]>
    + + + സുരക്ഷിതമായ കണക്ഷന്‍ പരാജയപ്പെട്ടിരിക്കുന്നു + + + +
  • ലഭ്യമായ ഡേറ്റയുടെ ആധികാരികത ഉറപ്പാക്കുവാന്‍ സാധ്യമാകാത്തതിനാൽ, നിങ്ങള്‍ കാണുവാന്‍ ശ്രമിക്കുന്ന താള്‍ ഇപ്പോള്‍ ലഭ്യമല്ല.
  • +
  • ഇതു് സംബന്ധിച്ചുള്ള വിവരങ്ങള്‍ ദയവായി വെബ്‍സൈറ്റ് ഉടമസ്ഥരെ അറിയിക്കുക.
  • +  ]]>
    + + + സുരക്ഷിതമായ കണക്ഷന്‍ പരാജയപ്പെട്ടു + + +
  • ഇത്തിന്റെ കാരണം സെര്‍വറിന്റെ ക്രമീകരണത്തിലുള്ള പിശകോ അല്ലെങ്കില്‍ ആരെങ്കിലും ഈ സെര്‍വറാണെന്നു തെറ്റിദ്ധരിപ്പിയ്ക്കാന്‍ ശ്രമിയ്ക്കുന്നതോ ആണ്.
  • +
  • നിങ്ങള്‍ ഇതിനു് മുമ്പു് ഈ സെര്‍വറിലേക്കു് വിജയകരമായി കണക്ട് ചെയ്തിട്ടുണ്ടെങ്കില്‍, ഇതു് വെറും ഒരു താല്‍ക്കാലിക പ്രശ്നമാവാം, അതുകൊണ്ടു് അല്പസമയം കഴിഞ്ഞ് ശ്രമിക്കുക.
  • + ]]>
    + + + സങ്കീർണ്ണമായവ… + + ആരോ ആൾമാറാട്ടം നടത്താൻ ശ്രമിക്കുന്നതിനാൽ നിങ്ങൾ പോജിൽ തുടരരുത്. +

    + ]]>
    + + തിരികെ പോകുക (ശുപാർശചെയ്യുന്നു) + + അപകടസാധ്യത സ്വീകരിച്ച് തുടരുക + + + കണക്ഷൻ തടസ്സപ്പെട്ടു + + + ബ്രൗസർ വിജയകരമായി കണക്റ്റുചെയ്തു, പക്ഷേ വിവരങ്ങൾ കൈമാറുമ്പോൾ കണക്ഷൻ തടസ്സപ്പെട്ടു. ദയവായി വീണ്ടും ശ്രമിക്കുക.

    +
      +
    • സൈറ്റ് താൽ‌ക്കാലികമായി ലഭ്യമല്ല അല്ലെങ്കിൽ‌ വളരെ തിരക്കിലാണ്. അല്പ സമയത്തിന് ശേഷം വീണ്ടും ശ്രമിക്കുക.
    • +
    • നിങ്ങൾക്ക് പേജുകളൊന്നും ലോഡുചെയ്യാൻ കഴിയുന്നില്ലെങ്കിൽ നിങ്ങളുടെ ഉപകരണത്തിന്റെ ഡാറ്റയോ വൈഫൈ കണക്ഷനോ പരിശോധിക്കുക.
    • +
    ]]>
    + + + കണക്ഷന്റെ സമയം കഴിഞ്ഞിരിക്കുന്നു + + ആവശ്യപ്പെട്ട സൈറ്റ് കണക്ഷനുള്ള മറുപടി നല്‍കിയിട്ടില്ല, ബ്രൌസര്‍ മറുപടിയ്ക്കായി കാത്തിരിക്കുന്നത് നിർത്തിയിരിയ്ക്കുന്നു.

    +
      +
    • െസര്‍വര്‍ തിരക്കിലാണോ അതോ താല്‍ക്കാലികമായി പ്രവര്‍ത്തനരഹിതമാണോ? കുറച്ച് കഴിഞ്ഞ് ശ്രമിക്കൂ.
    • +
    • നിങ്ങള്‍ക്ക് മറ്റ് സൈറ്റുകള്‍ തിരയുവാൻ സാധിയ്ക്കുന്നുണ്ടോ? കംപ്യൂട്ടറിന്റെ നെറ്റ്‌വര്‍ക്ക്കണക്ഷന്‍ പരിശോധിയ്ക്കൂ.
    • +
    • നിങ്ങളുടെ കംപ്യൂട്ടര്‍ അല്ലെങ്കില്‍ നെറ്റ്‌വര്‍ക്ക് ഫയര്‍വോളോ പ്രോക്സിയോ ഉപയോഗിച്ച് സുരക്ഷിതമാക്കിയിട്ടുണ്ടോ? തെറ്റായ ക്രമീകരണങ്ങള്‍ തിരച്ചിലിന് തടസ്സമുണ്ടാക്കുന്നതാണ് .
    • +
    • എന്നിട്ടും പ്രശ്നം ഉണ്ടോ? എങ്കില്‍ നെറ്റ്‌വര്‍ക്ക്അഡ്മിനിസ്ട്രേറ്റര്‍ അല്ലെങ്കില്‍ ഇന്റര്‍നെറ്റ് പ്രൊവൈഡറുമായി ബന്ധപ്പെടുക.
    • +
    ]]>
    + + + ബന്ധിപ്പിക്കാൻ കഴിയില്ല + + + +
  • ഈ സൈറ്റ് തിരക്കിലാണ് അല്ലെങ്കിൽ താത്കാലമായി ലഭ്യമല്ല. അല്പ സമയത്തിനു് ശേഷം വീണ്ടും ശ്രമിയ്ക്കൂ.
  • +
  • ഒരു പേജും ലഭ്യമാക്കാനാവുന്നില്ലെങ്കില്‍, ഉപകരണത്തിന്റെ ഡേറ്റാ അല്ലെങ്കില്‍ വൈഫൈ കണക്ഷന്‍ പരിശോധിയ്ക്കുക.
  • + ]]>
    + + + സെര്‍വറില്‍ നിന്നും പ്രതീക്ഷിക്കാത്ത പ്രതികരണം + + + നെറ്റ്‍വർക്കിൽ നിന്നും അപ്രതീക്ഷിതമായ പ്രതികരണമുണ്ടായി, അതിനാല്‍ ബ്രൗസറിനു് മുമ്പോട്ട് തുടരുവാന്‍ സാധ്യമാകുന്നില്ല.

    ]]>
    + + + ഈ പേജ് ശരിയായി റീഡയറക്‌ട് ചെയ്യുന്നില്ല + + ആവശ്യപ്പെട്ട താള്‍ ലഭ്യമാക്കുന്നതിനുള്ള ശ്രമം ബ്രൌസര്‍ നിര്‍ത്തിയിരിക്കുന്നു. താള്‍ റീഡയറക്‌ട് ചെയ്യുന്നത് ഒരിക്കലും അവസാനിക്കുന്നില്ല.

    +
      +
    • ഈ താളിന് ആവശ്യമുള്ള കൂക്കികള്‍ നിങ്ങള്‍ നിര്‍ജ്ജീവമാക്കുകയോ ഇല്ലാതാക്കുകയോ ചെയ്തിട്ടുണ്ടോ?
    • +
    • സൈറ്റിനുള്ള കുക്കികള്‍ അനുവദിച്ചിട്ടും പ്രശ്നം പരിഹരിക്കപ്പെട്ടില്ലയെങ്കിൽ അത് സെര്‍വറിന്റെ സജ്ജീകരണത്തിലുള്ള പിശക് ആകാം
    • +
    ]]>
    + + + ഓഫ്‌ലൈൻ മോഡ് + + + ബ്രൌസര്‍ ഓഫ്‌ലൈന്‍ മോഡില്‍ പ്രവര്‍ത്തിയ്ക്കുന്നതിനാല്‍ ആവശ്യപ്പെട്ട താളിലേയ്ക്കു് കണക്ട് ചെയ്യുവാന്‍ സാധിയ്ക്കന്നില്ല.

    +
      +
    • കമ്പ്യൂട്ടര്‍ സജീവമായൊരു നെറ്റ്‌വര്‍ക്കിലേയ്ക്ക് കണക്ട് ചെയ്തിട്ടുണ്ടോ?
    • +
    • വീണ്ടും ശ്രമിയ്ക്കുന്നതിനായി “വീണ്ടും ശ്രമിയ്ക്കുക” അമര്‍ത്തുക.
    • +
    ]]>
    + + + സുരക്ഷാ കാരണങ്ങളാല്‍ പോര്‍ട്ടിലേക്കു് പ്രവേശനം നിഷേധിച്ചിരിയ്ക്കുന്നു + + വിലാസത്തിൽ ആവശ്യപ്പെട്ട പോര്‍ട്ട് (ഉദാ mozilla.org-ലുള്ള പോര്‍ട്ട് 80-ക്കായി mozilla.org:80) , വെബ് ബ്രൗസിങ് ഒഴികെമറ്റ് ആവശ്യങ്ങള്‍ക്കായി സാധാരണ ഉപയോഗിക്കുന്നു. നിങ്ങളുടെ സുരക്ഷയ്ക്കായി ഈ ആവശ്യം ബ്രൗസര്‍ റദ്ദാക്കിയിരിക്കുന്നു.

    ]]>
    + + + കണക്ഷൻ പുനഃസജ്ജമാക്കിയതാണ് + + കണക്ഷൻ നെഗോഷ്യേറ്റ് ചെയ്യുമ്പോൾ നെറ്റ്‌വർക്ക് തടസ്സപ്പെട്ടു. ദയവായി വീണ്ടും ശ്രമിക്കുക.

    +
      +        
    • സൈറ്റ് താൽ‌ക്കാലികമായി ലഭ്യമല്ല അല്ലെങ്കിൽ‌ തിരക്കിലാണ്. അല്പം കഴിഞ്ഞ് വീണ്ടും ശ്രമിക്കുക.
    • +        
    • നിങ്ങൾക്ക് താളുകളൊന്നും ലോഡുചെയ്യാൻ കഴിയുന്നില്ലെങ്കിൽ നിങ്ങളുടെ ഉപകരണത്തിന്റെ ഡാറ്റയോ വൈഫൈ കണക്ഷനോ പരിശോധിക്കുക.
    • +
    ]]>
    + + + സുരക്ഷിതമല്ലാത്ത ഫയല്‍ + + +
  • ദയവായി വെബ് സൈറ്റ് ഉടമകളെ ഈ പ്രശ്നം അറിയിയ്ക്കുക.
  • + ]]>
    + + + ഉള്ളടക്കത്തിന്റെ തകരാറ് മൂലമുള്ള പിശക് + + + ഡേറ്റാ അയയ്ക്കുന്നതിലെ പിശകു് കാരണം നിങ്ങള്‍ കാണുവാന്‍ ശ്രമിയ്ക്കുന്ന പേജ് ലഭ്യമല്ല.

    +
      +
    • ഈ പ്രശ്നത്തെപ്പറ്റി ദയവായി വെബ്സൈറ്റിന്റെ ഉടമസ്ഥരെ അറിയിയ്ക്കുക.
    • +
    ]]>
    + + + ഉള്ളടക്കം തകർന്നു + + ഡേറ്റാ അയയ്ക്കുന്നതിലുള്ള പിശകു് കാരണം നിങ്ങള്‍ കാണുവാന്‍ ശ്രമിയ്ക്കുന്ന പേജ് ലഭ്യമല്ല.

    +
      +
    • ഈ പ്രശ്നത്തെപ്പറ്റി ദയവായി വെബ്‍സൈറ്റ് ഉടമസ്ഥരെ അറിയിയ്ക്കുക.
    • +
    ]]>
    + + + ഉള്ളടക്കം എന്‍കോഡ് ചെയ്യുന്നതില്‍ പിശകു് സംഭവിച്ചു + + നിങ്ങള്‍ ഇപ്പോള്‍ കാണുവാന്‍ ശ്രമിക്കുന്ന പോജ് ലഭ്യമല്ല. കാരണം അതു് തെറ്റായ ആല്ലെങ്കില്‍ പിന്തുണ ലഭ്യമല്ലാത്ത രീതിയിലുള്ള കംപ്രഷന്‍ ഉപയോഗിക്കുന്നു.

    +
      +
    • ഇതു് സംബന്ധിച്ചുള്ള വിവരങ്ങള്‍ ദയവായി വെബ്‍സൈറ്റ് ഉടമസ്ഥരെ അറിയിക്കുക.
    • +
    ]]>
    + + + വിലാസം ലഭ്യമായില്ല + + + നൽകിയ വിലാസത്തിന്റെ ഹോസ്റ്റ് സെർവർ കണ്ടെത്താൻ ബ്രൗസറിന് കഴിഞ്ഞില്ല. +      
      +        
    • താഴെ കാണിച്ചിട്ടുള്ളത് പോലുള്ള പിശകുകൾ വിലാസം നൽകുന്നതിൽ പറ്റിയിട്ടുണ്ടോയെന്ന് പരിശോധിക്കുക +           www .example.com എന്നതിന് പകരം +           ww .example.com.
    • +        
    • താങ്കൾക്ക് പേജുകളൊന്നും ലോഡുചെയ്യാൻ കഴിയുന്നില്ലെങ്കിൽ, താങ്കളുടെ ഉപകരണത്തിന്റെ ഡാറ്റയോ വൈഫൈ കണക്ഷനോ പരിശോധിക്കുക. +      
    ]]>
    + + + ഇന്റർനെറ്റ് കണക്ഷൻ ലഭ്യമല്ല + + നിങ്ങളുടെ നെറ്റ്‌വർക്ക് കണക്ഷൻ പരിശോധിക്കുക അല്ലെങ്കിൽ കുറച്ച് നിമിഷങ്ങൾക്ക് ശേഷം പേജ് വീണ്ടും ലഭ്യമാക്കാൻ ശ്രമിക്കുക. + + വീണ്ടും ലഭ്യമാക്കുക + + + അസാധുവായ വിലാസം + നല്‍കിയിരിക്കുന്ന വിലാസം പരിചിതമായ രീതിയിലുള്ളതല്ല. തെറ്റുകള്‍ക്കായി ലോക്കേഷന്‍ ബാര്‍ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക.

    ]]>
    + + ഈ വിലാസം സാധുവല്ല + + + +
  • വെബ് വിലാസങ്ങള്‍ സാധാരണ http://www.example.com/ എന്ന രീതിയിലാണ് എഴുതുന്നത്:
  • +
  • മുന്നോട്ടുള്ള സ്ലാഷ് ആണുപയോഗിക്കുന്നത് എന്നുറപ്പാക്കുക (അതായത്, /).
  • +]]>
    + + + അപരിചിതമായ പ്രോട്ടോക്കോള്‍ + + വിലാസം നൽകിയ പ്രോട്ടോകോൾ (ഉദാ. wxyz://) ബ്രൗസറിനു് തിരിച്ചറിയുവാന്‍ സാധിക്കുന്നില്ല, ആയതിനാൽ ബ്രൗസറിന് സൈറ്റിലേക്ക് ശരിയായി ബന്ധിപ്പിക്കാനാവുന്നില്ല

    • നിങ്ങള്‍ മള്‍ട്ടിമീഡിയയോ അതോ മറ്റേതെങ്കിലും ടെക്സ്റ്റല്ലാത്ത സേവനങ്ങളോ ലഭ്യമാക്കാനാണോ ശ്രമിക്കുന്നത്? കൂടുതല്‍ ആവശ്യങ്ങൾ എന്തെല്ലാം എന്നറിയുന്നതിനായി സൈറ്റ് പരിശോധിക്കുക.
    • ചില പ്രോട്ടോക്കോളുകള്‍ക്ക്, അവയെ ബ്രൗസറിന് തിരിച്ചറിയാനാവുന്നതിന് തേര്‍ഡ്-പാര്‍ട്ടി സോഫ്റ്റ്‌വെയര്‍ അല്ലെങ്കില്‍ പ്ലഗിനുകൾ ആവശ്യമുണ്ടാകാം.
    ]]>
    + + + ഫയൽ കണ്ടെത്താനായില്ല + +
  • വസ്തുവിന്റെ പേര് മാറ്റുകയോ, അതിനെ നീക്കം ചെയ്യുകയോ സ്ഥലം മാറ്റുകയോ ചെയ്തിട്ടുണ്ടാകുമോ?
  • അക്ഷരത്തെറ്റോ മറ്റെന്തെങ്കിലും തെറ്റുകളോ വിലാസത്തില്‍ ഉണ്ടോ?
  • ആവശ്യപ്പെട്ട വസ്തുവിലേക്ക് ലഭിക്കുന്നതിനുള്ള മതിയായ അനുവാദം നിങ്ങള്‍ക്കുണ്ടോ?
  • ]]>
    + + + ഫയലിലേക്കുള്ള പ്രവേശനം നിഷേധിച്ചു +
  • ഇത് നീക്കംചെയ്തിരിക്കാം, സ്ഥലം മാറ്റിയിരിക്കാം, അല്ലെങ്കിൽ ഫയൽ അനുമതികൾ പ്രവേശനം തടയുന്നുണ്ടാവാം
  • ]]>
    + + + പ്രോക്സി സര്‍വര്‍ ബന്ധം നിഷേധിച്ചിരിക്കുന്നു + ഒരു പ്രോക്സി സര്‍വര്‍ ഉപയോഗിക്കുന്നതിനായി ബ്രൗസറിനെ ക്രമീകരിച്ചിരിക്കുന്നു, പക്ഷേ പ്രോക്സി ബന്ധം നിഷേധിച്ചിരിക്കുന്നു.

    • ബ്രൗസറിന്റെ പ്രോക്സി ക്രമീകരണം ശരിയാണോ? അവ പരിശോധിച്ച ശേഷം വീണ്ടും ശ്രമിക്കുക.
    • ഈ ശൃംഘലയിൽ നിന്നുള്ള ബന്ധങ്ങൾ പ്രോക്സി സേവനം അനുവദിക്കുന്നുണ്ടോ?
    • ഇപ്പോഴും പ്രശ്നം ഉണ്ടോ? എങ്കില്‍ ശൃംഘല കാര്യനിർവാഹകർ അല്ലെങ്കില്‍ ഇന്റര്‍നെറ്റ് ദേതാവുമായി ബന്ധപ്പെടുക.
    ]]>
    + + + പ്രോക്സി സെർവർ കണ്ടെത്താനായില്ല + + ഒരു പ്രോക്സി സര്‍വര്‍ ഉപയോഗിക്കുന്നതിനായി ബ്രൗസറിനെ ക്രമീകരിച്ചിരിക്കുന്നു, പക്ഷേ പ്രോക്സി കണ്ടെത്താനായില്ല.

    • ബ്രൗസറിന്റെ പ്രോക്സി ക്രമീകരണം ശരിയാണോ? അവ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക.
    • കംപ്യൂട്ടര്‍ സജീവമായ ഒരു ശൃംഘലയിലാണോ?
    • ഇപ്പോഴും പ്രശ്നം ഉണ്ടോ? എങ്കില്‍ ശൃംഘല കാര്യനിർവാഹകർ അല്ലെങ്കില്‍ ഇന്റര്‍നെറ്റ് ദേതാവുമായി ബന്ധപ്പെടുക.
    ]]>
    + + + മാൽവെയർ സൈറ്റ് പ്രശ്നം + + + %1$s എന്നിടത്തുള്ള സൈറ്റ് ഒരു ആക്രമണ സൈറ്റായി രേഖപ്പെടുത്തിയിട്ടുള്ളതിനാൽ താങ്കളുടെ സുരക്ഷാ ക്രമീകരണങ്ങളനുസരിച്ച് തടയപ്പെട്ടിരിക്കുന്നു.

    ]]>
    + + + അനാവശ്യ സൈറ്റ് പ്രശ്നം + + + %1$s എന്നിടത്തുള്ള സൈറ്റ് ഒരു അനാവശ്യ സോഫ്റ്റ്‌വെയറുകൾ വ്യാപിപ്പിക്കുന്നു എന്ന് രേഖപ്പെടുത്തിയിട്ടുള്ളതിനാൽ താങ്കളുടെ സുരക്ഷാ ക്രമീകരണങ്ങളനുസരിച്ച് തടയപ്പെട്ടിരിക്കുന്നു.

    ]]>
    + + + ദോഷകരമായ സൈറ്റ് പ്രശ്നം + + + %1$s എന്നിടത്തുള്ള സൈറ്റ് ഒരു അപകടകരമായ സൈറ്റാണ് എന്ന് രേഖപ്പെടുത്തിയിട്ടുള്ളതിനാൽ താങ്കളുടെ സുരക്ഷാ ക്രമീകരണങ്ങളനുസരിച്ച് തടയപ്പെട്ടിരിക്കുന്നു.

    ]]>
    + + + തെറ്റിദ്ധരിപ്പിക്കുന്ന സൈറ്റ് + + %1$s എന്നിടത്തുള്ള സൈറ്റ് ഒരു തെറ്റിദ്ധരിപ്പിക്കുന്ന സൈറ്റാണ് എന്ന് രേഖപ്പെടുത്തിയിട്ടുള്ളതിനാൽ താങ്കളുടെ സുരക്ഷാ ക്രമീകരണങ്ങളനുസരിച്ച് തടയപ്പെട്ടിരിക്കുന്നു.

    ]]>
    + +
    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 @@ + + + + + पुन्हा प्रयत्न करा + + + विनंती पूर्ण करू शकत नाही + + या समस्येबद्दल किंवा त्रुटीबद्दल अतिरिक्त माहिती सध्या उपलब्ध नाही.

    ]]>
    + + + सुरक्षित जोडणी अयशस्वी + + + +
  • प्राप्त माहितीची सत्यता तपासता न आल्यामुळे आपणास इच्छित पृष्ठ पाहता येणार नाही.
  • +
  • कृपया संकेतस्थळाच्या मालकाला या अडचणी विषयी अवगत करा.
  • + ]]>
    + + + सुरक्षित जोडणी अयशस्वी + + + +
  • ही सर्व्हर च्या संयोजनातील अडचण असू शकते किंवा कोणीतरी या सर्व्हर सारखे रूप घेऊन फसवत असू शकते.
  • +
  • आपण या सर्व्हरशी पूर्वी यशस्वीरित्या जुळवणी स्थापीत केली असल्यास, त्रुटी तात्पूर्ती असू शकते, व आपण पुन्हा प्रयत्न करू शकता.
  • + ]]>
    + + + प्रगत… + + कोणीतरी साईटचे प्रतिरूपण करत असावे आपण पुढे जाऊ नये. +

    + ]]>
    + + मागे जा (शिफारसीय) + + जोखीम स्वीकारा आणि पुढे चला + + + जोडणी मध्ये अडथळा + + + ब्राउझरची जोडणी यशस्वी झाली, पण माहिती पाठवत असताना खंडीत झाली. कृपया पुन्हा प्रयत्न करा.

    +
      +
    • हे संकेतस्थळ अतिव्यस्त किंवा तात्पुरते बंद असू शकते. काही क्षणात पुन्हा प्रयत्न करा.
    • + +
    • आपण जर कोणतेही पृष्ठ लोड करू शकत नसाल तर आपल्या उपकरणाची डेटा किंवा वायफाय जोडणी तपासा
    • +
    ]]>
    + + + जोडणी कालबाह्य झाली + + + विनंती केलेल्या साईटने जोडणी विनंतीला प्रतिसाद दिला नाही आणि ब्राऊझरने उत्तराची प्रतीक्षा करणे थांबवले आहे.

    + +
      +
    • कदाचित सर्व्हर प्रचंड मागणी किंवा तात्पुरता व्यत्यय अनुभवत असेल का?
    • + +
    • इतर साईटही दिसत नाहीत?आपल्या डिव्हाईसची नेटवर्क जोडणी तपासून पहा.
    • + +
    • आपले डिव्हाईस किंवा नेटवर्क फायरवॉल किंवा प्रॉक्सी ने सुरक्षित आहे का?चुकीच्या सेटिंग इंटरनेट ब्राऊझिंगला व्यत्यय निर्माण करू शकतात.
    • + +
    • अजूनही समस्या आहेच?आपल्या नेटवर्क व्यवस्थापक किंवा इंटरनेट प्रदात्यांकडून मदत घ्या.
    • +
    ]]>
    + + + जोडणी होऊ शकत नाही + + + +
  • हे संकेतस्थळ तात्पुरते उपलब्ध नसेल किंवा अतिशय व्यस्त असेल. काही क्षणात पुन्हा प्रयत्न करा.
  • + +
  • जर आपण कोणतीही संकेतस्थळे उघडू शकत नसाल तर आपल्या उपकरणाची वायफाय किंवा डेटा जोडणी तपासून पहा.
  • + ]]>
    + + + सर्व्हरकडून अनपेक्षित प्रतिसाद + + + साईटने नेटवर्क विनंतीला अनपेक्षितरित्या उत्तर दिले व ब्राउझर पुढे जाऊ शकत नाही.

    ]]>
    + + + पृष्ठ योग्यरित्या पुनर्निर्देशित होत नाही + + + विनंती केलेली गोष्ट मिळवणे ब्राऊझरने थांबवले आहे. कधीही पूर्ण होणार नाही अशा पद्धतीने साईट ने विनंती वळवली आहे.

    +
      +
    • या साईटला हवे असलेल्या कुकीज आपण निष्क्रिय किंवा अवरोधित केल्या आहेत का?
    • + +
    • जर साईट कुकीज स्वीकारण्याची समस्या सुटली नाही तर ही कदाचित सर्व्हरची समस्या आहे आपल्या डिव्हाईसची नाही.
    • +
    ]]>
    + + + ऑफलाइन मोड + + + ब्राउझर आपल्या ऑफलाईन मोड मध्ये कार्यरत आहे व विनंती केलेल्या गोष्टीला जोडू शकत नाही.

    +
      + +
    • डिव्हाईस सक्रिय नेटवर्कला जोडलेले आहे का?
    • + +
    • ऑनलाईन मोड वर जाण्यासाठी “पुन्हा प्रयत्न करा” दाबा आणि पृष्ठ पुन्हा लोड करा.
    • +
    ]]>
    + + + सुरक्षा कारणास्तव पोर्ट प्रतिबंधित + + + विनंती केलेल्या पत्त्याने सहसा वेब ब्राऊझिंग सोडून इतर कारणासाठी वापरले जाणारे पोर्ट नमूद केले आहे (उदा. mozilla.org:80 mozilla.org वर पोर्ट क्र. 80). आपल्या संरक्षणार्थ ब्राउझर ने विनंती रद्द केली आहे

    ]]>
    + + + जोडणी पुनःप्रस्थापित करण्यात आली + + + जोडणीची देवाणघेवाण करताना नेटवर्क लिंक खंडित झाली. कृपया पुन्हा प्रयत्न करा.

    +
      +
    • हे संकेतस्थळ अतिव्यस्त किंवा तात्पुरते बंद असू शकते. काही क्षणात पुन्हा प्रयत्न करा.
    • + +
    • आपण जर कोणतेही पृष्ठ लोड करू शकत नसाल तर आपल्या उपकरणाची डेटा किंवा वायफाय जोडणी तपासा.
    • +
    ]]>
    + + + असुरक्षीत फाइल प्रकार + + + +
  • या समस्येबद्दल माहिती देण्यासाठी कृपया संकेतस्थळाच्या मालकाशी संपर्क करा.
  • + ]]>
    + + + दुषीत मजकूर त्रुटी + + + डाटा स्थानांतरनवेळी त्रुटी आढळल्याने आपण पाहू इच्छित पृष्ठ दाखवणे अशक्य आहे.

    +
      + +
    • या अडचणीविषयी माहिती पुरवण्याकरीता, कृपया संकेतस्थळाच्या मालकांशी संपर्क करा.
    • +
    ]]>
    + + + मजकूर भग्न झाला + + डाटा स्थानांतरनवेळी त्रुटी आढळल्याने आपण पाहू इच्छित पृष्ठ दाखवणे अशक्य आहे.

    +
      + +
    • या अडचणीविषयी माहिती पुरवण्याकरीता, कृपया संकेतस्थळाच्या मालकांशी संपर्क करा.
    • +
    ]]>
    + + + मजकूर एन्कोडींग त्रुटी + + पृष्ठ अवैध किंवा असमर्थीत संकुचन प्रकार वापरत असल्यामुळे इच्छित पृष्ठ पाहता येणार नाही.

    +
      + +
    • कृपया संकेतस्थळाच्या मालकाला या अडचणी विषयी अगत करा.
    • +
    ]]>
    + + + पत्ता आढळला नाही + + + पुरवलेल्या पत्त्याचा होस्ट सर्व्हर ब्राउझरला सापडला नाही.

    +
      +
    • अशा टाइपिंग चुकांसाठी पत्ता तपासा + www.example.com + च्याऐवजी ww.example.com
    • +
    • आपण कोणतीही पृष्ठे लोड करू शकत नसल्यास आपल्या डिव्हाइसचा डेटा किंवा वायफाय कनेक्शन तपासा.
    • +
    ]]>
    + + + इंटरनेट जोडणी नाही + + + आपली नेटवर्क जोडणी तपासा किंवा काही क्षणात पृष्ठ परत लोड करण्याचा प्रयत्न करा. + + पुन्हा लोड करा + + + अवैध पत्ता + पुरवलेला पत्ता ओळखण्याजोग्या स्वरूपात नाही. कृपया चुकांसाठी पत्ता पट्टी तपासा आणि पुन्हा प्रयत्न करा.

    ]]>
    + + पत्ता वैध नाही + + + +
  • वेब पत्ते सहसा असे लिहितात http://www.example.com/
  • + +
  • आपण पुढील छेद वापरताय याची खात्री करा (उदा. ).
  • + ]]>
    + + + अज्ञात प्रोटोकॉल + + पत्त्यामध्ये ब्राउझर ओळखत नाही असा प्रोटोकॉल (wxyz://) नमूद केला आहे, म्हणून ब्राउझर साईटबरोबर जोडणी करू शकत नाही.

    +
      + +
    • आपण मल्टिमीडिया किंवा अन्य लिखित मजकुरेतर सेवा वापरू इच्छिता का?अतिरिक्त आवश्यकतांकरिता साईट तपासा.
    • + +
    • काही प्रोटोकॉल ब्राउझर ने ओळखण्यासाठी तृतीयपक्षीय सॉफ्टवेअर किंवा प्लग-इन आवश्यक असू शकते.
    • +
    ]]>
    + + + फाइल आढळली नाही + + +
  • ह्या घटकाचे कदाचित नाव बदलले असेल, काढून टाकला किंवा हलवला असू शकतो का?
  • + +
  • पत्त्यामध्ये अक्षरलेखन,मोठी लिपी किंवा इतर लेखन त्रुटी असू शकते का?
  • + +
  • विनंती केलेल्या पत्त्यावरचा घटक वापरायची आपल्याला परवानगी आहे का?
  • + ]]>
    + + + फाइल वापर नाकारण्यात आला + + +
  • ते कदाचित काढून टाकले गेले, हलविले, किंवा त्यास फाइल परवानग्या प्रवेश प्रतिबंधित करत असतील.
  • + ]]>
    + + + प्रॉक्सी सर्व्हरने जोडणी नकारली + + ब्राउझरची संरचना प्रॉक्सी सर्व्हर वापरण्यासाठी केली आहे, पण प्रॉक्सीने जोडणी नकारली.

    +
      +
    • ब्राउझरची प्रॉक्सी संरचना अचूक आहे का? सेटिंग तपासा व पुन्हा प्रयत्न करा.
    • +
    • प्रॉक्सी सेवा या नेटवर्ककडून जोडणीला अनुमती देते का?
    • +
    • अजूनही समस्या आहे? मदतीसाठी आपल्या नेटवर्क प्रशासकाचा किंवा इंटरनेट प्रदात्याचा सल्ला घ्या.
    • +
    ]]>
    + + + प्रॉक्सी सर्व्हर आढळला नाही + + ब्राउझरची संरचना प्रॉक्सी सर्व्हर वापरण्यासाठी केली आहे, पण प्रॉक्सी सापडला नाही.

    +
      +
    • ब्राउझरची प्रॉक्सी संरचना अचूक आहे का? सेटिंग तपासा व पुन्हा प्रयत्न करा.
    • +
    • उपकरण सक्रिय नेटवर्कशी जोडले आहे का?
    • +
    • अजूनही समस्या आहे? मदतीसाठी आपल्या नेटवर्क प्रशासकाचा किंवा इंटरनेट प्रदात्याचा सल्ला घ्या.
    • +
    ]]>
    + + + मालवेअर साइट समस्या + + + %1$sयेथील साईट घातक म्हणून घोषित केली आहे आणि आपल्या सुरक्षितता प्राधान्यक्रमानुसार अवरोधित केली आहे.

    ]]>
    + + + अनिष्ट साईट समस्या + + + %1$s येथील साईट निरिच्छ सॉफ्टवेअर देणारी म्हणून घोषित केली आहे आणि आपल्या सुरक्षितता प्राधान्यक्रमानुसार अवरोधित केली आहे.

    ]]>
    + + + घातक साईट समस्या + + + %1$s वरील संकेतस्थळ धोकादायक म्हणून घोषीत केले गेले आहे व आपल्या सुरक्षा प्राधान्यक्रम आधारावर रोखले गेले आहे.

    ]]>
    + + + भ्रामक साइट समस्या + + %1$s वरील संकेतस्थळ फसवे म्हणून घोषीत केले गेले आहे व सुरक्षा प्राधान्यक्रम कारणास्तव रोखले गेले आहे.

    ]]>
    + +
    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 @@ + + + + + ထပ်ကြိုးစားပါ + + + တောင်းဆိုချက်ကို ပြီးဆုံးသည်အထိ မဆောင်ရွက်နိုင်ပါ + + + ဒီပြဿနာအတွက်နောက်ထပ်အချက်အလက်များကို လက်ရှိမရရှိနိုင်သေးပါ။

    ]]>
    + + + လုံခြုံသော ချိတ်ဆက်မှု မအောင်မြင်ခဲ့ပါ + + + +
  • သင်ကြည့်ရန်ကြိုးစားနေသော စာမျက်နှာကို မဖော်ပြနိုင်ပါ။ အဘယ့်ကြောင့်ဆိုသော် လက်ခံရရှိထားသော အချက်အလက်၏ ထောက်ခံချက်ကို အတည်မပြုနိုင်သောကြောင့် ဖြစ်သည်။
  • +
  • ဒီပြဿနာကို အသိပေးရန် ကျေးဇူးပြု၍ ဝဘ်ဆိုဒ်ပိုင်ရှင်ထံသို့ ဆက်သွယ်ပါ။
  • + ]]>
    + + + လုံခြုံသော ချိတ်ဆက်မှု မအောင်မြင်ခဲ့ပါ + + + +
  • ယခုပြဿနာသည် ဆာဗာ၏ အပြင်အဆင်ကြောင့်ဖြစ်နိုင်သည် သို့မဟုတ် တစ်ယောက်ယောက်က ဆာဗာကဲ့သို့ အယောင်ဆောင်နေခြင်းကြောင့် ဖြစ်နေနိုင်သည်။
  • +
  • သင်သည် ယခုဆာဗာကို အရင်က ချိတ်ဆက်ဖူးပါက ယခုပြဿနာသည် ယာယီဖြစ်နိုင်သည်၊ ထို့ပြင် နောက်မှ ပြန်ဖွင့်ကြည့်နိုင်သည်။
  • + ]]>
    + + + အဆင့်မြင့်… + + တစ်စုံတစ်ယောက်သည် ဝဘ်ဆိုက်ကဲ့သို့ အယောင်ဆောင်နေခြင်း ဖြစ်နေနိုင်ပြီး သင် ရှေ့ဆက်မလုပ်ဆောင်သင့်ပါ။ +

    + ]]>
    + + ပြန်သွားပါ (အကြံပြုချက်အရ) + + + အန္တရာယ်ကိုလက်ခံပြီးဆက်လုပ်ပါ + + + ချိတ်ဆက်မှု ပြတ်တောက်သွားခဲ့သည် + + + ဘရောင်ဇာသည် အောင်မြင်စွာ ချိတ်ဆက်နိုင်ခဲ့သော်လည်း အချက်အလက်လွှဲပြောင်းနေစဉ် ချိတ်ဆက်မှု ပြတ်တောက်ခဲ့သည်။ ကျေးဇူးပြု၍ ထပ်မံ ဆောင်ရွက်ကြည့်ပါ။

    +
      +
    • ဝဘ်ဆိုက်ကို ယာယီ ကြည့်ရှု၍ မရနိုင်ပါ သို့မဟုတ် ၄င်းသည် အလုပ်များနေသောကြောင့် ဖြစ်နိုင်သည်။ ခဏကြာလျှင် ထပ်ကြိုးစားကြည့်ပါ။
    • +
    • သင်သည် မည်သည့်ဝဘ်ဆိုက်မျှ မကြည့်ရှုနိုင်ပါက သင်၏ ကိရိယာ၏ ဒေတာ သို့မဟုတ် ဝိုင်ဖိုင်ချိတ်ဆက်မှုကို စစ်ဆေးပါ။
    • +
    ]]>
    + + + ချိတ်ဆက်မှုသည် သတ်မှတ်ချိန် ကျော်လွန်ခဲ့သည် + + + ချိတ်ဆက်မှုတောင်းဆိုချက်ကို ဝဘ်ဆိုက်က မတုံ့ပြန်နိုင်ပါ၊ ပြီးနောက် ဘရောင်ဇာသည် တုံ့ပြန်မှုစောင့်ဆိုင်းနေခြင်းမှ ရပ်ဆိုင်းသွားသည်။

    +
      +
    • ဆာဗာသည် ဝန်မနိုင်သောပြဿနာ ကြုံတွေ့နေခြင်း သို့မဟုတ် ယာယီပြတ်တောက်မှု ဖြစ်နိုင်ပါသလား။ နောက်မှ ပြန်ကြိုးစားကြည့်ပါ။
    • +
    • အခြားဝဘ်ဆိုက်များကို ကြည့်ရှု၍ မရနိုင် ဖြစ်နေပါသလား။ ကိရိယာ၏ ကွန်ယက်ချိတ်ဆက်မှုကို စစ်ဆေးပါ။
    • +
    • သင်၏ ကိရိယာ သို့မဟုတ် ကွန်ယက်ကို မီးနံရံ သို့မဟုတ် ကြားခံဆာဗာက ကာကွယ်ထားပါသလား။ အပြင်အဆင်အမှားများသည်လည်း ဝဘ်ကြည့်ရှုခြင်းကို နှောက်ယှက်နိုင်သည်။
    • +
    • ပြဿနာရှိနေဆဲ ဖြစ်နေပါသလား။ သင်၏ ကွန်ယက်ထိန်းချုပ်သူ သို့မဟုတ် အင်တာနက်ဝန်ဆောင်မှုပေးသူကို အကူအညီတောင်းပါ။
    • +
    ]]>
    + + + မချိတ်ဆက်နိုင်ပါ + + + +
  • ယခုဝဘ်ဆိုက်ကို ယာယီ အသုံးမပြုနိုင်ပါ သို့မဟုတ် အသုံးပြုသူ များနေသောကြောင့် ဖြစ်နိုင်သည်။ ခဏကြာလျှင် ပြန်ကြိုးစားပါ။
  • +
  • အကယ်၍ သင်သည် မည်သည့်ဝဘ်ဆိုက်မျှ မဖွင့်နိုင်ပါက သင့်မိုဘိုင်းကိရိယာ၏ ဒေတာ သို့မဟုတ် ဝိုင်ဖိုင် ချိတ်ဆက်မှုကို စစ်ဆေးပါ။
  • + ]]>
    + + + ဆာဗာမှမမျှော်လင့်သောတုံ့ပြန်မှု + + + ဝဘ်ဆိုက်သည် ကွန်ယက်တောင်းဆိုမှုကို မမျှော်မှန်းထားသောနည်းလမ်းဖြင့် တုန့်ပြန်ခဲ့ပြီး ဘရောင်ဇာသည် ဆက်လက်မဆောင်ရွက်နိုင်ပါ။

    ]]>
    + + + ယခုစာမျက်နှာသည် ကောင်းမွန်စွာ လမ်းညွှန်မပေးနိုင်ပါ + + + ဘရောင်ဇာသည် တောင်းဆိုထားသည့်အရာကို ရယူရန် ကြိုးစားနေစဉ် ရပ်ဆိုင်းသွားသည်။ ဝဘ်ဆိုက်သည် တောင်းဆိုချက်ကို ပြီးစီးအောင် ဆောင်ရွက်နိုင်မည်မဟုတ်သောပုံစံဖြင့် ပြန်ညွှန်းနေသည်။

    +
      +
    • ယခုဝဘ်ဆိုက်က လိုအပ်သောကွတ်ကီးများကို ပိတ် သို့မဟုတ် တားဆီးထားပါသလား။
    • +
    • ဝဘ်ဆိုက်၏ ကွတ်ကီးကို လက်ခံထားသော်လည်း ပြဿနာ မပြေလည်သေးပါက ၄င်းသည် ဆာဗာ၏ အပြင်အဆင်အမှား ဖြစ်နိုင်သည်၊ သင်၏ ကိရိယာကြောင့် မဟုတ်လောက်ပါ။
    • +
    ]]>
    + + + အော့ဖ်လိုင်း mode + + + ဘရောင်ဇာသည် ကွန်ယက်မဲ့သုံးပုံစံတွင် ဖြစ်နေပြီး တောင်းဆိုထားသည့်အရာကို မချိက်ဆက်နိုင်ပါ။

    +
      +
    • ယခုကိရိယာသည် ကွန်ယက်ကို ချိက်ဆက်ထားပါသလား။
    • +
    • ကွန်ယက်သုံးပုံစံကို ပြောင်းရန် “ထပ်မံကြိုးစားကြည့်ပါ” ကို နှိပ်ပြီး စာမျက်နှာကို ပြန်ခေါ်ပါ။
    • +
    ]]>
    + + + လုံခြုံရေး ကိစ္စများကြောင့်တားမြစ်ထားတဲ့ Port + + + တောင်းဆိုထားသော လိပ်စာသည် ပုံမှန်အားဖြင့် ဝက်ဆိုက်ကြည့်ရှုရန် မဟုတ်သည့် အပေါက် ( ဥပမာ mozilla.org:80 တွင် 80 သည် အပေါက်နံပတ်ဖြစ်သည်) တစ်ခုအား ညွှန်ထားသည် ။ သင့်အား ကာကွယ်ရန် ဘ‌ယောက်ဆာမှ တောင်းဆိုမှုအား မလုပ်တော့ပါ ။

    ]]>
    + + + ဆက်သွယ်မှုကို ပြန်လည်သတ်မှတ်သည် + + + စတင်ခေါ်ဆိုနေစဥ် နက်ဝက် ချိတ်ဆက်မှု ပြတ်တောက်သွားသည်။ ကျေးဇူးပြုပြီး ထပ်ကြိုးစားပါ။

    +
      +
    • ဝက်ဆိုက် အလုပ်အရမ်းများနေတာလည်း ဖြစ်နိုင်သည် ။ တစ်ခဏနေ မှ ထပ်ကြိုးစားသင့်သည်။
    • +
    • သင် အခြား ဝက်စာမျက်နှာများပါဖွင့် မရပါက သင့် စက်၏ အချက်အလက် ချိတ်ဆက်မှု သို့ ဝိုင်ဖိုင် အား စစ်ပါ
    • +
    ]]>
    + + + မလုံခြုံသောဖိုင်အမျိုးအစား + + + +
  • ကျေးဇူးပြု၍ ယခုပြဿနာကို အသိပေးရန် ဝဘ်ဆိုက်ပိုင်ရှင်ကို ဆက်သွယ်ပါ။
  • + ]]>
    + + + အကြောင်းအရာ မစုံလင်သော အမှား + + + သင် တောင်းဆိုထားသော စာမျက်နှာအား ရယူရာတွင် အခက်အခဲတခု ဖြစ်ပွား နေသဖြင့် ဖွင့်ပြမပေးနိုင်ပါ

    +
      +
    • ကျေးဇူးပြုပြီး ဝက်ဆိုက် ပိုင်ရှင်အား ယခု ပြသာနာနှင့်ပတ်သတ်ပြီး ဆက်သွယ် အကြောင်းကြားပေးပါ။
    • +
    ]]>
    + + + အကြောင်းအရာပျက်နေတယ် + + သင်တောင်းဆိုထားသောစာမျက်နှာအား ရယူရာတွင်အခက်အခဲဖြစ်နေသဖြင့် ဖွင့်ပြမပေးနိုင်ပါ။

    +
      +
    • ကျေးဇူးပြုပြီး ဝက်ဆိုက် ပိုင်ရှင်အား ယခု ပြသာနာနှင့်ပတ်သတ်ပြီး အကြောင်းကြားပေးပါ။
    • +
    ]]>
    + + + အကြောင်းအရာ အန်ကုဒ်ဒင်း အမှား + + သင်တောင်းဆိုထားသောစာမျက်နှာအား ရယူရာတွင်အခက်အခဲဖြစ်နေသဖြင့် ဖွင့်ပြမပေးနိုင်ပါ။

    +
      +
    • ကျေးဇူးပြုပြီး ဝက်ဆိုက် ပိုင်ရှင်အား ယခု ပြသာနာနှင့်ပတ်သတ်ပြီး အကြောင်းကြားပေးပါ။
    • +
    ]]>
    + + + လိပ်စာမတွေ့ပါ + + + ဘရောက်ဇာသည်ပေးထားသောလိပ်စာအတွက် host server ကိုရှာမရပါ။

    +
      +
    • ဥပမာ လိပ်စာအတွက်စာရိုက်အမှားများကိုစစ်ဆေးပါ။ + ww.example.com instead of + www.example.com.
    • +
    • စာမျက်နှာတစ်ခုမျှဖွင့်မရပါက ဝိုင်ဖိုင်ချိတ်ဆက်မှုကိုစစ်ပါ။
    • +
    ]]>
    + + + အင်တာနက်ချိတ်ဆက်မှု မရှိပါ + + သင်၏ကွန်ယက်ချိတ်ဆက်မှုကိုစစ်ဆေးပါ (သို့) အချိန်တိုအတွင်းစာမျက်နှာကိုပြန်ဖွင့်ရန်ကြိုးစားပါ။ + + ပြန်တင်ပါ + + + မမှန်ကန်သောလိပ်စာ + ပေးထားသောလိပ်စာသည် သတ်မှတ်ထားသော ပုံစံမျိုးဖြင့်မဟုတ်ပါ။ ကျေးဇူးပြုပြီး ရှာဖွေရေးဘားကို စစ်ဆေးပြီး ထပ်မံ ဆောင်ရွက်ကြည့်ပါ။

    ]]>
    + + လိပ်စာမမှန်ကန်ပါ + + + +
  • ဝဘ်လိပ်စာများကိုအများအားဖြင့်အောက်ပါအတိုင်းရေးကြသည်။http://www.example.com/
  • +
  • မျဉ်းစောင်းလေးပါသည်ကိုဂရုပြုပါ။ (နမူနာ/)
  • + ]]>
    + + + မသိသောပရိုတိုကော + + လိပ်စာက protocol ကိုသတ်မှတ်သည် (ဥပမာ -wxyz://) ဘရောက်ဇာမှ မသိသော‌ကြောင့် ဘရောက်ဇာမှ ဤဆိုက်ကိုမချိတ်ဆက်နိုင်ပါ။

    +
      +
    • သင် multimedia သို့မဟုတ်အခြားစာမဟုတ်သောဝန်ဆောင်မှုများကိုရယူရန်ကြိုးစားနေပါသလား။ အပိုလိုအပ်ချက်များအတွက် site ကိုစစ်ဆေးပါ။
    • +
    • browser သည်၎င်းတို့ကိုအသိမပြုမီ အချို့သော protocol များသည် third-party software သို့မဟုတ် plugins လိုအပ်လိမ့်မည်။
    • +
    ]]>
    + + + ဖိုင်ရှာမတွေ့ပါ + + +
  • ယခုအရာသည် အမည်ပြောင်း၊ ဖျက်၊ နေရာရွှေ့ခံထားရခြင်း ဖြစ်နိုင်ပါသလား။
  • +
  • လိပ်စာရေးရာတွင် သတ်ပုံ၊ စာလုံးအကြီးအသေး၊ စာလုံးပေါင်းမှားနေခြင်း ရှိနေပါသလား။
  • +
  • တောင်းဆိုထားသော အရာအတွက် သင့်တွင် လုံလောက်သော ခွင့်ပြုချက် ရှိပါသလား။
  • + ]]>
    + + + ဖိုင်အသုံးပြုခြင်းကို တားမြစ်ထားသည် + + +
  • ၄င်းကို ဖျက်ထား၊ ရွှေ့ထား သို့မဟုတ် ဖိုင်အသုံးပြုခွင့်က တားဆီးနေခြင်း ဖြစ်နိုင်သည်။
  • + ]]>
    + + + ကြားခံဆာဗာသည် ချိတ်ဆက်မှုကို ငြင်းဆန်ထားသည် + + ကြားခံဆာဗာသုံးရန် ဘရောင်ဇာကို ပြင်ဆင်ထားသော်လည်း ကြားခံဆာဗာက ချိတ်ဆက်မှုကို ငြင်းဆိုနေသည်။

    +
      +
    • ဘရောင်ဇာ၏ ကြားခံဆာဗာအပြင်အဆင် မှားနေသလား။ အပြင်အဆင်ကို စစ်ဆေးပြီး နောက်တစ်ကြိမ်ထပ်ကြိုးစားကြည့်ပါ။
    • +
    • ကြားခံဆာဗာဝန်ဆောင်မှုသည် ယခုကွန်ယက်မှ ချိိတ်ဆက်မှုများကို ခွင့်ပြုနေပါသလား။
    • +
    • ပြဿနာရှိနေဆဲ ဖြစ်နေပါသလား။ သင်၏ ကွန်ယက်ထိန်းချုပ်သူ သို့မဟုတ် အင်တာနက်ဝန်ဆောင်မှုပေးသူကို အကူအညီတောင်းပါ။
    • +
    ]]>
    + + + ကြားခံဆာဗာကို မတွေ့ရပါ + + ဤဘရောင်ဇာသည် ပရောက်ဆီသုံးရန်ပြုပြင်ထားသော်လည်း ပရောက်ဆီကို ရှာမတွေ့ပါ။

    +
      +
    • ပရောက်ဆီ၏ အပြင်အဆင်တွေမှန်ပါသလား။ အပြင်အဆင်များကိုစစ်ဆေးပြီးထပ်မံဆောင်ရွက်ကြည့်ပါ။
    • +
    • ကိရိယာသည် ကွန်ယက်သို့ ချိတ်ဆက်ထားပါသလား
    • +
    • ပြဿနာရှိနေဆဲ ဖြစ်နေပါသလား။ သင်၏ ကွန်ယက်ထိန်းချုပ်သူ သို့မဟုတ် အင်တာနက်ဝန်ဆောင်မှုပေးသူကို အကူအညီတောင်းပါ။
    • +
    ]]>
    + + + Malware site ပြဿနာ + + + %1$s တွင် ရှိသော ယခုဝဘ်ဆိုက်သည် တိုက်ခိုက်နိုင်ဖွယ်ရှိသောစာမျက်နှာဟု သတင်းပေးခြင်းခံရပြီး သင်၏လုံခြုံရေးအပြင်အဆင်များအရ ပိတ်ပင်ထားသည်။

    ]]>
    + + + မလိုလားအပ်သောဆိုက်ပြဿနာ + + + %1$s ရှိ ဆိုက်သည် မလိုအပ်သည့် ဆော့ဖ်ဝဲများ ဖြန့်သည်ဟု တိုင်ကြားခံထားရသည်။ ထို့ကြောင့် သင့် လုံခြုံရေးအပြင်အဆင်များအရ ထိုဆိုက်ကို ပိတ်ပင်တားဆီးထားသည်။

    ]]>
    + + + အန္တရာယ်ရှိသော site ပြဿနာ + + + %1$s ရှိ ဝဘ်ဆိုက်သည် အန္တရာယ်ရှိနိုင်သော ဝဘ်ဆိုက်ဖြစ်ကြောင်း သတင်းရထားပြီး လုံခြုံရေးအစီအမံများအရ ၎င်းကို ပိတ်ပင်ထားသည်။

    ]]>
    + + + လှည့်စားသော site ကိုပြဿနာ + + %1$s တွင် ရှိသော ယခုဝဘ်ဆိုက်သည် တိုက်ခိုက်နိုင်ဖွယ်ရှိသောစာမျက်နှာဟု သတင်းပေးခြင်းခံရပြီး သင်၏လုံခြုံရေးအပြင်အဆင်များအရ ပိတ်ပင်ထားသည်။

    ]]>
    + + + လုံခြုံသောဝဘ်ချိတ်ဆက်မှုမရနိုင်ပါ + + %1$s ၏ HTTPS ပုံစံသည် မရရှိနိုင်ပါ။]]> + + HTTP ဝဘ်သို့ဆက်သွားမည် +
    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 @@ + + + + + Prøv igjen + + + Klarte ikke fullføre forspørselen + + Mer informasjon om denne feilen er ikke tilgjengelig.

    + ]]>
    + + + Sikker tilkobling mislyktes + + +
  • Siden du forsøker åpne kan ikke vises fordi det ikke kunne bekreftes at overført data er autentisk.
  • +
  • Kontakt nettstedseieren og informer om problemet.
  • + + ]]>
    + + + Sikker tilkobling mislyktes + + + +
  • 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.
  • +
  • Dersom du tidligere har koblet til serveren er det mulig at feilen er midlertidig, og du kan prøve igjen senere.
  • + + ]]>
    + + + Avansert… + + Noen kan prøve å etterligne nettstedet, og du bør ikke fortsette. +

    + + ]]>
    + + Gå tilbake (Anbefalt) + + Godta risikoen og fortsett + + + Dette nettstedet krever en sikker tilkobling. + + +
  • Siden du prøver å vise kan ikke vises fordi dette nettstedet krever en sikker tilkobling.
  • +
  • Problemet er mest sannsynlig med nettstedet, og det er ingenting du kan gjøre for å løse det.
  • +
  • Du kan varsle nettstedets administrator om problemet.
  • + + ]]>
    + + + Avansert … + + + %1$s har en sikkerhetspolicy kalt HTTP Strict Transport Security (HSTS), som betyr at %2$s bare kan koble til den sikkert. Du kan ikke legge til et unntak for å besøke dette nettstedet. + ]]> + + Gå tilbake + + + Tilkoblingen ble avbrutt + + + Nettleseren ble tilkoblet, men tilkoblingen ble avbrutt under overføring av informasjon. Prøv igjen.

    +
      +
    • Siden kan være midlertidig utilgjengelig, eller opptatt. Prøv igjen om en stund.
    • +
    • Hvis du ikke klarer å laste noen sider, kontroller data- eller Wi-Fi-forbindelsen til enheten din.
    • +
    + ]]>
    + + + Tilkoblingen fikk tidsavbrudd + + + Det forespurte nettstedet svarte ikke på en tilkoblingsforespørsel, og nettleseren har sluttet å vente på svar.

    +
      +
    • Kan det hende at nettstedet har unormalt høy belastning akkurat nå, eller er midlertidig utilgjengelig? Prøv igjen senere.
    • +
    • Klarer du å koble til andre nettsted? Kontroller at datamaskinens nettverkstilkobling virker.
    • +
    • Er datamaskinen din beskyttet av en brannmur eller proxy? Feilaktige innstillinger kan gjøre det umulig å få tilgang til Internett.
    • +
    • Har du fortsatt problemer? Kontakt systemansvarlig eller Internett-tilbyderen for mer hjelp.
    • +
    + ]]>
    + + + Kan ikke koble til + + + +
  • Siden kan være midlertidig utilgjengelig, eller opptatt. Prøv igjen om en stund.
  • +
  • Hvis du ikke klarer å laste noen sider, kontroller data- eller Wi-Fi-forbindelsen til enheten din.
  • + + ]]>
    + + + Uventet svar fra server + + + Nettstedet svarte på en forespørsel på en uventet måte, og nettleseren kan ikke fortsette.

    + ]]>
    + + + Nettsiden videresender ikke ordentlig + + + Nettleseren har sluttet å forsøke å laste det ønskede elementet. Nettsiden omdirigerer forespørselen på en måte slik at den aldri vil fullføre.

    +
      +
    • Har du slått av eller blokkert infokapsler som er påkrevd av dette nettstedet?
    • +
    • Dersom det ikke hjelper å akseptere nettstedets infokapsler, kan det være et problem med nettstedets innstillinger, og problemet gjelder da ikke datamaskinen din.
    • +
    + ]]>
    + + + Frakoblet modus + + + Nettleseren er i frakoblet modus, og kan ikke koble til serveren.

    +
      +
    • Er datamaskinen koblet til et aktivt nettverk?
    • +
    • Trykk «Prøv igjen» for å bytte til tilkoblet modus og laste siden på nytt.
    • +
    + ]]>
    + + + Porten er adgangsbegrenset av sikkerhetsårsaker + + + Forespørselsadressen oppgav en port (f.eks. mozilla.org:80 for port 80 på mozilla.org) som normalt brukes til et annet formål enn nettlesing. Nettleseren har avbrutt forespørselen av sikkerhetsårsaker.

    + ]]>
    + + + Tilkoblingen ble avbrutt + + + Nettverkskoblingen ble avbrutt under forhandlinger om en tilkobling. Prøv igjen.

    +
      +
    • Siden kan være midlertidig utilgjengelig, eller opptatt. Prøv igjen om en stund.
    • +
    • Hvis du ikke klarer å laste noen sider, kontroller data- eller Wi-Fi-forbindelsen til enheten din.
    • +
    + ]]>
    + + + Usikker filtype + + + +
  • Kontakt eieren av nettstedet og informer dem om dette problemet.
  • + + ]]>
    + + + Ødelagt innholdsfeil + + + Siden du forsøker å vise kan ikke åpnes på grunn av en feil i dataoverføringen.

    +
      +
    • Kontakt nettstedseierne og informer dem om dette problemet.
    • +
    + ]]>
    + + + Innhold krasjet + + Siden du forsøker å vise kan ikke åpnes på grunn av en feil i dataoverføringen.

    +
      +
    • Kontakt nettstedseierne og informer dem om dette problemet.
    • +
    + ]]>
    + + + Feil med tegnkoding + + Nettsiden du forsøker å åpne kan ikke vises fordi den bruker en ukjent eller ugyldig komprimeringsmetode.

    +
      +
    • Kontakt den ansvarlige for nettstedet og informer dem om problemet.
    • +
    + ]]>
    + + + Adressen ikke funnet + + + Nettleseren kunne ikke finne serveren for den oppgitte adressen.

    +
      +
    • Kontroller adressen for skrivefeil som f.eks + ww.example.com i stedet for + www.example.com.
    • +
    • Hvis du ikke kan laste noen sider, kan du sjekke enhetens data- eller Wi-Fi-tilkobling.
    • +
    + ]]>
    + + + Ingen internettforbindelse + + Sjekk nettverkstilkoblingen din, eller prøv å laste siden på nytt om en stund. + + Last på nytt + + + Ugyldig adresse + Den oppgitte adressen er ikke i et gjenkjennbart format. Se i adresselinjen om det er skrivefeil, og prøv igjen.

    + ]]>
    + + Adressen er ugyldig + + + +
  • Nettadresser skrives vanligvis som http://www.example.com/
  • +
  • Pass på at du bruker framover-skråstrek (dvs. /).
  • + + ]]>
    + + + Ukjent protokoll + + Adressen oppgir en protokoll (f.eks. wxyz://) som nettleseren ikke forstår, slik at den ikke kan koble ordentlig til serveren.

    +
      +
    • Prøver du å få tilgang til multimedia eller annen ikke-tekstlig kilde? Sjekk om nettstedet oppgir at du trenger andre programmer i tillegg.
    • +
    • Noen protokoller krever at tredjeparts programvare eller programtillegg er tilgjengelig, før nettleseren kan gjenkjenne dem.
    • +
    + ]]>
    + + + Fant ikke filen + + +
  • Kan filen ha endret navn, blitt fjernet, eller kanskje endret plassering?
  • +
  • Er navnet skrevet korrekt, er stor bokstav liten bokstav byttet om, eller er det andre typografiske feil i adressen?
  • +
  • Har du tilstrekkelige tilgangsprivilegier for å åpne filen?
  • + + ]]>
    + + + Tilgang til filen ble nektet + + +
  • Den kan ha blitt fjernet, flyttet eller filrettighetene forhindrer tilgang.
  • + + ]]>
    + + + Proxy avviste tilkoblingen + + Nettleseren er innstilt på å bruke en proxy, men proxy avviste tilkoblingen.

    +
      +
    • Er nettleserens proxyinnstillinger korrekte? Kontroller innstillingene og prøv igjen.
    • +
    • Tillater proxyen tilkoblinger fra dette nettverket?
    • +
    • Har du fortsatt problem? Kontakt nettverksansvarlig eller Internett-tilbyderen din for mer hjelp.
    • +
    + ]]>
    + + + Proxy server ikke funnet + + Nettleseren er innstilt på å bruke en proxy, men nettleseren klarte ikke å finne proxyen.

    +
      +
    • Er nettleserens proxyinnstillinger korrekte? Kontroller innstillingene og prøv igjen.
    • +
    • Er datamaskinen tilkoblet et aktivt nettverk?
    • +
    • Har du fortsatt problem? Kontakt nettverksansvarlig eller Internett-tilbyder for mer hjelp.
    • +
    + ]]>
    + + + Problemer med skadelig kode + + + Nettstedet på %1$s er rapportert som et angrepsnettsted, og er blokkert på grunnlag av sikkerhetsinnstillingene dine.

    + ]]>
    + + + Problemer med uønsket programvarenettsted + + Nettstedet på %1$s er rapportert som at det leverer uønsket programvare, og er blokkert basert på sikkerhetsinnstillingene dine.

    + ]]>
    + + + Problemer med angrepsnettsted + + Nettstedet på %1$s er rapportert som et angrepsnettsted, og er blokkert på grunnlag av sikkerhetsinnstillingene dine.

    + ]]>
    + + + Problemer med villedende nettsted + + Denne nettsiden på %1$s er rapportert som et villedende nettsted, og er blokkert basert på sikkerhetsinnstillingene dine.

    + ]]>
    + + + Sikkert nettsted er ikke tilgjengelig + + %1$s er ikke tilgjengelig.]]> + + Fortsett til HTTP-nettstedet +
    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 @@ + + + + पुनः प्रयास गर्नुहोस् + + + अनुरोध पूर्ण गर्न सकिएन + + यस समस्या वा त्रुटिको बारेमा अतिरिक्त जानकारी हाल उपलब्ध छैन।

    ]]>
    + + + सुरक्षित जडान असफल भयो + + + +
  • तपाईंले हेर्न खोज्नु भएको पृष्ठ देखाउन सकिँदैन किनभने प्राप्त डाटाको प्रमाणिकता प्रमाणित गर्न सकिएन।
  • +
  • कृपया वेबसाइट मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउनको लागि सम्पर्क गर्नुहोस्।
  • + + ]]>
    + + + सुरक्षित जडान असफल भयो + + + +
  • यो सर्भरको कन्फिगरेसनमा समस्या हुन सक्छ, वा कसैले सर्भरको प्रतिरूपण गर्न खोजेको हुन सक्छ।
  • +
  • यदि तपाईंले विगतमा यो सर्भरमा सफलतापूर्वक जडान गर्नुभएको छ भने, त्रुटि अस्थायी हुन सक्छ, र तपाईंले केही बेर पछि पुन: प्रयास गर्न सक्नुहुन्छ।
  • + + ]]>
    + + + उन्नत… + + कसैले साइटको प्रतिरूपण गर्ने प्रयास गरिरहेको हुन सक्छ र तपाईंले जारी राख्नु हुँदैन। +

    + + ]]>
    + + पछाडि जानुहोस् (सिफारिस गरिएको) + + जोखिम स्वीकार्नुहोस् र जारी राख्नुहोस् + + + जडान अवरूद्ध भयो + + + ब्राउजर सफलतापूर्वक जडान भयो, तर जानकारी स्थानान्तरण गर्दा जडान अवरुद्ध भयो। कृपया पुन: प्रयास गर्नुहोस्।

    +
      +
    • साइट अस्थायी रूपमा अनुपलब्ध वा धेरै व्यस्त हुन सक्छ। केही क्षणमा पुन: प्रयास गर्नुहोस्।
    • +
    • यदि तपाईं कुनै पनि पृष्ठहरू लोड गर्न असमर्थ हुनुहुन्छ भने, आफ्नो उपकरणको डाटा वा Wi-Fi जडान जाँच गर्नुहोस्।
    • +
    + ]]>
    + + + जडान अवधि सकियो + + + अनुरोध गरिएको साइटले जडान अनुरोधको जवाफ दिएन र ब्राउजरले जवाफको प्रतिक्षा गर्न छोडेको छ।

    +
      +
    • के सर्भरले उच्च माग वा अस्थायी आउटेजको अनुभव गरिरहेको हुन सक्छ? पछि पुन: प्रयास गर्नुहोस्।
    • +
    • के तपाइँ अन्य साइटहरू ब्राउज गर्न असमर्थ हुनुहुन्छ? उपकरणको नेटवर्क जडान जाँच गर्नुहोस्।
    • +
    • तपाईँको उपकरण वा नेटवर्क फायरवाल वा प्रोक्सी द्वारा सुरक्षित छ? गलत सेटिङहरूले वेब ब्राउजिङमा हस्तक्षेप गर्न सक्छ।
    • +
    • अझै पनि समस्या भइरहेको छ? सहायताको लागि आफ्नो नेटवर्क प्रशासक वा इन्टरनेट प्रदायकसँग परामर्श गर्नुहोस्।
    • +
    + ]]>
    + + + जडान हुन सकेन + + + +
  • साइट अस्थायी रूपमा अनुपलब्ध वा धेरै व्यस्त हुन सक्छ। केही क्षणमा पुन: प्रयास गर्नुहोस्।
  • +
  • यदि तपाईं कुनै पनि पृष्ठहरू लोड गर्न असमर्थ हुनुहुन्छ भने, आफ्नो उपकरणको डाटा वा Wi-Fi जडान जाँच गर्नुहोस्।
  • + + ]]>
    + + + सर्भरबाट अप्रत्याशित प्रतिक्रिया + + + साइटले अप्रत्याशित तरिकाले नेटवर्क अनुरोधलाई प्रतिक्रिया दियो र ब्राउजरले कार्य जारी राख्न सक्दैन।

    + ]]>
    + + + पृष्ठ राम्रोसँग पुनः निर्देशित भइरहेको छैन + + + ब्राउजरले अनुरोध गरिएको वस्तु पुन: प्राप्त गर्ने प्रयास रोकेको छ। साइटले अनुरोधलाई कहिल्यै पूरा नहुने तरिकामा पुनः निर्देशित गर्दैछ।

    +
      +
    • के तपाईंले यस साइटलाई आवश्यक पर्ने कुकीजहरू असक्षम वा अवरुद्ध गर्नुभएको छ?
    • +
    • यदि साइटको कुकीजहरू स्वीकार गर्दा समस्या समाधान हुँदैन, यो सम्भवतः सर्भर कन्फिगरेसन समस्या हो र तपाईंको उपकरण होइन।
    • +
    + ]]>
    + + + अफलाइन मोड + + + ब्राउजरले अफलाइन मोडमा काम गरिरहेको छ र अनुरोध गरिएको वस्तुमा जडान हुन सक्दैन।

    +
      +
    • के तपाइँको यन्त्र सक्रिय नेटवर्कमा जोडिएको छ?
    • +
    • अनलाइन मोडमा स्विच गर्न र पृष्ठ पुन: लोड गर्न "पुनः प्रयास गर्नुहोस्" थिच्नुहोस्।
    • +
    + ]]>
    + + + सुरक्षा कारणहरुका लागि पोर्ट प्रतिबन्धित छ + + + अनुरोध गरिएको ठेगानाले पोर्ट निर्दिष्ट गरेको छ (उदाहरणका लागि, mozilla.org मा पोर्ट 80 को लागि mozilla.org:80) सामान्यतया वेब ब्राउजिङ्ग भन्दा अन्य उद्देश्यका लागि प्रयोग गरिन्छ। ब्राउजरले तपाईंको सुरक्षा र सुरक्षाको लागि अनुरोध रद्द गरेको छ।

    + ]]>
    + + + जडान रिसेट गरिएको थियो + + + एक जडान कुराकानी गर्दा नेटवर्क लिङ्क अवरोध भयो। कृपया पुन: प्रयास गर्नुहोस्।

    +
      +
    • साइट अस्थायी रूपमा अनुपलब्ध वा धेरै व्यस्त हुन सक्छ। केही क्षणमा पुन: प्रयास गर्नुहोस्।
    • +
    • यदि तपाईं कुनै पनि पृष्ठहरू लोड गर्न असमर्थ हुनुहुन्छ भने, आफ्नो उपकरणको डाटा वा Wi-Fi जडान जाँच गर्नुहोस्।
    • +
    + ]]>
    + + + असुरक्षित फाइल प्रकार + + + +
  • कृपया वेबसाइटका मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउन सम्पर्क गर्नुहोस्।
  • + + ]]>
    + + + भ्रष्ट सामग्री त्रुटि + + तपाईले हेर्न खोज्नु भएको पृष्ठ देखाउन सकिँदैन किनभने डाटा प्रसारणमा त्रुटि पत्ता लाग्यो।

    +
      +
    • कृपया वेबसाइटका मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउन सम्पर्क गर्नुहोस्।
    • +
    + ]]>
    + + + सामग्री क्रास भयो + + तपाईले हेर्न खोज्नु भएको पृष्ठ देखाउन सकिँदैन किनभने, डाटा प्रसारणमा त्रुटि पत्ता लाग्यो।

    +
      +
    • कृपया वेबसाइटका मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउन सम्पर्क गर्नुहोस्।
    • +
    + ]]>
    + + + सामग्री इन्कोडिङ्ग त्रुटि + तपाईले हेर्न खोज्नु भएको पृष्ठ देखाउन सकिँदैन किनभने, यसले कम्प्रेसनको अमान्य वा असमर्थित रूप प्रयोग गर्दछ।

    +
      +
    • कृपया वेबसाइट मालिकहरूलाई यस समस्याको बारेमा जानकारी गराउन सम्पर्क गर्नुहोस्।
    • +
    + ]]>
    + + + ठेगाना फेला परेन + + + ब्राउजरले प्रदान गरिएको ठेगानाको लागि होस्ट सर्भर फेला पार्न सकेन।

    +
      +
    • टाइपिङ्ग त्रुटिहरूको लागि ठेगाना जाँच गर्नुहोस् जस्तै + ww.example.com को सट्टा + www.example.com।
    • +
    • यदि तपाईं कुनै पनि पृष्ठहरू लोड गर्न असमर्थ हुनुहुन्छ भने, आफ्नो उपकरणको डाटा वा Wi-Fi जडान जाँच गर्नुहोस्।
    • +
    + ]]>
    + + + इन्टरनेट जडान छैन + + तपाईंको नेटवर्क जडान जाँच गर्नुहोस् वा केही क्षणमा पृष्ठ पुन: लोड गर्न प्रयास गर्नुहोस् । + + + पुनः लोड गर्नुहोस् + + + अवैध ठेगाना + प्रदान गरिएको ठेगाना मान्यता प्राप्त ढाँचामा छैन। कृपया, गल्तीहरूको लागि स्थान पट्टी जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्।

    + ]]>
    + + यो ठेगाना मान्य छैन + + + +
  • वेब ठेगानाहरू सामान्यतया लेखिएका हुन्छन् http://www.example.com/
  • +
  • तपाईंले फर्वार्ड स्ल्यासहरू प्रयोग गरिरहनुभएको छ भन्ने कुरा सुनिश्चित गर्नुहोस् (जस्तै /)।
  • + + ]]>
    + + + अज्ञात प्रोटोकल + + ठेगानाले एउटा प्रोटोकल निर्दिष्ट गर्दछ (जस्तै, wxyz://) ब्राउजरले चिन्न सक्दैन, त्यसैले ब्राउजरले साइटमा राम्रोसँग जडान गर्न सक्दैन।

    +
      +
    • के तपाइँले मल्टिमिडिया वा अन्य गैर-पाठ सेवाहरू पहुँच गर्न प्रयास गर्दै हुनुहुन्छ? अतिरिक्त आवश्यकताहरूको लागि साइट जाँच गर्नुहोस्।
    • +
    • केही प्रोटोकलहरूलाई ब्राउजरले पहिचान गर्न सक्नु अघि तेस्रो-पक्ष सफ्टवेयर वा प्लगइनहरू आवश्यक हुन सक्छ।
    • +
    + ]]>
    + + + फाइल फेला परेन + + +
  • वस्तुको पुन: नामाकरण, हटाइएको वा स्थानान्तरण गर्न सकिन्छ?
  • +
  • ठेगानामा हिज्जे, वा अन्य टाइपोग्राफिकल त्रुटि छ?
  • +
  • के तपाइँसँग अनुरोध गरिएको वस्तुमा पर्याप्त पहुँच अनुमतिहरू छन्?
  • + + ]]>
    + + + फाइलसम्मको पहुँच अस्वीकृत भयो + + +
  • यो हटाइएको, सारिएको हुन सक्छ, वा फाइल अनुमतिहरूले पहुँच रोक्न सक्छ।
  • + + ]]>
    + + + प्रोक्सी सर्भरले जडान अस्वीकार गर्यो + + ब्राउजरलाई प्रोक्सी सर्भर प्रयोग गर्न कन्फिगर गरिएको छ, तर प्रोक्सीले जडान अस्वीकार गर्यो।

    +
      +
    • के ब्राउजरको प्रोक्सी कन्फिगरेसन सही छ? सेटिङ्गहरू जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्।
    • +
    • के प्रोक्सी सेवाले यस नेटवर्कबाट जडानहरूलाई अनुमति दिन्छ?
    • +
    • अझै पनि समस्या भइरहेको छ? सहायताको लागि आफ्नो नेटवर्क प्रशासक वा इन्टरनेट प्रदायकसँग परामर्श गर्नुहोस्।
    • +
    + ]]>
    + + + प्रोक्सी सर्भर फेला परेन + + ब्राउजरलाई प्रोक्सी सर्भर प्रयोग गर्न कन्फिगर गरिएको छ, तर प्रोक्सी फेला पार्न सकिएन।

    +
      +
    • के ब्राउजरको प्रोक्सी कन्फिगरेसन सही छ? सेटिङ्गहरू जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्।
    • +
    • के यन्त्र सक्रिय नेटवर्कमा जोडिएको छ?
    • +
    • अझै पनि समस्या भइरहेको छ? सहायताको लागि आफ्नो नेटवर्क प्रशासक वा इन्टरनेट प्रदायकसँग परामर्श गर्नुहोस्।
    • +
    + ]]>
    + + + मालवेयर साइट मामिला + + + %1$s मा रहेको साइटलाई आक्रमण साइटको रूपमा प्रतिबेदन गरिएको छ र तपाईंको सुरक्षा प्राथमिकताहरूको आधारमा ब्लक गरिएको छ।

    + ]]>
    + + + नरुचाइएको साइट मामिला + + + %1$s मा रहेको साइटले नचाहिने सफ्टवेयर सेवा गरिरहेको रिपोर्ट गरिएको छ र तपाईंको सुरक्षा प्राथमिकताहरूको आधारमा ब्लक गरिएको छ।

    + ]]>
    + + + हानिकारक साइट मामिला + + + %1$s मा रहेको साइटलाई सम्भावित रूपमा हानिकारक साइटको रूपमा रिपोर्ट गरिएको छ र तपाईंको सुरक्षा प्राथमिकताहरूको आधारमा ब्लक गरिएको छ।

    + ]]>
    + + + भ्रामक साइट मामिला + + %1$s मा रहेको यो वेब पृष्ठलाई भ्रामक साइटको रूपमा रिपोर्ट गरिएको छ र तपाईंको सुरक्षा प्राथमिकताहरूको आधारमा ब्लक गरिएको छ।

    + ]]>
    + + + सुरक्षित साइट उपलब्ध छैन + + %1$s को HTTPS संस्करण उपलब्ध छैन।]]> + + HTTP साइटमा जारी राख्नुहोस् +
    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 @@ + + + + + Opnieuw proberen + + + Kan aanvraag niet voltooien + + + Extra informatie over dit probleem of deze fout is momenteel niet beschikbaar.

    + ]]>
    + + + Beveiligde verbinding mislukt + + + +
  • De pagina die u wilt bekijken kan niet worden weergegeven, omdat de echtheid van de ontvangen gegevens niet kon worden geverifieerd.
  • +
  • Neem contact op met de website-eigenaars om ze over dit probleem te informeren.
  • + + +]]>
    + + + Beveiligde verbinding mislukt + + + +
  • Dit kan een probleem met de serverconfiguratie zijn, of iemand probeert de server na te bootsen.
  • +
  • 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.
  • + + ]]>
    + + + Geavanceerd… + + Iemand kan proberen de website na te doen en u moet niet verdergaan. +

    + + ]]>
    + + Teruggaan (Aanbevolen) + + Het risico aanvaarden en doorgaan + + + Deze website vereist een beveiligde verbinding. + + +
  • De pagina die probeert te bekijken kan niet worden getoond, omdat deze website een beveiligde verbinding vereist.
  • +
  • Het issue ligt waarschijnlijk bij de website en er is niets dat u kunt doen om het op te lossen.
  • +
  • U kunt de beheerder van de website op de hoogte stellen van het probleem.
  • + + ]]>
    + + + Geavanceerd… + + + %1$s heeft een beveiligingsbeleid genaamd HTTP Strict Transport Security (HSTS), wat betekent dat %2$s er alleen beveiligd mee kan verbinden. U kunt geen uitzondering toevoegen om deze website te bezoeken. + ]]> + + Terug + + + De verbinding werd onderbroken + + + De browser is succesvol verbonden, maar de verbinding is verbroken tijdens het overbrengen van informatie. Probeer het opnieuw.

    +
      +
    • De website kan tijdelijk niet beschikbaar of te druk zijn. Probeer het over een paar seconden opnieuw.
    • +
    • Als u geen pagina’s kunt laden, controleer dan de gegevens of Wi-Fi-verbinding van uw apparaat.
    • +
    + ]]>
    + + + De wachttijd voor de verbinding is verstreken + + + De opgevraagde website heeft niet op een verbindingsaanvraag geantwoord, en de browser wacht niet meer op een antwoord.

    +
      +
    • Misschien wordt de server zwaar belast of is deze tijdelijk onbereikbaar? Probeer het later opnieuw.
    • +
    • Kunt u geen andere websites bezoeken? Controleer de netwerkverbinding van de computer.
    • +
    • Wordt uw computer of netwerk beschermd door een firewall of proxy? Onjuiste instellingen kunnen een goede werking tijdens het webbrowsen verstoren.
    • +
    • Hebt u nog steeds problemen? Raadpleeg uw netwerkbeheerder of internetprovider voor assistentie.
    • +
    + ]]>
    + + + Kan geen verbinding maken + + + +
  • Misschien is de website tijdelijk niet beschikbaar of overbelast. Probeer het over enkele ogenblikken opnieuw.
  • +
  • Als u geen enkele pagina kunt laden, controleer dan de gegevens- of wifi-verbinding van uw apparaat.
  • + + ]]>
    + + + Onverwacht antwoord van server + + + De website antwoordde op een onverwachte manier op de netwerkaanvraag, en de browser kan niet doorgaan.

    + ]]>
    + + + De pagina verwijst niet op een juiste manier door + + + 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.

    +
      +
    • Hebt u cookies die nodig zijn voor deze website uitgeschakeld of geblokkeerd?
    • +
    • 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.
    • +
    + ]]>
    + + + Offlinemodus + + + De browser werkt momenteel in offlinemodus en kan geen verbinding maken met het opgevraagde item.

    +
      +
    • Is de computer verbonden met een actief netwerk?
    • +
    • Klik op ‘Opnieuw proberen’ om naar de onlinemodus over te schakelen en de pagina opnieuw te laden.
    • +
    + ]]>
    + + + Poort beperkt om veiligheidsredenen + + + Het opgevraagde adres specificeert een poort (bv. mozilla.org:80 voor poort 80 op mozilla.org) die normaal gesproken voor andere doeleinden dan webbrowsen wordt gebruikt. De browser heeft de aanvraag voor uw bescherming en veiligheid geannuleerd.

    + ]]>
    + + + De verbinding werd geherinitialiseerd + + + De netwerkkoppeling werd onderbroken tijdens het onderhandelen over een verbinding. Probeer het opnieuw.

    +
      +
    • De website is mogelijk tijdelijk niet beschikbaar of te druk. Probeer het over een aantal ogenblikken opnieuw.
    • +
    • Als u geen pagina’s kunt laden, controleer dan de gegevens- of Wi-Fi-verbinding van uw apparaat.
    • +
    + ]]>
    + + + Onveilig bestandstype + + + +
  • Neem contact op met de website-eigenaars om ze over dit probleem te informeren.
  • + + ]]>
    + + + Beschadigde-inhoudsfout + + + De pagina die u wilt bekijken kan niet worden weergegeven, omdat er een fout in de gegevensoverdracht is gedetecteerd.

    + +
      + +
    • Neem contact op met de website-eigenaars om ze over dit probleem te informeren.
    • + +
    + + ]]>
    + + + Inhoud gecrasht + + De pagina die u wilt bekijken kan niet worden weergegeven, omdat er een fout in de gegevensoverdracht is gedetecteerd.

    + +
      + +
    • Neem contact op met de website-eigenaars om ze over dit probleem te informeren.
    • + +
    + + ]]>
    + + + Inhoudcoderingsfout + + De pagina die u wilt bekijken kan niet worden weergegeven, omdat deze gebruikmaakt van een ongeldige of niet-ondersteunde vorm van compressie.

    + +
      + +
    • Neem contact op met de website-eigenaars om ze over dit probleem te informeren.
    • + +
    + + ]]>
    + + + Adres niet gevonden + + + De browser kon de hostserver voor het opgegeven adres niet vinden.

    + +
      +
    • Controleer het adres op typefouten, zoals + + ww.example.com in plaats van + + www.example.com.
    • + +
    • Als u geen pagina’s kunt laden, controleer dan de gegevens- of wifi-verbinding van uw apparaat.
    • + +
    + + ]]>
    + + + Geen internetverbinding + + Controleer uw netwerkverbinding of probeer de pagina over enkele ogenblikken opnieuw te laden. + + Opnieuw laden + + + Ongeldig adres + Het opgegeven adres heeft geen herkenbare indeling. Controleer de locatiebalk op fouten en probeer het opnieuw.

    + + ]]>
    + + Het adres is niet geldig + + + + +
  • Webadressen worden doorgaans geschreven als http://www.example.com/
  • + +
  • Let erop dat u voorwaartse slashes gebruikt (d.i. /).
  • + + + + ]]>
    + + + Onbekend protocol + + Het adres specificeert een protocol (bv. wxyz://) dat de browser niet herkent, waardoor de browser niet op een juiste manier met de website kan verbinden.

    + +
      + +
    • Probeert u toegang te krijgen tot multimedia- of andere niet-tekstservices? Controleer de website op extra benodigdheden.
    • + +
    • Sommige protocollen kunnen software of plug-ins van derden vereisen voordat de browser ze kan herkennen.
    • + +
    + + ]]>
    + + + Bestand niet gevonden + + + +
  • Kan het item zijn hernoemd, verwijderd of verplaatst?
  • + +
  • Staat er een spel-, hoofdletter- of andere typografische fout in het adres?
  • + +
  • Hebt u voldoende toegangsrechten voor het opgevraagde item?
  • + + + + ]]>
    + + + Toegang tot het bestand is geweigerd + + + +
  • Het kan zijn verwijderd, verplaatst, of bestandsmachtigingen kunnen toegang verhinderen.
  • + + + + +]]>
    + + + Proxyserver weigerde verbinding + + De browser is geconfigureerd om een proxyserver te gebruiken, maar de proxy weigerde een verbinding.

    + +
      + +
    • Is de proxyconfiguratie van de browser in orde? Controleer de instellingen en probeer het opnieuw.
    • + +
    • Staat de proxyservice verbindingen van dit netwerk toe?
    • + +
    • Hebt u nog steeds problemen? Raadpleeg uw netwerkbeheerder of internetprovider voor assistentie.
    • + +
    + + ]]>
    + + + Proxyserver niet gevonden + + De browser is geconfigureerd om een proxyserver te gebruiken, maar de proxy kon niet worden gevonden.

    + +
      + +
    • Is de proxyconfiguratie van de browser in orde? Controleer de instellingen en probeer het opnieuw.
    • + +
    • Is de computer verbonden met een actief netwerk?
    • + +
    • Hebt u nog steeds problemen? Raadpleeg uw netwerkbeheerder of internetprovider voor assistentie.
    • + +
    + + ]]>
    + + + Probleem met malware op website + + De website op %1$s is gerapporteerd als een aanvalsite en is geblokkeerd op basis van uw beveiligingsvoorkeuren.

    + + ]]>
    + + + Probleem met ongewenste website + + De website op %1$s is gerapporteerd als een website die ongewenste software aanbiedt en is geblokkeerd op basis van uw beveiligingsvoorkeuren.

    + + ]]>
    + + + Probleem met schadelijke website + + De website op %1$s is gerapporteerd als een mogelijk schadelijke website en is geblokkeerd op basis van uw beveiligingsvoorkeuren.

    + + ]]>
    + + + Probleem met misleidende website + + De website op %1$s is gerapporteerd als een misleidende website en is geblokkeerd op basis van uw beveiligingsvoorkeuren.

    + + ]]>
    + + + Beveiligde website niet beschikbaar + + %1$s is niet beschikbaar.]]> + + Doorgaan naar HTTP-website +
    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 @@ + + + + + Prøv igjen + + + Klarte ikkje å fullføre førespurnaden + + + Meir informasjon om denne feilen er ikkje tilgjengeleg.

    + ]]>
    + + + Sikker tilkopling feila + + + +
  • Sida du prøver å opne kan ikkje visast fordi det ikkje kunne stadfestast at overførte data er autentiske.
  • +
  • Kontakt nettstadeigaren og informer om problemet.
  • + + ]]>
    + + + Sikker tilkopling feila + + + +
  • Dette kan vere på grunn av eit problem med innstillingane til serveren, eller det kan vere at nokon prøver å forfalske tilkoplinga til serveren.
  • +
  • Dersom du tidlegare har kopla til serveren kan det vere at feilen er kortvarig, og du kan prøve igjen seinare.
  • + + ]]>
    + + + Avansert… + + Nokon prøver å etterlikne nettsida, og du bør ikkje fortsetje. +

    + + ]]>
    + + Gå tilbake (Tilrådd) + + Godta risikoen og fortset + + + Denne nettstaden krev ei trygg tilkopling. + + + +
  • Sida du prøver å vise kan ikkje visast fordi denne nettstaden krev ei trygg tilkopling.
  • +
  • Problemet er mest sannsynleg på nettstaden, og det er ingenting du kan gjere for å løyse det.
  • +
  • Du kan varsle administrator for nettstaden om problemet.
  • + + ]]>
    + + + Avansert… + + + %1$s har ein sikkerheitspolicy kalt HTTP Strict Transport Security (HSTS), som tyder at %2$s berre kan kople til den sikkert. Du kan ikkje leggje til eit unntak for å besøkje dette nenne nettstaden. + ]]> + + Gå tilbake + + + Tilkoplinga vart avbroten + + + Nettlesaren vart tilkopla, men tilkoplinga vart avbroten under overføring av informasjon. Prøv igjen.

    +
      +
    • Sida kan vere kortvarig utilgjengeleg, eller opptatt. Prøv igjen om ei stund.
    • +
    • Viss du ikkje klarer å laste sider, kontroller data- eller Wi-Fi-sambandet til eininga di.
    • +
    + ]]>
    + + + Tilkoplinga fekk tidsavbrot + + + Nettstaden du spurde etter svarte ikkje på ein tilkoplingsførespurnad og nettlesaren har slutta å vente på svar.

    +
      +
    • Kan det hende at nettstaden har unormal høg belastning akkurat no, eller er mellombels utilgjengeleg? Prøv igjen seinare.
    • +
    • Klarer du å kople til andre nettstadar? Kontroller at nettverkstilkoplinga til datamaskina verkar.
    • +
    • Er datamaskina di verna av ein brannmur eller proxy? Feil-innstillinger kan hindre tilgang til nettet.
    • +
    • Har du framleis problem? Kontakt systemansvarleg eller Internett-tilbydar for meir hjelp.
    • +
    + ]]>
    + + + Kan ikkje kople til + + + +
  • Sida kan vere mellombels utilgjengeleg, eller opptatt. Prøv igjen om ei stund.
  • +
  • Viss du ikkje klarer å laste nokre sider, kontroller data- eller Wi-Fi-sambandet til eininga di.
  • + + ]]>
    + + + Uventa svar frå server + + + Nettstaden svarte på ein førespurnad på ein uventa måte, og nettlesaren kan ikkje fortsetje.

    + ]]>
    + + + Nettsida vidaresender ikkje skikkeleg + + + Nettlesaren har slutta å prøve å laste det ønskte elementet. Nettstaden omdirigerer førespurnaden på ein måte slik at den aldri vil fullføre.

    +
      +
    • Har du slått av eller blokkert infokapslar som er påkravd av denne nettstaden?
    • +
    • Dersom det ikkje hjelper å godta infokapslane til nettstaden, kan det vere eit problem med innstillingane til nettstaden, og ikkje noko problem på datamaskina di.
    • +
    + ]]>
    + + + Fråkopla-modus + + + Nettlesaren er i fråkopla modus, og kan ikkje kople til serveren.

    +
      +
    • Er datamaskina kopla til eit aktivt nettverk?
    • +
    • Trykk «Prøv igjen» for å byte til tilkopla modus og laste sida på nytt.
    • +
    + ]]>
    + + + Porten har sikkerheitsrestriksjonar + + + Den førespurde adressa spesifiserte ein port (t.d. mozilla.org:80 for port 80 på mozilla.org) som normalt vert brukt til eit anna føremål enn nettlesing. Nettlesaren braut av førespurnaden av sikkerheitsårsaker.

    + ]]>
    + + + Tilkoplinga vart avbroten + + + Nettverkskoplinga vart avbroten under forhandlingar om ei tilkopling. Prøv igjen.

    +
      +
    • Sida kan vere mellombels utilgjengeleg, eller opptatt. Prøv igjen om ei stund.
    • +
    • Viss du ikkje klarer å laste nokre sider, kontroller data- eller Wi-Fi-sambandet til eininga di.
    • +
    + ]]>
    + + + Usikker filtype + + + +
  • Kontakt eigaren av nettstaden og informer dei om dette problemet.
  • + + ]]>
    + + + Øydelagd innhaldsfeil + + + Sida du prøver å vise kan ikkje opnast på grunn av ein feil i dataoverføringa.

    +
      +
    • Kontakt nettstadeigarane og informer dei om dette problemet.
    • +
    + ]]>
    + + + Innhaldet krasja + + Sida du prøver å vise kan ikkje opnast på grunn av ein feil i dataoverføringa.

    +
      +
    • Kontakt nettstadeigarane og informer dei om dette problemet.
    • +
    + ]]>
    + + + Feil med teiknkoding + + Nettsida du prøver å opne kan ikkje visast fordi ho brukar ein ukjend eller ugyldig komprimeringsmetode.

    +
      +
    • Kontakt den ansvarlege for nettstaden og informer dei om problemet.
    • +
    + ]]>
    + + + Fann ikkje adressa + + + Nettlesaren fann ikkje serveren for den oppgitte adressa.

    +
      +
    • Kontroller adressa for skrivefeil som t.d. + ww.example.com i staden for + www.example.com.
    • +
    • Viss du ikkje kan laste nokre sider, kan du sjekke data- eller Wi-Fi-tilkoplinga til eininga.
    • +
    + ]]>
    + + + Inga internetttilkopling + + + Sjekk nettverkstilkoplinga di, eller prøv å laste sida på nytt om ei stund. + + Oppdater + + + Ugyldig adresse + Oppgjeven adresse er ikkje i eit attkjennande format. Sjå i adresselinja om det er skrivefeil, og prøv igjen.

    + ]]>
    + + Adressa er ugyldig + + + +
  • Nettadresser skriv ein på følgjande måte: http://www.example.com/
  • +
  • Pass på at du brukar framover-skråstrek (dvs. /).
  • + + ]]>
    + + + Ukjend protokoll + + Adressa spesifiserer ein protokoll (f.eks. wxyz://) som nettlesaren ikkje forstår, slik at han ikkje kan kople seg skikkeleg til serveren.

    +
      +
    • Prøver du å få tilgang til multimedia eller anna ikkje-tekstleg kjelde? Sjekk om nettstaden krev andre program i tillegg.
    • +
    • Nokre protokollar krev at tredjeparts programvare eller programtillegg er tilgjengelege før nettlesaren kan gjenkjenne dei.
    • +
    + ]]>
    + + + Fann ikkje fila + + +
  • Kan fila ha endra namn, blitt fjerna, eller kanskje endra plassering?
  • +
  • Er namnet skrive korrekt, er stor og liten bokstav bytt om, eller er det andre typografiske feil i adressa?
  • +
  • Har du tilstrekkelege tilgangsprivilegium for å opne fila?
  • + + ]]>
    + + + Tilgang til fila vart nekta + + +
  • Den kan ha blitt fjerna, flytta eller filrettane hindrar tilgang.
  • + + ]]>
    + + + Proxy avviste tilkoplinga + + Nettlesaren er innstilt på å bruke ein proxy, men proxyen avviste tilkoplinga.

    +
      +
    • Er proxyinnstillingane til nettlesaren korrekte? Kontroller innstillingane og prøv igjen.
    • +
    • Tillèt proxyen tilkoplingar frå dette nettverket?
    • +
    • Har du framleis problem? Kontakt nettverksansvarleg eller Internett-tilbydaren din for meir hjelp.
    • +
    + ]]>
    + + + Fann ikkje proxy-serveren + + Nettlesaren er innstilt på å bruke ein proxy, men nettlesaren klarte ikkje å finne proxyen.

    +
      +
    • Er proxyinnstillingane til nettlesaren korrekte? Kontroller innstillingane og prøv igjen.
    • +
    • Er datamaskina tilkopla eit aktivt nettverk?
    • +
    • Har du framleis problem? Kontakt nettverksansvarleg eller Internett-tilbydar for meir hjelp.
    • +
    + ]]>
    + + + Problem med skadeleg kode + + + Nettstaden på %1$s er rapportert som ein angrepsnettstad, og er blokkert på grunnlag av sikkerheitsinnstillingane dine.

    + ]]>
    + + + Problem med uønskt programvarenettstad + + + Nettstaden på %1$s er rapportert som at det leverer uønskt programvare, og er blokkert basert på sikkerheitsinnstillingane dine.

    + ]]>
    + + + Problem med angrepsnettstad + + + Nettstaden på %1$s er rapportert som ein angrepsnettstad, og er blokkert på grunnlag av sikkerheitsinnstillingane dine.

    + ]]>
    + + + Problemer med villeiande nettstad + + Denne nettsida på %1$s er rapportert som ein villeiande nettstad, og er blokkert basert på sikkerheitsinnstillingane dine.

    + ]]>
    + + + Trygg nettstad ikkje tilgjengeleg + + %1$s er ikkje tilgjengeleg.]]> + + Fortset til HTTP-nettstaden +
    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 @@ + + + + + Bínáánítááh + + + Wókeed bóhodoolníłígíí doo bíighah da. + + Díí ʼachʼįʼ nahwiisʼnááʼígíí hazhóʼó baa haneʼígíí kʼad doo hólǫ́ǫ da.

    ]]>
    + +
    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 @@ + + + + + Tornar ensajar + + + Impossible de completar la requèsta + + + I a pas cap d‘informacion disponibla actualament tocant aqueste problèma o error.

    ]]>
    + + + Fracàs de la connexion securizada + + + +
  • La pagina qu’ensajatz de consultar pòt pas èsser afichada perque l’autenticitat de las donadas recebudas pòt pas èsser verificada.
  • +
  • Contactatz los proprietaris del sit Web per los n’assabentar.
  • + ]]>
    + + + La connexion segura a pas capitat + + + +
  • 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.
  • +
  • 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.
  • + ]]>
    + + + Avançat… + + Qualqu’un poiriá usurpar l’identitat del site, deuriatz pas contunhar. +

    + ]]>
    + + Tornar (recomandat) + + Acceptar lo risc e contunhar + + + Aqueste site web requerís una connexion segura. + + + +
  • La pagina qu’ensajatz de visualizar se pòt pas afichar perque aqueste site requerís una connexion segura.
  • +
  • Lo problèma ven probablament del site web e i a pas res que poscatz far per lo resòlver;
  • +
  • Podètz senhalar lo problèma als administrators del site web.
  • + + ]]>
    + + + Avançat… + + + %1$s a una estrategia de seguretat HTTP Strict Transport Security (HSTS), valent a dire que %2$s pòt sonque s’i connectar amb una connexion securizada. Podètz pas apondre d’excepcion per consultar aqueste site. +]]> + + Tornar + + + La connexion es estada interrompuda + + + Lo navegador s’es corrèctament connectat, mas la connexion es estada interrompuda pendent lo transferiment d’informacions. Tornatz ensajar.

    +
      +
    • Lo site es benlèu pas accessible o subrecargat. Tornatz ensajar dins un momenton
    • +
    • Se capitatz pas de navegar sus cap de site, verificatz la connexion de donadas del periferic o la connexion Wi-Fi
    • +
    ]]>
    + + + Relambi d’espèra passat + + + Lo navegador a esperat tròp longtemps al moment de la connexion al site e a arrestat d’esperar una responsa.

    +
      +
    • Benlèu que lo servidor es en suscarga o es temporàriament en pana ? Ensajatz mai tard.
    • +
    • D’autres sites son tanben inaccessibles ? Verificatz la connexion a la ret de vòstre periferic.
    • +
    • 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.
    • +
    • 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.
    • +
    ]]>
    + + + Connexion impossibla + + + +
  • Aqueste site pòt èsser indisponible pel moment o subrecargat. Tornatz ensajar dins una estona.
  • +
  • S’es impossible de telecargar quitament una pagina, verificatz las donadas de connexion o Wifi de vòstre periferic mobil
  • + ]]>
    + + + Responsa inesperada del servidor + + Lo site a respondut a la requèsta de la ret d’una faiçon inesperada e lo navegador pòt pas contunhar.

    ]]>
    + + + Redireccion de pagina incorrècta + + + 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.

    +
      +
    • Avètz desactivat o blocat los cookies necessaris per aqueste site ?
    • +
    • 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.
    ]]>
    + + + Mòde fòra connexion + + + Lo navegador es en mòde fòra connexion e se pòt pas connectar a l’element indicat.

    +
      +
    • L’ordenador es connectat a la ret ?
    • +
    • Clicatz sul boton « Ensajar tornarmai » per tornar en mòde connectat e recargar la pagina.
    • +
    ]]>
    + + + Pòrt restrench per de rasons de seguretat. + + + L’adreça demandada indica un pòrt (per ex.mozilla.org:80 pel pòrt 80 sus mozilla.org) qu’es normalament utilizat per d’autres usatges que la navegacion sul Web. Lo navegador a anullat la requèsta per vòstra proteccion e vòstra seguretat.

    ]]>
    + + + La connexion es estada reïnicializada + + + Lo ligam ret es estat copat pendent la negociacion d’una connexion. Tornatz ensajar.

    +
      +
    • Lo site es benlèu pas accessible o subrecargat. Tornatz ensajar dins un momenton
    • +
    • Se capitatz pas de navegar sus cap de site, verificatz la connexion de donadas del periferic o la connexion Wi-Fi
    • +
    ]]>
    + + + Tipe de fichièr pas segur + + + +
  • Contactatz lo webmèstre del site per l’assabentar d’aqueste problèma.
  • + ]]>
    + + + Error deguda a un contengut corromput + + + La pagina qu’ensajatz de veire pòt pas èsser afichada perque una error dins la transmission de donadas es estada detectada.

    +
      +
    • Contactatz los proprietaris del site Web per los assabentar d’aqueste problèma.
    • +
    ]]>
    + + + Lo contengut a plantat + + La pagina qu’ensajatz de veire pòt pas èsser afichada perque una error dins la transmission de donadas es estada detectada.

    +
      +
    • Contactatz los proprietaris del site Web per los assabentar d’aqueste problèma.
    • +
    ]]>
    + + + Error d’encodatge de contengut + + La pagina qu’ensajatz de veire pòt pas èsser afichada perque utiliza un tipe de compression invalid o pas pres en carga.

    +
      +
    • Contactatz lo webmèstre del site Web per l’assabentar d’aqueste problèma.
    • +
    ]]>
    + + + Pagina pas trobada + + + Lo navegador a pas pogut trobar lo servidor òste per l’adreça fornida.

    +
      +
    • Verificatz l’adreça se per cas i a una deca coma + ww.exemple.com allòc de + www.exemple.com.
    • +
    • Se podètz pas cargar cap de pagina, verificatz la connexion de donada del periferic o Wi-Fi.
    • +
    ]]>
    + + + Cap de connexion Internet + + Verificatz vòstra connexion ret o ensajatz de recargar la pagina d’aquí un moment. + + Tornar cargar + + + Adreça invalida + + 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.

    ]]>
    + + L’adreça es pas valida + + + +
  • Las adreças web normalament son aital http://www.exemple.com/
  • +
  • Asseguratz-vos qu’utilizetz las barras inclinadas(/).
  • +]]>
    + + + Protocòl desconegut + + L’adreça indica un protocòl (per ex. wxyz://) desconegut del navegador que doncas se pòt pas connectar corrèctament al site.

    +
      +
    • Ensajatz d’accedir a de contengut multimèdia o d’autres servicis que son pas de tèxte ? Verificatz los prerequesits logicials del site.
    • +
    • D’unes protocòls pòdon necessitar un logiciel tèrç o de plugins per que lo navegador los pòsca reconéisser.
    • +
    ]]>
    + + + Fichièr introbable + + +
  • Lo fichièr es benlèu estat renomenat, suprimit o desplaçat ?
  • +
  • I a una deca de majuscula, d’accent o una autra error ?
  • +
  • Avètz las permissions d’accès que cal per aqueste fichièr ?
  • +]]>
    + + + L’accès al fichièr es estat refusat + + +
  • Benlèu es estat suprimit, bolegat o las permissions del fichièr n’empacharián l’accès.
  • +]]>
    + + + Lo servidor mandatari a refusat la connexion + + Lo navegador es configurat per utilizar un servidor mandatari mas lo proxy a refusat la connexion.

    +
      +
    • La configuracion proxy del navegador es corrècta ? Verificatz los paramètres e tornatz ensajar.
    • +
    • Lo servici proxy autoriza las connexions a partir d’aquesta ret ?
    • +
    • 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.
    • +
    ]]>
    + + + Impossible de trobar lo servidor mandatari + + Lo navegador es configurat per utilizar un servidor mandatari mas lo proxy es introbable.

    +
      +
    • La configuracion proxy del navegador es corrècta ? Verificatz los paramètres e tornatz ensajar.
    • +
    • Lo servici proxy autoriza las connexions a partir d’aquesta ret ?
    • +
    • 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.
    • +
    ]]>
    + + + Problèma de logicial malvolent + + + 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.

    ]]>
    + + + Problèma de site indesirable + + 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.

    ]]>
    + + + Problèma de site perilhós + + Lo site a %1$s es estat senhalat coma un site possiblament malfasent e es estat blocat segon vòstras preferéncias de seguretat.

    ]]>
    + + + Problèma de site enganaire + + 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.

    ]]>
    + + + Site securizat non disponible + + %1$s es pas disponibla.]]> + + Contunhar cap al site HTTP +
    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 @@ + + + + ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ + + + ଅନୁରୋଧକୁ ସମ୍ପୂର୍ଣ୍ଣ କରିହେବ ନାହିଁ + + + ଏହି ସମସ୍ୟା ବିଷୟରେ ଅତିରିକ୍ତ ସୂଚନା କିମ୍ବା ତ୍ରୁଟି ବର୍ତ୍ତମାନ ଉପଲବ୍ଧ ନାହିଁ।

    ]]>
    + + + ସୁରକ୍ଷିତ ସଂଯୋଗ ବିଫଳ ହେଲା + + +  
  • ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବା ପୃଷ୍ଠାକୁ ଦର୍ଶାଯାଇ ପାରିବ ନାହିଁ କାରଣ ଗ୍ରହଣ କରାଯାଇଥିବା ତଥ୍ୟର ବୈଧିକରଣକୁ ଯାଞ୍ଚ କରାଯାଇନାହିଁ।
  •  
  • ଏହି ସମସ୍ୟା ବିଷୟରେ ଅଗଚର କରାଇବା ପାଇଁ ଦୟାକରି ୱେବସାଇଟ ମାଲିକଙ୍କ ସହିତ ସମ୍ପର୍କ କରନ୍ତୁ।
  •  ]]>
    + + + ସୁରକ୍ଷିତ ସଂଯୋଗ ବିଫଳ ହେଲା + +
  • ଏହା ହୁଏତଃ ସର୍ଭର ସଂରଚନା ସମସ୍ୟା ହୋଇପାରେ, କିମ୍ବା ଏହି ସର୍ଭରର କେହି ପରିଚୟ ଚୋରି କରିଥାଇପାରେ।
  • ଯଦି ଆପଣ ଅତୀତରେ ଏହି ସର୍ଭର ସହିତ ସଫଳତାର ସହିତ ସଂଯୁକ୍ତ କରିଛନ୍ତି, ତେବେ ଏହି ତ୍ରୁଟିଟି ଅସ୍ଥାୟୀ ହୋଇପାରେ, ଏବଂ ଆପଣ ପରେ ପୁଣିଥରେ ଚେଷ୍ଟା କରିପାରିବେ।
  • ]]>
    + + + ଉନ୍ନତ… + + ପଛକୁ ଫେରି ଯାଆନ୍ତୁ (ପରାମର୍ଶିତ) + + ଆଶଙ୍କାକୁ ସ୍ୱୀକାର କରନ୍ତୁ ଏବଂ ଆଗାନ୍ତୁ + + + ଉନ୍ନତ… + + + ସଂଯୋଗଟି ବାଧାପ୍ରାପ୍ତ ହୋଇଛି + + + ସଂଯୋଗ ସମୟ ସମାପ୍ତ ହୋଇଯାଇଛି + + ଅନୁରୋଧ କରାଯାଇଥିବା ସାଇଟଟି ସଂଯୋଗ ପାଇଁ ଅନୁରୋଧକୁ ଉତ୍ତର ଦେଉ ନାହିଁ ଏବଂ ବ୍ରାଉଜର ଉତ୍ତରକୁ ଆଉ ଅପେକ୍ଷା କରୁ ନାହିଁ।

    • ସର୍ଭରଟି ଅଧିକ ଭାରାକ୍ରାନ୍ତ ହୋଇଥାଇପାରେ ଅଥବା ଅସ୍ଥାୟୀ ଭାବରେ ଖରାପ ଥାଇପାରେ? ପରେ ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ।
    • ଆପଣ ଅନ୍ୟ ସାଇଟଗୁଡ଼ିକୁ ବ୍ରାଉଜ କରିବାରେ ଅକ୍ଷମ କି? କମ୍ପୁଟରର ନେଟୱର୍କ ସଂଯୋଗକୁ ଯାଞ୍ଚ କରନ୍ତୁ।
    • ଆପଣଙ୍କର କମ୍ପୁଟର କିମ୍ବା ନେଟୱର୍କ ଫାୟାରୱାଲ କିମ୍ବା ପ୍ରକ୍ସି ଦ୍ୱାରା ପ୍ରତିରୋଧିତ କି? ଭୁଲ ସଂରଚନା ୱେବ ବ୍ରାଉଜିଙ୍ଗରେ ବାଧା ସୃଷ୍ଟି କରିପାରେ।
    • ଏପର୍ଯ୍ୟନ୍ତ ସମସ୍ୟା ଅଛି କି? ସହାୟତା ପାଇଁ ଆପଣଙ୍କର ନେଟୱର୍କ ପ୍ରଶାସକଙ୍କ କିମ୍ବା ଇଣ୍ଟରନେଟ ପ୍ରଦାନକାରୀ ସହିତ ଯୋଗାଯୋଗ କରନ୍ତୁ।
    ]]>
    + + + ସଂଯୋଗ କରିବାରେ ଅସମର୍ଥ + + + ସର୍ଭରରୁ ଅପ୍ରତ୍ୟାଶିତ ଉତ୍ତର + + ସାଇଟ ନେଟୱର୍କ ଅନୁରୋଧକୁ ଅପ୍ରତ୍ୟାଶିତ ଭାବରେ ଉତ୍ତର ଦେଇଥାଏ ଏବଂ ବ୍ରାଉଜର ଅଗ୍ରସର ହୋଇନପାରେ।

    ]]>
    + + + ଏହି ପୃଷ୍ଠାଟି ସଠିକ ଭାବରେ ପୁନଃପ୍ରେରଣ କରିପାରୁନାହିଁ + + ବ୍ରାଉଜର ଅନୁରୋଧ କରାଯାଇଥିବା ବସ୍ତୁକୁ କାଢ଼ିବା ବନ୍ଦ କରିଛି। ସାଇଟ ସେହି ଅନୁରୋଧକୁ କଦାପି ସମ୍ପୂର୍ଣ୍ଣ ହେଉନଥିବା ଉପାୟରେ ଦିଗ ପରିବର୍ତ୍ତନ କରୁଅଛି।

    • ଆପଣ ଏହି ସାଇଟ ଦ୍ୱାରା ଆବଶ୍ୟକ କୁକିଗୁଡ଼ିକୁ ନିଷ୍କ୍ରିୟ କରିଛନ୍ତି ଅଥବା ଅଟକ ରଖିଛନ୍ତି?
    • ଯଦି ସାଇଟର କୁକିଗୁଡ଼ିକୁ ଗ୍ରହଣ କରିବା ଦ୍ୱାରା ତାହା ସମସ୍ୟାର ସମାଧାନ କରିନଥାଏ, ତେବେ ଏହା ହୁଏତଃ ଆପଣଙ୍କ କମ୍ପୁଟର ପରିବର୍ତ୍ତେ ସର୍ଭର ସଂରଚନା ସମସ୍ୟା ହୋଇପାରେ।
    ]]>
    + + + ଅଫଲାଇନ ଅବସ୍ଥା + + ବ୍ରାଉଜର ଅଫଲାଇନ ଧାରାରେ ଚାଲୁଅଛି ଏବଂ ଅନୁରୋଧ କରିଥିବା ବସ୍ତୁ ସହିତ ସଂଯୋଗ କରିପାରିବେ ନାହିଁ।

    • କମ୍ପୁଟରଟି ସକ୍ରିୟ ନେଟୱର୍କ ସହିତ ସଂଯୁକ୍ତ ହୋଇଛି କି ନାହିଁ?
    • " କୁ ଦବାନ୍ତୁ ଏବଂ ଅନଲାଇନ ଧାରାକୁ ପରିବର୍ତ୍ତନ କରିବା ପାଇଁ ଏବଂ ପୃଷ୍ଠାକୁ ପୁନର୍ଧାରଣ କରିବା ପାଇଁ " କୁ ପୁଣିଥରେ ଚେଷ୍ଟାକରନ୍ତୁ।
    ]]>
    + + + ସୁରକ୍ଷା କାରଣ ହେତୁ ପୋର୍ଟକୁ ଅଟକାଯାଇଛି + + ଅନୁରୋଧ କରାଯାଇଥିବା ଠିକଣା ଗୋଟିଏ ପୋର୍ଟକୁ ଉଲ୍ଲେଖ କରିଥାଏ (ଯେପରିକି mozilla.org:80 mozilla.org ରେ ପୋର୍ଟ 80 ପାଇଁ) ସାଧାରଣତଃ ୱେବ ବ୍ରାଉଜିଙ୍ଗ ବ୍ୟତୀତ ଅନ୍ୟାନ୍ୟ କାର୍ଯ୍ୟ ପାଇଁ ବ୍ୟବହୃତ ହୋଇଥାଏ। ବ୍ରାଉଜର ଆପଣଙ୍କର ପ୍ରତିରକ୍ଷା ଏବଂ ସୁରକ୍ଷା ପାଇଁ ଅନୁରୋଧକୁ ବାତିଲ କରିଛି।

    ]]>
    + + + ସଂଯୋଗ ପୁନର୍ବିନ୍ୟାସ ହୋଇଥିଲା + + + ଅସୁରକ୍ଷିତ ଫାଇଲ ପ୍ରକାର + + + +
  • ଦୟାକରି ଏହି ସମସ୍ୟା ବିଷୟରେ ସୂଚନା ଦେବାକୁ ୱେବସାଇଟ୍ ମାଲିକଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ।
  • + + ]]>
    + + + ତ୍ରୁଟିଯୁକ୍ତ ବିଷୟବସ୍ତୁ ତ୍ରୁଟି + + ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବା ପୃଷ୍ଠାକୁ ଦର୍ଶାଯାଇ ପାରିବ ନାହିଁ କାରଣ ତଥ୍ୟ ପରିବହନରେ ଗୋଟିଏ ତ୍ରୁଟି ଦେଖା ଦେଇଛି।

    • ଏହି ସମସ୍ୟା ବିଷୟରେ ଅଗଚର କରାଇବା ପାଇଁ ଦୟାକରି ୱେବସାଇଟ ମାଲିକଙ୍କ ସହିତ ସମ୍ପର୍କ କରନ୍ତୁ।
    ]]>
    + + ଆପଣ ଦେଖିବାକୁ ଚାହୁଁଥିବା ପୃଷ୍ଠାକୁ ଦର୍ଶାଯାଇ ପାରିବ ନାହିଁ କାରଣ ତଥ୍ୟ ପରିବହନରେ ଗୋଟିଏ ତ୍ରୁଟି ଦେଖା ଦେଇଛି।

    • ଏହି ସମସ୍ୟା ବିଷୟରେ ଅଗଚର କରାଇବା ପାଇଁ ଦୟାକରି ୱେବସାଇଟ ମାଲିକଙ୍କ ସହିତ ସମ୍ପର୍କ କରନ୍ତୁ।
    ]]>
    + + + ବିଷୟବସ୍ତୁ ସାଙ୍କେତିକରଣ ତ୍ରୁଟି + ଆପଣ ଦେଖିବାକୁ ଚେଷ୍ଟାକରୁଥିବା ପୃଷ୍ଠାକୁ ଦର୍ଶାଯାଇପାରିବ ନାହିଁ କାରଣ ଏହା ଗୋଟିଏ ଅବୈଧ କିମ୍ବା ଅସମର୍ଥିତ ସଙ୍କୋଚନକୁ ବ୍ୟବହାର କରିଥାଏ।

    • ଏହି ସମସ୍ୟା ବିଷୟରେ ସୂଚନା ଦେବା ପାଇଁ ଦୟାକରି ୱେବସାଇଟ ମାଲିକଙ୍କ ସହିତ ଯୋଗାଯୋଗ କରନ୍ତୁ।
    ]]>
    + + + ଠିକଣା ମିଳୁନାହିଁ + + + ଇଣ୍ଟରନେଟ୍ ସଂଯୋଗ ନାହିଁ + + ଆପଣଙ୍କର ନେଟୱର୍କ ସଂଯୋଗ ଦେଖନ୍ତୁ କିମ୍ବା ପୃଷ୍ଠାଟିକୁ ଆଉ କିଛି କ୍ଷଣ ପରେ ପୁନର୍ଧାରଣ କରନ୍ତୁ + + ପୁନର୍ଧାରଣ + + + ଅବୈଧ ଠିକଣା + ଦିଆଯାଇଥିବା ଠିକଣାଟି ପରିଚିତ ଅବସ୍ଥାରେ ନାହିଁ। ଭୁଲ ପାଇଁ ଦୟାକରି ଅବସ୍ଥିତି ପଟିକୁ ଯାଞ୍ଚ କରନ୍ତୁ ଏବଂ ପୁଣି ଚେଷ୍ଟାକରନ୍ତୁ।

    ]]>
    + + ଏହି ଠିକଣାଟି ବୈଧ ନୁହେଁ + +
  • ୱେବ ଠିକଣାଗୁଡ଼ିକ ସାଧାରଣତଃ ଏହି ପ୍ରକାରେ ଲେଖାଯାଇଥାଏ http://www.example.com/
  • ନିଶ୍ଚିତ କରନ୍ତୁ ଯେ ଆପଣ ତୀର୍ଯକ ରେଖା ବ୍ୟବହାର କରୁଛନ୍ତି (i.e. /).
  • ]]>
    + + + ଅଜଣା ପ୍ରୋଟୋକଲ + ଏହି ଠିକଣାଟି ଏକ ପ୍ରଟୋକଲକୁ ଉଲ୍ଲେଖ କରିଥାଏ (ଯେପରିକି wxyz://) ବ୍ରାଉଜର ଟିହ୍ନିପାରୁ ନାହିଁ, ତେଣୁ ବ୍ରାଉଜର ସେହି ସାଇଟ ସହିତ ସଠିକ ଭାବରେ ସଂଯୋଗ ସ୍ଥାପନ କରିପାରିବ ନାହିଁ।

    • ଆପଣ ମଲଟିମେଡିଆ କିମ୍ବା ଅନ୍ୟାନ୍ୟ ପାଠ୍ୟ-ବିହୀନ ସର୍ଭରଗୁଡ଼ିକ ପାଇଁ ଚେଷ୍ଟା କରୁଛନ୍ତି କି? ଅଧିକ ଆବଶ୍ୟକତା ପାଇଁ ସାଇଟକୁ ଯାଞ୍ଚ କରନ୍ତୁ।
    • କିଛି ପ୍ରଟୋକଲଗୁଡ଼ିକ ବ୍ରାଉଜର ସେମାନଙ୍କୁ ଚିହ୍ନିବା ପୂର୍ବରୁ ହୁଏତଃ ତୃତୀୟ ପକ୍ଷ ସଫ୍ଟୱେର କିମ୍ବା ପ୍ଲଗଇନଗୁଡ଼ିକୁ ଆବଶ୍ୟକ କରିପାରନ୍ତି।
    ]]>
    + + + ଫାଇଲ ମିଳୁନାହିଁ +
  • ବସ୍ତୁଗୁଡ଼ିକୁ ପୁନଃ ନାମକରଣ, ଅପସାରଣ, କିମ୍ବା ସ୍ଥାନାନ୍ତରଣ କରିହେବ କି?
  • ଏହି ଠିକଣାରେ ବନାନ ତ୍ରୁଟି, ପୁଞ୍ଜିକରଣ, କିମ୍ବା ଅନ୍ୟାନ୍ୟ ଛପାଯୋଗ୍ୟ ତ୍ରୁଟି ଅଛି କି?
  • ଅନୁରୋଧ କରାଯାଇଥିବା ବସ୍ତୁ ପାଇଁ ଯଥେଷ୍ଟ ଅଭିଗମ୍ୟ ଅନୁମତି ଅଛି କି?
  • ]]>
    + + + ଫାଇଲକୁ ପ୍ରବେଶକୁ ବାରଣ କରାଯାଇଥିଲା + + + ପ୍ରକ୍ସି ସର୍ଭର ସଂଯୋଗ ପାଇଁ ମନା କରିଲା + ବ୍ରାଉଜରଟି ଏକ ପ୍ରକ୍ସି ସର୍ଭର ବ୍ୟବହାର ପାଇଁ ବିନ୍ୟାସିତ ହୋଇଛି, କିନ୍ତୁ ସେହି ପ୍ରକ୍ସି ସଂଯୋଗ ପାଇଁ ବାରଣ କରିଛି।

    • ବ୍ରାଉଜରର ପ୍ରକ୍ସି ସଂରଚନା ସଠିକ ଅଛି କି? ସଂରଚନାକୁ ଯାଞ୍ଚ କରନ୍ତୁ ଏବଂ ପୁଣିଥରେ ଚେଷ୍ଟା କରନ୍ତୁ।
    • ପ୍ରକ୍ସି ସର୍ଭିସ ଏହି ନେଟୱର୍କରୁ ସଂଯୋଗକୁ ଅନୁମତି ଦେଇଥାଏ କି?
    • ଏପର୍ଯ୍ୟନ୍ତ ସମସ୍ୟା ଅଛି କି? ସହାୟତା ପାଇଁ ଆପଣଙ୍କର ନେଟୱର୍କ ପ୍ରଶାସକଙ୍କ କିମ୍ବା ଇଣ୍ଟରନେଟ ପ୍ରଦାନକାରୀ ସହିତ ଯୋଗାଯୋଗ କରନ୍ତୁ।
    ]]>
    + + + ପ୍ରକ୍ସି ସର୍ଭର ମିଳୁନାହିଁ + + + ମାଲୱେୟାର ୱେବସାଇଟ ସମସ୍ୟା + + + ଅନାବଶ୍ୟକ ୱେବସାଇଟ ସମସ୍ୟା + + + ହାନିକାରକ ସାଇଟ ସମସ୍ୟା + +
    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 @@ + + + + + ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ + + + ਬੇਨਤੀ ਪੂਰੀ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ + + + ਇਸ ਸਮੱਸਿਆ ਬਾਰੇ ਵਧੀਕ ਜਾਣਕਾਰੀ ਜਾਂ ਗਲਤੀ ਇਸ ਵੇਲੇ ਉਪਲਬੱਧ ਨਹੀਂ ਹੈ।

    + ]]>
    + + + ਸੁਰੱਖਿਅਤ ਕੁਨੈਕਸ਼ਨ ਫੇਲ੍ਹ ਹੋਇਆ + + + +
  • ਜਿਸ ਸਫ਼ੇ ਨੂੰ ਤੁਸੀਂ ਵੇਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ, ਉਹ ਨਹੀਂ ਦਿਖਾਇਆ ਜਾ ਸਕਦਾ ਕਿਉਂਕਿ ਮਿਲੇ ਡਾਟੇ ਦੀ ਪ੍ਰਮਾਣਿਕਤਾ ਦੀ ਪੁਸ਼ਟੀ ਨਹੀਂ ਹੋ ਸਕੀ।
  • +
  • ਇਸ ਸਮੱਸਿਆ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦੇਣ ਲਈ ਵੈਬਸਾਈਟ ਮਾਲਕਾਂ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।
  • + + ]]>
    + + + ਸੁਰੱਖਿਅਤ ਕੁਨੈਕਸ਼ਨ ਫੇਲ੍ਹ ਹੋਇਆ + + + +
  • ਇਹ ਸਰਵਰ ਦੀ ਸੰਰਚਨਾ ਕਰਕੇ ਸਮੱਸਿਆ ਹੋ ਸਕਦੀ ਹੈ ਜਾਂ ਕੋਈ ਸਰਵਰ ਦੀ ਨਕਲ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਿਹਾ ਹੈ।
  • +
  • ਜੇ ਤੁਸੀਂ ਪਹਿਲਾਂ ਵੀ ਇਸ ਸਰਵਰ ਨਾਲ ਠੀਕ ਤਰ੍ਹਾਂ ਕਨੈਕਟ ਹੁੰਦੇ ਰਹੇ ਹੋ ਤਾਂ ਗਲਤੀ ਆਰਜ਼ੀ ਹੋ ਸਕਦਾ ਹੈ ਅਤੇ ਤੁਸੀਂ ਬਾਅਦ ‘ਚ ਕੋਸ਼ਿਸ਼ ਕਰ ਸਕਦੇ ਹੋ।
  • + + ]]>
    + + + ਤਕਨੀਕੀ… + + ਕੋਈ ਸਾਈਟ ਦੀ ਨਕਲ ਕਰਦਾ ਹੋ ਸਕਦਾ ਹੈ ਅਤੇ ਤੁਹਾਨੂੰ ਜਾਰੀ ਨਹੀਂ ਰੱਖਣਾ ਚਾਹੀਦਾ ਹੈ। +

    + + ]]>
    + + ਪਿੱਛੇ ਜਾਓ (ਸਿਫਾਰਸ਼ੀ) + + ਖ਼ਤਰਾ ਮੰਨੋ ਤੇ ਜਾਰੀ ਰੱਖੋ + + + ਇਸ ਵੈੱਬਸਾਈਟ ਲਈ ਸੁਰੱਖਿਅਤ ਕਨੈਕਸ਼ਨ ਚਾਹੀਦਾ ਹੈ। + + +
  • ਤੁਸੀਂ ਜਿਸ ਸਫ਼ੇ ਨੂੰ ਖੋਲ੍ਹਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ, ਉਸ ਇਸ ਵੈੱਬਸਾਈਟ ਵਲੋਂ ਸੁਰੱਖਿਅਤ ਕਨੈਕਸ਼ਨ ਜ਼ਰੂਰੀ ਹੋਣ ਕਰਕੇ ਦਿਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ ਹੈ।
  • +
  • ਮਸਲਾ ਅਕਸਰ ਵੈੱਬਸਾਈਟ ਨਾਲ ਹੈ ਅਤੇ ਇਸ ਨੂੰ ਠੀਕ ਕਰਨ ਲਈ ਤੁਸੀਂ ਕੁਝ ਵੀ ਨਹੀਂ ਕਰ ਸਕਦੇ ਹੋ।
  • +
  • ਤੁਸੀਂ ਸਮੱਸਿਆ ਬਾਰੇ ਵੈੱਬਸਾਈਟ ਦੇ ਪਰਸ਼ਾਸਕ ਨੂੰ ਜਾਣਕਾਰੀ ਦੇ ਸਕਦੇ ਹੋ।
  • + + ]]>
    + + + …ਤਕਨੀਕੀ + + + %1$s ਦੀ ਸੁਰੱਖਿਆ ਪਾਲਸੀ ਹੈ, ਜਿਸ ਨੂੰ HTTP ਸਟਰਿਕ ਟਰਾਂਸਪੋਰਟ ਸਕਿਉਰਟੀ (HSTS) ਕਹਿੰਦੇ ਹਨ, ਜਿਸ ਦਾ ਅਰਥ ਹੈ ਕਿ %2$s ਨੂੰ ਸਿਰਫ਼ ਸੁਰੱਖਿਅਤ ਢੰਗ ਨਾਲ ਹੀ ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ। ਤੁਸੀਂ ਇਸ ਸਾਈਟ ਨੂੰ ਛੋਟ ਨਹੀਂ ਦਿੱਤੀ ਜਾ ਸਕਦੀ ਹੈ। + ]]> + + ਪਿੱਛੇ ਜਾਓ + + + ਕਨੈਕਸ਼ਨ ‘ਚ ਰੁਕਾਵਟ ਆਈ ਸੀ + + + ਬਰਾਊਜ਼ਰ ਕਾਮਯਾਬੀ ਨਾਲ ਕਨੈਕਟ ਹੋ ਗਿਆ, ਪਰ ਜਾਣਕਾਰੀ ਤਬਦੀਲ ਕਰਨ ਦੇ ਦੌਰਾਨ ਕਨੈਕਸ਼ਨ ‘ਚ ਰੁਕਾਵਟ ਆਈ ਸੀ। ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।

    +
      +
    • ਸਾਈਟ ਆਰਜ਼ੀ ਤੌਰ ‘ਤੇ ਅਣ-ਉਪਲਬਧ ਹੋ ਸਕਦੀ ਹੈ ਜਾਂ ਜ਼ਿਆਦਾ ਰੁਝੀ ਹੋ ਸਕਦੀ ਹੈ। ਕੁਝ ਪਲ਼ਾਂ ‘ਚ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।
    • +
    • ਜੇ ਤੁਸੀਂ ਕੋਈ ਵੀ ਸਫ਼ਾ ਲੋਡ ਨਹੀਂ ਕਰ ਸਕਦੇ ਹੋ ਤਾਂ ਆਪਣੇ ਡਿਵਾਈਸ ਦੇ ਡਾਟੇ ਜਾਂ Wi-Fi ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।
    • +
    + ]]>
    + + + ਕਨੈਕਸ਼ਨ ਲਈ ਸਮਾਂ ਸਮਾਪਤ ਹੈ + + + ਮੰਗ ਕੀਤੀ ਸਾਇਟ ਨੇ ਸੰਪਰਕ ਦੀ ਬੇਨਤੀ ਦਾ ਜਵਾਬ ਨਹੀਂ ਦਿੱਤਾ ਅਤੇ ਬਰਾਊਜ਼ਰ ਨੇ ਜਵਾਬ ਲਈ ਉਡੀਕ ਕਰਨੀ ਛੱਡ ਦਿੱਤੀ ਹੈ।

    +
      +
    • ਕੀ ਸਰਵਰ ਦੀ ਮੰਗ ਬਹੁਤ ਵੱਧ ਹੋ ਗਈ ਹੋ ਸਕਦੀ ਹੈ ਜਾਂ ਆਰਜ਼ੀ ਤੌਰ ‘ਤੇ ਸਮੱਸਿਆ ਹੋ ਸਕਦੀ ਹੈ? ਬਾਅਦ ‘ਚ ਕੋਸ਼ਿਸ਼ ਕਰੋ।
    • +
    • ਕੀ ਤੁਸੀਂ ਹੋਰ ਸਾਈਟਾਂ ਨੂੰ ਬਰਾਊਜ਼ ਕਰ ਸਕਦੇ ਹੋ? ਕੰਪਿਊਟਰ ਦੇ ਨੈੱਟਵਰਕ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।
    • +
    • ਕੀ ਤੁਹਾਡਾ ਕੰਪਿਊਟਰ ਜਾਂ ਨੈੱਟਵਰਕ ਫਾਇਰਵਾਲ ਜਾਂ ਪਰਾਕਸੀ ਰਾਹੀਂ ਸੁਰੱਖਿਅਤ ਹੈ? ਗਲਤ ਸੈਟਿੰਗਾਂ ਵੈੱਬ ਬਰਾਊਜ਼ ਕਰਨ ਦੇ ਰਾਹ ਵਿੱਚ ਅੜਿੱਕਾ ਪਾ ਸਕਦੀਆਂ ਹਨ।
    • +
    • ਹਾਲੇ ਵੀ ਮੁਸ਼ਕਲ ਆ ਰਹੀ ਹੈ? ਸਹਾਇਤਾ ਲਈ ਆਪਣੇ ਨੈੱਟਵਰਕ ਪਰਸ਼ਾਸ਼ਕ ਜਾਂ ਇੰਟਰਨੈੱਟ ਦੇਣ ਵਾਲੇ ਨਾਲ ਸੰਪਰਕ ਕਰੋ
    • +
    + + ]]>
    + + + ਕਨੈਕਟ ਕਰਨ ਲਈ ਅਸਮਰੱਥ ਹੈ + + + +
  • ਸਾਈਟ ਆਰਜ਼ੀ ਰੂਪ ਵਿੱਚ ਬੰਦ ਹੋ ਸਕਦੀ ਹੈ ਜਾਂ ਬਹੁਤ ਰੁੱਝੀ ਹੋ ਸਕਦੀ ਹੈ। ਕੁੱਝ ਕੁ ਪਲਾਂ ਵਿੱਚ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।
  • +
  • ਜੇ ਤੁਸੀਂ ਕੋਈ ਵੀ ਵਰਕਾ ਲੋਡ ਨਹੀਂ ਕਰ ਸਕਦੇ ਹੋ ਤਾਂ ਆਪਣੇ ਡਿਵਾਈਸ ਦੇ ਡਾਟੇ ਜਾਂ Wi-Fi ਕੁਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ ਜੀ।
  • + + ]]>
    + + + ਸਰਵਰ ਤੋਂ ਅਣਚਿਤਵਿਆ ਜਵਾਬ ਮਿਲਿਆ + + + ਸਾਇਟ ਨੈੱਟਵਰਕ ਬੇਨਤੀ ਨੂੰ ਇੱਕ ਅਣਜਾਣੇ ਢੰਗ ਨਾਲ ਜਵਾਬ ਦੇ ਰਹੀ ਹੈ ਅਤੇ ਬਰਾਊਜ਼ਰ ਜਾਰੀ ਨਹੀਂ ਰੱਖ ਸਕਦਾ ਹੈ।

    + ]]>
    + + + ਸਫ਼ੇ ਲਈ ਠੀਕ ਤਰ੍ਹਾਂ ਮੁੜ-ਦਿਸ਼ਾ ਪਰਿਵਰਤਨ ਨਹੀਂ ਕੀਤਾ ਗਿਆ + + + ਬਰਾਊਜਰ ਨੇ ਮੰਗ ਕੀਤੀ ਚੀਜ਼ ਪ੍ਰਾਪਤ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਨੂੰ ਰੋਕ ਦਿੱਤਾ ਹੈ। ਸਾਇਟ ਬੇਨਤੀ ਨੂੰ ਇਸ ਢੰਗ ਨਾਲ ਦਿਸ਼ਾ ਪਰਿਵਰਤਨ ਕਰ ਰਹੀ ਹੈ ਜੋ ਕਿ ਕਦੇ ਪੂਰੀ ਨਹੀਂ ਹੋਵੇਗੀ।

    +
      +
    • ਕੀ ਤੁਸੀਂ ਇਸ ਸਾਈਟ ਲਈ ਚਾਹੀਦੇ ਕੂਕੀਜ਼ ਅਸਮਰੱਥ ਕੀਤੇ ਜਾਂ ਪਾਬੰਦੀ ਲਗਾਏ ਹਨ?
    • +
    • ਜੇ ਸਾਈਟ ਦੇ ਕੂਕੀਜ਼ ਨੂੰ ਮਨਜ਼ੂਰ ਕਰਨ ਨਾਲ ਸਮੱਸਿਆ ਹੱਲ ਨਹੀਂ ਹੁੰਦੀ ਹੈ ਤਾਂ ਇਹ ਸਰਵਰ ਦੀ ਸੰਰਚਨਾ ਨਾਲ ਮਸਲਾ ਹੋ ਸਕਦਾ, ਨਾ ਕਿ ਤੁਹਾਡੇ ਕੰਪਿਊਟਰ ਨਾਲ।
    • +
    + ]]>
    + + + ਆਫ਼ਲਾਈਨ ਢੰਗ + + + ਬਰਾਊਜ਼ਰ ਆਫ਼ਲਾਈਨ ਢੰਗ ਵਿੱਚ ਕੰਮ ਕਰ ਰਿਹਾ ਹੈ ਅਤੇ ਮੰਗ ਕੀਤੀ ਚੀਜ਼ ਨਾਲ ਕਨੈਕਟ ਨਹੀਂ ਕਰ ਸਕਦਾ ਹੈ।

    +
      +
    • ਕੀ ਕੰਪਿਊਟਰ ਸਰਗਰਮ ਨੈੱਟਵਰਕ ਨਾਲ ਕਨੈਕਟ ਹੈ?
    • +
    • ਆਨਲਾਈਨ ਢੰਗ ‘ਚ ਜਾਣ ਅਤੇ ਸਫ਼ੇ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰਨ ਲਈ “ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ” ਨੂੰ ਦਬਾਓ।
    • +
    + ]]>
    + + + ਸੁਰੱਖਿਆ ਕਾਰਨਾਂ ਕਰਕੇ ਪੋਰਟ ਉੱਤੇ ਪਾਬੰਦੀ ਲਗਾਈ + + + ਮੰਗੇ ਗਏ ਸਿਰਨਾਵੇਂ ਨੇ ਪੋਰਟ ਦਿੱਤੀ ਹੈ, (ਜਿਵੇਂ ਕਿ mozilla.org:80 mozilla.org ਲਈ 809) ਜੋ ਕਿ ਆਮ ਕਰਕੇ ਵਰਤੀ ਜਾਂਦੀ ਹੈ, ਹੋਰ ਵੈਬ ਝਲਕੀ ਤੋਂ ਬਿਨਾਂ। ਬਰਾਊਜ਼ਰ ਨੇ ਤੁਹਾਡੀ ਬਚਾਅ ਅਤੇ ਸੁਰੱਖਿਆ ਲਈ ਬੇਨਤੀ ਨੂੰ ਰੱਦ ਕਰ ਦਿੱਤਾ ਹੈ।

    + ]]>
    + + + ਕਨੈਕਸ਼ਨ ਮੁੜ-ਸੈੱਟ ਕੀਤਾ ਗਿਆ ਸੀ + + + ਸੰਪਰਕ ਦੇ ਸਮਝੌਤੇ ਵੇਲੇ ਨੈੱਟਵਰਕ ਲਿੰਕ ਵਿੱਚ ਰੁਕਾਵਟ ਆਈ ਸੀ। ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।

    +
      +
    • ਸਾਈਟ ਆਰਜ਼ੀ ਤੌਰ ‘ਤੇ ਉਪਲਬਧ ਨਹੀਂ ਹੋ ਸਕਦੀ ਜਾਂ ਬਹੁਤ ਰੁੱਝੀ ਹੋ ਸਕਦੀ ਹੈ। ਕੁੱਝ ਪਲ਼ਾਂ ਵਿੱਚ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।
    • +
    • ਜੇ ਤੁਸੀਂ ਕੋਈ ਵੀ ਵਰਕਾ ਲੋਡ ਨਹੀਂ ਕਰ ਸਕਦੇ ਹੋ ਤਾਂ ਆਪਣੇ ਡਿਵਾਈਸ ਦੇ ਡਾਟੇ ਜਾਂ Wi-Fi ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।
    • +
    + ]]>
    + + + ਅਸੁਰੱਖਿਅਤ ਫਾਈਲ ਕਿਸਮ + + +
  • ਇਸ ਸਮੱਸਿਆ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦੇਣ ਲਈ ਵੈੱਬਸਾਈਟ ਦੇ ਮਾਲਕਾਂ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।
  • + + ]]>
    + + + ਖਰਾਬ ਹੋਈ ਦੀ ਸਮੱਗਰੀ ਗਲਤੀ + + + ਸਫ਼ਾ, ਜੋ ਤੁਸੀਂ ਵੇਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਨੂੰ ਵੇਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ ਹੈ, ਕਿਉਂਕਿ ਡਾਟਾ ਲੈਣ-ਦੇਣ ਵਿੱਚ ਗਲਤੀ ਖੋਜੀ ਗਈ ਹੈ।

    +
      +
    • ਇਹ ਸਮੱਸਿਆ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦੇਣ ਲਈ ਵੈੱਬਸਾਈਟ ਮਾਲਕਾਂ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।
    • +
    + ]]>
    + + + ਸਮੱਗਰੀ ਕਰੈਸ਼ ਹੋਈ + + ਜਿਸ ਵਰਕੇ ਨੂੰ ਤੁਸੀਂ ਵੇਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ ਉਹ ਵੇਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ ਹੈ, ਕਿਉਂਕਿ ਡਾਟਾ ਲੈਣ-ਦੇਣ ਵਿੱਚ ਗਲਤੀ ਖੋਜੀ ਗਈ ਹੈ।

    +
      +
    • ਇਹ ਸਮੱਸਿਆ ਬਾਰੇ ਜਾਣਕਾਰੀ ਦੇਣ ਲਈ ਵੈੱਬਸਾਈਟ ਮਾਲਕਾਂ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।
    • +
    + ]]>
    + + + ਸਮੱਗਰੀ ਇੰਕੋਡਿੰਗ ਗਲਤੀ + + ਸਫ਼ਾ, ਜਿਸ ਨੂੰ ਤੁਸੀਂ ਵੇਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ, ਵੇਖਾਇਆ ਨਹੀਂ ਜਾ ਸਕਦਾ, ਕਿਉਂਕਿ ਇਹ ਗਲਤ ਜਾਂ ਗ਼ੈਰ-ਸਹਾਇਕ ਕੰਪਰੈਸ਼ਨ ਫਾਰਮ ਵਰਤਦਾ ਹੈ।

    +
      +
    • ਇਹ ਸਮੱਸਿਆ ਬਾਰੇ ਵੈੱਬ ਸਾਇਟ ਦੇ ਓਨਰ (ਮਾਲਕ) ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।
    • +
    + ]]>
    + + + ਸਿਰਨਾਵਾਂ ਨਹੀਂ ਲੱਭਿਆ + + + ਬਰਾਊਜ਼ਰ ਦਿੱਤੇ ਸਿਰਨਾਵਾਂ ਲਈ ਹੋਸਟ ਸਰਵਰ ਲੱਭ ਨਹੀਂ ਸਕਿਆ ਸੀ।

    +
      +
    • ਲਿਖਣ ਦੀਆਂ ਗਲਤੀਆਂ ਲਈ ਸਿਰਨਾਵੇਂ ਦੀ ਜਾਂਚ ਕਰੋ ਜਿਵੇਂ ਕਿ + www.example.com ਦੀ ਬਜਾਏ + ww.example.com ਲਿਖਣਾ
    • +
    • ਜੇ ਤੁਸੀਂ ਕੋਈ ਵੀ ਸਫ਼ਾ ਲੋਡ ਕਰਨ ਲਈ ਅਸਮਰੱਥ ਹੋ ਤਾਂ ਆਪਣੇ ਡਿਵਾਈਸ ਦੇ ਡਾਟੇ ਜਾਂ Wi-Fi ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ।
    • +
    + ]]>
    + + + ਕੋਈ ਇੰਟਰਨੈੱਟ ਕਨੈਕਸ਼ਨ ਨਹੀਂ ਹੈ + + ਆਪਣੇ ਨੈੱਟਵਰਕ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰੋ ਜਾਂ ਕੁਝ ਪਲ਼ਾਂ ਵਿੱਚ ਸਫ਼ੇ ਨੂੰ ਮੁੜ-ਲੋਡ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ। + + ਮੁੜ-ਲੋਡ ਕਰੋ + + + ਅਢੁੱਕਵਾਂ ਸਿਰਨਾਵਾਂ + ਦਿੱਤਾ ਗਿਆ ਪਤਾ ਪਰਵਾਨਤ ਬਣਤਰ ਨਾਲ ਮੇਲ ਨਹੀਂ ਖਾਂਦਾ। ਗਲਤੀਆਂ ਲਈ ਟਿਕਾਣਾ ਪੱਟੀ ਦੀ ਜਾਂਚ ਕਰੋ ਤੇ ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।

    + ]]>
    + + ਸਿਰਨਾਵਾਂ ਵਾਜਬ ਨਹੀਂ ਹੈ + + + +
  • ਵੈੱਬ ਸਿਰਨਾਵੇਂ ਨੂੰ ਆਮ ਤੌਰ http://www.example.com/ ਵਜੋਂ ਲਿਖਿਆ ਜਾਂਦਾ ਹੈ
  • +
  • ਧਿਆਨ ਦਿਓ ਕਿ ਤੁਸੀਂ ਫਾਰਵਰਡ ਸਲੈਸ਼ (ਜਿਵੇਂ /) ਹੀ ਵਰਤ ਰਹੇ ਹੋ।
  • + + ]]>
    + + + ਅਣਪਛਾਤਾ ਪਰੋਟੋਕਾਲ + + ਸਿਰਨਾਵਾਂ ਪਰੋਟੋਕਾਲ (ਜਿਵੇਂ ਕਿ wxyz://) ਦਿੰਦਾ ਹੈ, ਜਿਸ ਦੀ ਪਛਾਣ ਬਰਾਊਜ਼ਰ ਨਹੀਂ ਕਰ ਸਕਦਾ ਹੈ, ਇਸਕਰਕੇ ਬਰਾਊਜ਼ਰ ਸਾਈਟ ਨਾਲ ਠੀਕ ਤਰ੍ਹਾਂ ਕਨੈਕਟ ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ।

    +
      +
    • ਕੀ ਤੁਸੀਂ ਮਲਟੀਮੀਡਿਆ ਜਾਂ ਹੋਰ ਗ਼ੈਰ-ਲਿਖਤ ਸੇਵਾ ਨੂੰ ਵਰਤਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਹੇ ਹੋ? ਸਾਈਟਾਂ ਦੀ ਵਾਧੂ ਲੋੜਾਂ ਨੂੰ ਚੈੱਕ ਕਰੋ
    • +
    • ਕੁਝ ਪਰੋਟੋਕਾਲ ਲਈ ਸੁਤੰਤਰ ਧਿਰ ਸਾਫਟਵੇਅਰ ਜਾਂ ਪਲੱਗਇਨ ਚਾਹੀਦੇ ਹੋ ਸਕਦੇ ਹਨ ਤਾਂ ਕਿ ਬਰਾਊਜ਼ਰ ਉਹਨਾਂ ਦੀ ਪਛਾਣ ਕਰ ਸਕੇ।
    • +
    + ]]>
    + + + ਫਾਈਲ ਨਹੀਂ ਲੱਭੀ + + +
  • ਆਈਟਮ ਦਾ ਨਾਂ ਬਦਲਿਆ ਗਿਆ ਹੋ ਸਕਦਾ ਹੈ, ਹਟਾਇਆ ਗਿਆ ਹੋ ਸਕਦਾ ਹੈ ਜਾਂ ਹੋਰ ਥਾਂ ਭੇਜੀ ਗਈ ਹੋ ਸਕਦੀ ਹੈ?
  • +
  • ਕੋਈ ਸ਼ਬਦ ਗਲਤ ਲਿਖੇ ਗਏ ਹਨ, ਅੱਖਰ ਵੱਡੇ ਛੋਟੋ ਹੋ ਗਏ ਹਨ ਜਾਂ ਸਿਰਨਾਵੇਂ ਵਿੱਚ ਕੋਈ ਅੱਖਰ ਦੀ ਗਲਤੀ ਹੈ?
  • +
  • ਕੀ ਤੁਹਾਡੇ ਕੋਲ ਮੰਗੀ ਗਈ ਆਈਟਮ ਦੀ ਵਰਤੋਂ ਲਈ ਢੁੱਕਵੇਂ ਅਧਿਕਾਰ ਹਨ?
  • + + ]]>
    + + + ਫਾਈਲ ਲਈ ਪਹੁੰਚ ਤੋਂ ਨਾਂਹ ਕੀਤੀ + + +
  • ਇਸਨੂੰ ਹਟਾਇਆ, ਕਿਤੇ ਹੋਰ ਭੇਜਿਆ ਗਿਆ ਹੋ ਸਕਦਾ ਹੈ ਜਾਂ ਫਾਈਲ ਮੰਨਜੂਰੀਆਂ ਇਸ ਤੱਕ ਪਹੁੰਚ ਹੋਣ ਤੋ ਰੋਕ ਰਹੀਆਂ ਹੋਣਗੀਆਂ।
  • + + ]]>
    + + + ਪਰਾਕਸੀ ਸਰਵਰ ਨੇ ਕਨੈਕਸ਼ਨ ਤੋਂ ਇਨਕਾਰ ਕੀਤਾ + + ਬਰਾਊਜ਼ਰ ਨੂੰ ਪਰਾਕਸੀ ਸਰਵਰ ਵਰਤਣ ਲਈ ਸੰਰਚਿਤ ਕੀਤਾ ਗਿਆ ਹੈ, ਪਰ ਪਰਾਕਸੀ ਸਰਵਰ ਨੇ ਕਨੈਕਸ਼ਨ ਤੋਂ ਇਨਕਾਰ ਕਰ ਦਿੱਤਾ ਹੈ।

    +
      +
    • ਕੀ ਬਰਾਊਜ਼ਰ ਦੀ ਪਰਾਕਸੀ ਸੰਰਚਨਾ ਠੀਕ ਹੈ? ਸੈਟਿੰਗਾਂ ਚੈੱਕ ਕਰਕੇ ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।
    • +
    • ਕੀ ਪਰਾਕਸੀ ਸੇਵਾ ਇਸ ਨੈੱਟਵਰਕ ਤੋਂ ਕੁਨੈਕਸ਼ਨ ਮਨਜ਼ੂਰ ਕਰਦੀ ਹੈ?
    • +
    • ਹਾਲੇ ਵੀ ਸਮੱਸਿਆ ਹੈ? ਮੱਦਦ ਲਈ ਆਪਣੇ ਨੈੱਟਵਰਕ ਪਰਸ਼ਾਸ਼ਕ ਜਾਂ ਇੰਟਰਨੈੱਟ ਪਰੋਵਾਇਡਰ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।
    • +
    + ]]>
    + + + ਪਰਾਕਸੀ ਸਰਵਰ ਨਹੀਂ ਲੱਭਿਆ + + ਬਰਾਊਜ਼ਰ ਨੂੰ ਪਰਾਕਸੀ ਸਰਵਰ ਵਰਤਣ ਲਈ ਸੰਰਚਿਤ ਕੀਤਾ ਗਿਆ ਹੈ, ਪਰ ਪਰਾਕਸੀ ਲੱਭਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ।

    +
      +
    • ਕੀ ਬਰਾਊਜ਼ਰ ਦੀ ਪਰਾਕਸੀ ਸੰਰਚਨਾ ਠੀਕ ਹੈ? ਸੈਟਿੰਗਾਂ ਚੈੱਕ ਕਰਕੇ ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।
    • +
    • ਕੀ ਕੰਪਿਊਟਰ ਸਰਗਰਮ ਨੈੱਟਵਰਕ ਨਾਲ ਕਨੈਕਟ ਹੈ?
    • +
    • ਹਾਲੇ ਵੀ ਸਮੱਸਿਆ ਹੈ? ਮਦਦ ਲਈ ਆਪਣੇ ਨੈੱਟਵਰਕ ਪਰਸ਼ਾਸ਼ਕ ਜਾਂ ਇੰਟਰਨੈੱਟ ਪਰੋਵਾਇਡਰ ਨਾਲ ਸੰਪਰਕ ਕਰੋ ਜੀ।
    • +
    + ]]>
    + + + ਮਾਲਵੇਅਰ ਸਾਈਟ ਮਸਲਾ + + + %1$s ਤੇ ਸਾਈਟ ਨੂੰ ਇੱਕ ਅਟੈਕ ਸਾਈਟ ਵਜੋਂ ਦੱਸਿਆ ਗਿਆ ਹੈ ਅਤੇ ਤੁਹਾਡੀ ਸੁਰੱਖਿਆ ਤਰਜੀਹਾਂ ਦੇ ਅਧਾਰ ਤੇ ਬਲੌਕ ਕੀਤਾ ਗਿਆ ਹੈ।

    +    ]]>
    + + + ਬੇਲੋੜੀ ਸਾਈਟ ਮਸਲਾ + + + %1$s ਤੋਂ ਸਾਈਟ ਨੂੰ ਬੇਲੋੜੇ ਸਾਫਟਵੇਅਰ ਵੰਡਣ ਵਾਲੇ ਵਜੋਂ ਗਰਦਾਨਿਆ ਗਿਆ ਹੈ ਅਤੇ ਤੁਹਾਡੀਆਂ ਸੁਰੱਖਿਆ ਪਸੰਦਾਂ ਦੇ ਮੁਤਾਬਕ ਪਾਬੰਦੀ ਲਗਾਈ ਹੈ।

    + ]]>
    + + + ਨੁਕਸਾਨਦੇਹ ਸਾਈਟ ਮਸਲਾ + + + %1$s ਉਤਲੀ ਸਾਈਟ ਦੀ ਸੰਭਾਵਿਤ ਹਮਲਾਵਰ ਸਾਈਟ ਵਜੋਂ ਇਤਲਾਹ ਹੋਈ ਹੈ ਅਤੇ ਤੁਹਾਡੀਆਂ ਸੁਰੱਖਿਆ ਤਰਜੀਹਾਂ ਦੇ ਮੁਤਾਬਕ ਇਸ ਉੱਤੇ ਪਾਬੰਦੀ ਲਗਾਈ ਹੈ।

    + ]]>
    + + + ਭਰਮਪੂਰਨ ਸਾਈਟ ਮਸਲਾ + + %1$s ਤੋਂ ਇਸ ਵੈੱਬ ਸਫ਼ੇ ਨੂੰ ਭਰਮਪੂਰਕ ਸਾਈਟ ਵਜੋਂ ਇਤਲਾਹ ਦਿੱਤੀ ਗਈ ਹੈ ਅਤੇ ਤੁਹਾਡੀਆਂ ਸੁਰੱਖਿਆ ਪਸੰਦਾਂ ਦੇ ਮੁਤਾਬਕ ਪਾਬੰਦੀ ਲਗਾਈ ਹੈ।

    + ]]>
    + + + ਸੁਰੱਖਿਅਤ ਸਾਈਟ ਮੌਜੂਦ ਨਹੀਂ ਹੈ + + %1$s ਦਾ HTTPS ਵਰਜ਼ਨ ਮੌਜੂਦ ਨਹੀਂ ਹੈ।]]> + + HTTP ਸਾਈਟ ਨਾਲ ਜਾਰੀ ਰੱਖੋ +
    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 @@ + + + + فیر کرو + + + بنیتی پوری نہیں کیتی جا سکدی + + ایس مسئلے بارے ودھیک جاݨکاری یا غلطی ایس ویلے اپلبدھ نہیں اے۔

    + ]]>
    + + + سرکھیت جوڑن توں غلطی ہو گئی اے + + + +
  • جیس صفحے نوں تسیں ویکھݨ دی کوشش کر رہے او، اوہ نہیں دکھایا جا سکدا کیوں‌کہ ملے ڈیٹے دی پرماݨکتا دی پشٹی نہیں ہو سکی۔
  • +
  • ایس سمسیا بارے جاݨکاری دیݨ لئی سائٹ مالکاں نال دسیو۔
  • + + +]]>
    + + + سرکھیت جوڑن توں غلطی ہو گئی اے + + + +
  • ایہہ سرور دی سنرچنا کرکے سمسیا ہو سکدی اے یا کوئی سرور دی نکل کرن دی کوشش کر رہا اے۔
  • +
  • جے تسیں پہلاں وی ایس سرور نال ٹھیک طرحاں جوڑن ہندے رہے او تاں غلطی آرزی ہو سکدا اے تے تسیں بعد چ کوشش کر سکدے او۔
  • + + ]]>
    + + + اضافی… + + + پتہ نہیں لبھیاں + + + پھر لوڈ کرو + + + غلط پتہ + + + غیر سرکھیت دی سائٹ نوں فیر وی جاؤ +
    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 @@ + + + + + Spróbuj ponownie + + + Wystąpił błąd + + Dodatkowe informacje o tym problemie lub błędzie nie są obecnie dostępne.

    + ]]>
    + + + Bezpieczne połączenie się nie powiodło + + + +
  • Otwierana strona nie może zostać wyświetlona, ponieważ nie udało się potwierdzić autentyczności otrzymanych danych.
  • +
  • Proszę poinformować właścicieli witryny o tym problemie.
  • + + ]]>
    + + + Bezpieczne połączenie się nie powiodło + + +
  • Może to być problem konfiguracji serwera lub próba podania się za ten serwer przez podmiot nieuprawniony.
  • +
  • Jeśli łączono się wcześniej z tym serwerem, błąd może być tymczasowy i należy spróbować ponownie później.
  • + + ]]>
    + + + Zaawansowane… + + Ktoś może próbować podszywać się pod tę witrynę. Odradzamy kontynuowanie. +

    + + ]]>
    + + Wróć do poprzedniej strony (zalecane) + + Akceptuję ryzyko, kontynuuj + + + Witryna wymaga zabezpieczonego połączenia. + + +
  • Otwierana strona nie może zostać wyświetlona, ponieważ witryna wymaga zabezpieczonego połączenia.
  • +
  • Problem leży prawdopodobnie po stronie witryny i nie masz możliwości jego rozwiązania.
  • +
  • Możesz powiadomić administratora witryny o problemie.
  • + + ]]>
    + + + Zaawansowane… + + + Witryna „%1$s” określa poprzez HSTS (HTTP Strict Transport Security), że %2$s ma się z nią łączyć jedynie w sposób zabezpieczony. Dodanie wyjątku w celu odwiedzenia tej witryny jest niemożliwe. + ]]> + + Wróć do poprzedniej strony + + + Przerwane połączenie + + + Przeglądarka nawiązała połączenie, ale zostało ono przerwane podczas przesyłania informacji.

    +
      +
    • Witryna może być tymczasowo niedostępna lub przeciążona. Spróbuj ponownie za pewien czas.
    • +
    • Jeśli nie możesz otworzyć żadnej strony, sprawdź swoje połączenie sieciowe.
    • +
    + ]]>
    + + + Przekroczono czas oczekiwania + + + Witryna przez dłuższy czas nie odpowiedziała na żądanie połączenia i przeglądarka przestała czekać na odpowiedź.

    +
      +
    • Witryna może być tymczasowo niedostępna lub przeciążona. Spróbuj ponownie za pewien czas.
    • +
    • Jeśli nie możesz otworzyć żadnej strony, sprawdź swoje połączenie sieciowe.
    • +
    • Jeśli to urządzenie jest chronione przez zaporę sieciową lub serwer proxy, sprawdź, czy ten program jest uprawniony do łączenia się z Internetem.
    • +
    • Jeśli nadal występują problemy, skonsultuj się z administratorem sieci lub dostawcą usług internetowych.
    • +
    ]]>
    + + + Nie można połączyć + + +
  • Strona może być tymczasowo niedostępna lub przeciążona. Spróbuj ponownie za chwilę.
  • +
  • Jeśli nie możesz otworzyć żadnej strony, sprawdź swoje połączenie z Internetem.
  • + + ]]>
    + + + Nieoczekiwana odpowiedź serwera + + + Witryna odpowiedziała w sposób nieoczekiwany i przeglądarka nie może kontynuować.

    + ]]>
    + + + Nieprawidłowe przekierowanie strony + + + Przeglądarka przerwała próby pobrania żądanego elementu. Witryna przekierowuje żądanie w sposób, który uniemożliwia jego dokończenie.

    +
      +
    • Czy ciasteczka zostały wyłączone lub zablokowane dla tej witryny?
    • +
    • 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.
    • +
    ]]>
    + + + Tryb offline + + Przeglądarka pracuje w trybie offline i nie może pobrać żądanego elementu.

    +
      +
    • Czy urządzenie podłączone jest do działającej sieci?
    • +
    • Naciśnij „Spróbuj ponownie”, aby przejść do trybu online i ponownie wczytać stronę.
    • +
    ]]>
    + + + Zastrzeżony adres + + + Żądany adres zawiera numer portu (np. w adresie mozilla.org:80 liczba 80 to port na serwerze mozilla.org), który zazwyczaj nie jest wykorzystywany do przeglądania witryn WWW. Przeglądarka anulowała to żądanie ze względów bezpieczeństwa.

    + ]]>
    + + + Przerwane połączenie + + Połączenie sieciowe zostało przerwane podczas negocjacji.

    +
      +
    • Witryna może być tymczasowo niedostępna lub przeciążona. Spróbuj ponownie za pewien czas.
    • +
    • Jeśli nie możesz otworzyć żadnej strony, sprawdź swoje połączenie sieciowe.
    • +
    + ]]>
    + + + Niebezpieczny typ pliku + + +
  • Proszę poinformować właścicieli witryny o tym problemie.
  • + + ]]>
    + + + Błąd: treść uszkodzona + + Otwierana strona nie może zostać wyświetlona, ponieważ wykryto błąd w transmisji danych.

    +
      +
    • Proszę poinformować właścicieli witryny o tym problemie.
    • +
    + ]]>
    + + + Zawartość uległa awarii + Otwierana strona nie może zostać wyświetlona, ponieważ wykryto błąd w transmisji danych.

    +
      +
    • Proszę poinformować właścicieli witryny o tym problemie.
    • +
    + ]]>
    + + + Błąd kodowania zawartości + Otwierana strona nie może zostać wyświetlona, ponieważ używa nieprawidłowych lub nieobsługiwanych metod kompresji.

    +
      +
    • Proszę poinformować właścicieli witryny o tym problemie.
    • +
    + ]]>
    + + + Nie odnaleziono adresu + + + Przeglądarka nie mogła odnaleźć adresu serwera dla podanego adresu.

    +
      +
    • Upewnij się, że wprowadzony adres nie zawiera takich literówek, jak + ww.example.com zamiast + www.example.com.
    • +
    • Jeśli nie możesz otworzyć żadnej strony, sprawdź połączenie sieciowe.
    • +
    + ]]>
    + + + Brak połączenia z Internetem + + Sprawdź połączenie z Internetem lub spróbuj za chwilę ponownie wczytać stronę. + + Wczytaj ponownie + + + Nieprawidłowy adres + + Podano adres w nierozpoznawalnym formacie. Sprawdź, czy w pasku adresu nie ma błędów, a następnie spróbuj ponownie.

    + ]]>
    + + Adres jest nieprawidłowy + + + +
  • Adresy internetowe są zwykle postaci http://www.example.com/
  • +
  • Upewnij się, że adres zawiera prawidłowe ukośniki (tzn. /).
  • + + ]]>
    + + + Nieznany protokół + + Adres zawiera protokół (np. wxyz://), który nie jest rozpoznawany przez przeglądarkę. Nie może więc ona poprawnie połączyć się z daną witryną.

    +
      +
    • Czy próbowano korzystać z multimediów lub z innych usług nieopartych na tekście? Sprawdź, czy witryna nie ma dodatkowych wymagań.
    • +
    • Obsługa niektórych protokołów może wymagać oprogramowania lub wtyczek dostarczonych przez zewnętrznych producentów.
    • +
    + ]]>
    + + + Nie odnaleziono pliku + + +
  • Możliwe, że element ten został usunięty, przeniesiony lub zmieniono mu nazwę.
  • +
  • Sprawdź, czy w podanym adresie nie ma błędu w pisowni, w tym wielkości liter, ani innych błędów typograficznych.
  • +
  • Upewnij się, czy masz odpowiednie uprawnienia do przeglądania żądanej strony.
  • + + ]]>
    + + + Odmowa dostępu do pliku + + +
  • Plik mógł zostać usunięty, przeniesiony lub jego uprawnienia uniemożliwiają dostęp.
  • + + ]]>
    + + + Serwer proxy odrzucił połączenie + Przeglądarka została skonfigurowana tak, aby używać serwera proxy, który jednak odrzucił połączenie.

    +
      +
    • Czy konfiguracja serwerów proxy w przeglądarce jest prawidłowa? Sprawdź ustawienia i spróbuj ponownie.
    • +
    • Upewnij się, że serwer proxy dopuszcza połączenia z tej sieci.
    • +
    • Jeśli nadal występują problemy, skonsultuj się z administratorem sieci lub dostawcą usług internetowych.
    • +
    + ]]>
    + + + Nie odnaleziono serwera proxy + Przeglądarka została skonfigurowana tak, aby używać serwera proxy, ale serwer proxy nie może zostać odnaleziony.

    +
      +
    • Czy konfiguracja serwerów proxy w przeglądarce jest prawidłowa? Sprawdź ustawienia i spróbuj ponownie.
    • +
    • Upewnij się, że urządzenie jest podłączone do działającej sieci.
    • +
    • Jeśli nadal występują problemy, skonsultuj się z administratorem sieci lub dostawcą usług internetowych.
    • +
    ]]>
    + + + Witryna ze złośliwym oprogramowaniem + + Witryna „%1$s” została zgłoszona jako stanowiąca zagrożenie i została zablokowana zgodnie z ustawieniami bezpieczeństwa.

    + ]]>
    + + + Witryna z niechcianym oprogramowaniem + + Witryna „%1$s” została zgłoszona jako rozprowadzająca niechciane oprogramowanie i została zablokowana zgodnie z ustawieniami bezpieczeństwa.

    + ]]>
    + + + Potencjalnie szkodliwa witryna + + Witryna „%1$s” została zgłoszona jako potencjalnie szkodliwa i została zablokowana zgodnie z ustawieniami bezpieczeństwa.

    + ]]>
    + + + Podejrzana witryna + + Witryna „%1$s” została zgłoszona jako przypadek oszustwa i została zablokowana zgodnie z ustawieniami bezpieczeństwa.

    + ]]>
    + + + Zabezpieczona witryna jest niedostępna + + %1$s nie jest dostępna.]]> + + Otwórz witrynę przez HTTP +
    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 @@ + + + + Shikejeku uksenpa + + + Tesu welik muajshitia ne tajtanilis + + + Sanuk tesu nemi ukse informacion ipanpa ini uwijkayut u tajtakul.

    + ]]>
    + + + Tesu welik muchiwa se musalulis seguruj + + + +
  • Ne iswat taja tikneki tikita tesu weli muneshtia ika tesu weli tikneltia asu ijkia ne itukey ne datos tikwijtiwit .
  • +
  • Shiknutza ne itejtekuyu ne sitioj matapan pal tiknawatilia ini uijkayu.
  • + + ]]>
    + + + Tesu welik muchiwa se musalulis seguruj + + + +
  • Ini anka se uijkayu iwan ne iconfiguración ne servidor, u anka se akaj kiejekua tashijshikua mukwepa ne servidor.
  • +
  • Asu ikman taja timusalujtuk yek itech ini servidor, ne tatakul yu ishtuna chupi, wan tiweli taejekua nemanha.
  • + + ]]>
    + + + Chayawtuk… + + Se akaj kiejekua tashijshikua mukwepa ne sitioj wan te nemi pal tipanu. +

    + + ]]>
    + + Shimukwepa (tanawatilis) + + + Nikwi ne riesgoj wan nipanu + + + Ini tzawalsitioj kineki se tasalulis seguroj. + + + +
  • Ne iswat tikneki tikita tesu weli muneshtia ika ini sitioj matapan kineki se tasalulis seguruj.
  • +
  • Ini uijkayu anka iwan ne sitioj matapan, wan tesu tiweli tikchiwa te tatka pal tikishtia.
  • +
  • Tiweli tiknawatilia ne administrador pal ini sitioj ini uijkayu.
  • + + ]]>
    + + + Chayawtuk… + + + Shimukwepa + + + Ne tasalulis kutunik + + + Ne tasalulis tamik + + + Tesu welik musalua + + + Ne servidor tesu nankilia ken muchiya. + + + Ne iswat tesu yek tatijtitania senpa. + + + Modoj kupintuk + + + Tesu weli kalaki tik ne puertoj ipanpa ne tajpiyalis. + + + Ne tasalulis mupewaltijtuk senpa. + + + Ne tipoj archivoj tesajsay + + + Tatakul kalijtik palantuk + + + Ne tay nemi kalijtik kichiwki tajtakul + + + Tajtakul pal ne itasenputzulis ne tay nemi kalijtik. + + + Ne dirección tesu muajsik + + + Te musalujtuk tech Internet + + Takimiltia senpa + + + Tesu muajsituk ne archivoj + +
    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 @@ + + + + + Tentar novamente + + + Não foi possível concluir a solicitação + + Informações adicionais sobre este problema ou erro não estão disponíveis no momento.

    + ]]>
    + + + Falha na conexão segura + + + +
  • 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.
  • +
  • Entre em contato com os responsáveis pelo site para informar este problema.
  • + + ]]>
    + + + Falha na conexão segura + + + +
  • Pode ser um problema com a configuração do servidor, ou alguém tentando se passar por ele.
  • +
  • Caso já tenha conseguido se conectar com este servidor antes, talvez o erro seja temporário e você pode tentar novamente mais tarde.
  • + + ]]>
    + + + Avançado… + + Alguém pode estar tentando se passar pelo site e você não deve continuar. +

    + + ]]>
    + + Voltar (recomendado) + + Aceitar o risco e continuar + + + Este site requer uma conexão segura. + + + +
  • A página que você está tentando ver não pode ser exibida porque este site requer uma conexão segura.
  • +
  • O problema provavelmente está no site e não há nada que você possa fazer para resolver.
  • +
  • Você pode notificar o administrador do site sobre o problema.
  • + + ]]>
    + + + Avançado… + + + %1$s tem uma diretiva de segurança chamada HTTP Strict Transport Security (HSTS), que significa que o %2$s só pode se conectar a ele com segurança. Você não pode adicionar uma exceção para visitar este site. + ]]> + + Voltar + + + A conexão foi interrompida + + + O navegador se conectou com sucesso, mas a conexão foi interrompida durante a transferência de informações. Tente novamente.

    +
      +
    • O site pode estar temporariamente indisponível ou ocupado demais. Tente novamente daqui a pouco.
    • +
    • Se não estiver conseguindo carregar nenhuma página, verifique a conexão de dados móveis ou WiFi do seu dispositivo.
    • +
    + ]]>
    + + + Atingiu o limite de tempo da conexão + + + O site solicitado não respondeu a um pedido de conexão e o navegador deixou de esperar uma resposta.

    +
      +
    • O servidor pode estar sobrecarregado ou temporariamente indisponível? Tente novamente mais tarde.
    • +
    • Não consegue navegar para outros sites? Verifique a conexão de rede do computador.
    • +
    • Seu computador ou a rede são protegidos por um firewall ou proxy? Configurações incorretas podem interferir na navegação.
    • +
    • O problema persiste? Peça ajuda ao seu administrador de rede ou ao suporte do provedor de internet.
    • +
    + ]]>
    + + + Não foi possível conectar + + + +
  • O site pode estar temporariamente indisponível ou ocupado demais. Tente novamente daqui a pouco.
  • +
  • Se não estiver conseguindo carregar nenhuma página, verifique a conexão de dados móveis ou WiFi do seu dispositivo.
  • + + ]]>
    + + + Resposta não esperada do servidor + + + O site respondeu à solicitação de rede de forma não esperada e o navegador não pode continuar.

    + ]]>
    + + + A página não está redirecionando corretamente + + + O navegador deixou de tentar obter o item solicitado. O site está redirecionando o pedido de uma forma que nunca será concluído.

    +
      +
    • Você desativou ou bloqueou cookies exigidos por este site?
    • +
    • Se aceitar cookies do site não resolver, é provavelmente um problema de configuração do servidor, não no seu computador.
    • +
    + ]]>
    + + + Modo offline + + + O navegador está operando em modo offline e não consegue se conectar com o item solicitado.

    +
      +
    • O computador está conectado a uma rede ativa?
    • +
    • Pressione “Tentar novamente” para sair do modo offline e recarregar a página.
    • +
    + ]]>
    + + + Porta restringida por motivos de segurança + + + O endereço solicitado especifica uma porta (por exemplo, mozilla.org:80 para a porta 80 em mozilla.org) normalmente usada para propósitos diferentes da navegação na web. O navegador cancelou a solicitação para sua proteção e segurança.

    + ]]>
    + + + A conexão foi reiniciada + + + O link de rede foi interrompido ao negociar uma conexão. Tente novamente.

    +
      +
    • O site pode estar temporariamente indisponível ou ocupado demais. Tente novamente daqui a pouco.
    • +
    • Se não estiver conseguindo carregar nenhuma página, verifique a conexão de dados móveis ou WiFi do seu dispositivo.
    • +
    + ]]>
    + + + Tipo de arquivo não seguro + + + +
  • Entre em contato com os responsáveis pelo site para informar o problema.
  • + + ]]>
    + + + Erro de conteúdo corrompido + + + A página que você está tentando ver não pode ser exibida porque foi detectado um erro na transmissão de dados.

    +
      +
    • Entre em contato com os responsáveis pelo site para informar o problema.
    • +
    + ]]>
    + + + Conteúdo travado + + A página que você está tentando ver não pode ser exibida porque foi detectado um erro na transmissão de dados.

    +
      +
    • Entre em contato com os responsáveis pelo site para informar o problema.
    • +
    + ]]>
    + + + Erro na codificação do conteúdo + + 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.

    +
      +
    • Entre em contato com os responsáveis pelo site para informar o problema.
    • +
    + ]]>
    + + + Endereço não encontrado + + + O navegador não conseguiu encontrar o servidor de hospedagem do endereço fornecido.

    +
      +
    • Verifique se há erros de digitação no endereço, como + ww.example.com em vez de + www.example.com.
    • +
    • Se não estiver conseguindo carregar nenhuma página, verifique a conexão de dados móveis ou WiFi do seu dispositivo.
    • +
    + ]]>
    + + + Sem conexão com a internet + + Verifique sua conexão de rede ou tente recarregar a página daqui a pouco. + + Recarregar + + + Endereço inválido + O endereço fornecido não está em um formato conhecido. Verifique se há erros na barra de endereços e tente novamente.

    + ]]>
    + + O endereço não é válido + + + +
  • Endereços web geralmente são escritos como http://www.example.com/
  • +
  • Verifique se está usando barras comuns (ou seja, /).
  • + + ]]>
    + + + Protocolo desconhecido + + O endereço especifica um protocolo (por exemplo, wxyz://) que o navegador não reconhece, assim ele não consegue se conectar corretamente com o site.

    +
      +
    • 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.
    • +
    • Alguns protocolos podem necessitar de softwares ou plugins de terceiros para que o navegador possa reconhecer.
    • +
    + ]]>
    + + + Arquivo não encontrado + + +
  • O item pode ter sido renomeado, removido ou realocado?
  • +
  • Há algum erro de grafia, maiúsculas/minúsculas, ou outro erro tipográfico no endereço?
  • +
  • Você tem suficiente permissão de acesso ao item solicitado?
  • + + ]]>
    + + + O acesso ao arquivo foi negado + + +
  • Ele pode ter sido removido, movido, ou as permissões do arquivo podem estar impedindo o acesso.
  • + + ]]>
    + + + Servidor proxy recusou a conexão + + O navegador está configurado para usar um servidor proxy, mas o proxy recusou uma conexão.

    +
      +
    • A configuração de proxy do navegador está correta? Verifique a configuração e tente novamente.
    • +
    • O serviço proxy permite conexões a partir desta rede?
    • +
    • O problema persiste? Peça ajuda ao administrador de rede ou ao suporte do provedor de internet.
    • +
    + ]]>
    + + + Servidor proxy não encontrado + + O navegador está configurado para usar um servidor proxy, mas o proxy não foi encontrado.

    +
      +
    • A configuração de proxy do navegador está correta? Verifique a configuração e tente novamente.
    • +
    • O computador está conectado a uma rede ativa?
    • +
    • O problema persiste? Peça ajuda ao administrador de rede ou ao suporte do provedor de internet.
    • +
    + ]]>
    + + + Problema de site com malware + + O site em %1$s foi denunciado como foco de ataques e foi bloqueado com base nas suas preferências de segurança.

    + ]]>
    + + + Problema de site indesejado + + O site em %1$s foi denunciado por servir software indesejado e foi bloqueado com base nas suas preferências de segurança.

    + ]]>
    + + + Problema de site prejudicial + + O site em %1$s foi denunciado como potencialmente prejudicial e foi bloqueado com base nas suas preferências de segurança.

    + ]]>
    + + + Problema de site enganoso + + 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.

    + ]]>
    + + + Site seguro não disponível + + %1$s não está disponível.]]> + + Continuar para a versão HTTP do site +
    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 @@ + + + + + Tentar novamente + + + Não é possível concluir o pedido + + Neste momento, não está disponível informação adicional sobre este problema ou erro.

    + ]]>
    + + + A ligação segura falhou + + +
  • 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.
  • +
  • Por favor, reporte o problema aos proprietários do site.
  • + + ]]>
    + + + A ligação segura falhou + + + +
  • Isto pode dever-se a um problema de configuração do servidor, ou pode ser alguém a tentar usurpar a identidade do servidor.
  • +
  • Se conseguiu ligar-se com sucesso a este servidor no passado, o erro poderá ser temporário e poderá tentar novamente mais tarde.
  • + + ]]>
    + + + Avançado… + + Alguém pode estar a tentar usurpar a identidade do site e não deve continuar. +

    + + ]]>
    + + Retroceder (recomendado) + + Aceitar o risco e continuar + + + Este website precisa de uma ligação segura. + + +
  • A página que está a tentar visualizar não pode ser exibida porque este website precisa de uma ligação segura.
  • +
  • O problema provavelmente está no website e não há nada que possa fazer para resolvê-lo.
  • +
  • Pode notificar o administrador do website sobre o problema.
  • + + ]]>
    + + + Avançado… + + + %1$s tem uma política de segurança chamada HTTP Strict Transport Security (HSTS), que significa que o %2$s apenas pode ligar-se com este protocolo. Não é possível adicionar uma exceção para este site. + ]]> + + Retroceder + + + A ligação foi interrompida + + O navegador estabeleceu a ligação com sucesso, mas a ligação foi interrompida enquanto transferia informação. Por favor, tente novamente.

    +
      +
    • O site pode estar temporariamente indisponível ou demasiado ocupado. Tente novamente dentro de alguns instantes.
    • +
    • Se não consegue carregar quaisquer páginas, confirme a ligação de dados ou de rede sem fios.
    • +
    + ]]>
    + + + A ligação expirou + + + O site solicitado não respondeu ao pedido de ligação e o navegador deixou de esperar por uma resposta.

    +
      +
    • O servidor poderá estar sobrecarregado ou temporariamente indisponível? Tente novamente mais tarde.
    • +
    • Não consegue navegar noutros sites? Verifique a ligação do computador à rede.
    • +
    • O seu computador ou rede estão protegidos por uma firewall ou proxy? Definições incorretas podem interferir com a navegação na Internet.
    • +
    • Continua a ter problemas? Contacte o seu administrador de rede ou fornecedor de Internet para apoio.
    • +
    + ]]>
    + + + Não é possível ligar + + +
  • O site pode estar temporariamente indisponível ou demasiado ocupado. Volte a tentar dentro de alguns momentos.
  • +
  • Se não conseguir carregar nenhuma página, verifique a ligação de dados ou de rede sem fios do dispositivo.
  • + + ]]>
    + + + Resposta inesperada do servidor + + + O site respondeu ao pedido de rede de uma forma inesperada e o navegador não pode continuar.

    ]]>
    + + + A página não está a redirecionar corretamente + + 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.

    +
      +
    • Desativou ou bloqueou cookies que são necessárias para este site?
    • +
    • 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.
    • +
    ]]>
    + + + Modo desligado + + + O navegador está a operar no modo desligado e não consegue estabelecer ligação ao objeto solicitado.

    +
      +
    • O computador está ligado a uma rede ativa?
    • +
    • Pressione "Tentar novamente" para mudar para o modo ligado e recarregar a página.
    • +
    ]]>
    + + + Porta restringida por questões de segurança + + + O endereço solicitado especifica uma porta (por exemplo, mozilla.org:80 para a porta 80 em mozilla.org) normalmente utilizada para outros fins para além de navegação na Internet. Para sua proteção e segurança o navegador cancelou o pedido.

    ]]>
    + + + A ligação foi reposta + + + A ligação de rede foi interrompida ao negociar uma ligação. Por favor, tente novamente.

    +
      +
    • O site pode estar temporariamente indisponível ou demasiado ocupado. Tente novamente dentro de alguns instantes.
    • +
    • Se não consegue carregar quaisquer páginas, confirme a ligação de dados ou de rede sem fios do seu dispositivo.
    • +
    ]]>
    + + + Tipo de ficheiro inseguro + + + +
  • Por favor, contacte os proprietários do site para os informar deste problema.
  • + ]]>
    + + + Erro de conteúdo corrompido + + + A página que está a tentar ver não pode ser apresentada porque foi detetado um erro na transmissão de dados.

    +
      +
    • Por favor, contacte os proprietários do site para os informar deste problema.
    • +
    ]]>
    + + + O conteúdo falhou + + A página que está a tentar ver não pode ser apresentada porque foi detetado um erro na transmissão de dados.

    +
      +
    • Por favor, contacte os proprietários do site para os informar deste problema.
    • +
    ]]>
    + + + Erro de codificação de conteúdo + + 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.

    +
      +
    • Por favor, contacte os proprietários do site para os informar deste problema.
    • +
    ]]>
    + + + Endereço não encontrado + + + O navegador não conseguiu encontrar o servidor anfitrião para o endereço fornecido.

    +
      +
    • Verifique se o endereço não tem erros de escrita, tais como ww.example.com em vez de + www.example.com.
    • +
    • Se não consegue carregar quaisquer páginas, verifique a ligação de dados ou de rede sem fios do seu dispositivo.
    • +
    ]]>
    + + + Sem ligação à Internet + + Verifique a sua ligação de rede ou tente recarregar a página dentro de alguns momentos. + + Recarregar + + + Endereço inválido + 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.

    ]]>
    + + O endereço não é válido + + + +
  • Os endereços de Internet são habitualmente escritos como http://www.example.com/
  • +
  • Verifique se está a utilizar barras (por exemplo, /).
  • + ]]>
    + + + Protocolo desconhecido + + O endereço especifica um protocolo (por exemplo, wxyz://) que o navegador não reconhece, motivo pelo qual o navegador não consegue ligar-se corretamente ao site.

    +
      +
    • Está a tentar aceder a recursos multimédia ou a outros serviços não baseados em texto? Verifique os requisitos adicionais do site.
    • +
    • Alguns protocolos podem necessitar de software ou de plugins de terceiros para que o navegador os possa reconhecer.
    • +
    ]]>
    + + + Ficheiro não encontrado + + +
  • O item pode ter sido renomeado, removido, ou movido?
  • +
  • Existe algum erro ortográfico, de maiúsculas/minúsculas ou outro erro tipográfico no endereço?
  • +
  • Tem permissões de acesso suficientes ao item solicitado?
  • + ]]>
    + + + O acesso ao ficheiro foi negado + + +
  • Pode ter sido removido, movido ou as permissões do ficheiro podem estar a impedir o acesso.
  • + ]]>
    + + + O servidor proxy recusou a ligação + + O navegador está configurado para utilizar um servidor proxy, mas este recusou a ligação.

    +
      +
    • A configuração de proxy do navegador está correta? Verifique as definições e tente novamente.
    • +
    • O serviço de proxy permite ligações a partir desta rede?
    • +
    • Continua a ter problemas? Consulte o seu administrador de rede ou fornecedor de Internet para apoio.
    • +
    ]]>
    + + + Servidor proxy não encontrado + + O navegador está configurado para utilizar um servidor proxy, mas este não foi encontrado.

    +
      +
    • A configuração de proxy do navegador está correta? Verifique as definições e tente novamente.
    • +
    • O computador está ligado a uma rede ativa?
    • +
    • Continua a ter problemas? Consulte o seu administrador de rede ou fornecedor de Internet para apoio.
    • +
    ]]>
    + + + Problema de software malicioso no site + + + 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.

    ]]>
    + + + Problema de site indesejado + + + 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.

    ]]>
    + + + Problema de site nocivo + + 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.

    ]]>
    + + + Problema de site decetivo + + 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.

    ]]>
    + + + Site seguro não disponível + + %1$s .]]> + + Continuar para o site HTTP +
    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 @@ + + + + + Empruvar anc ina giada + + + Impussibel d\'exequir la dumonda + + Ulteriuras infurmaziuns davart quest problem u questa errur n\'èn per il mument betg disponiblas.

    + ]]>
    + + + La connexiun segira n\'è betg reussida + + +
  • La pagina dumandada na po betg vegnir mussada, perquai ch\'i na va betg da verifitgar l\'autenticitad da las datas.
  • +
  • Contactescha per plaschair ils administraturs da la website per als infurmar davart quest problem.
  • + ]]>
    + + + La connexiun segirada n\'è betg reussida + + +
  • Quai pudess esser in problem cun la configuraziun dal server u ch\'insatgi vul sa dar per quest server.
  • +
  • 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.
  • +]]>
    + + + Avanzà… + + Eventualmain emprova in\'autra website da sa dar per la website giavischada. I vegn recumandà da betg cuntinuar. +

    + ]]>
    + + Turnar (recumandà) + + Acceptar la ristga e cuntinuar + + + Questa website pretenda ina connexiun segirada. + + +
  • La pagina che ti emprovas da visitar na po betg vegnir mussada perquai che questa website pretenda ina connexiun segirada.
  • +
  • I sa tracta probablamain dad in problem da la website e ti na pos far nagut per al schliar.
  • +
  • Ti pos dentant infurmar l\'administratur da la website davart il problem.
  • + + ]]>
    + + + Avanzà… + + + %1$s ha ina directiva da segirezza che sa numna HTTP Strict Transport Security (HSTS). Quai munta che %2$s po mo connectar a moda segirada cun la website. I n\'è betg pussaivel dad agiuntar ina excepziun per visitar questa website. +]]> + + Turnar + + + La connexiun è interrutta + + + Il navigatur ha pudì stabilir ina connexiun, ma ella è interrutta durant la transmissiun da datas. Emprova per plaschair anc ina giada.

    +
      +
    • Eventualmain n\'è la website temporarmain betg cuntanschibla u ch\'ella è surchargiada. Emprova pli tard anc ina giada.
    • +
    • Sche ti na pos era betg chargiar autras paginas, controllescha per plaschair la connexiun da datas u da WLAN da tes apparat.
    • +
    ]]>
    + + + Surpassà il temp per la connexiun + + La website dumandada n\'ha betg respundì ad ina emprova da connectar ed il navigatur ha chalà da spetgar ina resposta.

    +
      +
    • Eventualmain è il server surchargià u temporarmain ord funcziun? Emprova pli tard anc ina giada.
    • +
    • Na pos ti era betg chargiar autras websites? Controllescha la configuraziun da la connexiun cun la rait da tes computer.
    • +
    • Èn tia rait u tes computer protegids dad ina firewall u dad in proxy? Parameters incorrects pon disturbar la navigaziun en il web.
    • +
    • Na funcziuneschi anc adina betg? Contactescha l\'administratur da la rait u il provider e dumonda agid.
    • +
    ]]>
    + + + Connexiun betg reussida + + +
  • Eventualmain n\'è la website temporarmain betg cuntanschibla u ch\'ella è surchargiada. Emprova pli tard anc ina giada.
  • +
  • Sche ti na pos era betg chargiar autras paginas, controllescha la connexiun da datas u da WLAN da tes apparat.
  • +]]>
    + + + Resposta nunspetgada dal server + + + La website dumandada ha respundì a moda nunspetgada. Perquai na po la connexiun betg vegnir mantegnida.

    ]]>
    + + + La pagina na renviescha betg a moda correcta + + + 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.

    +
      +
    • Has ti bloccà u deactivà cookies indispensabels per questa website?
    • +
    • 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.
    • +
    ]]>
    + + + Modus offline + + Il navigatur è en il modus offline e na po perquai betg connectar cun l\'element dumandà.

    +
      +
    • È l\'apparat collià cun ina rait activa?
    • +
    • Clicca sin «Empruvar anc ina giada» per midar en il modus online e rechargiar la pagina.
    • +
    ]]>
    + + + Il port è bloccà per motivs da segirezza + + + L\'adressa dumandada ha inditgà in port (per exempel mozilla.org:80 per il port 80 sin mozilla.org) che na vegn normalmain betg utilisà per navigar en il web. Il navigatur ha annullà la dumonda per ta proteger.

    ]]>
    + + + La connexiun è interrutta + + + La connexiun a la rait è vegnida interrutta durant stabilir ina connexiun. Emprova p.pl. anc ina giada.

    +
      +
    • Eventualmain n\'è la website temporarmain betg cuntanschibla u ch\'ella è surchargiada. Emprova pli tard anc ina giada.
    • +
    • Sche ti na pos era betg chargiar autras paginas, controllescha per plaschair la connexiun da datas u da WLAN da tes apparat.
    • +
    ]]>
    + + + Tip da datoteca malsegir + + +
  • Contactescha per plaschair ils possessurs da la website per als infurmar davart quest problem.
  • + ]]>
    + + + Errur: Cuntegn donnegià + + + Impussibel da visualisar la pagina che ti vuls chargiar perquai ch\'ina errur è capitada en la transmissiun da datas.

    +
      +
    • Contactescha per plaschair ils possessurs da la website per als infurmar davart quest problem.
    • +
    ]]>
    + + + Il cuntegn è collabà + Impussibel da visualisar la pagina che ti vuls chargiar perquai ch\'ina errur è capitada en la transmissiun da datas.

    +
      +
    • Contactescha per plaschair ils possessurs da la website per als infurmar davart quest problem.
    • +
    ]]>
    + + + Cuntegns cun codaziun nunvalida + + Impussibel da visualisar la pagina che ti vuls chargiar perquai ch\'ella utilisescha ina furma da cumpressiun nunvalida u betg cumpatibla.

    +
      +
    • Contactescha per plaschair ils possessurs da la website per als infurmar davart quest problem.
    • +
    ]]>
    + + + Impussibel da chattar l\'adressa + + Impussibel da chattar il server da host da l\'adressa dumandada.

    +
      +
    • Controllescha sche l\'adressa cuntegna sbagls da scriver sco + ww.example.com empè da + www.example.com
    • +
    • Sch\'i na va era betg da chargiar autras paginas, controllescha per plaschair la connexiun da datas u da WLAN da tes apparat.
    • +
    ]]>
    + + + Nagina connexiun cun l\'internet + + + Controllescha tia connexiun cun la rait u emprova da chargiar danovamain la pagina en in mument. + + Rechargiar + + + Adressa nunvalida + L\'adressa inditgada ha in format nunenconuschent. Controllescha per plaschair sche l\'adressa cuntegna sbagls ed emprova anc ina giada.

    ]]>
    + + L\'adressa è nunvalida + + +
  • Adressas d\'internet vegnan normalmain scrittas uschia: + http://www.example.com/
  • +
  • Controllescha che ti has utilisà stritgs diagonals che mussan enavant (q.v.d. + /).
  • + ]]>
    + + + Protocol nunenconuschent + + L\'adressa inditgescha in protocol (per exempel wxyz://) ch\'il navigatur n\'enconuscha betg. Perquai na po il navigatur betg connectar correctamain cun la website.

    +
      +
    • Emprovas ti dad acceder a multimedia u auters servetschs betg textuals? Controllescha sche la website pretenda premissas spezialas.
    • +
    • Tscherts protocols dovran software da terzs u plug-ins per ch\'il navigatur als reconuschia.
    • +
    ]]>
    + + + Betg chattà la datoteca + + +
  • Forsa è l\'element vegnì renumnà, stizzà u spustà?
  • +
  • Cuntegna l\'adressa in sbagl ortografic, in sbagl da scripziun grond e pitschen u in auter sbagl da scriver?
  • +
  • Has ti ils dretgs d\'access necessaris per chargiar l\'element dumandà?
  • + ]]>
    + + + Refusà l\'access a la datoteca + +
  • Forsa è ella stizzada, spustada u che ti n\'es betg autorisà per l\'access.
  • +]]>
    + + + Il proxy server ha refusà la connexiun + Il navigatur è configurà per l\'utilisaziun dad in proxy server, ma il proxy ha refusà ina connexiun.

    +
      +
    • È la configuraziun dal proxy correcta? Controllescha ils parameters ed emprova anc ina giada.
    • +
    • Permetta il servetsch da proxy connexiuns or da questa rait?
    • +
    • Na funcziuneschi anc adina betg? Contactescha l\'administratur da la rait u il provider e dumonda agid.
    • +
    ]]>
    + + + Impussibel da chattar il proxy server + Il navigatur è configurà per l\'utilisaziun dad in proxy server, ma i n\'è betg reussì da chattar il proxy.

    +
      +
    • È la configuraziun dal proxy correcta? Controllescha ils parameters ed emprova anc ina giada.
    • +
    • È l\'apparat connectà ad ina rait activa?
    • +
    • Na funcziuneschi anc adina betg? Contactescha l\'administratur da la rait u il provider e dumonda agid.
    • +
    ]]>
    + + + Problem cun ina pagina da malware + + + La pagina %1$s è vegnida annunziada sco pagina che attatga. Ella vegn bloccada sin fundament da tias preferenzas da segirezza.

    ]]>
    + + + Problem cun ina pagina nungiavischada + + La pagina %1$s è annunziada sco pagina che distribuescha software nungiavischada. Ella vegn bloccada sin fundament da tias preferenzas da segirezza.

    ]]>
    + + + Problem cun ina pagina privlusa + + La pagina %1$s è vegnida annunziada sco pagina potenzialmain privlusa. Ella vegn bloccada sin fundament da tias preferenzas da segirezza.

    ]]>
    + + + Problem cun ina pagina che engiona + + La pagina %1$s è annunziada sco pagina che engiona. Ella vegn bloccada sin fundament da tias preferenzas da segirezza.

    ]]>
    + + + Website segirada betg disponibla + + %1$s na stat betg a disposiziun.]]> + + Cuntinuar cun la website HTTP +
    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 @@ + + + + + Încearcă din nou + + + Cererea nu poate fi finalizată + + + Nu sunt disponibile momentan informații suplimentare despre această problemă sau eroare.

    ]]>
    + + + Conexiunea securizată a eșuat + + +        
  • Pagina pe care încerci să o vizualizezi nu poate fi afișată, deoarece autenticitatea datelor primite nu a putut fi verificată.
  • +        
  • Te rugăm să contactezi proprietarii site-ului web pentru a-i informa despre această problemă.
  • +      ]]>
    + + + Conexiunea securizată a eșuat + + +
  • Ar putea fi din cauza unei probleme în configurația serverului sau ar putea să fie cineva care încearcă să uzurpe identitatea serverului.
  • +
  • Dacă te-ai conectat cu succes în trecut la acest server, eroarea poate fi temporară și poți încerca din nou mai târziu.
  • +]]>
    + + + Avansat… + + Cineva ar putea încerca să uzurpe site-ul și ar fi bine să nu mergi mai departe. +

    +        ]]>
    + + Înapoi (recomandat) + + Acceptă riscul și continuă + + + Conexiunea a fost întreruptă + + Browserul s-a conectat cu succes, dar conexiunea a fost întreruptă în timpul transferului de informații. Încearcă din nou.

    +      
      +        
    • Site-ul ar putea fi temporar indisponibil sau prea ocupat. Încearcă din nou în câteva momente.
    • +        
    • Dacă nu poți încărca nicio pagină, verifică datele dispozitivului tău sau conexiunea Wi-Fi.
    • +      
    ]]>
    + + + Conexiunea a expirat + + + Site-ul solicitat nu a răspuns la o solicitare de conectare și browserul a încetat să aștepte un răspuns.

    +
      +        
    • S-ar putea ca serverul să aibă o cerere ridicată sau o întrerupere temporară? Încearcă din nou mai târziu.
    • +        
    • Nu poți naviga pe alte site-uri? Verifică-ți conexiunea la rețea.
    • +        
    • Dispozitivul tău sau rețeaua ta este protejat(ă) de un firewall sau proxy? Setările incorecte pot interfera cu navigarea pe Web.
    • +        
    • Mai ai probleme? Consultă administratorul de rețea sau furnizorul de internet pentru asistență.
    ]]>
    + + + Conectare eșuată + + +        
  • Site-ul ar putea fi temporar indisponibil sau prea ocupat. Încearcă din nou în câteva momente.
  • +        
  • Dacă nu poți încărca nicio pagină, verifică datele dispozitivului sau conexiunea Wi-Fi.
  • +      ]]>
    + + + Răspuns neașteptat de la server + + + Site-ul a răspuns într-un mod neașteptat solicitării rețelei și browserul nu poate continua.

    ]]>
    + + + Pagina nu se redirecționează corect + + + Browserul a încetat să încerce să recupereze articolul solicitat. Site-ul redirecționează solicitarea într-un mod în care nu va fi niciodată finalizat.

    +      
      +        
    • Ai dezactivat sau ai blocat cookie-urile solicitate de acest site?
    • +        
    • Dacă acceptarea cookie-urilor site-ului nu rezolvă problema, este probabil o problemă de configurare a serverului și nu pe calculatorul tău.
    • +      
    ]]>
    + + + Mod offline + + + Browserul este în modul offline și nu se poate conecta la articolul solicitat.

    +
      +
    • Dispozitivul este conectat la o rețea activă?
    • +
    • Apasă „Încearcă din nou” pentru trecerea pe modul online și reîncărcarea paginii.
    • +
    ]]>
    + + + Port restricționat din motive de securitate + + + Adresa solicitată a specificat un port (de ex. mozilla.org:80 pentru portul 80 oe mozilla.org) folosit în mod normal în alte decât navigarea Web. Browserul a anulat cererea de protecție și securitate.

    ]]>
    + + + Conexiunea a fost resetată + + + Legătura la rețea a fost întreruptă în timpul negocierii unei conexiuni. Încearcă din nou.

    +
      +
    • S-ar putea ca site-ul să fie temporar indisponibil sau prea ocupat. Încearcă din nou în câteva momente.
    • +
    • Dacă nu poți încărca nicio pagină, verifică datele dispozitivului sau conexiunea Wi-Fi.
    • +
    ]]>
    + + + Tip de fișier nesigur + + + +
  • Te rugăm să contactezi proprietarii site-ului web ca să-i informezi despre această problemă.
  • + ]]>
    + + + Eroare de conținut corupt + + + Pagina pe care încerci să o vizualizezi nu poate fi afișată deoarece s-a depistat o eroare în transmisia de date.

    +
      +
    • Te rugăm să contactezi proprietarii site-ului să-i informezi despre problemă.
    • +
    ]]>
    + + + Conținut blocat + Pagina pe care încerci să o vizualizezi nu poate fi afișată deoarece s-a depistat o eroare în transmisia de date.

    +
      +
    • Te rugăm să contactezi proprietarii site-ului să-i informezi despre problemă.
    • +
    +]]>
    + + + Eroare de codificare a conținutului + + Pagina pe care încerci să o vezi nu poate fi afișată pentru că folosește o formă de compresie nevalidă sau nesuportată.

    +
      +
    • Te rugăm să contactezi proprietarii site-ului web pentru a-i informa despre această problemă.
    • +
    ]]>
    + + + Nu s-a găsit adresa + + + Browserul nu a găsit serverul-gazdă pentru adresa furnizată.

    +
      +
    • Verifică adresa pentru erori de tastare precum + ww.exemplu.com în loc de + www.exemplu.com.
    • +
    • Dacă nu poți încărca nicio pagină, verifică datele dispozitivului sau conexiunea Wi-Fi.
    • +
    ]]>
    + + + Lipsă conexiune la internet + + Verifică-ți conexiunea la rețea sau încearcă să reîncarci pagina în câteva momente. + + Reîncarcă + + + Adresă nevalidă + Adresa furnizată nu este într-un format recunoscut. Verifică să nu fie greșeli în bara de adrese și încearcă din nou

    ]]>
    + + Adresa nu este validă + + + +
  • Adresele web sunt scrise în mod normal așa: http://www.example.com/
  • +
  • Asigură-te că folosești bare înclinate la dreapta (adică /).
  • + ]]>
    + + + Protocol necunoscut + + Adresa specifică un protocol (de ex. wxyz://) pe care browserul nu îl recunoaște și, prin urmare, nu se poate conecta corect la site.

    +
      +
    • Încerci să accesezi conținut multimedia sau alte servicii non-text? Verifică site-ul pentru cerințe suplimentare.
    • +
    • Este posibil ca unele protocoale să necesite softuri de la terți sau pluginuri înainte ca browserul să le poată recunoaște.
    • +
    ]]>
    + + + Fișierul nu a fost găsit + + +
  • Se poate ca articolul să fi fost redenumit, șters sau mutat?
  • +
  • Este vreo eroare de ortografie, majuscule/minuscule sau altă eroare tipografică în adresă?
  • +
  • Ai suficiente drepturi de acces la articolul solicitat?
  • + ]]>
    + + + Accesul la fișier a fost refuzat + + +
  • Este posibil să fi fost șters, mutat sau poate că permisiunile fișierului împiedică accesul.
  • + ]]>
    + + + Serverul proxy a refuzat conexiunea + + Browserul este configurat să folosească un server proxy, însă proxyul a refuzat o conexiune.

    +
      +
    • Configurația proxy a browserului este corectă? Verifică setările și încearcă din nou.
    • +
    • Serviciul proxy permite conexiuni din această rețea?
    • +
    • Problemele persistă? Contactează administratorul de rețea sau furnizorul de servicii internet pentru asistență.
    • +
    +]]>
    + + + Serverul proxy nu a fost găsit + + Browserul este configurat să folosească un server proxy, însă proxyul nu a putut fi găsit.

    +
      +
    • Configurația proxy a browserului este corectă? Verifică setările și încearcă din nou.
    • +
    • Dispozitivul este conectat la o rețea activă?
    • +
    • Problemele persistă? Contactează administratorul de rețea sau furnizorul de servicii internet pentru asistență.
    • +
    ]]>
    + + + Problemă de soft rău intenționat pe acest site + + Site-ul de la %1$s a fost raportat ca sursă de atacuri și a fost blocat în baza preferințelor tale de securitate.

    ]]>
    + + + Problemă de site nedorit + + 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.

    ]]>
    + + + Problemă de site dăunător + + 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.

    ]]>
    + + + Problemă de site înșelător + + 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.

    ]]>
    + +
    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 @@ + + + + + Попробовать снова + + + Не удалось выполнить запрос + + Дополнительные сведения об этой проблеме или ошибке сейчас недоступны.

    + ]]>
    + + + Не удалось установить безопасное соединение + + +
  • Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как достоверность полученных данных не может быть проверена.
  • +
  • Пожалуйста, свяжитесь с владельцами сайта и сообщите им об этой проблеме.
  • + ]]>
    + + + Не удалось установить безопасное соединение + + +
  • Это может быть вызвано неправильной настройкой сервера, или, возможно, кто-то пытается подменить нужный вам сервер другим.
  • +
  • Если раньше вы успешно соединялись с этим сервером, то ошибка может быть временной. В таком случае попробуйте снова позже.
  • + + ]]>
    + + + Дополнительно… + + Возможно, кто-то пытается подменить нужный вам сайт другим, поэтому вам лучше не продолжать. +

    + + ]]>
    + + Назад (желательно) + + Принять риск и продолжить + + + Этот сайт требует использования безопасного соединения. + + + +
  • Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как этот веб-сайт требует использования безопасного соединения.
  • +
  • Скорее всего, проблема связана с веб-сайтом, и вы никак не можете её решить.
  • +
  • Вы можете уведомить администратора веб-сайта об этой проблеме.
  • + + ]]>
    + + + Дополнительно… + + + %1$s использует политику безопасности под названием Форсированное безопасное соединение HTTP (HSTS), согласно которой %2$s может соединяться с ним только с помощью безопасного соединения. Вы не можете добавить исключение для посещения этого сайта. + + ]]> + + Назад + + + Соединение было прервано + + Браузер успешно установил соединение, но оно было прервано во время передачи информации. Пожалуйста, попробуйте снова.

    +
      +
    • Возможно, сайт временно недоступен или перегружен запросами. Попробуйте снова через некоторое время.
    • +
    • Если вам не удаётся загрузить ни одну страницу, проверьте соединение своего устройства с мобильной или Wi-Fi сетью.
    • +
    + ]]>
    + + + Время ожидания соединения истекло + + Сайт не отвечал на запрос соединения, и браузер прекратил ожидание.

    +
      +
    • Возможно, сервер сайта перегружен или временно недоступен. Попробуйте снова позже.
    • +
    • Если вам не удаётся открыть и другие сайты, проверьте соединение своего устройства с сетью.
    • +
    • Если ваше устройство или локальная сеть защищены межсетевым экраном или прокси-сервером, проверьте их, так как неправильные настройки могут препятствовать просмотру веб-сайтов.
    • +
    • Проблема не исчезла? Обратитесь к своему системному администратору или Интернет-провайдеру за помощью.
    • +
    + ]]>
    + + + Попытка соединения не удалась + + +
  • Возможно, сайт временно недоступен или перегружен запросами. Попробуйте снова через некоторое время.
  • +
  • Если вам не удаётся загрузить ни одну страницу, проверьте соединение своего устройства с мобильной или Wi-Fi сетью.
  • + + ]]>
    + + + Неизвестный/неопознанный ответ сервера + + Сайт ответил на запрос неожиданным образом и браузер не может обработать его ответ.

    + ]]>
    + + + Циклическое перенаправление на странице + + Браузер прекратил попытки загрузки страницы, так как сайт перенаправляет запрос таким образом, что его выполнение никогда не завершится.

    +
      +
    • Возможно, вы отключили или заблокировали куки, необходимые для работы этого сайта.
    • +
    • Если разрешение кук сайта не решило проблему, то, скорее всего, она связана с настройками сервера, а не с вашим устройством.
    • +
    + ]]>
    + + + Автономный режим + + Браузер работает в автономном режиме и не может установить соединение с запрашиваемым сайтом.

    +
      +
    • Проверьте, что устройство подключено к работающей сети.
    • +
    • Нажмите «Попробовать снова», чтобы выйти из автономного режима и перезагрузить страницу.
    • +
    + ]]>
    + + + В целях безопасности обращение к порту было заблокировано + + Для запрошенного адреса указан порт (например, mozilla.org:80 — это порт 80 на mozilla.org), который обычно не используется для работы с веб-сайтами. В целях безопасности браузер отменил этот запрос.

    + ]]>
    + + + Соединение было сброшено + + Связь с сайтом была оборвана на этапе установки соединения. Пожалуйста, попробуйте снова.

    +
      +
    • Возможно, сайт временно недоступен или перегружен запросами. Попробуйте снова через некоторое время.
    • +
    • Если вам не удаётся загрузить ни одну страницу, проверьте соединение своего устройства с мобильной или Wi-Fi сетью.
    • +
    + ]]>
    + + + Небезопасный тип файла + + +
  • Пожалуйста, свяжитесь с владельцами веб-сайта и сообщите им об этой проблеме.
  • + ]]>
    + + + Загружаемое содержимое повреждено + + Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как при передаче данных была обнаружена ошибка.

    +
      +
    • Пожалуйста, свяжитесь с владельцами веб-сайта и сообщите им об этой проблеме.
    • +
    + ]]>
    + + + Загружаемое содержимое повреждено + Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как при передаче данных была обнаружена ошибка.

    +
      +
    • Пожалуйста, свяжитесь с владельцами веб-сайта и сообщите им об этой проблеме.
    • +
    + ]]>
    + + + Ошибка сжатия содержимого + Страница, которую вы пытаетесь просмотреть, не может быть отображена, так как использует неправильный или неподдерживаемый способ сжатия данных.

    +
      +
    • Пожалуйста, свяжитесь с владельцами веб-сайта и сообщите им об этой проблеме.
    • +
    + ]]>
    + + + Адрес не найден + + Браузеру не удалось найти сервер по указанному адресу.

    +
      +
    • Проверьте, нет ли в адресе опечаток, например, + ww.example.com вместо + www.example.com.
    • +
    • Если вам не удаётся загрузить ни одну страницу, проверьте соединение своего устройства с мобильной или Wi-Fi сетью.
    • +
    + ]]>
    + + + Нет соединения с Интернетом + + Проверьте своё соединение с сетью или попробуйте перезагрузить страницу через некоторое время. + + Перезагрузить + + + Неправильный формат адреса + + Не удаётся распознать формат указанного адреса. Проверьте адрес на наличие ошибок и попробуйте снова.

    + ]]>
    + + Неправильный формат адреса + + +
  • Веб-адреса обычно имеют вид http://www.example.com/
  • +
  • Убедитесь, что вы используете обычную косую черту (т.е. /).
  • + + ]]>
    + + + Неизвестный протокол + Адрес содержит неизвестный браузеру протокол (такой как wxyz://), поэтому браузер не может правильно установить соединение с сайтом.

    +
      +
    • Если вы пытаетесь открыть мультимедиа или другие нетекстовые сервисы, проверьте сайт на наличие особых требований.
    • +
    • Некоторые протоколы могут требовать установки стороннего программного обеспечения или плагинов для того, чтобы браузер мог их распознать.
    • +
    + ]]>
    + + + Файл не найден + +
  • Возможно, файл был переименован, удалён или перемещён.
  • +
  • Проверьте, не допустили ли вы ошибку при вводе адреса.
  • +
  • Убедитесь, что у вас достаточно прав для просмотра запрашиваемого файла.
  • + + ]]>
    + + + Отказано в доступе к файлу + +
  • Возможно, файл был удалён, перемещён или у вас недостаточно прав для его просмотра.
  • + + ]]>
    + + + Прокси-сервер отказал в установке соединения + В браузере настроено использование прокси-сервера, но прокси-сервер отказал в установке соединения.

    +
      +
    • Проверьте настройки прокси браузера и попробуйте снова.
    • +
    • Проверьте, разрешает ли служба прокси соединения из этой сети.
    • +
    • Проблема не исчезла? Обратитесь к своему системному администратору или Интернет-провайдеру за помощью.
    • +
    + ]]>
    + + + Прокси-сервер не найден + В браузере настроено использование прокси-сервера, но прокси-сервер не удалось найти.

    +
      +
    • Проверьте настройки прокси браузера и попробуйте снова.
    • +
    • Проверьте, подключено ли ваше устройство к активной сети.
    • +
    • Проблема не исчезла? Обратитесь к своему системному администратору или Интернет-провайдеру за помощью.
    • +
    + ]]>
    + + + Вредоносный сайт + + Есть информация, что сайт %1$s используется для атак на устройства пользователей, поэтому он был заблокирован в соответствии с вашими настройками защиты.

    + ]]>
    + + + Нежелательный сайт + + Есть информация о том, что сайт %1$s используется для распространения нежелательного программного обеспечения, поэтому он был заблокирован в соответствии с вашими настройками защиты.

    + ]]>
    + + + Опасный сайт + + Есть информация о том, что сайт %1$s потенциально опасен, поэтому он был заблокирован в соответствии с вашими настройками защиты.

    + ]]>
    + + + Поддельный сайт + + Есть информация о том, что эта веб-страница на %1$s — поддельная, поэтому она была заблокирована в соответствии с вашими настройками защиты.

    + ]]>
    + + + Безопасная версия сайта недоступна + + %1$s недоступна.]]> + + Перейти на HTTP-версию +
    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 @@ + + + + + ᱫᱚᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ + + + ᱱᱮᱦᱚᱨ ᱵᱟᱝ ᱯᱩᱨᱟᱹᱣ ᱞᱮᱱᱟ + + + ᱟᱨᱦᱚᱸ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱛ ᱵᱟᱰᱟᱭ ᱞᱟᱹᱜᱤᱫ ᱟᱨ ᱵᱟᱝ ᱵᱷᱩᱞ ᱵᱟᱵᱚᱛ ᱫᱚ ᱵᱟᱹᱱᱩᱜᱼᱟ ᱾

    + ]]>
    + + + ᱡᱟᱯᱛᱤ ᱡᱚᱱᱚᱲᱟᱹᱣ ᱰᱤᱜᱟᱹᱣ + + +
  • ᱚᱠᱟ ᱥᱟᱦᱴᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ ᱠᱷᱚᱡᱚᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱸᱡ ᱚᱱᱟ ᱫᱚ ᱰᱟᱴᱟ ᱨᱮᱭᱟᱜ ᱚᱛᱷᱮᱱᱴᱤᱥᱤᱴᱭ ᱫᱚ ᱵᱟᱝ ᱯᱩᱥᱴᱟᱹᱣ ᱫᱟᱲᱮᱞᱟᱱᱟ ᱾
  • +
  • ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱫᱽ ᱠᱷᱚᱵᱚᱨ ᱮᱢ ᱢᱮ ᱾
  • + + ]]>
    + + + ᱡᱟᱯᱛᱤ ᱡᱚᱱᱚᱲᱟᱣ ᱦᱩᱲᱟᱹᱜ + + +
  • ᱱᱚᱶᱟ ᱥᱟᱨᱵᱷᱟᱨ ᱠᱚᱱᱯᱷᱤᱜᱽᱨᱮᱥᱚᱱ ᱥᱟᱶ ᱮᱴᱠᱮᱴᱚᱬᱮ ᱦᱩᱭ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ, ᱥᱮ ᱱᱚᱶᱟ ᱡᱟᱦᱟᱸᱭ ᱦᱚᱛᱮᱛᱮ ᱤᱢᱯᱚᱨᱥᱳᱱᱮᱴ ᱨᱤᱠᱟᱹ ᱦᱩᱭ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱾
  • ᱡᱩᱫᱤ ᱟᱢ ᱞᱟᱦᱟ ᱛᱮ ᱥᱟᱨᱵᱷᱟᱨ ᱥᱟᱶ ᱡᱚᱲᱟᱣ ᱢᱮᱱᱟᱜ ᱢᱮᱭᱟ, ᱤᱨᱳᱨ ᱛᱟᱦᱮᱸ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ, ᱟᱨ ᱛᱟᱭᱚᱢ ᱛᱮᱢ ᱨᱤᱠᱟᱹ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱾
  • +]]>
    + + + ᱞᱟᱦᱟᱱᱛᱤ… + + ᱤᱢᱯᱚᱨᱥᱳᱱᱮᱴ ᱥᱟᱭᱤᱴ ᱡᱟᱦᱟᱸᱭ ᱠᱚ ᱨᱤᱠᱟᱹ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱟᱨ ᱟᱢ ᱫᱚ ᱪᱟᱞᱟᱣ ᱛᱮ ᱦᱩᱭ ᱟᱢᱟ ᱾ +

    +]]>
    + + ᱛᱟᱭᱚᱢ ᱥᱮᱱᱚᱜ ᱢᱮ (ᱠᱷᱚᱡᱚᱜ ᱜᱮᱭᱟ) + + ᱡᱤᱢᱟ ᱦᱟᱛᱟᱣ ᱢᱮ ᱟᱨ ᱥᱮᱱᱚᱜ ᱢᱮ + + + ᱱᱚᱶᱟ ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱵᱟᱹᱱᱩᱜᱼᱟ ᱾ + + + +
  • ᱟᱢ ᱧᱮᱞ ᱥᱮᱱᱟ ᱥᱟᱦᱴᱟ ᱫᱚ ᱵᱟᱝ ᱧᱮᱞ ᱫᱟᱲᱮᱭᱟᱜᱼᱟᱢ ᱪᱮᱫᱟᱜ ᱥᱮ ᱱᱚᱶᱟ ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱡᱩᱲᱟᱹᱣ ᱫᱚᱨᱠᱟᱨ ᱠᱟᱱᱟ ᱾
  • +
  • ᱱᱚᱶᱟ ᱫᱚ ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱨᱮᱭᱟᱜ ᱵᱤᱥᱚᱭ ᱛᱮ ᱠᱟᱱᱟ, ᱱᱚᱶᱟ ᱵᱟᱵᱚᱛ ᱥᱩᱡᱷᱟᱹᱣ ᱫᱚ ᱟᱢ ᱯᱟᱦᱴᱟ ᱠᱷᱚᱱ ᱪᱮᱫ ᱥᱩᱡᱷᱟᱹᱣ ᱠᱟᱹᱢᱤ ᱵᱟᱭ ᱮᱢᱟ ᱾
  • +
  • ᱱᱚᱶᱟ ᱰᱤᱜᱟᱹᱣ ᱫᱚ ᱟᱢ ᱣᱮᱵᱽᱥᱟᱭᱤᱴ ᱮᱰᱢᱤᱱᱥᱴᱨᱮᱴᱚᱨ ᱠᱷᱚᱵᱚᱨ ᱫᱟᱲᱮ ᱠᱚᱣᱟᱢ ᱾
  • + + ]]>
    + + + ᱞᱟᱦᱟᱱᱛᱤ… + + + %1$s ᱴᱷᱚᱱ ᱢᱤᱫ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱱᱤᱛᱤ ᱡᱟᱦᱟᱸ ᱫᱚ HTTP ᱥᱴᱨᱤᱠᱴ ᱴᱨᱟᱱᱥᱯᱚᱴ ᱠᱚ ᱥᱮᱠᱭᱚᱨᱤᱴᱤ (HSTS) ᱠᱚ ᱢᱮᱛᱮᱜ ᱠᱟᱱᱟ, ᱡᱟᱦᱟᱸ ᱢᱮᱱᱮᱛ ᱫᱚ %2$s ᱫᱚ ᱨᱩᱠᱷᱤᱭᱟᱹ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱡᱩᱲᱟᱹᱣᱜᱼᱟ ᱾ ᱱᱚᱶᱟ ᱥᱟᱭᱤᱴ ᱛᱮ ᱪᱟᱞᱟᱜ ᱞᱟᱹᱜᱤᱫ ᱪᱮᱫ ᱱᱤᱥᱮᱫᱷ ᱞᱟᱹᱠᱛᱤ ᱵᱟᱝ ᱠᱟᱱᱟ ᱾ + ]]> + + ᱛᱟᱭᱚᱢ ᱥᱮᱫ ᱪᱟᱞᱟᱣ + + + ᱡᱚᱲᱟᱣ ᱵᱟᱝ ᱧᱮᱞ ᱞᱮᱱᱟ + + + ᱵᱽᱨᱟᱣᱡᱟᱨ ᱡᱚᱲᱟᱣ ᱞᱟᱦᱟ ᱮᱱᱟ, ᱢᱮᱱᱠᱷᱟᱱ ᱡᱚᱲᱟᱣ ᱵᱟᱝ ᱧᱮᱞ ᱮᱱᱟ ᱡᱚᱠᱷᱚᱱ ᱠᱷᱚᱵᱚᱨ ᱵᱷᱮᱡᱟᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱱᱟ ᱾ ᱫᱟᱭᱟ ᱠᱟᱛᱮ ᱫᱚᱦᱲᱟ ᱨᱤᱠᱟᱹᱭ ᱢᱮ

    • ᱥᱟᱭᱤᱴ ᱵᱟᱝ ᱧᱟᱢ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱥᱮ ᱠᱟᱹᱢᱤ ᱨᱮ ᱛᱟᱦᱮᱱᱟ ᱾ ᱠᱤᱪᱷᱩ ᱜᱷᱟᱹᱲᱤᱡ ᱛᱟᱭᱚᱢ ᱨᱤᱠᱟᱹᱭ ᱢᱮ ᱾
    • ᱡᱩᱫᱤ ᱥᱟᱦᱴᱟ ᱵᱟᱝ ᱮᱢ ᱞᱟᱫᱮ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ, ᱟᱢᱟᱜ ᱰᱤᱵᱷᱟᱭᱥ ᱨᱮᱱᱟᱜ ᱰᱟᱴᱟ ᱥᱮ ᱣᱟᱭ-ᱯᱷᱟᱭ ᱡᱚᱲᱟᱣ ᱧᱮᱞ ᱞᱮᱜᱟᱭ ᱢᱮ ᱾
    ]]>
    + + + ᱡᱚᱯᱚᱲᱟᱣ ᱚᱠᱛᱚ ᱪᱟᱵᱟ ᱮᱱᱟ + + + ᱱᱮᱦᱚᱨ ᱟᱠᱟᱱ ᱥᱟᱭᱤᱴ ᱫᱚ ᱡᱩᱲᱟᱹᱣ ᱵᱟᱭ ᱟᱸᱡᱚᱢ ᱮᱫᱟᱭ ᱟᱨ ᱵᱷᱮᱡᱟ ᱨᱩᱣᱟᱹᱲ ᱮ ᱵᱚᱸᱫᱚ ᱠᱮᱜᱼᱟᱭ ᱾

    +
      +
    • ᱥᱟᱹᱨᱣᱟᱹᱨ ᱦᱟᱤ ᱰᱤᱢᱟᱸᱰ ᱟᱨ ᱵᱟᱝ ᱵᱟᱛᱷᱟᱱᱤᱛ ᱟᱣᱴᱨᱮᱡ ᱞᱟᱹᱤᱫ ᱦᱩᱭ ᱠᱚᱜᱼᱟ? ᱛᱟᱭᱚᱢ ᱛᱮ ᱪᱮᱥᱴᱟᱭ ᱢᱮ ᱾
    • +
    • ᱟᱢ ᱪᱮᱫ ᱚᱞᱜᱟ ᱥᱟᱭᱤ ᱵᱟᱧ ᱵᱟᱨᱩᱡ ᱫᱟᱲᱟᱭᱟᱜ ᱠᱟᱱᱟᱢ? ᱥᱟᱫᱷᱚᱱ ᱨᱮᱭᱟᱜ ᱱᱮᱴᱣᱟᱨᱠ ᱡᱩᱲᱟᱹᱣ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱾
    • +
    • ᱪᱮᱫ ᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱫᱚ ᱯᱷᱟᱭᱟᱨᱣᱟᱞ ᱟᱨ ᱵᱟᱝ ᱯᱨᱳᱠᱥᱭ ᱱᱮᱣᱴᱣᱟᱨᱠ ᱯᱨᱚᱴᱮᱠᱴᱮᱰ ᱜᱮᱭᱟ? ᱵᱷᱩᱞ ᱥᱟᱡᱟᱣ ᱠᱚ ᱠᱟᱹᱜᱤᱫ ᱣᱮᱵ ᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱨᱮ ᱵᱟᱫᱷᱟ ᱮᱢ ᱫᱟᱲᱮᱜᱼᱟᱭ ᱾
    • +
    • ᱱᱤᱛ ᱦᱟᱹᱵᱤᱡ ᱥᱸᱝᱠᱚᱴ ᱨᱮ?ᱜᱚᱲᱚ ᱞᱟᱹᱜᱤᱫ ᱟᱢᱤᱡ ᱱᱮᱴᱣᱟᱨᱠ ᱟᱰᱢᱤᱱᱤᱥᱴᱨᱮᱴᱚᱨ ᱟᱨ ᱵᱟᱝ ᱤᱱᱴᱚᱨᱱᱮᱴ ᱯᱨᱚᱣᱟᱭᱰᱟᱹᱨ ᱥᱟᱞᱟᱜ ᱡᱩᱜᱟᱡᱩᱜ ᱢᱮ
    • +
    + ]]>
    + + + ᱵᱟᱝ ᱡᱩᱲᱟᱹᱣ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ + + + +
  • ᱥᱟᱭᱤᱴ ᱵᱮᱛᱷᱟᱱᱤᱠ ᱛᱟᱦᱮᱸ ᱠᱚᱜᱼᱟ ᱟᱨ ᱵᱟᱝ ᱡᱟᱹᱛᱤ ᱵᱤᱡᱭ ᱛᱟᱦᱮᱸ ᱠᱚᱜᱼᱟ ᱾ ᱠᱤᱪᱷᱤ ᱥᱚᱢᱚᱭ ᱛᱟᱭᱚᱢ ᱛᱮ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ ᱾
  • +
  • ᱟᱢ ᱡᱚᱫᱤ ᱥᱟᱦᱴᱟ ᱵᱟᱝ ᱞᱚᱰ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟᱢ, ᱥᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱨᱮᱭᱟᱜ ᱰᱟᱴᱟ ᱟᱨ ᱵᱟᱝ Wi-Fi ᱡᱩᱲᱟᱹᱣ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱾
  • + + ]]>
    + + + ᱥᱟᱹᱨᱣᱟᱨ ᱠᱷᱚᱱ ᱵᱟᱝ ᱟᱸᱥ ᱨᱚᱲ ᱨᱩᱣᱟᱹᱲ + + + ᱥᱟᱭᱤᱴ ᱫᱚ ᱱᱮᱴᱣᱟᱨᱠ ᱱᱮᱦᱚᱨ ᱴᱷᱤᱠ ᱥᱮ ᱵᱟᱭ ᱟᱸᱡᱚᱢ ᱞᱮᱫᱟᱭ ᱟᱨ ᱵᱨᱟᱣᱡᱚᱨ ᱵᱟᱝ ᱞᱟᱦᱟ ᱫᱟᱲᱮᱟᱜᱼᱟᱭ ᱾

    + ]]>
    + + + ᱥᱟᱦᱴᱟ ᱵᱮᱥ ᱞᱮᱠᱟᱛᱮ ᱵᱟᱭ ᱪᱟᱞᱟᱣ ᱨᱩᱣᱟᱹᱲ ᱠᱟᱱᱟ + + + ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱫᱚ ᱱᱮᱦᱚᱨ ᱨᱤᱴᱨᱭᱤᱵᱷ ᱞᱟᱹᱜᱤᱫ ᱪᱮᱥᱴᱟ ᱵᱚᱸᱫᱚᱭ ᱠᱮᱜᱼᱟᱭ ᱾ ᱥᱟᱭᱴ ᱱᱚᱶᱟ ᱦᱤᱥᱟᱵ ᱨᱮ ᱨᱤᱰᱟᱭᱨᱮᱠᱼᱴ ᱮᱜᱼᱟᱭ ᱡᱮ ᱱᱮᱦᱚᱨ ᱛᱤᱥᱚᱜ ᱵᱟᱭ ᱯᱩᱨᱟᱹᱣᱜᱼᱟ ᱾

    +
      +
    • ᱟᱢ ᱪᱮᱫ ᱠᱩᱩᱠᱤᱡ ᱠᱚ ᱮᱢᱟᱱ ᱵᱚᱸᱫᱚ ᱟᱠᱟᱫᱼᱟᱢ ᱟᱨ ᱵᱟᱝ ᱵᱞᱚᱠ ᱟᱠᱟᱫᱼᱟᱢ ᱚᱠᱟ ᱫᱚ ᱥᱟᱭᱤᱴ ᱛᱟᱭ ᱫᱚᱨᱠᱟᱨ?
    • +
    • ᱡᱚᱫᱤ ᱠᱩᱩᱠᱤᱡ ᱤᱫᱤ ᱠᱟᱛᱮᱫ ᱦᱚᱸ ᱫᱤᱜᱫᱷᱟᱹ ᱵᱟᱝ ᱴᱷᱤᱠ ᱚᱜᱼᱟ, ᱮᱱᱰᱮᱠᱷᱟᱱ ᱱᱚᱶᱟ ᱫᱚ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱠᱚᱱᱯᱷᱤᱜᱭᱩᱨᱮᱥᱚᱱ ᱨᱮ ᱵᱷᱩᱞ ᱛᱟᱦᱮᱸ ᱠᱚᱜᱼᱟ ᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱨᱮ ᱫᱚ ᱵᱟᱝᱟ᱾
    • +
    + ]]>
    + + + ᱚᱯᱷᱞᱟᱭᱤᱱ ᱢᱳᱰ + + + ᱵᱨᱟᱣᱡᱚᱨ ᱫᱚ ᱚᱯᱷᱞᱟᱭᱤᱱ ᱢᱳᱰ ᱨᱮ ᱠᱷᱩᱞᱟᱹᱜ ᱠᱟᱱᱟ ᱟᱨ ᱱᱮᱦᱚᱨ ᱟᱠᱟᱱ ᱡᱤᱱᱤᱥ ᱥᱟᱞᱟᱜ ᱵᱟᱝ ᱡᱩᱰᱟᱹᱣᱼᱜ ᱠᱟᱱᱟ ᱾

    +
      +
    • ᱪᱮᱫ ᱱᱚᱶᱟ ᱥᱟᱫᱷᱱ ᱫᱚ ᱮᱠᱴᱤᱵᱷ ᱱᱮᱴᱣᱟᱨᱠ ᱥᱟᱞᱟᱜ ᱡᱩᱲᱟᱹᱣ ᱠᱟᱱᱟ ?
    • +
    • ᱫᱚᱭᱟᱠᱟᱛᱮ ᱚᱱᱞᱟᱭᱤᱱ ᱢᱳᱰ ᱟᱨ ᱥᱟᱦᱴᱟ ᱨᱤᱞᱚᱰ ᱞᱟᱹᱜᱤᱫ "ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟ " ᱛᱮ ᱴᱤᱯᱟᱹᱣ ᱯᱮ ᱾
    • +
    + ]]>
    + + + ᱡᱟᱹᱯᱛᱤ ᱠᱟᱨᱚᱬ ᱞᱟᱹᱜᱤᱫ ᱟᱠᱚᱴ ᱛᱤᱸᱜᱩ ᱴᱷᱟᱸᱣ + + + ᱱᱮᱸᱦᱚᱨᱟᱠᱟᱱ ᱴᱷᱤᱠᱬᱟᱹ ᱯᱚᱨᱴ ᱮ ᱩᱫᱩᱜᱮᱟᱭ(ᱡᱮᱢᱚᱱ, mozilla.org:80 ᱯᱚᱨᱴ 80 ᱞᱟᱹᱜᱤᱫ mozilla.org ᱨᱮ) ᱚᱫᱷᱤᱠᱟᱸᱥ ᱵᱮᱵᱷᱟᱨᱟᱠᱚ ᱚᱞᱟᱜᱟ ᱦᱤᱹᱥᱟᱵ ᱛᱮ ᱣᱮᱵᱽᱨᱟᱣᱩᱡᱤᱝ ᱪᱷᱚᱰᱟ ᱾ ᱵᱨᱟᱣᱡᱟᱹᱨ ᱫᱚ ᱟᱢᱟᱜ ᱠᱷᱟᱹᱛᱤᱨ ᱟᱨ ᱥᱨᱩᱠᱷᱭᱟᱹ ᱞᱟᱹᱜᱤᱫ ᱱᱮᱦᱚᱨ ᱵᱟᱛᱤᱞ ᱠᱮᱜᱼᱟᱭ ᱾

    + ]]>
    + + + ᱡᱚᱱᱚᱲᱟᱹᱣ ᱫᱚ ᱨᱤᱥᱮᱴ ᱮᱱᱟ + + + ᱡᱩᱲᱟᱹᱣ ᱱᱮᱜᱚᱥᱤᱭᱮᱴ ᱡᱚᱠᱷᱟᱜ ᱱᱮᱴᱣᱟᱨᱠ ᱞᱤᱝᱠ ᱠᱚᱴᱟᱣᱮᱱᱟ ᱾ ᱫᱟᱭᱟ ᱠᱟᱛᱮ ᱫᱚᱦᱲᱟ ᱨᱤᱠᱟᱹᱭ ᱢᱮ

    • ᱥᱟᱭᱤᱴ ᱵᱟᱝ ᱧᱟᱢ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ ᱥᱮ ᱠᱟᱹᱢᱤ ᱨᱮ ᱛᱟᱦᱮᱱᱟ ᱾ ᱠᱤᱪᱷᱩ ᱜᱷᱟᱹᱲᱤᱡ ᱛᱟᱭᱚᱢ ᱨᱤᱠᱟᱹᱭ ᱢᱮ ᱾
    • ᱡᱩᱫᱤ ᱥᱟᱦᱴᱟ ᱵᱟᱝ ᱮᱢ ᱞᱟᱫᱮ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ, ᱟᱢᱟᱜ ᱰᱤᱵᱷᱟᱭᱥ ᱨᱮᱱᱟᱜ ᱰᱟᱴᱟ ᱥᱮ ᱣᱟᱭ-ᱯᱷᱟᱭ ᱡᱚᱲᱟᱣ ᱧᱮᱞ ᱞᱮᱜᱟᱭ ᱢᱮ ᱾
    ]]>
    + + + ᱨᱮᱫ ᱯᱨᱚᱠᱟᱨ ᱵᱟᱝᱨᱟᱠᱷᱭᱟᱼᱟᱜ ᱠᱟᱱᱟ + + +
  • ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱤᱥᱚᱭ ᱨᱮ ᱠᱷᱚᱵᱚᱨ ᱮᱢᱟ ᱠᱚᱢ
  • + + ]]>
    + + + ᱨᱟᱹᱯᱩᱫ ᱡᱤᱱᱤᱥ ᱦᱩᱲᱟᱹᱜ + + + ᱚᱠᱟ ᱥᱟᱦᱴᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ ᱠᱷᱚᱡᱚᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱸᱡ ᱚᱱᱟ ᱫᱚ ᱰᱟᱴᱟ ᱴᱨᱟᱱᱥᱢᱤᱥᱥᱚᱱ ᱨᱮ ᱵᱷᱩᱞ ᱞᱟᱹᱜᱤᱫ ᱵᱟᱝ ᱫᱮᱠᱷᱟᱣᱞᱮᱱᱟ᱾

    +
      +
    • ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱫ ᱠᱷᱚᱵᱚᱨ ᱮᱢ ᱾
    • +
    + ]]>
    + + + ᱡᱤᱱᱤᱥ ᱠᱨᱟᱥ + + ᱚᱠᱟ ᱥᱟᱦᱴᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ ᱠᱷᱚᱡᱚᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱸᱡ ᱚᱱᱟ ᱫᱚ ᱰᱟᱴᱟ ᱴᱨᱟᱱᱥᱢᱤᱥᱥᱚᱱ ᱨᱮ ᱵᱷᱩᱞ ᱞᱟᱹᱜᱤᱫ ᱵᱟᱞᱮ ᱫᱮᱠᱷᱟᱣ ᱫᱟᱲᱮᱭᱟᱞᱮᱢ ᱠᱟᱱᱟ ᱾

    +
      +
    • ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱫ ᱠᱷᱚᱵᱚᱨ ᱮᱢ ᱾
    • +
    + ]]>
    + + + ᱡᱤᱱᱤᱥ ᱮᱱᱠᱳᱰᱤᱸᱝ ᱵᱷᱩᱞ + + ᱚᱠᱟ ᱥᱟᱦᱴᱟ ᱧᱮᱞ ᱞᱟᱹᱜᱤᱫ ᱠᱷᱚᱡᱚᱜ ᱠᱟᱱ ᱛᱟᱦᱮᱸᱡ ᱮᱢ ᱚᱱᱟ ᱫᱚ ᱤᱱᱣᱮᱞᱤᱰ ᱟᱨ ᱵᱟᱝ ᱵᱟᱭ ᱥᱟᱯᱯᱚᱴ ᱮᱫ ᱠᱚᱢᱯᱨᱮᱥᱥᱚᱱ ᱯᱷᱚᱨᱢ ᱨᱮ ᱢᱮᱱᱟᱜ ᱠᱷᱟᱹᱛᱤᱨ ᱵᱟᱝ ᱫᱮᱠᱷᱟᱣᱜ ᱠᱟᱱᱟ ᱾

    +
      +
    • ᱫᱚᱭᱟᱠᱟᱛᱮ ᱣᱮᱵᱥᱟᱭᱤᱴ ᱢᱟᱞᱤᱠ ᱡᱩᱜᱟᱡᱩᱜ ᱮᱢ ᱟᱨ ᱦᱩᱲᱟᱹᱜ ᱵᱟᱵᱚᱫ ᱠᱷᱚᱵᱚᱨ ᱮᱢ ᱾
    • +
    + ]]>
    + + + ᱴᱷᱤᱠᱬᱟᱹ ᱵᱟᱝ ᱧᱟᱞ ᱞᱟᱱᱟ + + + ᱮᱢ ᱟᱠᱟᱱ ᱴᱷᱤᱠᱬᱟᱹ ᱨᱮ ᱵᱨᱟᱣᱡᱚᱨ ᱦᱚᱥᱴ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱵᱟᱭ ᱧᱟᱢ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟᱭ ᱾

    +
      +
    • ᱵᱷᱩᱞ ᱴᱭᱯᱤᱝ ᱞᱟᱹᱜᱤᱫ ᱴᱷᱤᱠᱬᱟᱹ ᱠᱚ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱡᱮᱢᱚᱱ + ww.example.com ᱵᱚᱫᱚᱞ ᱛᱮ + www.example.com.
    • +
    • ᱟᱢ ᱡᱩᱫᱤ ᱥᱟᱦᱴᱟ ᱵᱟᱝ ᱞᱚᱰ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟᱢ, ᱢᱮᱱᱠᱷᱟᱱ ᱟᱢᱟᱜ ᱥᱟᱫᱷᱚᱱ ᱨᱮᱭᱟᱜ ᱰᱟᱴᱟ ᱟᱨ ᱵᱟᱝ Wi-Fi ᱡᱩᱲᱟᱹᱣ ᱧᱮᱞ ᱵᱤᱲᱟᱹᱣ ᱢᱮ ᱾
    • +
    + ]]>
    + + + ᱤᱱᱴᱟᱹᱨᱱᱮᱴ ᱡᱚᱱᱚᱲᱟᱣ ᱵᱟᱹᱱᱩᱜ-ᱟ + + ᱱᱮᱴᱣᱟᱨᱠ ᱡᱩᱲᱟᱹᱣ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱟᱨ ᱵᱟᱝ ᱠᱤᱪᱷᱤ ᱥᱚᱢᱚᱭ ᱛᱟᱭᱚᱢ ᱛᱮ ᱥᱟᱦᱴᱟ ᱫᱩᱦᱲᱟᱹ ᱨᱤᱞᱚᱰ ᱢᱮ ᱾ + + ᱫᱚᱦᱲᱟ ᱞᱟᱫᱤ + + + ᱵᱷᱩᱞ ᱴᱷᱤᱠᱬᱟᱹ + ᱮᱢ ᱟᱠᱟᱱ ᱴᱷᱤᱠᱬᱟᱹ ᱫᱚ ᱵᱟᱝ ᱵᱟᱰᱟᱭ ᱯᱷᱚᱨᱢᱟᱴ ᱨᱮ ᱢᱮᱱᱟᱜᱼᱟ ᱾ ᱫᱚᱭᱟᱠᱟᱛᱮ ᱡᱟᱭᱜᱟ ᱵᱟᱨ ᱨᱮ ᱵᱷᱩᱞ ᱠᱚ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱟᱨ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ ᱾

    + ]]>
    + + ᱴᱷᱤᱠᱬᱟᱹ ᱵᱟᱭ ᱴᱷᱤᱠ ᱟ + + + +
  • ᱣᱮᱵ ᱴᱷᱤᱠᱬᱟᱹ ᱫᱚhttp://www.example.com/ᱠᱚ ᱚᱞᱟ
  • +
  • ᱯᱷᱚᱨᱣᱟᱰ ᱥᱞᱟᱥ ᱮᱢ ᱟᱞᱚ ᱦᱤᱲᱤᱧᱟᱢ (ᱡᱮᱢᱚᱱ /).
  • + + ]]>
    + + + ᱵᱟᱝ ᱵᱟᱰᱟᱭ ᱯᱚᱨᱴᱟᱞ + + ᱴᱷᱤᱠᱬᱟᱹ ᱫᱚ ᱯᱨᱚᱴᱚᱠᱚᱞ ᱮ ᱫᱮᱠᱷᱟᱣᱮᱫᱟᱭ (ᱡᱮᱢᱚᱱ, wxyz://) ᱵᱨᱟᱣᱡᱟᱹᱨ ᱵᱟᱝ ᱪᱤᱱᱦᱟᱹᱣ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ, ᱵᱟᱨᱣᱡᱟᱹᱨ ᱥᱟᱭᱤᱴ ᱨᱮ ᱵᱟᱝ ᱡᱩᱲᱟᱣᱼ ᱫᱟᱲᱮᱭᱟᱜ ᱠᱟᱱᱟ ᱾

    +
      +
    • ᱟᱢ ᱪᱮᱫ ᱢᱟᱹᱞᱴᱤᱢᱤᱰᱤᱭᱟ ᱟᱨ ᱵᱟᱝ ᱵᱟᱝᱼᱚᱞ ᱥᱟᱹᱨᱣᱤᱥ ᱫᱚᱨᱠᱟᱨ ᱥᱮ? ᱡᱟᱹᱥᱛᱤ ᱡᱤᱱᱤᱥ ᱞᱟᱹᱜᱤᱫ ᱥᱟᱭᱤᱴ ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣ ᱢᱮ ᱾
    • +
    • ᱠᱤᱪᱷᱤ ᱯᱚᱨᱴᱟᱞ ᱫᱚ ᱛᱷᱟᱰ ᱯᱟᱨᱴᱭ ᱥᱚᱯᱷᱴᱣᱮᱨ ᱟᱨ ᱵᱟᱝ ᱯᱞᱟᱹᱜᱤᱱᱥ ᱫᱚᱨᱠᱟᱨ ᱚᱠᱟ ᱫᱚ ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱪᱤᱷᱟᱣ ᱨᱮ ᱜᱚᱲᱚᱣᱟᱭ ᱾
    • +
    + ]]>
    + + + ᱨᱮᱫ ᱵᱟᱝ ᱧᱟᱢ ᱞᱮᱱᱟ +
  • ᱪᱮᱫ ᱟᱭᱴᱚᱢ ᱫᱚᱦᱲᱟ ᱛᱮ ᱧᱩᱛᱩᱢ ᱟᱹᱛᱩ ᱫᱟᱲᱮᱭᱟᱜ-ᱟ, ᱚᱪᱚᱫ, ᱥᱮ ᱫᱚᱦᱲᱟ ᱛᱷᱟᱯᱚᱱᱚᱜ-ᱟ?
  • ᱪᱮᱫ ᱮᱰᱨᱮᱥ ᱨᱮ ᱟᱹᱲ ᱠᱮᱯᱤᱴᱭᱞᱤᱡᱮᱥᱚᱱ, ᱥᱮ ᱮᱴᱟᱜ ᱴᱟᱭᱯᱳᱜᱽᱨᱟᱯᱷᱤᱠᱟᱞ ᱨᱮ ᱦᱩᱲᱟᱹᱜ ᱢᱮᱱᱟᱜ-ᱟ?
  • ᱪᱮᱫ ᱟᱢ ᱴᱷᱮᱱ ᱫᱟᱣ ᱮᱢ ᱞᱟᱹᱜᱤᱫ ᱥᱟᱳᱷᱤᱥᱤᱭᱚᱱᱴ ᱯᱚᱨᱢᱤᱥᱚᱱ ᱢᱮᱱᱟᱜ-ᱟ?
  • ••]]>
    + + + ᱨᱮᱫ ᱨᱮ ᱫᱚᱠᱷᱚᱞ ᱵᱟᱹᱰ + + +
  • ᱱᱚᱶᱟ ᱫᱚ ᱚᱰᱚᱠ, ᱩᱪᱟᱹᱲ, ᱟᱨ ᱵᱟᱝ ᱨᱮᱫ ᱨᱮᱭᱟᱜ ᱟᱹᱱᱩᱢᱟᱫᱛᱤ ᱠᱚ ᱠᱷᱟᱹᱛᱤᱨ ᱦᱩᱭ ᱠᱚᱜᱼᱟ ᱾
  • + + ]]>
    + + + ᱮᱲᱮ ᱥᱟᱹᱨᱚᱣᱟᱹᱨ ᱡᱚᱱᱟᱲᱟᱣ ᱵᱟᱭ ᱦᱮᱸ ᱞᱮᱫᱟ + + ᱱᱚᱶᱟ ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱫᱚ ᱯᱨᱚᱠᱥᱭ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱞᱟᱹᱤᱫ ᱜᱮ ᱴᱷᱤᱠ ᱟᱠᱟᱱᱟ, ᱦᱮᱞᱮ ᱯᱨᱚᱠᱥᱭ ᱫᱚ ᱵᱟᱭ ᱡᱩᱰᱟᱹᱣ ᱞᱮᱱᱟᱭ ᱾

    +
      +
    • ᱵᱨᱟᱣᱡᱟᱹᱨ ᱯᱨᱚᱠᱥᱭ ᱠᱚᱱᱯᱷᱤᱜᱭᱩᱨᱮᱥᱚᱱ ᱴᱷᱤᱠ ᱜᱮᱭᱟ ᱥᱮ? ᱥᱟᱡᱟᱣ ᱠᱚ ᱧᱮᱞ ᱢᱮ ᱟᱨ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ
    • +
    • ᱪᱮᱫ ᱯᱨᱚᱠᱥᱭ ᱥᱟᱹᱨᱣᱤᱥ ᱱᱚᱶᱟ ᱱᱮᱴᱣᱟᱨᱠ ᱥᱟᱞᱟᱜ ᱡᱩᱲᱟᱹᱣ ᱟᱱᱩᱢᱟᱹᱛᱭ ᱮᱢᱚ ᱟᱭ ?
    • +
    • ᱱᱤᱛ ᱦᱚᱸ ᱵᱟᱭ ᱴᱷᱤᱠ ᱟ? ᱜᱚᱲᱚ ᱞᱟᱹᱜᱤᱫ ᱟᱢᱟᱜ ᱱᱮᱴᱣᱟᱨᱠ ᱮᱰᱢᱤᱱᱤᱥᱴᱨᱮᱴᱚᱨ ᱟᱨ ᱵᱟᱝ ᱤᱱᱴᱚᱨᱱᱮᱴ ᱯᱨᱚᱣᱟᱭᱰᱟᱹᱨ ᱥᱟᱞᱟᱜ ᱠᱟᱛᱷᱟᱜ ᱢᱮ ᱾
    • +
    + ]]>
    + + + ᱮᱲᱮ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱵᱟᱝ ᱧᱟᱢ ᱞᱮᱱᱟ + + ᱱᱚᱶᱟ ᱵᱽᱨᱟᱣᱩᱡᱟᱹᱨ ᱫᱚ ᱯᱨᱚᱠᱥᱭ ᱥᱟᱹᱨᱣᱟᱹᱨ ᱞᱟᱹᱤᱫ ᱜᱮ ᱴᱷᱤᱠ ᱟᱠᱟᱱᱟ, ᱦᱮᱞᱮ ᱯᱨᱚᱠᱥᱭ ᱫᱚ ᱵᱟᱭ ᱧᱟᱢ ᱞᱟᱱᱟ ᱾

    +
      +
    • ᱵᱨᱟᱣᱡᱟᱹᱨ ᱯᱨᱚᱠᱥᱭ ᱠᱚᱱᱯᱷᱤᱜᱭᱩᱨᱮᱥᱚᱱ ᱴᱷᱤᱠ ᱜᱮᱭᱟ ᱥᱮ? ᱥᱟᱡᱟᱣ ᱠᱚ ᱧᱮᱞ ᱢᱮ ᱟᱨ ᱫᱩᱦᱲᱟᱹ ᱪᱮᱥᱴᱟᱭ ᱢᱮ
    • +
    • ᱪᱮᱫ ᱯᱨᱚᱠᱥᱭ ᱥᱟᱹᱨᱣᱤᱥ ᱱᱚᱶᱟ ᱱᱮᱴᱣᱟᱨᱠ ᱥᱟᱞᱟᱜ ᱡᱩᱲᱟᱹᱣ ᱟᱱᱩᱢᱟᱹᱛᱭ ᱮᱢᱚ ᱟᱭ ?
    • +
    • ᱱᱤᱛ ᱦᱚᱸ ᱵᱟᱭ ᱴᱷᱤᱠ ᱟ? ᱜᱚᱲᱚ ᱞᱟᱹᱜᱤᱫ ᱟᱢᱟᱜ ᱱᱮᱴᱣᱟᱨᱠ ᱮᱰᱢᱤᱱᱤᱥᱴᱨᱮᱴᱚᱨ ᱟᱨ ᱵᱟᱝ ᱤᱱᱴᱚᱨᱱᱮᱴ ᱯᱨᱚᱣᱟᱭᱰᱟᱹᱨ ᱥᱟᱞᱟᱜ ᱠᱟᱛᱷᱟᱜ ᱢᱮ ᱾
    • +
    + ]]>
    + + + ᱢᱟᱞᱣᱮᱨ ᱥᱟᱭᱤᱴ ᱤᱥᱥᱩ + + + %1$s ᱨᱮ ᱢᱮᱱᱟᱜ ᱟᱠᱟᱱᱟ ᱥᱟᱭᱴ ᱫᱚ ᱟᱴᱴᱟᱠ ᱥᱟᱭᱤᱴ ᱞᱮᱠᱷᱟ ᱛᱮ ᱠᱷᱚᱵᱚᱨ ᱟᱠᱟᱱᱟ ᱟᱨ ᱟᱢᱟᱜ ᱥᱤᱠᱭᱚᱨᱭᱴᱤ ᱞᱟᱫᱜᱤᱫ ᱚᱱᱟ ᱫᱚ ᱵᱚᱸᱫᱚ ᱟᱠᱟᱱᱟ ᱾

    + ]]>
    + + + ᱵᱮᱠᱷᱟᱛᱤᱨ ᱥᱟᱭᱤᱴ ᱤᱥᱥᱩ + + + %1$s ᱨᱮ ᱢᱮᱱᱟᱜ ᱥᱟᱭᱤᱴ ᱫᱚ ᱵᱮᱠᱟᱨ ᱥᱚᱯᱷᱴᱣᱮᱨ ᱠᱚ ᱵᱟᱛᱚᱣ ᱟᱨ ᱟᱢᱟᱜ ᱥᱮᱠᱭᱚᱨᱟᱹᱴᱭ ᱯᱨᱤᱯᱷᱮᱨᱮᱱᱥ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱵᱞᱚᱠ ᱟᱠᱟᱱᱟ ᱾

    + ]]>
    + + + ᱵᱚᱛᱚᱨ ᱥᱟᱭᱤᱴ ᱤᱢ ᱪᱟᱞ + + + %1$s ᱨᱮ ᱢᱮᱱᱟᱜ ᱥᱟᱭᱤᱴ ᱫᱚ ᱠᱷᱟᱹᱛᱨᱟ ᱜᱮᱭᱟ ᱟᱨ ᱟᱢᱟᱜ ᱥᱮᱠᱭᱚᱨᱟᱹᱴᱭ ᱯᱨᱤᱯᱷᱮᱨᱮᱱᱥ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱵᱞᱚᱠ ᱟᱠᱟᱱᱟ ᱾

    + ]]>
    + + + ᱮᱲᱮ ᱥᱟᱭᱤᱴ ᱵᱟᱵᱚᱛ + + %1$s ᱨᱮ ᱢᱮᱱᱟᱜ ᱥᱟᱭᱤᱴ ᱫᱚ ᱠᱷᱟᱹᱛᱨᱟ ᱟᱨ ᱮᱲᱮ ᱜᱮᱭᱟ ᱢᱮᱱᱛᱮ ᱠᱚ ᱠᱷᱚᱵᱚᱨ ᱟᱠᱟᱫᱼᱟ ᱟᱨ ᱟᱢᱟᱜ ᱥᱮᱠᱭᱚᱨᱟᱹᱴᱭ ᱯᱨᱤᱯᱷᱮᱨᱮᱱᱥ ᱦᱤᱥᱟᱹᱵ ᱛᱮ ᱵᱞᱚᱠ ᱟᱠᱟᱱᱟ ᱾

    + ]]>
    + + + ᱨᱩᱠᱷᱤᱭᱟᱹ ᱥᱟᱭᱤᱴ ᱫᱚ ᱵᱟᱹᱱᱩᱜ ᱟᱹᱱᱤᱡ + + %1$s ᱫᱚ ᱵᱟᱹᱱᱩᱜᱟᱹᱱᱤᱡ ᱾]]> + + HTTP ᱥᱟᱭᱤᱴ ᱛᱮ ᱞᱟᱦᱟᱜ ᱢᱮ +
    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 @@ + + + + Torra a proare + + + Impossìbile cumpletare sa rechesta + + Immoe non ddoe at a disponimentu àteras informatziones subra de custu problema o faddina.

    + ]]>
    + + + Faddina in sa connessione segura + + + Faddina in sa connessione segura + + + Avantzadu… + + A coa (cussigiadu) + + Atzeta s’arriscu e sighi + + + Custu situ rechedet una connessione segura. + + + Avantzadu… + + + A coa + + + Connessione interrùmpida + + + Tempus pro sa connessione iscadidu + + + Impossìbile connètere + + + Risposta imprevista de su serbidore + + + Sa pàgina no est torrende a indiritzare in manera curreta + + + Modalidade foras de lìnia + + + Ghenna restrinta pro resones de seguresa + + + Connessione torrada a aviare + + + Genia de archìviu non segura + + +
  • Informa s’amministratzione de su situ web de custu problema.
  • + + ]]>
    + + + Faddina de cuntenutu corrotu + + + 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.

    +
      +
    • Cuntata is meres de su situ pro informare de custu problema.
    • +
    + ]]>
    + + + Cuntenutu faddidu + 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.

    +
      +
    • Cuntata is meres de su situ pro informare de custu problema.
    • +
    + ]]>
    + + + Faddina de codìfica de su cuntenutu + + + Indiritzu no agatadu + + + Nissuna connessione de internet + + Controlla sa cunfiguratzione de rete o torra a carrigare sa pàgina luego. + + Torra a carrigare + + + Indiritzu non vàlidu + + + S’indiritzu no est vàlidu + + + +
  • Is indiritzos web in generale s’iscrient in custa manera: http://www.esempru.com/
  • +
  • Assegura·ti chi ses impreende is barras curretas (/).
  • + + ]]>
    + + + Protocollu disconnotu + + + Archìviu no agatadu + + + Atzessu denegadu a s’archìviu + + +
  • Podet dare chi siat istadu cantzelladu o mòvidu, o chi is permissos de archìviu bi siant blochende s’atzessu.
  • + + ]]>
    + + + Connessione cun su serbidore intermediàriu refudada + + + Impossìbile agatare su serbidore intermediàriu + + + 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.

    + ]]>
    + + + 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.

    + ]]>
    + + + 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.

    + ]]>
    + + + Problema de situ ingannosu + + 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.

    + ]]>
    + + + Situ seguru non disponìbile + + %1$s.]]> + + Sighi in su situ HTTP +
    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 @@ + + + + නැවත + + + ඉල්ලීම නිම කළ නොහැකිය + + දැනට මෙම ගැටලුව හෝ දෝෂය පිළිබඳව අතිරේක තොරතුරු නැත.

    +]]>
    + + + ආරක්‍ෂිත සම්බන්‍ධතාවය බිඳ වැටුණි + + + +
  • ලැබුණු දත්ත සත්‍යාපනයට නොහැකි නිසා ඔබ දැකීමට උත්සාහ කරන පිටුව පෙන්වීමට නොහැකිය.
  • +
  • මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.
  • + +]]>
    + + + ආරක්‍ෂිත සම්බන්‍ධතාවය බිඳ වැටුණි + + + +
  • මෙය සේවාදායකයේ වින්‍යාසයෙහි ගැටලුවක් විය හැකිය, හෝ යමෙක් සේවාදායකය ලෙස වංචනිකව පෙනී සිටීමට තැත් කරනවා විය හැකිය.
  • +
  • ඔබ කලින් මෙම සේවාදායකයට සාර්ථකව සම්බන්‍ධ වී ඇත්නම්, මෙය තාවකාලික දෝෂයක් විය හැකිය, පසුව උත්සාහ කරන්න.
  • + + ]]>
    + + + වැඩිදුර… + + යමෙක් වංචනිකව මෙම අඩවිය හසුරුවීමට තැත් කරන හෙයින් ඔබ තවදුරටත් ඉදිරියට නොයා යුතුය. +

    + + ]]>
    + + ආපසු යන්න (නිර්දේශිතයි) + + අවදානම පිළිගෙන ඉදිරියට + + + අඩවියට ආරක්‍ෂිත සම්බන්ධතාවයක් අවශ්‍යයි. + + + +
  • ඔබ දැකීමට උත්සාහ කරන අඩවියට ආරක්‍ෂිත සම්බන්ධතාවයක් අවශ්‍ය නිසා පෙන්වීමට නොහැකිය.
  • +
  • බොහෝ විට අඩවියෙහි ගැටලුවක් නිසා, එය විසඳීමට ඔබට කළ හැකි කිසිවක් නැත.
  • +
  • මෙම ගැටලුව පිළිබඳව අඩවියේ පරිපාලකයා වෙත දැනුම් දීමට හැකිය.
  • + + ]]>
    + + + වැඩිදුර… + + + %1$s හි HTTP දැඩි පරිවහන ආරක්‍ෂාව (HSTS) නම් ආරක්‍ෂණ ප්‍රතිපත්තියක් ඇත, එනම් %2$s මගින් එයට සම්බන්ධ වීමට හැකි වන්නේ ආරක්‍ෂිතව පමණි. ඔබට මෙම අඩවියට ගොඩවැදීමට හැරදැමීමක් එක් කිරීමට නොහැකිය. + ]]> + + ආපසු යන්න + + + සම්බන්ධතාවයට බාධා විය + + + අතිරික්සුව සාර්ථකව සම්බන්ධ විය, නමුත් තොරතුරු මාරු කිරීමේ දී සම්බන්ධතාවයට බාධා විය. නැවත උත්සාහ කරන්න.

    +
      +
    • අඩවිය තාවකාලිකව නොතිබේ හෝ ඉතා කාර්ය බහුලයි. මොහොතකින් නැවත බලන්න.
    • +
    • කිසිදු පිටුවක් පූරණය නොවේ නම්, ඔබගේ උපාංගයේ දත්ත හෝ වයි-ෆයි සම්බන්ධතාවය පරීක්‍ෂා කරන්න.
    • +
    + ]]>
    + + + සම්බන්ධතාවය ඉකුත් වී ඇත + + + ඉල්ලන ලද අඩවිය සම්බන්ධතා ඉල්ලීමකට ප්‍රතිචාර නොදැක්වූ අතර අතිරික්සුව පිළිතුරක් අපේක්‍ෂාවෙන් සිටීම නතර කර ඇත.

    +
      +
    • සේවාදායකය වෙත ඉහළ ඉල්ලුමක් හෝ තාවකාලික ඇණහිටීමක් විය හැකිද? පසුව උත්සාහ කරන්න.
    • +
    • ඔබට වෙනත් අඩවි පිරික්සීමට නොහැකිද? පරිගණකයේ ජාල සම්බන්ධතාව පරීක්‍ෂා කරන්න.
    • +
    • ඔබගේ පරිගණකය හෝ ජාලය ගිනි පවුරකින් හෝ ප්‍රතියුක්තයකින් ආරක්‍ෂා කර තිබේද? වැරදි සැකසුම් නිසා මෙවැනි දෑ සිදු වීමට හැකිය.
    • +
    • තවමත් ගැටළු තිබේද? සහාය සඳහා ඔබගේ ජාලයේ පරිපාලක හෝ අන්තර්ජාල සැපයුම්කරු අමතන්න.
    • +
    + ]]>
    + + + සම්බන්ධ වීමට නොහැකිය + + + +
  • අඩවිය තාවකාලිකව නොතිබේ හෝ කාර්ය බහුලයි. මොහොතකින් උත්සාහ කරන්න.
  • +
  • කිසිදු පිටුවක් පූරණය නොවේ නම්, උපාංගයේ දත්ත හෝ වයි-ෆයි සම්බන්ධතාවය පරීක්‍ෂා කරන්න.
  • + + ]]>
    + + + සේවාදායකයෙන් අනපේක්‍ෂිත ප්‍රතිචාරයකි + + + ජාල ඉල්ලීමට අනපේක්‍ෂිත අයුරින් අඩවිය ප්‍රතිචාර දැක්වූ අතර අතිරික්සුවට ඉදිරියට යාමට නොහැකිය.

    + ]]>
    + + + පිටුව නිසි අයුරින් හරවා යවන්නේ නැත + + + අතිරික්සුව ඉල්ලූ අථකය ලබා ගැනීමට උත්සාහ කිරීම නවතා ඇත. අඩවිය ඉල්ලීම කිසිම විටෙක නිම නොවන අයුරින් යළි හරවා යවයි.

    +
      +
    • මෙම අඩවියට වුවමනා දත්තකඩ අබල හෝ අවහිර කර තිබේ ද?
    • +
    • අඩවියෙහි දත්තකඩ පිළිගැනීමෙන් ගැටලුව නොවිසඳෙයි නම්, එය ඔබගේ උපාංගයේ නොව සේවාදායකයේ වින්‍යාසයෙහි වරදක් විය හැකිය.
    • +
    + ]]>
    + + + මාර්ගඅපගත ප්‍රකාරය + + + අතිරික්සුව එහි මාර්ගඅපගත ප්‍රකාරයෙන් ක්‍රියාත්මක වන නිසා ඉල්ලූ අථකයට සම්බන්ධ වීමට නොහැකිය.

    +
      +
    • උපාංගය සක්‍රිය ජාලයකට සම්බන්ධිත ද?
    • +
    • මාර්ගගත ප්‍රකාරයට මාරු වීමට සහ පිටුව නැවත පූරණයට “යළි උත්සාහ කරන්න” ඔබන්න.
    • +
    + ]]>
    + + + ආරක්‍ෂණ හේතුන් මත තොට සීමා කර ඇත. + + + ඉල්ලන ලද ලිපිනය වියමන පිරික්සීමට සාමාන්‍යයෙන් භාවිතා නොකරන තොටක් (උදා. mozilla.org හි තොට 80 සඳහා mozilla.org:80) භාවිතා කරයි. ඔබගේ රැකවරණය හා ආරක්‍ෂාව සඳහා අතිරික්සුව එම ඉල්ලීම අවලංගු කරන ලදි.

    + ]]>
    + + + සම්බන්ධතාවය යළි සැකසිණි + + + සම්බන්ධතාවයක් කතිකා වන අතරතුර ජාල සබැඳියට බාධා විය. යළි උත්සාහ කරන්න.

    +
      +
    • අඩවිය තාවකාලිකව නොතිබේ හෝ ඉතා කාර්ය බහුලයි. මොහොතකින් නැවත බලන්න.
    • +
    • කිසිදු පිටුවක් පූරණය නොවේ නම්, ඔබගේ උපාංගයේ දත්ත හෝ වයි-ෆයි සම්බන්ධතාවය පරීක්‍ෂා කරන්න.
    • +
    + ]]>
    + + + අනාරක්‍ෂිත ගොනු වර්ගයකි + + + +
  • කරුණාකර මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.
  • + +]]>
    + + + හානි වූ අන්තර්ගත දෝෂයකි + + + දත්ත සම්ප්‍රේෂණ දෝෂයක් අනාවරණය වූ නිසා ඔබ දැකීමට උත්සාහ කරන පිටුව පෙන්වීමට නොහැකිය.

    +
      +
    • මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.
    • +
    + ]]>
    + + + අන්තර්ගතය බිඳ වැටුණි + + දත්ත සම්ප්‍රේෂණ දෝෂයක් අනාවරණය වූ නිසා ඔබ දැකීමට උත්සාහ කරන පිටුව පෙන්වීමට නොහැකිය.

    +
      +
    • මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.
    • +
    + ]]>
    + + + අන්තර්ගත ආකේතන දෝෂයකි + + ඔබ දැකීමට උත්සාහ කරන පිටුව වලංගු නොවන හෝ සහාය නොදක්වන හැකිළුම් අන්දමක් භාවිතා කරන නිසා පෙන්වීමට නොහැකිය.

    +
      +
    • මෙම ගැටලුව පිළිබඳව අඩවියේ හිමිකරුවන්ට දන්වන්න.
    + ]]>
    + + + ලිපිනය හමු නොවුණි + + + අතිරික්සුවට ලබා දුන් ලිපිනයෙහි සත්කාරක සේවාදායකය සොයා ගැනීමට නොහැකිය.

    +
      +
    • මෙවැනි ලිවීමේ දෝෂ තිබේදැයි බලන්න + www.example.com වෙනුවට + ww.example.com.
    • +
    • කිසිදු පිටුවක් පූරණය නොවේ නම්, ඔබගේ උපාංගයේ දත්ත හෝ වයි-ෆයි සම්බන්ධතාවය පරීක්‍ෂා කරන්න.
    • +
    + ]]>
    + + + අන්තර්ජාල සම්බන්ධතාවයක් නැත + + ඔබගේ ජාලයේ සම්බන්ධතාවය පරීක්‍ෂා කරන්න හෝ මොහොතකින් පිටුව යළි පූරණය කර බලන්න. + + යළි පූරණය + + + සාවද්‍ය ලිපියකි + ලබා දුන් ලිපිනය පිළිගත් ආකෘතියක නැත. වැරදි සඳහා ස්ථාන තීරුව පරීක්‍ෂා කර නැවත උත්සාහ කරන්න.

    + ]]>
    + + ලිපිනය වලංගු නොවේ + + + +
  • ලිපින සාමාන්‍යයෙන් ලියන්නේ http://www.example.com/ ලෙසය
  • +
  • ඔබ ඉදිරි ඇල ඉරි භාවිතා කරන බවට වග බලා ගන්න (එනම් /).
  • + + ]]>
    + + + නොදන්නා කෙටුම්පතකි + + අතිරික්සුව හඳුනා නොගන්නා කෙටුම්පතක් ලිපිනයෙන් නිරූපනය කෙරේ (උදා: wxyz://), එබැවින් අඩවියට නිසි ලෙස සම්බන්ධ වීමට නොහැකිය.

    +
      +
    • ඔබ බහුමාධ්‍ය හෝ වෙනත් අකුරු නොවන සේවා වෙත ප්‍රවේශ වීමට උත්සාහ කරනවාද? අමතර අවශ්‍යතා සඳහා අඩවිය පරීක්‍ෂා කරන්න.
    • +
    • සමහර කෙටුම්පත් සඳහා ඒවා අතිරික්සුවකින් හඳුනා ගැනීමට තෙවන පාර්ශ්ව මෘදුකාංග හෝ පේනු අවශ්‍ය විය හැකිය.
    • +
    + ]]>
    + + + ගොනුව හමු නොවුණි + + +
  • අථකය යළි නම් කර, ඉවත් කර, හෝ අන් තැනකට ගෙන ගොස් ද?
  • +
  • ලිපිනයෙහි අකුරු වින්‍යාසය, හැඩයේ හෝ වෙනත් මුද්‍රණ දෝෂයක් තිබේද?
  • +
  • ඔබ ඉල්ලන ලද අථකයට ප්‍රමාණවත් ප්‍රවේශ වීමේ අවසර තිබේද?
  • + + ]]>
    + + + ගොනුවට ප්‍රවේශය ප්‍රතික්‍ෂේප විය + + +
  • එය ඉවත් කර, ගෙන ගොස්, හෝ ගොනුවට ප්‍රවේශ වීමේ අවසර නැත.
  • + + ]]>
    + + + ප්‍රතියුක්ත සේවාදායකය සබැඳුම ප්‍රතික්‍ෂේප කෙරිණි + ප්‍රතියුක්ත සේවාදායකයක් භාවිතයට අතිරික්සුව වින්‍යාසගත කර ඇත, නමුත් ප්‍රතියුක්තය සම්බන්ධතාවයක් ප්‍රතික්‍ෂේප කෙරිණි.

      +
    • අතිරික්සුවෙහි ප්‍රතියුක්ත වින්‍යාසය නිවැරදි ද? සැකසුම් පරීක්‍ෂා කර යළි උත්සාහ කරන්න.
    • +
    • ප්‍රතියුක්ත සේවාව මෙම ජාලයෙහි සම්බන්ධතා සඳහා ඉඩ දෙන්නේද?
    • +
    • තවමත් ගැටළු තිබේද? සහාය සඳහා ඔබගේ ජාලයේ පරිපාලක හෝ අන්තර්ජාල සැපයුම්කරු අමතන්න.
    • +
    + ]]>
    + + + ප්‍රතියුක්ත සේවාදායකය හමු නොවුණි + + ප්‍රතියුක්ත සේවාදායකයක් භාවිතයට අතිරික්සුව වින්‍යාසගත කර ඇත, නමුත් ප්‍රතියුක්තය හමු නොවිණි.

      +
    • අතිරික්සුවෙහි ප්‍රතියුක්ත වින්‍යාසය නිවැරදි ද? සැකසුම් පරීක්‍ෂා කර යළි උත්සාහ කරන්න.
    • +
    • උපාංගය සක්‍රිය සම්බන්ධතාවයක් වෙත සම්බන්ධිත ද?
    • +
    • තවමත් ගැටළු තිබේද? සහාය සඳහා ඔබගේ ජාලයේ පරිපාලක හෝ අන්තර්ජාල සැපයුම්කරු අමතන්න.
    • +
    + ]]>
    + + + අනිෂ්ට අඩවියක ගැටලුවකි + + + ප්‍රහාරක අඩවියක් ලෙස %1$s වාර්තා කර ඇත. ඔබගේ ආරක්‍ෂණ අභිප්‍රේත මත පදනම්ව අවහිර කර ඇත.

    + ]]>
    + + + අනවශ්‍ය අඩවියක ගැටලුවකි + + + අනවශ්‍ය මෘදුකාංග අඩවියක් ලෙස %1$s වාර්තා කර ඇත. ඔබගේ ආරක්‍ෂණ අභිප්‍රේත මත පදනම්ව අවහිර කර ඇත.

    + + ]]>
    + + + හානිකර අඩවියක ගැටලුවකි + + + %1$s අඩවිය හානිකර යැයි වාර්තා කර ඇත. ඔබගේ ආරක්‍ෂණ අභිප්‍රේත මත පදනම්ව අවහිර කර ඇත.

    + ]]>
    + + + කූට අඩවියක ගැටලුවකි + + + කූට පිටුවක් ලෙස %1$s වාර්තා කර ඇත. ඔබගේ ආරක්‍ෂණ අභිප්‍රේත මත පදනම්ව අවහිර කර ඇත.

    + ]]>
    + + + ආරක්‍ෂිත අඩවිය නැත + + %1$s හි HTTPS අනුවාදයක් නැත.]]> + + HTTP අඩවියට යන්න +
    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 @@ + + + + + Skúsiť znova + + + Nepodarilo sa dokončiť požiadavku + + Dodatočná informácia o tomto probléme alebo chybe je momentálne nedostupná.

    + ]]>
    + + + Zabezpečené pripojenie zlyhalo + + +
  • Stránku nemožno zobraziť, pretože pravosť prijímaných údajov sa nedá overiť.
  • +
  • Obráťte sa na vlastníkov stránky a informujte ich o tomto probléme.
  • + + ]]>
    + + + Zabezpečené pripojenie zlyhalo + + +
  • Toto môže byť problém s konfiguráciou servera alebo sa ho niekto snaží napodobniť.
  • +
  • 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.
  • + + ]]>
    + + + Pokročilé… + + Niekto sa môže vydávať za daný server a preto by ste nemali pokračovať. +

    + + ]]>
    + + Prejsť naspäť (odporúča sa) + + Rozumiem riziku a chcem pokračovať + + + Táto webová stránka vyžaduje zabezpečené pripojenie. + + + +
  • Stránka, ktorú sa pokúšate zobraziť, sa nedá zobraziť, pretože táto webová lokalita vyžaduje zabezpečené pripojenie.
  • +
  • Problém je s najväčšou pravdepodobnosťou na webových stránkach a nemôžete ho nijako vyriešiť.
  • +
  • O probléme môžete informovať správcu webových stránok.
  • + + ]]>
    + + + Pokročilé… + + + %1$s má bezpečnostnú politiku s názvom HTTP Strict Transport Security (HSTS), čo znamená, že %2$s sa k nemu môže pripojiť iba zabezpečene. Na návštevu tohto webu nemôžete pridať výnimku. + ]]> + + Prejsť naspäť + + + Pripojenie bolo prerušené + + + Prehliadač sa úspešne pripojil k serveru, ale spojenie bolo v priebehu prenosu údajov prerušené. Prosím, skúste to znova.

    +
      +
    • Stránka môže byť dočasne nedostupná alebo zaneprázdnená. Skúste to znova neskôr.
    • +
    • Ak sa vám nedarí načítať žiadnu stránku, skontrolujte pripojenie svojho zariadenia na internet.
    • +
    + ]]>
    + + + Čas pripojenia vypršal + + Vypršal čas pri pokuse o pripojenie na zadaný server.

    +
      +
    • Server môže byť preťažený, alebo preťaženie siete spôsobuje prílišné omeškanie dát.
    • +
    • Ak sa dá predpokladať, že server je preťažený, skúste pred ďalším pokusom o pripojenie chvíľu počkať.
    • +
    • Ak sú vaše zariadenie alebo sieť chránené pomocou firewallu alebo servera proxy, skontrolujte ich nastavenia.
    • +
    • Pokiaľ budú problémy pretrvávať, kontaktujte svojho správcu siete alebo poskytovateľa pripojenia.
    • +
    + ]]>
    + + + Nie je možné sa pripojiť + + +
  • Stránka môže byť dočasne nedostupná alebo zaneprázdnená. Svoj pokus opakujte neskôr.
  • +
  • Ak sa nedá načítať žiadna stránka, skontrolujte pripojenie svojho zariadenia k internetu.
  • + + ]]>
    + + + Neočakávaná odpoveď servera + + + Server odpovedal na sieťovú požiadavku nečakaným spôsobom a prehliadač nedokáže pokračovať.

    + ]]>
    + + + Stránku sa nepodarilo správne presmerovať + + + Prehliadač prerušil spojenie, pretože server smeruje požiadavku sám na seba v nekonečnej slučke.

    +
      +
    • Toto nastáva, ak stránka vyžaduje cookies a vy ste cookies nepovolili alebo sú pre túto stránku blokované.
    • +
    • Toto je častý problém s nastavením servera a nie vášho zariadenia.
    • +
    + ]]>
    + + + Režim offline + + + Prehliadač je momentálne v režime offline a nemôže sa pripojiť na požadovaný server.

    +
      +
    • Je zariadenie pripojené k aktívnej sieti?
    • +
    • Kliknutím na tlačidlo „Skúsiť znova“ prejdete do režimu online a opätovne načítate obsah stránky.
    • +
    + ]]>
    + + + Bezpečnostné obmedzenie prístupu na port + + + Zadaná adresa (URL) špecifikovala port (napr. mozilla.org:80 je port 80 na mozilla.org), ktorý je normálne určený na iné služby ako prehliadanie webu. Prehliadač požiadavku kvôli vašej ochrane a bezpečnosti neakceptoval.

    + ]]>
    + + + Výpadok pripojenia + + + Spojenie bolo v priebehu otvárania komunikačného kanála so serverom nečakane prerušené. Skúste to znova

    +
      +
    • Stránka môže byť dočasne nedostupná alebo zaneprázdnená. Svoj pokus opakujte neskôr.
    • +
    • Ak sa nedá načítať žiadna stránka, skontrolujte pripojenie svojho zariadenia k internetu.
    • +
    + ]]>
    + + + Nebezpečný typ obsahu + + + +
  • Prosím, obráťte sa na vlastníkov stránky a informujte ich o tomto probléme.
  • + + ]]>
    + + + Poškodený obsah stránky + + + Táto stránka nemohla byť zobrazená kvôli chybe pri prenose údajov.

    +
      +
    • Kontaktujte prevádzkovateľa webovej stránky a informujte ho o tomto probléme.
    • +
    + ]]>
    + + + Obsah zlyhal + + Táto stránka nemohla byť zobrazená kvôli chybe pri prenose údajov.

    +
      +
    • Kontaktujte prevádzkovateľa webovej stránky a informujte ho o probléme.
    • +
    + ]]>
    + + + Chyba kódovania obsahu + + Stránka nemôže byť zobrazená, pretože používa neplatné alebo nepodporované formátovanie.

    +
      +
    • Prosím, kontaktujte majiteľov stránky a informujte ich o tomto probléme.
    • +
    + ]]>
    + + + Adresa sa nenašla + + + URL adresa nezodpovedá žiadnemu serveru a nie je možné ju načítať.

    +
      +
    • Skontrolujte, či ste adresu napísali správne (napr. že neobsahuje + ww.example.com namiesto + www.example.com).
    • +
    • Ak nemôžete načítať žiadnu stránku, skontrolujte pripojenie svojho zariadenia k internetu.
    • +
    + ]]>
    + + + Žiadne pripojenie k internetu + + Skontrolujte pripojenie k sieti alebo skúste načítať stránku o chvíľu. + + Obnoviť + + + Neplatná adresa + Adresa (URL) nie je platná a nemožno ju načítať. Prosím, skontrolujte text v paneli s adresou a skúste to znova.

    + ]]>
    + + Adresa nie je platná + + + +
  • Webové adresy sú zvyčajne zadávané v tvare http://www.example.com/
  • +
  • Skontrolujte, či používate správne lomky (t.j. /).
  • + + ]]>
    + + + Neznámy protokol + + Adresa sa začína protokolom (napr. wxyz://), ktorý prehliadač nepozná, takže sa nemôže správne pripojiť k serveru.

    +
      +
    • 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.
    • +
    • Niektoré protokoly môžu vyžadovať softvér alebo doplnky tretích strán, aby ich prehliadač mohol rozpoznať.
    • +
    + ]]>
    + + + Súbor nebol nájdený + + +
  • Je možné, že bol odstránený, premenovaný alebo premiestnený?
  • +
  • Skontrolujte, či má adresa správny formát a či neobsahuje chyby.
  • +
  • Máte príslušné povolenia na zobrazenie daného súboru?
  • + + ]]>
    + + + Prístup k súboru bol zamietnutý + + +
  • Mohol byť odstránený, premiestnený alebo vám v prístupe bránia oprávnenia.
  • + + ]]>
    + + + Proxy server odmietol spojenie + + Prehliadač je nakonfigurovaný na používanie proxy servera, no tento odmietol pripojenie.

    +
      +
    • Skontrolujte nastavenia proxy servera v prehliadači a skúste to znova.
    • +
    • Má služba proxy povolený prístup k sieti?
    • +
    • Ak problémy pretrvávajú, kontaktujte, prosím, svojho správcu siete alebo poskytovateľa internetu.
    • +
    + ]]>
    + + + Proxy server nenájdený + + Prehliadač je nakonfigurovaný na používanie proxy servera, no tento odmietol pripojenie.

    +
      +
    • Skontrolujte nastavenia proxy servera v prehliadači a skúste to znova.
    • +
    • Je zariadenie pripojené k aktívnej sieti?
    • +
    • Ak problémy pretrvávajú, kontaktujte, prosím, svojho správcu siete alebo poskytovateľa internetu.
    • +
    + ]]>
    + + + Stránka so škodlivým softvérom + + Stránka %1$s bola označená ako nebezpečná a na základe nastavení zabezpečenia bola zablokovaná.

    + ]]>
    + + + Nežiadúca webová stránka + + Stránka %1$s bola označená ako ponúkajúca nevyžiadaný softvér a na základe nastavení zabezpečenia bola zablokovaná.

    + ]]>
    + + + Škodlivá stránka + + Stránka %1$s bola označená ako nebezpečná a na základe nastavení zabezpečenia bola zablokovaná.

    + ]]>
    + + + Podvodná stránka + + Stránka %1$s bola označená ako podvodná a na základe nastavení zabezpečenia bola zablokovaná.

    + ]]>
    + + + Zabezpečená verzia stránky nie je k dispozícii + + %1$s nie je k dispozícii.]]> + + Pokračovať na nezabezpečenú stránku +
    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 @@ + + + + + ولدا کوشش کرو + + + ارداس پوری کائنی کر سڳدا + + + ایں مسئلے بارے وادھوں معلومات فی الحال دستیاب کائنی۔

    ]]>
    + + + قابل بھروسہ کنکشن ناکام تھی ڳیا + + + +
  • جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو موصول ڈیٹا دی اصلیت دی تصدیق کائنی تھی سڳی۔
  • +
  • ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔
  • + + ]]>
    + + + قابل بھروسہ کنکشن ناکام تھی ڳیا + + + +
  • ایہ سرور کانفیگریشن دا کوئی مسئلہ تھی سڳدا ہے یا تھی سڳدے جو کوئی سرور کوں نقلی بݨاوݨ دی کوشش کریندا پیا ہے۔
  • +
  • جے تساں ماضی وچ ایں سرور نال جڑے ہو تاں تھی سڳدا ہے جو خرابی کجھ وقت کیتے ہووے تے تساں کجھ دیر بعد وت کوشش کر سڳدے ہو۔
  • + ]]>
    + + + ودھایا۔۔۔ + + تھی سڳدا ہے جو کوئی سائٹ دی نقل بݨیندا پیا ہے تے تساں جاری نہ رکھو۔ +

    + ]]>
    + + واپس ون٘ڄو(سفارش کیتی ویندی ہے) + + خطرے کوں قبول کرو تے جاری رکھو + + + ایں ویب سائٹ کوں محفوظ کنکشن دی لوڑ ہے۔ + + + +
  • جہڑا ورقہ تساں ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیوں جو ایں ویب سائٹ کوں محفوظ کنکشن دی لوڑ ہے۔
  • +
  • ایہ مسئلہ غالباً ویب سائٹ وی وجہ نال ہے۔ ایندے حل کیتے تہاݙے کول کجھ کائنی۔
  • +
  • تساں ایں مسئلے بارے ویب سائٹ ایڈمن کوں ݙسا سڳدے ہو۔
  • + + ]]>
    + + + ودھایا۔۔۔ + + + %1$s دی ہک حفاظتی پالیسی ہے جیکوں ایچ ٹی ٹی پی سخت ٹرانسپورٹ حفاظت(HSTS) آہدے ہن، جیندے مطلب ایہ ہے جو %2$s صرف ایندے نال حفاظت نال کنکٹ کر سڳدے۔ تساں ایں سائٹ تے ون٘ڄݨ کیتے کوئی استثناء شامل نہوے کر سڳدے۔ + ]]> + + واپس ون٘ڄو + + + کنکشن خراب تھی ڳیا ہائی۔ + + براؤزر کامیابی نال کنکٹ تھی ڳیا ہے، پر معلومات دی منتقلی دے دوران کنکشن وچ رکاوٹ پیدا تھئی۔ براہ کرم، ولدا کوشش کرو۔

    +
      +
    • سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد وت کوشش کرو۔
    • +
    • جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے ، تاں آپݨاں ڈیوائس ڈیٹا یا وائی-فائی کنیکشن دی پڑتال کرو۔
    • +
    ]]>
    + + + کنکشن ٹائم آوٹ تھی ڳیا ہے + + + ارداس کیتی ڳئی سائٹ نے کنکشن دی ارداس دا جواب کائنی ݙتا تے براؤزر نے جواب دی تانگھ کرݨ بند کر ݙتی ہے۔

    +
      +
    • تھی سڳدے جو سرور زیادہ مانگ یا عارضی وقفے دا سامھݨا کریندا پیا ہووے؟ ولدا بعد وچ کوشش کرو۔
    • +
    • پھلا تساں ݙوجھی سائٹ تے وی براؤز نہوے کر سڳدے پئے؟ ڈیوائس دے نیٹ ورک کنکشن دی جانچ کرو۔
    • +
    • بھلا تہاݙی ڈیوائس یا نیٹ ورک کہیں فائروال یا پراکسی دے ذریعے محفوظ ہے؟غلط ترتیباں ویب براؤز کرݨ وچ مداخلت کر سڳدی ہے۔
    • +
    • ہݨ وی مسئلہ درپیش ہے؟ مدد کیتے آپݨے نیٹ ورک کے نگران یا انٹرنیٹ مہیا کار نال رابطہ کرو۔
    • +
    ]]>
    + + + جڑݨ وچ ناکام ریہا + + + +
  • سائٹ عارضی طور تے غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد ولدا کوشش کرو۔
  • +
  • جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے، تاں آپݨی ڈیوائس دا ڈیٹا یا وائی-فائی کنکشن دی جانچ کرو۔
  • + ]]>
    + + + سرور ولوں غیر متوقع جواب + + + سائٹ نے نیٹ ورک ارداس دا غیر متوقع طریقے نال جواب ݙتا تے براؤزر جاری کائنی رہ سڳدا۔

    + ]]>
    + + + ورقہ ٹھیک طرح ری ڈائریکٹ کائنی تھیندا پیا + + + براؤزر نے ارداس کیتے ڳئے آئٹم کوں واگزار کرواوݨ دی کوشش روک ݙتی ہے۔ سائٹ ایں ارداس کوں ایں طرحاں ٻئے پاسے بھیڄیندا پیا ہے جہڑی کݙاہیں وی مکمل کائناں تھیسی۔

    +
      +
    • بھلا تساں سائٹ کیتے ضروری کوکیاں کوں معذور یا بلاک کر ݙتے؟
    • +
    • اگر سائٹ دیاں کوکیاں کوں قبول کرݨ نال وی مسئلہ ٹھیک نئیں تھیندا، تاں ممکن ہے جو ایہ سرور کنفگریشن دا کوئی مسئلہ ہووے نا کہ تہاݙی ڈیوائس دا۔
    • +
    ]]>
    + + + آف لائن موڈ + + + براؤزر آپݨے آف لائن موڈ وچ کم کریندا پئے تے ارداس تھئے آئٹم چیز نال کائنی جڑ سڳدا۔ +

    +
      +
    • بھلا ڈیوائس فعال نیٹ ورک نال کنکٹ تھیا ہویا ہے؟
    • +
    • آن لائن موڈ وچ گھن آوݨ کیتے “ولدا کوشش کرو” دباؤ تے ورقے کوں ولدا لوڈ کرو۔
    • +
    ]]>
    + + + حفاظتی وجوہات کیتے پورٹ محدود کر ݙتا ڳیا ہے + + + ارداس کیتا پتہ ہک خاص پورٹ (مثلاً، mozilla.org تے پورٹ 80 کیتے mozilla.org:80) + کوں مخصوص کریندا ہے جہڑا عام طور تے ویب براؤز کرݨ دے علاوہ کجھ ٻئے مقاصد کیتے ورتیندے۔ تہاݙی حفاظت تے سلامتی کیتے براؤزر نے ارداس کوں منسوخ کر ݙتا ہے۔

    ]]>
    + + + کنکشن ریسٹ تھی ڳیا + + + کنکشن قائم کرݨ دے دوران نیٹ ورک لنک وچ رکاوٹ پیدا تھئی۔ براہ کرم، ولدا کوشش کرو۔

    +
      +
    • سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف تھی سڳدی ہے۔ کجھ دیر بعد وت کوشش کرو۔
    • +
    • جے تساں کوئی وی ورقہ لوڈ نہوے کر سڳدے پئے ، تاں آپݨاں ڈیوائس ڈیٹا یا وائی-فائی کنیکشن دی پڑتال کرو۔
    • +
    ]]>
    + + + غیر محفوظ فائل قسم + + +
  • ویب سائٹ دے مالکاں کوں ایہ مسلے ݙسݨ کیتے رابطہ کرو۔
  • + ]]>
    + + + خراب مواد نقص + + + جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ڈیٹا ترسیل وچ ہک خرابی دا سراغ لڳے۔

    +
      +
    • ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔
    • +
    + ]]>
    + + + مواد تباہ تھی ڳیا + + جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ڈیٹا ترسیل وچ ہک خرابی دا سراغ لڳے۔

    +
      +
    • ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔
    • +
    + ]]>
    + + + مواد اینکوڈ کرݨ وچ خرابی + + جہڑا ورقہ ݙیکھݨ چاہندے ہو، کائنی ݙکھایا ون٘ڄ سڳدا کیونجو ایہ ہک غلط یا بغیر سہارا تھئی کمپریشن ورتیندے۔

    +
      +
    • ویب سائٹ دے مالک کوں ایں مسئلے دا ݙسݨ کیتے انہاں نال رابطہ کرو۔
    • +
    + ]]>
    + + + پتہ کائنی لبھا + + + ݙتے ہوئے پتے کیتے براؤزر ہوسٹ سرور کائنی لبھ سڳا۔

    +
      +
    • پتے وچ ایں طرحاں دیاں ٹائپنگ غلطیاں دی پڑتال کرو + ww.example.com پر لکھݨا ہائی + www.example.com.
    • +
    • جے تساں کہیں وی ورقے کوں لوڈ نہوے کر سڳدے تاں ڈیوائس ڈیٹا یا وائی فائی کنکشن دی پڑتال کرو
    • +
    + ]]>
    + + + کوئی انٹرنیٹ کنکشن کائنی + + اپݨے نیٹ ورک کنکشن دی پڑتال کرو یا کجھ لمحے بعد ولدا کوشش کرو۔ + + ولدا لوڈ کرو + + + غلط پتہ + + مہیا کیتا ڳیا پتہ سُن٘ڄاپُو فارمیٹ وچ کائنی۔ سوہݨا، غلطیاں کیتے لوکیشن پٹی دی پڑتال کرو تے ولدا کوشش کرو۔

    + ]]>
    + + پتہ ٹھیک کائنی + + + +
  • ویب پتے عمومی طور تے این٘ویں http://www.example.com/ لکھے ویندے ہن
  • +
  • یقینی بݨاؤ جو تساں فارورڈ سلیشیز (جین٘ویں /) ورتندے پئے ہو۔
  • + ]]>
    + + + نامعلوم پروٹوکول + + پتے وچ پروٹوکول (مثال دے طور تے wxyz://) جیکوں براؤز کائنی سُن٘ڄݨیندا، تہوں تاں براؤزر سائٹ کوں ٹھیک طرحاں کنکٹ کائنی کر سڳدا۔

    +
      +
    • بھلا تساں ملٹی میڈیا تے ٻیاں غیرعبارت خدمتاں تے رسائی دی کوشش کریندے پئے ہو؟ ٻیاں ضروریات کیتے سائٹ دی پڑتال کرو۔
    • +
    • براؤزر دے سُن٘ڄاݨݨ کنوں پہلے کجھ پروٹوکولاں کوں تریجھی پارٹی سافٹ ویئر یا پلگ اناں دی لوڑ ہوندی ہے۔
    • +
    + ]]>
    + + + فائل کائنی لبھی + + +
  • بھلا آئٹم دا ناں وٹایا ون٘ڄ سڳدا ہائی، ہٹا ون٘ڄ سڳدا ہائی، یا ٻئی جاء تے منتقل کیتا ون٘ڄ سڳدا ہائی؟
  • +
  • بھلا پتے وچ ہجیاں، وݙے حروف یا ٹائپ دی کوئی ٻئی غلطی ہے؟
  • +
  • بھلا تہاݙے کول مطلوبہ آئٹم تائیں رسائی دی کافی اجازت ہے؟
  • + + ]]>
    + + + فائل تائیں رسائی مسترد کر ݙتی ڳئی ہائی + + +
  • ایہ ہٹا یا ٹور ݙتا ڳیا ہوسی یا فائل اجازتاں رسائی کائناں تھیوݨ ݙیندیاں پیاں ہوسن
  • + + ]]>
    + + + پراکسی سرور کنکشن دا انکار کر ݙتے + + براؤزر پراکسی سرر ورتݨ کیتے کنفیگر تھیا ہویا ہے، پر پراکسی نے کنکشن دا انکار کر ݙتے۔

    +
      +
    • بھلا براؤزر دی پراکسی کنفیگریشن ٹھیک ہے؟ ترتیباں دی پڑتال کرو تے ولدا کوشش کرو۔
    • +
    • بھلا پراکسی سروس ایں نیٹ ورک کنوں اجازت ݙیندی ہے؟
    • +
    • اڄݨ وی مشکل وچ ہو؟ مدد کیتے نیٹ ورک ایڈمن یا انٹرنیٹ فراہم کرݨ آلے نال مشورہ کرو۔
    • +
    + ]]>
    + + + پراکسی سرور کائنی لبھا + + براؤزر پراکسی سرر ورتݨ کیتے کنفیگر تھیا ہویا ہے، پر پراکسی کائنی لبھ سڳی۔

    +
      +
    • بھلا براؤزر دی پراکسی کنفیگریشن ٹھیک ہے؟ ترتیباں دی پڑتال کرو تے ولدا کوشش کرو۔
    • +
    • بھلا ڈیوائس فعال نیٹ ورک نال کنکٹ تھئی ہوئی ہے؟
    • +
    • اڄݨ وی مشکل وچ ہو؟ مدد کیتے نیٹ ورک ایڈمن یا انٹرنیٹ فراہم کرݨ آلے نال مشورہ کرو۔
    • +
    + ]]>
    + + + مالویئر سائٹ مسئلہ + + + %1$s تے موجود سائٹ کوں حملہ سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔

    + ]]>
    + + + ناپسندیدہ سائٹ مسئلہ + + + %1$s تے موجود سائٹ کوں ناپسندیدہ سافٹ ویئر خدمت دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔

    + ]]>
    + + + نقصان دہ سائٹ مسئلہ + + + %1$s تے موجود سائٹ کوں ممکنہ نقصان دہ سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔

    + ]]>
    + + + فریبی سائٹ مسئلہ + + + %1$s تے موجود ویب ورقے کوں فریبی سائٹ دے طور تے رپورٹ کیتا ڳئے تے تہاݙی سیکیورٹی ترجیحات دی بݨیاد تے بلاک کر ݙتا ڳئے۔

    + ]]>
    + + + محفوظ سائٹ دستیاب کائنی + + %1$s دا ایچ ٹی ٹی پی ایس ورشن دستیاب کائنی۔]]> + + ایچ ٹی ٹی پی سائٹ تے جاری رکھو +
    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 @@ + + + + + Poskusi znova + + + Zahteve ni mogoče izpolniti + + + Trenutno ni na voljo dodatnih informacij o tej težavi ali napaki.

    + ]]>
    + + + Varna povezava ni uspela + + + +
  • Strani, ki si jo želite ogledati, ni mogoče prikazati, ker ni mogoče preveriti pristnosti sprejetih podatkov.
  • +
  • O napaki obvestite lastnike spletnega mesta.
  • + + ]]>
    + + + Varna povezava ni uspela + + + +
  • Težava se lahko nahaja v nastavitvah strežnika ali pa ga kdo poskuša oponašati.
  • +
  • Če ste se v preteklosti že uspešno povezali na ta strežnik, je napaka morda samo začasna, zato lahko ponovno poskusite kasneje.
  • + + ]]>
    + + + Napredno … + + Nekdo lahko poskuša oponašati to spletno mesto, zato nadaljevanje ni priporočeno. +

    + + ]]>
    + + Pojdi nazaj (priporočeno) + + Sprejmi tveganje in nadaljuj + + + To spletno mesto zahteva varno povezavo. + + + +
  • Strani, ki si jo želite ogledati, ni mogoče prikazati, ker to spletno mesto zahteva varno povezavo.
  • +
  • Težava je najverjetneje na spletnem mestu, zato je sami ne morete odpraviti.
  • +
  • O težavi lahko tudi obvestite skrbnika spletnega mesta.
  • + + ]]>
    + + + Napredno … + + + %1$s uporablja varnostni pravilnik, imenovan HTTP Strict Transport Security (HSTS), kar pomeni, da se lahko %2$s nanj poveže zgolj varno. Za obisk tega spletnega mesta ne morete dodati izjeme. + ]]> + + Nazaj + + + Povezava je bila prekinjena + + + Brskalnik se je uspešno povezal, vendar se je povezava prekinila med prenosom podatkov. Poskusite znova.

    +
      +
    • Spletno mesto je morda začasno nedosegljivo ali preobremenjeno. Poskusite znova + nekoliko pozneje.
    • +
    • Če ne morete naložiti nobenega spletnega mesta, preverite nastavitve podatkovne povezave ali Wi-Fi.
    • +
    + ]]>
    + + + Povezava je potekla + + + Zahtevana stran ni odgovorila na zahtevo po povezavi, brskalnik pa je prenehal čakati na odgovor.

    +
      +
    • Če mislite, da bi strežnik lahko bil zelo obremenjen ali trenutno nedostopen, poskusite znova nekoliko kasneje.
    • +
    • Če se ne morete povezati na nobeno spletno mesto, preverite povezavo svoje naprave do interneta.
    • +
    • Če uporabljate posrednika ali požarni zid, se prepričajte, da so vaše nastavitve pravilne.
    • +
    • Če težava ne izgine, se posvetujte s svojim skrbnikom sistema ali s ponudnikom internetnih storitev.
    • +
    + ]]>
    + + + Povezave ni mogoče vzpostaviti + + + +
  • Spletno mesto je morda začasno nedosegljivo ali preobremenjeno. Poskusite znova + nekoliko pozneje.
  • +
  • Če ne morete naložiti nobene strani, preverite nastavitve podatkovne povezave ali Wi-Fi.
  • + + ]]>
    + + + Nepričakovan odgovor strežnika + + + Spletno mesto je na mrežno zahtevo odgovorilo na nepričakovan način in brskalnik ne more nadaljevati.

    + ]]>
    + + + Stran se ne preusmerja pravilno + + + Brskalnik je prenehal s prenašanjem zahtevane strani. Spletno mesto preusmerja zahtevek tako, da se krog ne bi nikoli zaključil.

    +
      +
    • Ste onemogočili ali prepovedali piškotke, ki bi jih to spletno mesto potrebovalo?
    • +
    • Č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.
    • +
    ]]>
    + + + Brez povezave + + + Brskalnik trenutno deluje v načinu brez povezave in se zato ne more povezati na zahtevano stran.

    +
      +
    • Je naprava povezana v delujoče omrežje?
    • +
    • Pritisnite “Poskusi znova” za preklop v povezan način in ponovno nalaganje strani.
    • +
    ]]>
    + + + Vrata nesprejemljiva iz varnostnih razlogov + + + Zahtevan naslov določa, katera vrata naj se uporabijo (npr. mozilla.org:80 pomeni vrata 80 na strežniku mozilla.org) za namene, ki niso brskanje po spletu. Brskalnik je preprečil to zahtevo zaradi varnostnih razlogov.

    + ]]>
    + + + Povezava je bila ponastavljena + + + Povezava s stranjo je bila nepričakovano prekinjena med pogajanjem za povezavo. Poskusite znova.

    +
      +
    • Spletno mesto je morda začasno nedosegljivo ali preobremenjeno. Poskusite znova + nekoliko pozneje.
    • +
    • Če ne morete naložiti nobene strani, preverite nastavitve podatkovne povezave ali Wi-Fi.
    • +
    + ]]>
    + + + Nevarna vrsta datoteke + + + +
  • O napaki obvestite lastnike spletnega mesta.
  • + + ]]>
    + + + Napaka zaradi pokvarjene vsebine + + + Strani, ki si jo želite ogledati, ni mogoče prikazati, ker je bila zaznana napaka pri prenosu podatkov.

    +
      +
    • O napaki obvestite lastnike spletnega mesta.
    • +
    + ]]>
    + + + Vsebina se je sesula + + Strani, ki si jo želite ogledati, ni mogoče prikazati, ker je bila zaznana napaka pri prenosu podatkov.

    +
      +
    • O napaki obvestite lastnike spletnega mesta.
    • +
    + ]]>
    + + + Napaka pri kodiranju vsebine + + Strani, ki si jo želite ogledati, ni mogoče prikazati, ker uporablja neveljavno ali nepodprto obliko stiskanja.

    +
      +
    • O napaki obvestite lastnike spletnega mesta.
    • +
    + ]]>
    + + + Naslova ni mogoče najti + + + Strani ni bilo mogoče najti.

    +
      +
    • Morda ste se zmotili pri črkovanju naslova, npr. + ww.example.com namesto + www.example.com.
    • +
    • Če ne morete naložiti nobene strani, preverite nastavitve podatkovne povezave ali Wi-Fi.
    • +
    ]]>
    + + + Ni internetne povezave + + Preverite omrežno povezavo ali poskusite znova naložiti stran. + + Ponovno naloži + + + Neveljaven naslov + Vneseni naslov ni v prepoznani obliki. Preverite, da v vrstici z naslovom ni napak, in poskusite znova.

    + ]]>
    + + Neveljaven naslov + + + +
  • Spletni naslovi so običajno napisani v obliki http://www.example.com/
  • +
  • Prepričajte se, da uporabljate poševnice naprej (tj. /).
  • + + ]]>
    + + + Neznan protokol + + Naslov navaja protokol (npr. wxyz://), ki ga brskalnik ne pozna, zato se ne more pravilno povezati s spletnim mestom.

    +
      +
    • Želite uporabiti večpredstavnost ali drugo nebesedilno storitev? Preverite morebitne dodatne programske zahteve spletnega mesta.
    • +
    • Nekateri protokoli zahtevajo programsko opremo drugih izdelovalcev ali dodatke, preden jih lahko brskalnik uporablja.
    • +
    + ]]>
    + + + Datoteke ni mogoče najti + + +
  • Morda je bila želena datoteka preimenovana, izbrisana ali prestavljena.
  • +
  • Je njeno ime napačno črkovano, napisano z nepravilnimi velikimi in malimi črkami ali kako drugače narobe napisano?
  • +
  • Imate ustrezna dovoljenja za dostop do želene datoteke?
  • + + ]]>
    + + + Dostop do datoteke je bil zavrnjen + + +
  • Morda je bila odstranjena, premaknjena ali pa dovoljenja datoteke preprečujejo dostop.
  • + + ]]>
    + + + Posrednik zavrnil povezavo + Povezava z internetom je nastavljena prek posrednika, ki pa odklanja povezavo.

    +
      +
    • Je nastavitev posrednika ustrezna? Preverite nastavitve in poskusite ponovno.
    • +
    • Ali ta posrednik dovoli povezave s te mreže?
    • +
    • Če težava ne izgine, se posvetujte s svojim skrbnikom sistema ali s ponudnikom internetnih storitev.
    • +
    + ]]>
    + + + Posrednika ni mogoče najti + + Povezava z internetom je nastavljena prek posrednika, ki ga ni mogoče najti.

    +
      +
    • Je nastavitev posrednika ustrezna? Preverite nastavitve in poskusite ponovno.
    • +
    • Je naprava povezana v delujoče omrežje?
    • +
    • Če težava ne izgine, se posvetujte s svojim skrbnikom sistema ali s ponudnikom internetnih storitev.
    • +
    + ]]>
    + + + Težava zlonamerne strani + + Spletno mesto %1$s je bilo prijavljeno kot napadalno in je zaradi vaših varnostnih nastavitev zavrnjeno.

    + ]]>
    + + + Težava neželene strani + + Spletno mesto %1$s je bilo prijavljeno, da razširja neželeno programsko opremo, in je zaradi vaših varnostnih nastavitev zavrnjeno.

    + ]]>
    + + + Težava škodljive strani + + Spletno mesto %1$s je bilo prijavljeno kot morebitno škodljivo in je bilo zavrnjeno na podlagi vaših varnostnih nastavitev.

    + ]]>
    + + + Težava zavajajoče strani + + Spletna stran na naslovu %1$s je bila prijavljena kot zavajajoča in je bila zavrnjena na podlagi vaših varnostnih nastavitev.

    + ]]>
    + + + Varno spletno mesto ni na voljo + + %1$s ni na voljo.]]> + + Nadaljuj na stran HTTP +
    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 @@ + + + + + Riprovoni + + + Kërkesa Nuk Plotësohet Dot + + Hëpërhë s’ka hollësi shtesë të gatshme rreth këtij problemi apo gabimi.

    + ]]>
    + + + Dështoi Lidhja e Sigurt + + + +
  • Faqja që po provoni të shihni s’shfaqet dot, ngaqë mirëfilltësia e të dhënave të marra s’u vërtetua dot.
  • +
  • Ju lutemi, lidhuni me të zotët e sajtit, për t’u bërë të ditur këtë problem.
  • + ]]>
    + + + Dështoi Lidhja e Sigurt + + +
  • Mund të jetë problem i formësimit të shërbyesit, ose mundet që dikush të jetë duke imituar shërbyesin.
  • +
  • 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ë.
  • + ]]>
    + + + Të mëtejshme… + + + Dikush mund të jetë duke provuar të imitojë sajtin dhe s’duhet të vazhdoni. +

    + ]]>
    + + Kthehuni Mbrapsht (E këshilluar) + + Pranoni Rrezikun dhe Vazhdoni + + + Ky sajt lyp një lidhje të siguruar. + + + +
  • Faqja që po rrekeni të shihni, s’mund të shfaqet, ngaqë ky sajt lyp një lidhje të siguruar.
  • +
  • Gjasat janë që problemi të jetë me sajtin dhe s’ka gjë që mund ta bëni për ta zgjidhur.
  • +
  • Mund të njoftoni përgjegjësin e sajtit për problemin.
  • + + ]]>
    + + + Të mëtejshme… + + + %1$s përmban një rregull sigurie të quajtur HTTP Strict Transport Security (HSTS), që do të thotë se %2$s-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. + ]]> + + Kthehu Mbrapsht + + + Lidhja u ndërpre + + + Shfletuesi u lidh me sukses, por lidhja u ndërpre teksa shpërnguleshin të dhëna. Ju lutemi, riprovoni.

    +
      +
    • Sajti mund të jetë përkohësisht jashtë funksionimi, ose shumë i zënë. Riprovoni pas pak çastesh.
    • +
    • Nëse s’jeni në gjendje të ngarkoni çfarëdo faqe, kontrolloni lidhjen me rrjetin celular, ose Wi-Fi të pajisjes tuaj.
    • +
    ]]>
    + + + Lidhjes i mbaroi koha + + + Sajti i kërkuar s’iu përgjigj kërkesës për lidhje dhe shfletuesi reshti së prituri për përgjigje.

    +
      +
    • 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ë.
    • +
    • S’jeni në gjendje të shfletoni sajte të tjerë? Kontrolloni lidhjen në rrjet të pajisjes.
    • +
    • A është pajisja, ose rrjeti juaj i mbrojtur me një firewall apo ndërmjetës? Rregullime të pasakta mund të prekin shfletimin në Web.
    • +
    • Keni ende vështirësi? Për ndihmë, lidhuni me administruesin e rrjetit tuaj, ose me furnizuesin e Internetit.
    • +
    ]]>
    + + + S’arrin të lidhet + + +
  • Sajti mund të jetë përkohësisht jashtë funksionimit ose shumë i zënë. Riprovoni pas pak çastesh.
  • +
  • Nëse s’jeni në gjendje të ngarkoni çfarëdo faqe, kontrolloni lidhjen me rrjetin celular, ose Wi-Fi të pajisjes tuaj.
  • + ]]>
    + + + Përgjigje e papritur prej shërbyesit + + Sajti iu përgjigj kërkesës së rrjetit në një mënyrë të papritur dhe shfletuesi s’vazhdon dot.

    ]]>
    + + + Faqja s’është ridrejtuar si duhet + + + 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ë.

    +
      +
    • I keni çaktivizuar apo bllokuar cookie-t e domosdoshme për këtë sajt?
    • +
    • 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.
    • +
    ]]>
    + + + Mënyrë Jo Në Linjë + + Shfletuesi po punon nën mënyrën jashtë linje dhe s’lidhet dot te objekti i kërkuar.

    +
      +
    • A është pajisja e lidhur te një rrjet aktiv?
    • +
    • Shtypni “Riprovoni” që të kalohet nën mënyrën në linjë dhe të ringarkohet faqja.
    • +
    ]]>
    + + + Portë e kufizuar për arsye sigurie + + + Adresa e kërkuar donte një portë (p.sh., mozilla.org:80 për portën 80 te mozilla.org) normalisht e përdorur për qëllime të tjera dhe jo për shfletim Web. Shfletuesi e anuloi kërkesën me qëllim mbrojtjen dhe sigurinë tuaj.

    ]]>
    + + + Lidhja u rivendos + + Ndërlidhja me rrjetin u ndërpre teksa negociohej një lidhje. Ju lutemi, riprovoni.

    +
      +
    • Sajti mund të jetë përkohësisht jashtë funksionimi, ose shumë i zënë. Riprovoni pas pak çastesh.
    • +
    • Nëse s’jeni në gjendje të ngarkoni çfarëdo faqe, kontrolloni lidhjen me rrjetin celular, ose Wi-Fi të pajisjes tuaj.
    • +
    ]]>
    + + + Lloj Jo i Parrezik Kartelash + + + +
  • Ju lutemi, lidhuni me të zotët e sajtit, për t’u bërë të ditur këtë problem.
  • + + ]]>
    + + + Gabim nga Lëndë e Dëmtuar + + + Faqja që po rrekeni të shihni, s’mund të shfaqet, ngaqë u pikas një gabim në transmetimin e të dhënave.

    +
      +
    • Ju lutemi, lidhuni me të zotët e sajtit që t’u njoftoni këtë problem.
    • +
    ]]>
    + + + Vithisje lënde + Faqja që po rrekeni të shihni, s’mund të shfaqet, ngaqë u pikas një gabim në transmetimin e të dhënave.

    +
      +
    • Ju lutemi, lidhuni me të zotët e sajtit që t’u njoftoni këtë problem.
    • +
    ]]>
    + + + Gabim Kodimi Lënde + + Faqja që po rrekeni të shihni s’shfaqet dot, sepse përdor një formë ngjeshjeje të mangët, ose të pambuluar.

    +
      +
    • Ju lutemi, lidhuni me të zotët e sajtit për t’u njoftuar këtë problem.
    • +
    ]]>
    + + + S’u Gjet Adresë + + Shfletuesi s’gjeti dot shërbyesin strehë për adresën e dhënë.

    +
      +
    • Kontrolloni adresën për gabime shkrimi, bie fjala, + ww.example.com në vend të + www.example.com.
    • +
    • Nëse s’jeni në gjendje të ngarkoni çfarëdo faqe, kontrolloni lidhjen e pajisjes tuaj me rrjetin celular, ose atë Wi-Fi.
    • +
    ]]>
    + + + Pa lidhje internet + + Kontrolloni lidhjen tuaj në rrjet, ose provoni të ringarkoni faqen pas pak çastesh. + + Ringarkoje + + + Adresë e Pavlefshme + Adresa e dhënë nuk është sipas ndonjë formati të pranuar. Ju lutemi, kontrolloni për gabime te shtylla e vendeve dhe riprovoni.

    + ]]>
    + + Adresa s’është e vlefshme + + + +
  • Adresat Web zakonisht shkruhen si http://www.example.com/
  • +
  • Sigurohuni se po përdorni pjerrake djathtas (d.m.th., /).
  • + + ]]>
    + + + Protokoll i Panjohur + + Adresa tregon një protokoll (p.sh., wxyz://) të cilin shfletuesi s’e njeh, kështu që s’mund të lidhet si duhet me këtë sajt.

    +
      +
    • Mos po përpiqeni të hapni shërbime multimedia apo shërbim tjetër jo tekst? Shihni te sajti për domosdoshmëri ekstra.
    • +
    • Disa protokolle mund të lypin software ose shtojca prej palësh të treta, përpara se shfletuesi të mund t’i njohë.
    • +
    + ]]>
    + + + S’u Gjet Kartelë + + +
  • Mundet që objekti të jetë riemërtuar, hequr, ose shpërngulur gjetkë?
  • +
  • A ka ndonjë gabim shkrimi apo gabim tjetër tipografik te adresa?
  • +
  • A keni leje të mjafta hyrjeje për te objekti i kërkuar?
  • + + ]]>
    + + + Hyrja te kartela u mohua + + +
  • Mund të jetë hequr, lëvizur, ose hyrjen e pengojnë lejet mbi kartelën.
  • + + ]]>
    + + + Shërbyesi Ndërmjetës Hodhi Poshtë Kërkesën për Lidhje + + Shfletuesi është formësuar të përdorë shërbyes ndërmjetës, por ndërmjetësi s’pranoi lidhje.

    +
      +
    • Është i saktë formësimi i shfletuesit për ndërmjetësin? Kontrolloni rregullimet dhe riprovoni.
    • +
    • A lejon shërbimi ndërmjetës lidhje prej këtij rrjeti?
    • +
    • Keni ende vështirësi? Për ndihmë, lidhuni me administruesin e rrjetit tuaj, ose me furnizuesin e Internetit.
    • +
    + ]]>
    + + + S’u Gjet Shërbyes Ndërmjetës + + Shfletuesi është formësuar të përdorë një shërbyes ndërmjetës, por ndërmjetësi s’u gjet dot.

    +
      +
    • Është i saktë formësimi i shfletuesit për ndërmjetësin? Kontrolloni rregullimet dhe riprovoni.
    • +
    • A është e lidhur pajisja te një rrjet aktiv?
    • +
    • Keni ende vështirësi? Për ndihmë, lidhuni me administruesin e rrjetit tuaj, ose me furnizuesin e Internetit.
    • +
    ]]>
    + + + Problem sajti malware-i + + Sajti te %1$s është raportuar si sajt sulmesh dhe është bllokuar bazuar në parapëlqimet tuaja për sigurinë.

    + ]]>
    + + + Problem sajti të padëshiruar + + Sajti te %1$s është raportuar se shërben software të padëshiruar dhe është bllokuar bazuar në parapëlqimet tuaja për sigurinë.

    + ]]>
    + + + Problem sajti të dëmshëm + + Sajti te %1$s është raportuar si sajt potencialisht i dëmshëm dhe është bllokuar bazuar në parapëlqimet tuaja për sigurinë.

    + ]]>
    + + + Problem sajti të rremë + + Kjo faqe web te %1$s është raportuar si sajt i rremë dhe është bllokuar bazuar në parapëlqimet tuaja për sigurinë.

    + ]]>
    + + + S’ka Sajt të Sigurt + + %1$s.]]> + + Vazhdo te Sajti HTTP +
    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 @@ + + + + + Покушајте поново + + + Не могу довршити захтев + + + Додатне информације о овом проблему или грешци тренутно нису доступне.

    ]]>
    + + + Безбедна веза није успостављена + + + +
  • Страница коју желите погледати не може се приказати јер се не може проверити веродостојност примљених података.
  • +
  • Контактирајте власнике веб странице и обавестите их о овом проблему.
  • + ]]>
    + + + Безбедна веза није успостављена + + + +
  • Ова грешка је можда последица погрешно конфигурисаног сервера или неко покушава да се лажно представља за сервер.
  • +
  • Ако је веза са сервером у прошлости била успешна, проблем може бити привремен. Покушајте поново касније.
  • + ]]>
    + + + Напредно… + + Неко покушава да се лажно представља за страницу и не препоручује се да наставите. +

    + ]]>
    + + + Врати се назад (препоручено) + + Прихватите ризик и наставите + + + Овај веб сајт захтева сигурну везу. + + +
  • Страница коју покушавате отворити не може бити приказана јер она захтева безбедну везу.
  • +
  • Проблем се вероватно налази на самој страници и не можете га решити самостално.
  • +
  • Можете обавестити администратора веб странице о овом проблему.
  • + + ]]>
    + + + Напредно… + + + %1$s проводи сигурносну политику под називом HTTP Strict Transport Security (HSTS), што значи да %2$s може на исту повезати само сигурно. Не можете да додате изузетак за посете овој страници. + ]]> + + Иди назад + + + Веза је прекинута + + + Прегледач се успешно повезао, али је веза прекинута током преноса података. Покушајте поново касније.

    +
      +
    • Страница је можда привремено недоступна или презаузета. Покушајте поново за неколико тренутака.
    • +
    • Ако не можете да учитате ниједну страницу, проверите мобилни пренос података или Wi-Fi везу.
    • +
    ]]>
    + + + Веза је истекла + + + Захтевана страница није одговорила на захтев за повезивање и прегледач је зауставио чекање на одговор.

    +
      +
    • Можда је сервер оптерећен великом количином захтева или је привремено остао без напајања? Покушајте поново касније.
    • +
    • Можете ли прегледати друге странице? Проверите мрежну везу вашег рачунара.
    • +
    • Да ли је ваш уређај или мрежа заштићена заштитним зидом или проксијем? Неисправна подешавања могу ометати ваше прегледање.
    • +
    • Још увек имате проблема? За помоћ се обратите администратору мреже или интернет добављачу.
    • +
    + ]]>
    + + + Веза није успела + + + +
  • Страница може бити привремено недоступна или заузета. Покушајте поново за неколико тренутака.
  • +
  • Ако не можете да учитате ниједну страницу, проверите мобилни пренос података или Wi-Fi везу.
  • + ]]>
    + + + Неочекивани одговор сервера + + + Страница је неочекивано одговорила на захтев и прегледач не може да настави.

    ]]>
    + + + Страница се не преусмерава исправно + + + Прегледач је престао да покушава да успостави тражену везу. Страница преусмерава упит на начин који никада неће бити довршен.

    +
      +
    • Да ли сте онемогућили или блокирали колачиће који су потребни за ову страницу?
    • +
    • Ако прихватање колачића са ове странице не решава проблем, онда је проблем вероватно са конфигурацијом сервера, а не са вашим уређајем.
    • +
    ]]>
    + + + Офлајн режим + + + Прегледач је у офлајн режиму и не може успоставити тражену везу.

    +
      +
    • Да ли је уређај повезан на активну мрежу?
    • +
    • Притисните “Покушај поново” да бисте прешли на онлајн режим и поново учитали страницу.
    • +
    ]]>
    + + + Порт је ограничен из безбедносних разлога + + Тражена адреса је одредила порт (нпр. mozilla.org:80 за порт 80 на mozilla.org) који се, поред за прегледање, обично користи у друге интернет сврхе. Прегледач је прекинуо упит из безбедносних разлога.

    ]]>
    + + + Веза је ресетована + + + Мрежна веза прекинута је током преговора о вези. Покушајте поново.

    +
      +
    • Страница може бити привремено недоступна или превише заузета. Покушајте поново за пар тренутака.
    • +
    • Ако не можете да учитате ниједну страницу, проверите мобилни пренос података или Wi-Fi везу.
    • +
    ]]>
    + + + Несигуран тип датотеке + + +
  • Обратите се власницима веб странице и обавестите их о овом проблему.
  • + ]]>
    + + + Грешка оштећеног садржаја + + Ова страница се не може приказати јер је дошло до грешке приликом преноса података.

    +
      +
    • Обратите се власницима веб странице и обавестите их о овом проблему.
    • +
    ]]>
    + + + Садржај се срушио + + Затражена страница се не може приказати јер је дошло до грешке током преноса података.

    +
      +
    • Обратите се власницима веб странице и обавестите их о овом проблему.
    • +
    ]]>
    + + + Грешка у кодирању садржаја + + Затражена страница се не може приказати јер користи неисправан или неподржан вид компресије.

    +
      +
    • Обратите се власницима веб странице и обавестите их о овом проблему.
    • +
    ]]>
    + + + Адреса није пронађена + + + Прегледач није могао пронаћи сервер за дату адресу.

    +
      +
    • Пазите да немате грешке у куцању као што је + ww.example.com уместо + www.example.com.
    • +
    • Ако не можете да учитате ниједну страницу, проверите мобилни пренос података или Wi-Fi везу.
    • +
    ]]>
    + + + Нема интернет везе + + Проверите мрежну везу или пробајте поново учитати страницу за неколико тренутака. + + Поново учитај + + + Адреса није исправна + + Наведена адреса није у препознатљивом формату. Проверите да ли има грешака у адресној траци и покушајте поново.

    ]]>
    + + Адреса није исправна + + + +
  • Адресе веб страница се обично пишу као http://www.example.com/
  • +
  • Проверите да ли користите косу црту (тј. /).
  • + ]]>
    + + + Непознат протокол + + Адреса означава протокол (нпр. wxyz://) који прегледач не препознаје и који не може повезати са траженом страницом.

    +
      +
    • Да ли покушавате да приступите мултимедијалном или неким другим нетекстуланим услугама? Проверите има ли страница додатних захтева.
    • +
    • Неки протоколи захтевају софтвер или прикључке треће стране, како би их прегледач могао препознати.
    • +
    ]]>
    + + + Датотека није нађена + + +
  • Да није можда преименована, уклоњена или премештена?
  • +
  • Постоји ли у адреси везе правописна грешка или грешка при куцању?
  • +
  • Имате ли довољна овлашћења за приступ траженој ставци?
  • + ]]>
    + + + Приступ датотеци је одбијен + + +
  • Можда је уклоњена, премештена или су овлашћења датотеке таква да спречавају приступ.
  • + ]]>
    + + + Прокси сервер је одбио везу + + Прегледач је подешен да користи прокси сервер, али је он одбио везу.

    +
      +
    • Да ли је прокси конфигурација прегледача исправна? Проверите подешавања и покушајте поново.
    • +
    • Да ли прокси услуга дозвољава повезивања из ове мреже?
    • +
    • Још увек имате проблема? За помоћ се обратите мрежном администратору или интернет добављачу.
    • +
    ]]>
    + + + Не могу да пронађем прокси сервер + + Прегледач је подешен да користи прокси сервер, али он није пронађен.

    +
      +
    • Да ли је прокси конфигурација прегледача исправна? Проверите подешавања и покушајте поново.
    • +
    • Да ли је уређај повезан на активну мрежу?
    • +
    • Још увек имате проблема? За помоћ се обратите мрежном администратору или интернет добављачу.
    • +
    ]]>
    + + + Проблем са злонамерним софтвером + + + Страница на %1$s је пријављена као злонамерна и блокирана је на основу ваших сигурносних подешавања.

    ]]>
    + + + Проблем са непожељном страницом + + + Страница на %1$s је пријављена због послуживања непожељног софтвера и блокирана је на основу ваших сигурносних подешавања.

    ]]>
    + + + Проблем са штетном страницом + + + Страница на %1$s је пријављена као потенцијално штетна и блокирана је на основу ваших сигурносних подешавања.

    ]]>
    + + + Проблем са обманљивом страницом + + Страница на %1$s је пријављена као обманљива и блокирана је на основу ваших сигурносних подешавања.

    ]]>
    + + + Безбедна страница није доступна + + %1$s није доступна.]]> + + Настави на HTTP сајт +
    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 @@ + + + + + Pecakan Deui + + + Rekés Gagal + + Can aya iber lianna ngeunaan ieu masalah atawa galat.

    + ]]>
    + + + Sambungan Aman Gagal + + +
  • Kaca anu rék ditempo teu bisa ditémbongkeun alatan oténtisitas data anu katarima teu bisa dipuguhkeun.
  • +
  • Mangga kontak anu boga raramatloka pikeun ngiberan ieu masalah.
  • + + ]]>
    + + + Sambungan Aman Gagal + + + +
  • Sigana alatan masalah dina konpigurasi server, atawa aya nu nyobaan or it could be someone trying to impersonate the server.
  • +
  • If you have connected to this server successfully in the past, the error may be temporary, and you can try again later.
  • + + ]]>
    + + + Leuwih lengkep… + + Batur bisa nyobaan malsukeun lokana, mending ulah diteruskeun. +

    + ]]>
    + + Balik (Disarankeun) + + Tampa Risiko jeung Teruskeun + + + Ieu raramatloka butuh sambungan anu aman. + + +
  • Kaca anu rék dibuka teu bisa ditémbongkeun kusabab ieu raramatloka butuh sambungan anu aman.
  • +
  • Masalahna sigana di raramatlokana, jadi anjeun teu bisa kumaha.
  • +
  • Anjeun bisa ngiberan kuncén raramatlokana ngeunaan ieu masalah.
  • + + ]]>
    + + + Leuwih lengkep… + + + %1$s miboga kawijakan kaamanan anu katelah HTTP Strict Transport Security (HSTS), anu hartina %2$s ngan bisa nyambung ka dinya ku cara anu aman. Anjeun teu bisa ngiwalkeun pikeun muka ieu loka. + ]]> + + Balik Deui + + + Sambunganana kapegat + + + Panyungsi laksana nyambung, tapi sambunganana kapegat sabot nransferkeun émbaran. Mangga cobaan deui.

    +
      +
    • Lokana sigana keur pareum samentawis atawa sibuk pisan. Cobaan sakeudeung deui.
    • +
    • Lamun anjeun teu bisa muka loka hiji hiji acan, pariksa sambungan data atawa Wi-Fina.
    • +
    ]]>
    + + + Sambunganana béakeun waktu + + + Loka anu dipénta henteu ngaréspon kana paménta sambungan sareng panyungsi parantos lirén ngantosan balesan.

    +
      +
    • Bisa pangladén ngalaman paménta anu luhur atanapi pamiayaan samentawis? Cobian deui engké.
    • +
    • Naha anjeun teu tiasa ngotéktak situs sanés? Parios sambungan jaringan alat na.
    • +
    • Naha alat anjeun atanapi jaringan ditangtayungan ku firewall atanapi proksi? Setélan anu salah tiasa ngaganggu keur nyungsi Web.
    • +
    • Masih kénéh gangguan? Taroskeun ka admin jaringan anjeun atanapi panyadia Internét pikeun bantuan.
    • +
    ]]>
    + + + Teu bisa nyambung + + + +
  • Lokana sigana keur teu bisa dibuka atawa keur riweuh. Pecakan sakeudeung deui.
  • +
  • Lamun teu bisa ngamuat kacana hiji hiji acan, pariksa data ponsél atawa sambungan Wi-Fi anjeun.
  • + ]]>
    + + + Tanggapan anu teu kaduga ti serper + + + Loka éta ngabales panyambungan jaringan ku cara anu teu kaduga sareng panyungsi teu tiasa diteruskeun.

    ]]>
    + + + Kacana teu bener kaalihkeunna + + + Pamaluruh eureun nyobaan mulut hal anu direkéskeun. Lokana ngarahkeun rekésna nepi ka moal anggeus-anggeus.

    +
      +
    • Anjeun numpurkeun atawa meungpeuk réréméh anu dipénta ku ieu loka?
    • +
    • Lamun teu bisa bérés ku nampa réréméh loka, sigana masalahna dina konpigurasi serper sarta lain tina piranti anjeun.
    • +
    ]]>
    + + + Mode Oplén + + + Panyungsi keur leumpang dina mode luring na jeung teu tiasa nyambung ka barang anu dipénta.

    +
      +
    • Ieu parangkat nyambung ka jaringan aktip heunteu?
    • +
    • Pencét "Cobi deui" pikeun ngalih ka modeu daring sareng ngamuat deui ieu kaca.
    • +
    ]]>
    + + + Port diwates pikeun kaamanan + + + Alamat anu dipénta nyebutkeun port (contona mozilla.org:80 pikeun port 80 di mozilla.org) galibna dipaké lain pikeun nyungsi Raramat. Panyungsi geus ngabolaykeun paménta pikeun kaamanan anjeun.

    ]]>
    + + + Sambunganana dirését + + + Tutumbu jaringan kaganggu bari parundingan sambungan. Cobaan deui.

    +
      +
    • Loka éta samentawis henteu kapayun atanapi sibuk teuing. Cobian deui dina sababaraha waktos.
    • +
    • Upami anjeun teu tiasa muka kaca, pariksa data paranti atanapi sambungan Wi-Fi anjeun.
    • +
    ]]>
    + + + Tipe Berkas Teu Aman + + + +
  • Mangga kontak anu boga raramatloka pikeun ngiberkeun ieu masalah.
  • + + ]]>
    + + + Galat Kontén Ruksak + + + Kaca anu rék ditempo teu bisa ditémbongkeun alatan kadetéksi aya galat dina transmisi data.

    +
      +
    • Mangga kontak anu miboga raramatloka pikeun ngiberan ieu masalah.
    • +
    ]]>
    + + + Kontén ruksak + + Kaca anu rék ditempo teu bisa ditémbongkeun alatan kadetéksi aya galat dina transmisi data.

    +
      +
    • Mangga kontak anu miboga raramatloka pikeun ngiberan ieu masalah.
    • +
    ]]>
    + + + Galat pengkodean kontén + + Kaca anu rék ditempo teu bisa ditémbongkeun alatan maké bentuk komprési anu teu sah atawa teu didukung.

    +
      +
    • Mangga kontak anu miboga raramatloka pikeun ngiberan ieu masalah.
    • +
    ]]>
    + + + Alamat Teu Kapanggih + + + Panyungsi teu bisa manggihan serper host pikeun alamat nu disadiakeun.

    +
      +
    • Pariksa alamatna bisi aya salah ketik kawas + ww.conto.com batan + www.conto.com.
    • +
    • Lamun teu hiji hiji acan anu bisa dibuka, pariksa data perangkat atawa sambungan Wi-Fina.
    • +
    ]]>
    + + + Euweuh sambungan internét + + Pariksa sambungan jaringan anjeun atawa cobaan muat deui kacana engké. + + Muat ulang + + + Alamat Teu Bener + Alamat anu diasupkeun formatna teu dpikawanoh. Pariksa palang lokasi bisi aya nu salah, laju cobaan deui.

    + ]]>
    + + Alamatna teu bener + + + +
  • Alamat raramat ilaharna ditulis siga http://www.conto.com/
  • +
  • Pastikeun anjeunn maké gurat condong maju (i.e. /).
  • + ]]>
    + + + Protokol Teu Dipikawanoh + + Alamatna méré protocol (e.g., wxyz://) anu teu dipikawanoh ku pamaluruh, ku kituna pamaluruhna teu bisa nyambung kalawan bener ka lokana.

    +
      +
    • Badé muka multimédia atawa layanan non-téks lianna? Pariksa lokana pikeun kalengkepanana.
    • +
    • Sababaraha protokol butuh sopwér atawa plugin pihak katilu sangkan bisa dipikawanoh ku pamaluruhna.
    • +
    ]]>
    + + + Berkas Teu Kapanggih + + +
  • Boa itemna geus ganti ngaran, dipiceun, atawa dipindahkeun?
  • +
  • Aya salah ketik, kapitalisasi, atawa galat tipograpik dina alamatna?
  • +
  • Naha anjeun boga idin aksés nu cukup kana item anu dipundut?
  • + ]]>
    + + + Aksés ka berkas ditolak + + +
  • Bisa jadi kulantaran geus dipupus, dipindahkeun, atawa ayana idin berkas anu matak nyaram aksés.
  • + ]]>
    + + + Server proxy Nolak Sambungan + + Pamaluruhna disetél maké serper proksi, tapi proksina nolak sambungan.

    +
      +
    • Konfigurasi proksi pamaluruhna geus bener? Pariksa setélanana sarta cobaan deui.
    • +
    • Layanan proksina ngidinan sambungan ti ieu jaringan?
    • +
    • Can bener kénéh? Cobi pundut bantuan ka administrator jaringan atawa panyadia internétna.
    • +
    ]]>
    + + + Server Proksi Teu Kapanggih + + Pamaluruhna disetél maké serper proksi, tapi proksina teu kapanggih.

    +
      +
    • Konpigurasi proksi pamaluruhna geus bener? Pariksa setélanana sarta cobaan deui.
    • +
    • Perangkatna nyambung ka jaringan aktip?
    • +
    • Can bener kénéh? Cobi pundut bantuan ka administrator jaringan atawa panyadia internétna.
    • +
    ]]>
    + + + Isu loka malwér + + Lokana di %1$s dilaporkeun salaku loka gangas sarta geus dipeungpeuk dumasar kana préperénsi kaamanan anjeun.

    ]]>
    + + + Isu loka teu dipiharep + + Lokana di %1$s dilaporkeun nginangan sopwér anu teu dipiharep sarta geus dipeungpeuk dumasar kana préperénsi kaamanan anjeun.

    ]]>
    + + + Isu loka pibahyaeun + + Lokana di %1$s dilaporkeun salaku loka pibahyaeun sarta geus dipeungpeuk dumasar kana préperénsi kaamanan anjeun.

    ]]>
    + + + Masalah loka nu nipu + + Kaca web ieu di %1$s dilaporkeun salaku loka nu nipu jeung tos diblokir dumasar kana pilihan kahoyong kaamanan.

    ]]>
    + + + Situs Aman Teu Sayaga + + %1$s teu sayaga.]]> + + Teruskeun ka Situs HTTP +
    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 @@ + + + + + Försök igen + + + Begäran kan inte slutföras + + Ytterligare information om detta problem eller fel är för närvarande inte tillgänglig.

    + ]]>
    + + + Säker anslutning misslyckades + + + +
  • Sidan du försöker visa kan inte visas eftersom autenticiteten för de mottagna uppgifterna inte kunde verifieras.
  • +
  • Kontakta webbplatsägarna för att informera dem om detta problem.
  • + + ]]>
    + + + Säker anslutning misslyckades + + + +
  • Detta kan vara ett problem med serverns konfiguration eller det kan vara någon som försöker efterapa servern.
  • +
  • Om du har anslutit till denna server framgångsrikt kan felet vara tillfälligt och du kan försöka igen senare.
  • + + ]]>
    + + + Avancerat… + + + Någon kan försöka efterlikna webbplatsen och du bör inte fortsätta. +

    + + ]]>
    + + Gå tillbaka (rekommenderas) + + Acceptera risken och fortsätt + + + Denna webbplats kräver en säker anslutning. + + +
  • Sidan du försöker visa kan inte visas eftersom den här webbplatsen kräver en säker anslutning.
  • +
  • Problemet ligger troligen på webbplatsen och det finns inget du kan göra för att lösa det.
  • +
  • Du kan meddela webbplatsens administratör om problemet.
  • + + ]]>
    + + + Avancerat… + + + %1$s har en säkerhetspolicy som kallas HTTP Strict Transport Security (HSTS), vilket innebär att %2$s 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.]]> + + Gå tillbaka + + + Anslutningen avbröts + + + Webbläsaren anslöt framgångsrikt, men anslutningen avbröts vid överföring av information. Försök igen.

    +
      +
    • Webbplatsen kan vara tillfälligt otillgänglig eller för upptagen. Försök igen om några minuter.
    • +
    • Om du inte kan ladda några sidor kan du kontrollera enhetens data eller Wi-Fi-anslutning.
    • +
    + ]]>
    + + + Anslutningens tidsgräns överskreds + + Den begärda webbplatsen svarade inte på en anslutningsbegäran och webbläsaren har slutat vänta på ett svar.

    +
      +
    • Kan servern ha hög efterfrågan eller tillfälligt avbrott? Försök igen senare.
    • +
    • Kan du inte surfa på andra webbplatser? Kontrollera enhetens nätverksanslutning.
    • +
    • Är din enhet eller nätverk skyddad av en brandvägg eller proxy? Felaktiga inställningar kan påverka surfningen.
    • +
    • Har du fortfarande problem? Kontakta nätverksadministratören eller internetleverantören för hjälp.
    • +
    ]]>
    + + + Kan inte ansluta + + + +
  • Webbplatsen kan vara tillfälligt otillgänglig eller för upptagen. Försök igen om några minuter.
  • +
  • Om du inte kan ladda några sidor kan du kontrollera enhetens data eller Wi-Fi-anslutning.
  • + + ]]>
    + + + Oväntat svar från servern + + Webbplatsen svarade på nätverksbegäran på ett oväntat sätt och webbläsaren kan inte fortsätta.

    + ]]>
    + + + Sidan dirigeras om felaktigt + + 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.

    +
      +
    • Har du inaktiverat eller blockerat kakor som krävs av denna webbplats?
    • +
    • Om att acceptera webbplatsens kakor inte löser problemet, är det troligtvis ett konfigurationsproblem i servern och inte din enhet.
    • +
    ]]>
    + + + Offlineläge + + Webbläsaren arbetar i sitt offlineläge och kan inte ansluta till det begärda objektet.

    +
      +
    • Är datorn ansluten till ett aktivt nätverk?
    • +
    • Tryck på "Försök igen" för att växla till online-läge och ladda om sidan igen.
    • +
    + ]]>
    + + + Porten har säkerhetsrestriktioner + + Den begärda adressen specificerade en port (t.ex. mozilla.org:80 för port 80 på mozilla.org) som normalt används för andra syften än surfning. Webbläsaren har avbrutit begäran för skydd och säkerhet.

    + ]]>
    + + + Anslutningen återställdes + + Nätverkslänken avbröts under förhandlingar om en anslutning. Försök igen.

    +
      +
    • Webbplatsen kan vara tillfälligt otillgänglig eller för upptagen. Försök igen om några minuter.
    • +
    • Om du inte kan ladda några sidor kan du kontrollera enhetens data eller Wi-Fi-anslutning.
    • +
    + ]]>
    + + + Osäker filtyp + + + +
  • Kontakta webbplatsägarna för att informera dem om detta problem.
  • + + ]]>
    + + + Korrupt innehållsfel + + + Sidan du försöker visa kan inte visas eftersom ett fel i dataöverföringen upptäcktes.

    +
      +
    • Kontakta webbplatsägarna för att informera dem om detta problem.
    • +
    + ]]>
    + + + Innehållet kraschade + + Sidan du försöker visa kan inte visas eftersom ett fel i dataöverföringen upptäcktes.

    +
      +
    • Kontakta webbplatsägarna för att informera dem om detta problem.
    • +
    + ]]>
    + + + Kodningsfel av innehållet + Sidan du försöker visa kan inte visas eftersom den använder en ogiltig eller icke-stödd form av komprimering.

    +
      +
    • Kontakta webbplatsägarna för att informera dem om detta problem.
    • +
    + ]]>
    + + + Adressen hittades inte + + + Webbläsaren kunde inte hitta värdservern för den angivna adressen.

    +
      +
    • Kontrollera adressen för skrivfel som t.ex. + ww.example.com istället för + www.example.com.
    • +
    • Om du inte kan ladda några sidor kan du kontrollera enhetens data eller Wi-Fi-anslutning.
    • +
    + ]]>
    + + + Ingen internetanslutning + + Kontrollera din nätverksanslutning eller försök ladda om sidan om en stund. + + Ladda om + + + Ogiltig adress + Den angivna adressen är inte i ett erkänt format. Kontrollera adressfältet för fel och försök igen.

    + ]]>
    + + Adressen är inte giltig + + + +
  • Webbadresser är vanligtvis skrivna som http://www.example.com/
  • +
  • Se till att du använder framåtriktade snedstreck (dvs. /).
  • + + ]]>
    + + + Okänt protokoll + + Adressen anger ett protokoll (t.ex. wxyz://) webbläsaren inte känner igen, så webbläsaren kan inte ansluta till webbplatsen korrekt.

    +
      +
    • Försöker du få tillgång till multimedia eller andra icke-texttjänster? Kontrollera webbplatsen för extra krav.
    • +
    • Vissa protokoll kan kräva programvara eller plugins från tredje part innan webbläsaren kan känna igen dem.
    • +
    + ]]>
    + + + Filen hittades inte + + +
  • Kan objektet ha bytt namn, tagits bort eller flyttat?
  • +
  • Finns det stavfel, stor bokstav eller annat typografiskt fel i adressen?
  • +
  • Har du tillräckliga åtkomstbehörigheter till den begärda objektet?
  • + + ]]>
    + + + Åtkomst till filen nekades + + +
  • Den kan ha tagits bort, flyttats eller filbehörigheter kan förhindra åtkomst.
  • + + ]]>
    + + + Proxyservern avslog anslutningen + Webbläsaren är konfigurerad för att använda en proxyserver, men proxyn vägrade en anslutning.

    +
      +
    • Är webbläsarens proxykonfiguration korrekt? Kontrollera inställningarna och försök igen.
    • +
    • Tillåter proxytjänsten anslutningar från det här nätverket?
    • +
    • Har du fortfarande problem? Kontakta nätverksadministratören eller internetleverantören för hjälp.
    • +
    + ]]>
    + + + Proxyservern hittades inte + + Webbläsaren är konfigurerad för att använda en proxyserver, men proxyn kunde inte hittas.

    +
      +
    • Är webbläsarens proxykonfiguration korrekt? Kontrollera inställningarna och försök igen.
    • +
    • Är datorn ansluten till ett aktivt nätverk?
    • +
    • Har du fortfarande problem? Kontakta nätverksadministratören eller internetleverantören för hjälp.
    • +
    + ]]>
    + + + Problem med skadlig kod + + Webbplatsen %1$s har rapporterats som en attackplats och har blockerats baserat på dina säkerhetsinställningar.

    + ]]>
    + + + Problem med oönskad webbplats + + Webbplatsen %1$s har rapporterats betjäna oönskad programvara och har blockerats baserat på dina säkerhetsinställningar.

    + ]]>
    + + + Problem med skadlig webbplats + + Webbplatsen %1$s har rapporterats som en potentiellt skadlig webbplats och har blockerats baserat på dina säkerhetsinställningar.

    + ]]>
    + + + Problem med vilseledande webbplats + + Denna webbsida på %1$s har rapporterats som en vilseledande webbplats och har blockerats baserat på dina säkerhetsinställningar.

    + ]]>
    + + + Säker webbplats finns inte tillgänglig + + %1$s finns inte tillgänglig.]]> + + Fortsätt till HTTP-webbplats +
    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 @@ + + + + + மீண்டும் முயற்சி செய் + + + கோரிக்கையை முடிக்க முடியவில்லை + + இச்சிக்கல் அல்லது பிழைகுறித்து கூடுதல் விவரங்கள் தற்போது இல்லை.

    + ]]>
    + + + பாதுகாப்பான இணைப்பு முறிந்தது + + +
  • பெற்ற தரவின் நம்பகத்தன்மையை சரிபார்க்க முடியவில்லை, ஏனெனில் நீங்கள் காண முயற்சிக்கும் பக்கம் பார்க்க முடியாது.
  • +
  • இந்தப் பிரச்சனைகுறித்து இணையதளத்தில் உரிமையாளர்களை தொடர்பு கொள்க.
  • + + ]]>
    + + + பாதுகாப்பான இணைப்பு முறிந்தது + + +
  • இந்தச் சேவையகத்தின் கட்டமைப்பில் சிக்கல் இருக்க வேண்டும், அல்லது அதில் யாராவது ஆள்மாறாட்டம் செய்ய முயற்சி செய்திருப்பார்கள்.
  • +
  • நீங்கள் இதற்கு முன்பு வெற்றிகரமாக இந்தச் சேவையகத்துடன் இணைந்து இருந்தால், இந்தப் பிழை தற்காலிகமானதாக இருக்கும், மேலும் நீங்கள் பிறகு மீண்டும் முயற்சி செய்யலாம்.
  • + + ]]>
    + + + மேம்பட்டது… + + தளத்தை ஆள்மாறாட்டம் செய்ய யாரோ ஒருவர் முயற்சிக்கலாம் எனவே நீங்கள் தொடர வேண்டாம். +

    + ]]>
    + + பின் செல்க (பரிந்துரைக்கப்படுகிறது) + + + இடரை ஏற்றுத் தொடருங்கள் + + + இந்த இணையத் தளத்திற்குப் பாதுகாப்பான இணைப்பு தேவை. + + + மேம்பட்டது… + + + பின் செல்க + + + இணைப்பில் தடங்கல் உள்ளது + + உலாவி வெற்றிகரமாக இணைக்கப்பட்டது, ஆனால் தகவலை மாற்றும்போது இணைப்பு தடைப்பட்டது. மீண்டும் முயற்சிக்கவும்.

    +
      +
    • இந்தத் தளம் தற்காலிகமாகக் கிடைக்கவில்லை அல்லது மிகவும் பிஸியாக இருக்கலாம். சிறிது நேரத்திற்க்கு பின் மீண்டும் முயற்சிக்கவும்.
    • +
    • உங்களால் எந்தப் பக்கங்களையும் ஏற்ற முடியவில்லை என்றால், உங்கள் சாதனத்தின் தரவு அல்லது வைஃபை இணைப்பைச் சரிபார்க்கவும்.
    • +
    + ]]>
    + + + இணைப்பு நேரம் முடிந்தது + + + கோரிய தளம் வேண்டுகோளுக்கு பதிலளிக்கவில்லை மேலும் உலாவி காத்திருத்தலை நிறுத்தியது.

    • வழங்கி பற்றாக்குறை அல்லது தற்காலிக செயலிழப்பில் இருக்கலாம்? பின்னர் முயற்சிக்க.
    • பிற தளங்களில் உலாவ முடியவில்லையா? கணினி இணைப்பை சோதிக்க.
    • உங்கள் கணினி பிணையம் தீயரண் அல்லது பதிலாளால் பாதுகாக்கப்பட்டுள்ளதா? தவறான அமைப்புகள் வலை உலாவலில் இடைபுகலாம்.
    • இன்னும் சிக்கல் உள்ளதா? உதவிக்கு பிணைய நிர்வாகி அல்லது இணைய வழங்குநரை ஆலோசிக்கவும்.
    ]]>
    + + + இணைக்க இயலவில்லை + + + +
  • இத்தளம் தற்சமயம் கிடைக்காமலோ சேவகன் மிகவும் மும்முரமாகவோ இருக்கலாம். சிறிது நேரம் கழித்து முயற்சிக்கவும்.
  • +
  • எந்த வலைதளத்தையும் அணுக முடியாவிட்டால், கைபேசியில் அல்லது அருகலை இணையம் உள்ளதா என உறுதிசெய்யவும்.
  • + ]]>
    + + + சேவையகத்திலிருந்து எதிர்பாராத பதில் வந்துள்ளது + + + இந்த வலைத்தளம் வலைப்பின்னலின் கோரிக்கைக்கு எதிர்பாரா வழியில் பதிலளித்தது எனவே உலாவியால் தொடர இயலாது.

    ]]>
    + + + பக்கம் ஒழுங்காகத் திருப்பிவிடவில்லை + + + உலாவி கோரப்பட்ட உருப்படியைப் பெறுவதற்கான முயற்சியை நிறுத்தியது. தளம் நிறைவடையாத வழியில் கோரிக்கையைத் திருப்பி விடுகிறது.

    +
      +
    • நீங்கள் இத்தளத்திற்குத் தேவையான நினைவிகளைச் செயல்நீக்கி அல்லது முடக்கியுள்ளீர்களா?
    • +
    • தளத்தின் நினைவிகளை ஏற்பது பிரச்சினையைச் சரிசெய்யவில்லையெனில், இது சேவையகக் கட்டமைப்புப் பிழையாக இருக்க வாய்ப்புள்ளது, உங்கள் சாதனத்தில் பிரச்சனையில்லை.
    • +
    ]]>
    + + + இணையமற்ற முறை + + + உலாவி இணையமிலா முறைமையில் செயல்படுகிறது கோரிய உருப்படியுடன் இணைக்க இயலாது.

    +
      +
    • சாதனம் செயல்படும் இணைய வலைப்பின்னலுடன் இணைக்கப்பட்டுள்ளதா?
    • +
    • பக்கத்தை மீளேற்றி இணைய முறைமைக்கு மாற “மீண்டும் முயற்சிக்க” என்பதை அழுத்துங்கள்.
    • +
    ]]>
    + + + பாதுகாப்புக் காரணங்களுக்காக முனையம் தடைசெய்யப்பட்டுள்ளது + + + கோரப்பட்ட முகவரியானது பொதுவாக வலை உலாவல் அல்லாத பிற நோக்கங்களுக்காகப் பயன்படுத்தப்படும் முனையத்தைக் (எ.கா., mozilla.org:80 mozilla.org இன் முனையம் 80 காக) குறிப்பிட்டது. உங்கள் பாதுகாப்பிற்காக உலாவி கோரிக்கையை இரத்து செய்தது.

    ]]>
    + + + இணைப்பு மீட்டமைக்கப்பட்டது + + + இணைக்க முயற்சிக்கும்போது வலைப்பின்னல் இணைப்பு தடைபட்டது. மீண்டும் முயற்சியுங்கள்.

    +
      +
    • தளம் தற்காலிகமாகக் கிடைக்கவில்லை அல்லது பளுவான வேலையில் உள்ளது. சிறிது நேரத்திற்குப் பின் முயற்சியுங்கள்.
    • +
    • உங்கள் எந்தப் பக்கத்தையும் ஏற்ற முடியவில்லையெனில், உங்கள் சாதனத்தின் தரவு அல்லது அருகலை இணைப்பைச் சரிபாருங்கள்.
    • +
    ]]>
    + + + பாதுகாப்பற்ற கோப்பு வகை + + + +
  • இணைய உரிமையாளர்களைத் தொடர்பு கொண்டு இந்தப் பிரச்சனைகுறித்து தெரிவிக்கவும்.
  • + + ]]>
    + + + சிதைந்த உள்ளடக்கப் பிழை + + + தரவுப் பரிமாற்றத்தில் ஒரு பிழை கண்டறியப்பட்டுள்ளதால், நீங்கள் காண முயற்சிக்கும் பக்கத்தைக் காண்பிக்க முடியாது.

    +
      +
    • இணைய உரிமையாளர்களைத் தொடர்பு கொண்டு இந்தப் பிரச்சனைகுறித்து தெரிவிக்கவும்.
    • +
    + ]]>
    + + + உள்ளடக்கம் செயலிழந்தது + + தரவுப் பரிமாற்றத்தில் ஒரு பிழை கண்டறியப்பட்டுள்ளதால், நீங்கள் காண முயற்சிக்கும் பக்கத்தைக் காண்பிக்க முடியாது.

    +
      +
    • இணைய உரிமையாளர்களைத் தொடர்பு கொண்டு இந்தப் பிரச்சனைகுறித்து தெரிவிக்கவும்.
    • +
    ]]>
    + + + உள்ளடக்க குறிமுறை பிழை + + நீங்கள் காண முயற்சிக்கும் பக்கம் பார்க்க முடியாது ஏனெனில் அதைச் சுருக்க ஒரு தவறான அல்லது ஆதரவற்ற வடிவம் பயன்படுத்துகிறது.

    +
      +
    • இணைய உரிமையாளர்களைத் தொடர்பு கொண்டு இந்தப் பிரச்சனைகுறித்து தெரிவிக்கவும்.
    • +
    + ]]>
    + + + முகவரி கிடைக்கவில்லை + + + உலாவியால் கொடுக்கப்பட்ட முகவரியின் வழங்கல் சேவையகத்தைக் கண்டறிய இயலவில்லை.

    +
      +
    • முகவரியில் தட்டச்சுப் பிழைகள் உள்ளனவா என சரிபாருங்கள் எ.கா + ww.example.com என்பதற்குப் பதிலாக + www.example.com.
    • +
    • உங்கள் எந்தப் பக்கங்களையும் ஏற்ற இயலவில்லையெனில், உங்கள் சாதனத்தின் தரவு அல்லது அருகலை இணைப்பைச் சரிபாருங்கள்.
    • +
    ]]>
    + + + இணையத் தொடர்பு இல்லை + + உங்கள் இணைய இணைப்பைச் சரிபாருங்கள் அல்லது சிறிதுநேரத்திற்குப் பின் பக்கத்தை மறுஏற்ற முயற்சியுங்கள். + + மீளேற்று + + + செல்லாத முகவரி + வழங்கிய முகவரி அடையாளங்காணத்தக்க வடிவில் இல்லை. இருப்பிடப்பட்டியைப் பார்த்து ஏதேனும் பிழைகள் உள்ளதா எனச் சரிபார்த்துவிட்டு மீண்டும் முயற்சிக்கவும்.

    + ]]>
    + + இந்த முகவரி தவறானது + + + +
  • இணைய முகவரி http://www.example.com/ போன்று இருக்கும்
  • +
  • நீங்கள் / குறியைப் பயன்படுத்தினீர்களா எனப் பார்க்கவும் (i.e. /).
  • + + ]]>
    + + + நெறிமுறை தெரியவில்லை + + முகவரி உலாவியால் புரிந்துகொள்ள முடியாத நெறிமுறையைக் (எ.கா, wxyz://) குறிப்பிடுகிறது, எனவே உலாவியால் தளத்துடன் சரியாக இணைக்க முடியவில்லை.

    +
      +
    • நீங்கள் பல்லூடகம் அல்லது பிற உரையல்லாச் சேவைகளை அணுக முயற்சிக்கிறீர்களா? கூடுதல் தேவைகளுள்ளதா எனத் தளத்தைப் பாருங்கள்.
    • +
    • உலாவியால் அடையாளம் காணப்படுவதற்குச் சில நெறிமுறைகளுக்கு மூன்றாம் தரப்பு மென்பொருள் அல்லது செருகுநிரல்கள் தேவைப்படலாம்.
    • +
    ]]>
    + + + கோப்பைக் காணவில்லை + + +
  • உருப்படி பெயர்மாற்ற, நீக்க, இடம்மாற்றப்பட்டிருக்கலாமா?
  • +
  • முகவரியில் உச்சரிப்பு, எழுத்துப்பிழை, அல்லது பிற தட்டச்சுப் பிழைகள் உள்ளனவா?
  • +
  • கோரப்பட்ட உருப்படிக்கான போதுமான அணுகல் அனுமதிகள் உங்களிடம் உள்ளனவா?
  • + ]]>
    + + + கோப்பு அணுகல் மறுக்கப்பட்டது + + +
  • கோப்பு நீக்கப்பட்டிருக்கலாம், நகர்த்தப்பட்டிருக்கலாம் அல்லது அனுமதி மறுக்கப்பட்டிருக்கலாம்.
  • + + ]]>
    + + + பதிலாளி சேவையகம் இணைப்பை மறுத்துவிட்டது + + உலாவி பதிலாள் சேவையகத்தைப் பயன்படுத்துமாறு அமைவாக்கம் செய்யப்பட்டுள்ளது, ஆனால் பதிலாள் இணைப்பை மறுத்தது.

    +
      +
    • உலாவியின் பதிலாள் அமைவுகள் சரியாக உள்ளனவா? அமைவுகளைச் சரிபார்த்து மீண்டும் முயற்சியுங்கள்.
    • +
    • பதிலாள் சேவைகள் இந்த வலைப்பின்னலிலிருந்து வரும் இணைப்புகளை அனுமதிக்கின்றனவா?
    • +
    • இன்னும் சிக்கல் உள்ளதா? உதவிக்கு உங்கள் வலைப்பின்னல் நிர்வாகி அல்லது இணைய சேவை வழங்குநரைத் தொடர்பு கொள்ளுங்கள்.
    • +
    ]]>
    + + + பதிலாளி சேவையகம் கிடைக்கவில்லை + + உலாவி பதிலாள் சேவையகத்தைப் பயன்படுத்துமாறு அமைவாக்கம் செய்யப்பட்டுள்ளது, ஆனால் பதிலாளைக் கண்டுபிடிக்க இயலவில்லை.

    +
      +
    • உலாவியின் பதிலாள் அமைவுகள் சரியாக உள்ளனவா? அமைவுகளைச் சரிபார்த்து மீண்டும் முயற்சியுங்கள்.
    • +
    • இச்சாதனம் செயல்பாட்டிலுள்ள வலைப்பின்னலுடன் இணைக்கப்பட்டுள்ளதா?
    • +
    • இன்னும் சிக்கல் உள்ளதா? உதவிக்கு உங்கள் வலைப்பின்னல் நிர்வாகி அல்லது இணைய சேவை வழங்குநரைத் தொடர்பு கொள்ளுங்கள்.
    • +
    ]]>
    + + + தீம்பொருள் தள சிக்கல் + + + %1$s இல் உள்ள தளம் தாக்குதல் தளமாக புகாரளிக்கப்பட்டுள்ளது மற்றும் உங்கள் பாதுகாப்பு விருப்பங்களின் அடிப்படையில் தடுக்கப்பட்டுள்ளது.

    +    ]]>
    + + + தேவையற்ற தள சிக்கல் + + + %1$s இல் உள்ள தளம் தேவையற்ற மென்பொருளை வழங்குவதாக அறிவிக்கப்பட்டுள்ளது மற்றும் உங்கள் பாதுகாப்பு விருப்பங்களின் அடிப்படையில் தடுக்கப்பட்டுள்ளது.

    +    ]]>
    + + + தீங்கு விளைவிக்கும் தள சிக்கல் + + + %1$s இல் உள்ள தளம் தீங்கு விளைவிக்கும் தளமாக புகாரளிக்கப்பட்டுள்ளது மற்றும் உங்கள் பாதுகாப்பு விருப்பங்களின் அடிப்படையில் தடுக்கப்பட்டுள்ளது.

    +    ]]>
    + + + ஏமாற்றும் தள சிக்கல் + + %1$s இல் உள்ள இந்த வலைப்பக்கம் ஒரு ஏமாற்றும் தளமாக புகாரளிக்கப்பட்டுள்ளது மற்றும் உங்கள் பாதுகாப்பு விருப்பங்களின் அடிப்படையில் தடுக்கப்பட்டுள்ளது.

    +    ]]>
    + + + பாதுகாப்பான தளம் கிடைக்கப்பெறவில்லை + + %1$s இன் HTTPS பதிப்பு கிடைக்கவில்லை.]]> + + HTTP தளத்தில் தொடரவும் +
    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 @@ + + + + + తిరిగి ప్రయత్నించు + + + అభ్యర్థనను పూర్తి చేయలేము + + ఈ సమస్యకు లేదా దోషానికి సంబంధించిన అదనపు సమాచారం ప్రస్తుతం అందుబాటులో లేదు

    ]]>
    + + + సురక్షిత అనుసంధానం విఫలమైంది + + + +
  • మీరు చూడటానికి ప్రయత్నిస్తున్న పేజీని చూపించలేము ఎందుకంటే అందుకున్న డేటా ప్రామాణికతను తనిఖీ చేయలేకపోయాం.
  • +
  • దయచేసి ఈ వెబ్‌సైట్ యజమానులను సంప్రదించి వారికి ఈ సమస్య గురించి తెలియజేయండి.
  • + ]]>
    + + + సురక్షిత అనుసంధానం విఫలమైంది + + + +
  • ఇది స్వరరు స్వరూపణంలో సమస్య కావచ్చు, లేదా ఎవరో సర్వరును అనుకరించడానికి ప్రయత్నిస్తూండవచ్చు.
  • +
  • మీరు ఇంతకుముందు ఈ సర్వరుకి విజయవంతంగా అనుసంధానమైతే, ఈ తప్పిదం తాత్కాలికం కావచ్చు, మీరు కాసేపు ఆగి మళ్ళీ ప్రయత్నించవచ్చు.
  • + ]]>
    + + + ఉన్నతం… + + + ఎవరో ఈ సైటును అనుకరించడానికి ప్రయత్నిస్తూండవచ్చు, మీరు ముందుకెళ్ళకూడదు. +

    + ]]>
    + + వెనక్కి వెళ్ళండి (సిఫారసు చేయబడింది) + + నష్టభయాన్ని అంగీకరించి ముందుకు కొనసాగండి + + + ఈ వెబ్‌సైటుకి సురక్షిత అనుసంధానం కావాలి. + + + ఉన్నతం… + + + వెనుకకు వెళ్ళు + + + అనుసంధానానికి అంతరాయం కలిగింది + + + విహారిణి విజయవంతంగా అనుసంధానమయ్యింది, కానీ సమాచారం బదిలీ అవుతున్నప్పుడు అనుసంధానానికి అంతరాయం కలిగింది. దయచేసి మళ్ళీ ప్రయత్నించండి.

    +
      +
    • సైటు తాత్కాలికంగా అందుబాటులో లేకపోవచ్చు లేదా చాలా ఒత్తిడిలో ఉండొచ్చు. కొంతసేపు ఆగి మళ్ళీ ప్రయత్నించండి.
    • +
    • మీకు వేరే పేజీలు కూడా తెరుచుకోకపోతూంటే, మీ పరికరంలో డేటా లేక వై-ఫై అనుసంధానాన్ని సరిచూసుకోండి.
    • +
    ]]>
    + + + అనుసంధానానికి కాలం చెల్లిపోయింది + + + మీరు అడిగిన సైటు అనుసంధాన అభ్యర్థనకు స్పందించలేదు, విహారిణి ఇక స్పందన కోసం చూడటం ఆపివేసింది.

    +
      +
    • సర్వరు అధిక ఒత్తిడిలో ఉందేమో లేక తాత్కాలికంగా పనిచేయడం లేదేమో? కాసేపటి తర్వాత మళ్ళీ పయత్నించండి.
    • +
    • మీకు వేరే సైట్లు కూడా తెరుచుకోవడం లేదా? మీ పరికరపు అనుసంధానాన్ని సరిచూసుకోండి.
    • +
    • మీ పరికరం ఫైర్‌వాల్ లేదా ప్రాక్సీ ద్వారా సంరక్షితమై ఉందా? తప్పుడు అమరికలు మీ జాల విహరణకు అడ్డుపడుతూండవచ్చు.
    • +
    • ఇంకా సమస్య ఉందా? సహాయం కొరకు మీ నెట్‌వర్క్ నిర్వాహకులను గానీ లేక అంతర్జాల సేవాదారుని గానీ సంప్రదించండి.
    • +
    ]]>
    + + + అనుసంధానం సాధ్యం కావడంలేదు + + + +
  • సైటు తాత్కాలికంగా అందుబాటులో లేదు లేక చాలా ఒత్తిలో ఉండవచ్చు. దయచేసి కాపేపు ఆగి మళ్ళీ ప్రయత్నించండి.
  • +
  • మీకు మరే ఇతర పేజీలు తెరుచుకోకపోతూంటే, మీ పరికరపు డేటా లేక వై-ఫై అనుసంధానాన్ని సరిచూసుకోండి.
  • + ]]>
    + + + సర్వరు నుండి అనుకోని స్పందన + + + నెట్‌వర్క్ అభ్యర్థనకు సైటు అనుకోని విధంగా స్పందించింది, కనుక విహారిణి ముందుకు వెళ్ళలేదు.

    ]]>
    + + + పేజీ సరిగా దారిమళ్ళించడం లేదు + + + అడిగిన అంశం తేవడాన్ని విహారిణి ఆపివేసింది. అభ్యర్థనను ఎప్పటికీ పూర్తికాని విధంగా ఈ సైటు దారిమళ్ళిస్తోంది.

    +
      +
    • ఈ సైటుకు కావలసిన కుకీలను మీరు అచేతనించడం లేక నిరోధించడం గానీ చేసారా?
    • +
    • ఈ సైటు కుకీలను అనుమతించడం సమస్యను పరిష్కరించకపోతే, అది బహుశా సర్వరు స్వరూపణంలో సమస్య కావచ్చు, మీ పరికరంలో కాకపోవచ్చు.
    • +
    ]]>
    + + + ఆఫ్‌లైన్ రీతి + + + విహారిణి ఆఫ్‌లైన్ రీతితో పనిచేస్తుంది, అడిగిన అంశానికి అనుసంధానం కాలేకున్నది.

    +
      +
    • ఈ పరికరం క్రియాశీల నెట్‌వర్కుకి అనుసంధానమై ఉందా?
    • +
    • ఆన్‌లైన్ రీతికి మారి పేజీని మళ్ళీ లోడుచేయడానికి “మళ్ళీ ప్రయత్నించు” నొక్కండి.
    • +
    ]]>
    + + + భద్రతా కారణాల దృష్ట్యా పోర్టు నియంత్రించబడింది + + + అడిగిన చిరునామా సామాన్యంగా జాల విహారణ కోసం కాకుండా ఇతర ఉద్దేశాల కోసం వాడే ఒక పోర్టును పేర్కొన్నది (ఉదా॥ mozilla.orgలో పోర్టు 80 కొరకుmozilla.org:80). మీ సంరక్షణ, భద్రతలకై విహారిణి ఈ అభ్యర్థనను రద్దుచేసింది.

    ]]>
    + + + అనుసంధానానికి అంతరాయం కలిగింది + + + అనుసంధానానికి ప్రయత్నిస్తూన్నప్పుడు నెట్‌వర్క్ లంకెకు అంతరాయం కలిగింది. దయచేసి మళ్ళీ ప్రయత్నించండి.

    +
      +
    • ఆ సైటు తాత్కాలికంగా అందుబాటులో లేకపోవచ్చు లేక చాలా ఒత్తిడిలో ఉండివుండొచ్చు. కాసేపు ఆగి మళ్ళీ ప్రయత్నించండి.
    • +
    • మీకు మరే ఇతర పేజీలు కూడా తెరుచుకోకపోతూంటే, మీ పరికరపు డేటా లేక వై-ఫై అనుసంధానాన్ని సరిచూసుకోండి.
    • +
    ]]>
    + + + సురక్షితం కాని ఫైలు రకం + + +
  • దయచేసి వెబ్‌సైటు యజమానులను సంప్రదించి వారిగి ఈ సమస్యను తెలియజేయండి.
  • + ]]>
    + + + పాడయిన విషయపు తప్పిదం + + మీరు చూడడానికి ప్రయత్నిస్తున్న పేజీని చూపించలేము ఎందుకంటే డేటా బదలాయింపులో తప్పిదం కనుగొనబడింది.

    +
      +
    • దయచేసి వెబ్‌సైటు యజమానులను సంప్రదించి వారికి ఈ సమస్య గురించి తెలియజేయండి.
    • +
    ]]>
    + + + విషయం దెబ్బ తిన్నది + మీరు చూడడానికి ప్రయత్నిస్తున్న పేజీని చూపించలేము ఎందుకంటే డేటా బదలాయింపులో తప్పిదం కనుగొనబడింది.

    +
      +
    • దయచేసి వెబ్‌సైటు యజమానులను సంప్రదించి వారికి ఈ సమస్య గురించి తెలియజేయండి.
    • +
    ]]>
    + + + విషయపు ఎన్‌కోడింగ్ తప్పిదం + మీరు చూడడానికి ప్రయత్నిస్తున్న పేజీని చూపించలేము ఎందుకంటే అది చెల్లని లేక తోడ్పాటు లేని కంప్రెషన్ పద్ధతిని ఉపయోగిస్తోంది.

    +
      +
    • దయచేసి వెబ్‌సైటు యజమానులను సంప్రదించి వారికి ఈ సమస్య గురించి తెలియజేయండి.
    • +
    ]]>
    + + + చిరునామా కనపడలేదు + + + ఇచ్చిన చిరునామా కొరకు హోస్టు సర్వరును విహారిణి కనుగొనలేకపోయింది.

    +
      +
    • చిరునామాలో + www.example.com బదులుగా + ww.example.com అని టైపు చేసారేమో + సరిచూసుకోండి.
    • +
    • మీరు మరే ఇతర పేజీలను తెరవలేకపోతూంటే, మీ పరికరపు డేటా లేక వై-ఫై అనుసంధానాన్ని సరిచూసుకోండి.
    • +
    ]]>
    + + + అంతర్జాల అనుసంధానం లేదు + + + మీ నెట్‌వర్క్ అనుసంధానాన్ని సరిచూసుకోండి లేక కొన్ని క్షణాల తర్వాత ఈ పేజీని మళ్ళీ తెరవండి. + + మళ్ళీ లోడుచేయి + + + చెల్లని చిరునామా + + ఇచ్చిన చిరునామా గుర్తించగలిగే ఆకృతిలో లేదు. దయచేసి చిరునామా పట్టీలో తప్పులను సరిచేసి అప్పుడు ప్రయత్నించండి.

    ]]>
    + + ఈ చిరునామా చెల్లదు + + + +
  • జాల చిరునామాలను మాములుగా ఇలా వ్రాస్తారు http://www.example.com/
  • +
  • మీరు ఫార్వార్డు స్లాషులనే (అంటే /) వాడేలా చూసుకోండి.
  • + ]]>
    + + + తెలియని ప్రొటోకాల్ + + ఆ చిరునామా విహారిణి గుర్తించలేని ప్రొటోకాలును పేర్కొంది (ఉదా॥ wxyz://), కాబట్టి విహారిణి ఆ సైటుకి సరిగా అనుసంధానం కాజాలదు.

    +
      +
    • మీరు మల్టీమీడియా లేదా పాఠ్యేతర సేవలను పొందడానికి ప్రయత్నిస్తున్నారా? అదనపు ఆవశ్యకాల కోసం సైటులో చూడండి.
    • +
    • కొన్ని ప్రొటోకాల్లను ముడో-పక్ష సాఫ్ట్‌వేరు లేక ప్లగిన్లు కావలసిరావచ్చు, ఆ తర్వాత వాటిని విహారిణి గుర్తించగలదు.
    • +
    ]]>
    + + + ఫైలు కనబడలేదు + + +
  • పేరుమార్చబడి ఉండొచ్చు, తొలగింబడి ఉండొచ్చు, లేదా వేరే చోటికి తరలించబడి ఉండచ్చేమో?
  • +
  • చిరునామాలో స్పెల్లింగు, క్యాపిటలైజేషన్ లేదా ఇతర పాఠ్య సంబంధిత పొరపాట్లు ఏమైనా ఉన్నాయా?
  • +
  • అభ్యర్థించిన అంశాన్ని చూడడానికి మీకు తగిన అనుమతులు ఉన్నాయా?
  • + ]]>
    + + + ఫైలుకి ప్రాప్యత నిరాకరించబడింది + + +
  • ఇది తీసివేయబడి ఉండచ్చు, తరలించవేయబడి ఉండచ్చు, లేదా ఫైలు అనుమతులు దాన్ని చూడడాన్ని నిరోధిస్తూండవచ్చు.
  • + ]]>
    + + + ప్రాక్రీ సర్వరు అనుసంధానాన్ని తిరస్కరించింది + + ఈ విహారిణి ప్రాక్సీ సర్వరును వాడేలా స్వరూపించబడింది, కానీ ప్రాక్సీ అనుసంధానాన్ని నిరాకరించింది.

    +
      +
    • విహారిణి ప్రాక్సీ స్వరూపణం సరియేనా? అమరికలను సరిచూసి అప్పుడు ప్రయత్నించండి.
    • +
    • ప్రాక్సీ సర్వరు ఈ నెట్‌వర్కు నుండి అనుసంధానాలను అనుమతిస్తుందా?
    • +
    • ఇంకా సమస్య ఉందా? సహాయం కొరకు మీ నెట్‌వర్క్ నిర్వాహకులను గానీ లేదా అంతర్జాల సేవాదారుని గానీ సంప్రదించండి.
    • +
    ]]>
    + + + ప్రాక్సీ సర్వరు కనబడలేదు + + ఈ విహారిణి ప్రాక్సీ సర్వరును వాడేలా స్వరూపించబడింది, కానీ ప్రాక్సీ కనబడటం లేదు.

    +
      +
    • విహారిణి ప్రాక్సీ స్వరూపణం సరియేనా? అమరికలను సరిచూసి అప్పుడు ప్రయత్నించండి.
    • +
    • ఈ పరికరం క్రియాశీలమైన నెట్‌వర్కుకు అనుసంధానమై ఉందా?
    • +
    • ఇంకా సమస్య ఉందా? సహాయం కొరకు మీ నెట్‌వర్క్ నిర్వాహకులను గానీ లేదా అంతర్జాల సేవాదారుని గానీ సంప్రదించండి.
    • +
    ]]>
    + + + మాల్‌వేర్ సైటు సమస్య + + %1$s వద్దగల సైటు దాడిచేసే సైటుగా నివేదించబడింది, మీ భద్రత అభిరుచుల మేరకు ఆ సైటు నిరోధించబడింది.

    ]]>
    + + + అవాంఛిత సైటు సమస్య + + + %1$s వద్దగల సైటు అవాంఛిత సాఫ్ట్‌వేరును అందించేదిగా నివేదించబడింది. మీ భద్రత అభిరుచుల మేరకు ఈ సైటు నిరోధించబడింది.

    ]]>
    + + + హానికరమైన సైటు సమస్య + + + %1$s వద్దగల సైటు హానికరంకాగల సైటుగా నివేదించబడింది, మీ భద్రత అభిరుచుల మేరకు ఆ సైటు నిరోధించబడింది.

    ]]>
    + + + మోసపూరితమైన సైటు సమస్య + + %1$s వద్ద గల వెబ్ పేజీ మోసపూరితమైన సైటుగా నివేదించబడి ఉంది, మీ భద్రతా అభిరుచుల మేరకు ఆ సైటు నిరోధించబడింది.

    ]]>
    + + + సురక్షితమైన సైటు అందుబాటులో లేదు + + HTTP సైటుకి కొనసాగు +
    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 @@ + + + + + Аз нав кӯшиш кардан + + + Дархостро ба анҷом расонида наметавонад + + Маълумоти иловагӣ дар бораи ин мушкилӣ ё хато ҳоло дастнорас аст.

    + ]]>
    + + + Пайвасти бехатар иҷро нашуд + + + +
  • Браузер саҳифаеро, ки шумо дидан мехоҳед, нишон дода наметавонад, зеро эътиборнокии маълумоти гирифташуда наметавонад тасдиқ карда шавад.
  • +
  • Лутфан, бо соҳибони сомона дар тамос шавед ва ба онҳо дар бораи ин мушкилӣ хабар диҳед.
  • + + ]]>
    + + + Пайвасти бехатар иҷро нашуд + + + +
  • Ин метавонад дар танзимоти сервер мушкилӣ дошта бошад ё касе кӯшиш мекунад, ки серверро тақаллуб намояд.
  • +
  • Агар шумо қаблан ба ин сервер бо муваффақият пайваст шуда бошед, эҳтимол ин хато муваққатӣ аст ва шумо метавонед баъдтар амалро аз нав кӯшиш намоед.
  • + + ]]>
    + + + Иловагӣ… + + Касе метавонад тақаллуб кардани сомонаро кӯшиш намояд ва шумо бояд идома надиҳед. +

    + + ]]>
    + + Бозгашт (тавсия дода мешавад) + + Таваккалро қабул кунед ва идома диҳед + + + Ин сомона пайвасти бехатареро талаб мекунад. + + +
  • Саҳифае, ки шумо мехоҳед аз назар гузаронед, намоиш дода намешавад, зеро ки ин сомона пайвасти бехатареро талаб мекунад.
  • +
  • Чунин менамояд, ки сомона дорои мушкилие мебошад, ки шумо онро мустақилона ҳал карда наметавонед.
  • +
  • Шумо метавонед дар бораи ин мушкилӣ ба маъмури сомона хабар диҳед.
  • + + ]]>
    + + + Иловагӣ… + + + %1$s дорои сиёсати амниятӣ бо номи «Амнияти интиқоли қатъии HTTP» (HSTS) мебошад, ва ин маънои онро дорад, ки %2$s метавонад ба он танҳо тавассути алоқаи бехатар пайваст шавад. Шумо барои ворид шудан ба ин сомона истисноро илова карда наметавонед. + ]]> + + Бозгашт + + + Пайвастшавӣ қатъ карда шуд + + + Браузер бо муваффақият пайваст шуд, аммо ҳангоми интиқоли иттилоот пайвастшавӣ қатъ карда шуд. Лутфан, аз нав кӯшиш кунед.

    +
      +
    • Сомона метавонад муваққатан дастнорас бошад ё бо дархостҳои зиёд хеле машғул бошад. Пас аз чанд лаҳза аз нав кӯшиш кунед.
    • +
    • Агар шумо ягон саҳифаро бор карда натавонед, пайвасти мобилии дастгоҳ ё пайвасти Wi-Fi -ро санҷед.
    • +
    + ]]>
    + + + Вақти пайвастшавӣ ба анҷом расид + + + Сомонаи дархостшуда ба дархости пайвастшавӣ посух надод ва браузер мунтазири посухро қатъ кард.

    +
      +
    • Эҳтимол сервер бо дархостҳои зиёд машғул аст ё ин ки муваққатан дастнорас аст? Баъдтар аз нав кӯшиш кунед.
    • +
    • Шумо метавонед, ки сомонаҳои дигарро бинед? Пайвасти шабакавии дастгоҳро санҷед.
    • +
    • Дастгоҳ ё шабакаи шумо бо девори оташ ё прокси муҳофизат шудааст? Танзимоти нодуруст метавонад ба дидани сомона халал расонад.
    • +
    • Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат намоед.
    • +
    + ]]>
    + + + Пайваст ғайриимкон аст + + + +
  • Сомона метавонад муваққатан дастнорас бошад ё бо дархостҳои зиёд хеле машғул бошад. Пас аз чанд лаҳза аз нав кӯшиш кунед.
  • +
  • Агар шумо ягон саҳифаро бор карда натавонед, пайвасти мобилии дастгоҳ ё пайвасти Wi-Fi -ро санҷед.
  • + + ]]>
    + + + Ҷавоби ногаҳон аз сервер + + + Сомона ба дархости шабака ба таври ғайричашмдошт посух дод ва браузер наметавонад идома диҳад.

    + ]]>
    + + + Саҳифа ба таври дуруст равона карда намешавад + + + Браузер кӯшиши ҷустуҷӯи маводи дархостшударо қатъ кард. Сомона дархостро тавре равона мекунад, ки раванд ҳеҷ гоҳ ба анҷом намерасад.

    +
      +
    • Шумо кукиҳоеро, ки ин сомона талаб мекунад, ғайрифаъол кардед ё бастед?
    • +
    • Агар қабули кукиҳои сомона мушкилиро ҳал накунад, ин эҳтимолан мушкилии танзимоти сервер мебошад, на дастгоҳи шумо.
    • +
    + ]]>
    + + + Реҷаи офлайн + + + Браузер дар реҷаи офлайн қарор дорад ва бо сомонаи дархостшуда пайваст шуда наметавонад.

    +
      +
    • Оё дастгоҳ ба шабакаи фаъол пайваст аст?
    • +
    • Барои гузаштан ба реҷаи онлайн ва аз нав бор кардани саҳифа, тугмаи "Аз нав кӯшиш кардан"-ро пахш кунед.
    • +
    + ]]>
    + + + Порт бо сабабҳои амниятӣ маҳдуд карда шудааст + + + Нишонии дархостшуда портеро муайян кард (масалан, mozilla.org:80 барои порти 80 дар mozilla.org), ки одатан барои мақсадҳои ба ғайр аз тамошокунии сомонаҳо истифода мешавад. Браузер барои муҳофизат ва амнияти шумо дархостро бекор кард.

    + ]]>
    + + + Пайвастшавӣ аз нав танзим карда шуд + + + Алоқаи шабақа ҳангоми танзими пайвастшавӣ қатъ карда шуд. Лутфан, аз нав кӯшиш кунед.

    +
      +
    • Сомона метавонад муваққатан дастнорас бошад ё бо дархостҳои зиёд хеле машғул бошад. Пас аз чанд лаҳза аз нав кӯшиш кунед.
    • +
    • Агар шумо ягон саҳифаро бор карда натавонед, пайвасти мобилии дастгоҳ ё пайвасти Wi-Fi -ро санҷед.
    • +
    + ]]>
    + + + Навъи файли беэътимод + + + +
  • Лутфан, бо соҳибони сомона дар тамос шавед ва дар бораи ин мушкилӣ ба онҳо хабар диҳед.
  • + + ]]>
    + + + Хатои муҳтавои вайроншуда + + + Браузер саҳифаеро, ки шумо дидан мехоҳед, нишон дода наметавонад, зеро дар интиқоли маълумот хато муайян карда шуд.

    +
      +
    • Лутфан, бо соҳибони сомона дар тамос шавед ва ба онҳо дар бораи ин мушкилӣ хабар диҳед.
    • +
    + ]]>
    + + + Муҳтавои вайрон + + Браузер саҳифаеро, ки шумо дидан мехоҳед, нишон дода наметавонад, зеро дар интиқоли маълумот хато муайян карда шуд.

    +
      +
    • Лутфан, бо соҳибони сомона дар тамос шавед ва ба онҳо дар бораи ин мушкилӣ хабар диҳед.
    • +
    + ]]>
    + + + Хатои рамзгузории муҳтаво + + Браузер саҳифаеро, ки шумо дидан мехоҳед, нишон дода наметавонад, зеро он аз шакли фишурдасозии дастгиринашаванда ё беэътибор истифода мебарад.

    +
      +
    • Лутфан, бо соҳибони сомона дар тамос шавед ва ба онҳо дар бораи ин мушкилӣ хабар диҳед.
    • +
    + ]]>
    + + + Нишонӣ ёфт нашуд + + + Браузер барои нишонии пешниҳодшуда мизбони серверро ёфт накард.

    +
      +
    • Нишониро барои хатоҳои зерин санҷед: + ww.example.com ба ҷойи + www.example.com.
    • +
    • Агар шумо ягон саҳифаро бор карда натавонед, пайвасти мобилии дастгоҳ ё пайвасти Wi-Fi -ро санҷед.
    • +
    + ]]>
    + + + Пайвасти интернет нест + + Пайвасти Интернетро санҷед ё саҳифаро пас аз чанд лаҳза аз нав бор кунед. + + + Аз нав бор кардан + + + Нишонии нодуруст + Нишонии пешниҳодшуда дар шакли эътирофшуда намебошад. Лутфан, навори нишониро барои хатоҳо санҷед ва аз нав кӯшиш намоед.

    + ]]>
    + + Нишонӣ дуруст нест + + + +
  • Нишониҳои сомона одатан дар ин шакл навишта мешаванд: http://www.example.com/
  • +
  • Мутмаин шавед, ки шумо хатҳои каҷро истифода мебаред (масалан, /).
  • + + ]]>
    + + + Протоколи номаълум + + Нишонӣ протоколеро нишон медиҳад, ки браузер эътироф намекунад (масалан, wxyz: // ). Аз ин рӯ браузер ба сомона дуруст пайваст шуда наметавонад.

    +
      +
    • Оё шумо кӯшиши дастрас кардани мултимедия ё дигар хидматҳои ғайриматни доред? Талаботи иловагиро аз сомона санҷед.
    • +
    • Баъзеи протоколҳо метавонанд нармафзор ё плагинҳои тарафи сеюмро пеш аз шинохтани браузер талаб кунанд.
    • +
    + ]]>
    + + + Файл ёфт нашуд + + +
  • Эҳтимол аст, ки номи мавод иваз карда шуд, ё ин ки мавод тоза карда шуд, ё ба ҷойи дигар гузошта шуд?
  • +
  • Дар нишонӣ ягон хатои имлоӣ, ҳуруфчинӣ ё дигар хатои типографӣ вуҷуд дорад?
  • +
  • Шумо ба маводи дархостшуда иҷозати дастрасии кофӣ доред?
  • + + ]]>
    + + + Дастрасӣ ба файл манъ карда шуд + + +
  • Эҳтимол, он тоза карда шудааст, ба ҷойи дигар интиқол дода шудааст ё иҷозатҳои файл дастрасиро манъ мекунанд.
  • + + ]]>
    + + + Сервери прокси пайвастро рад кард + + Браузер барои истифодаи сервери прокси танзим карда шудааст, аммо прокси аз пайвастшавӣ даст кашид.

    +
      +
    • Танзимоти прокси дар браузер дуруст аст? Танзимотро санҷед ва аз нав кӯшиш кунед.
    • +
    • Хидмати прокси барои пайвастшавӣ аз ин шабака иҷозат медиҳад?
    • +
    • Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат кунед.
    • +
    + ]]>
    + + + Сервери прокси ёфт нашуд + + Браузер барои истифодаи сервери прокси танзим карда шудааст, аммо прокси ёфт нашуд.

    +
      +
    • Танзимоти прокси дар браузер дуруст аст? Танзимотро санҷед ва аз нав кӯшиш кунед.
    • +
    • Дастгоҳ ба шабакаи фаъол пайваст аст?
    • +
    • Ҳоло ҳам мушкилӣ мекашед? Барои кумак ба маъмури шабака ё провайдери интернети худ муроҷиат кунед.
    • +
    + ]]>
    + + + Сомона бо нармафзори зарарнок + + + Сомона дар %1$s ҳамчун cомонаи ҳамлакунанда гузориш дода шудааст ва дар асоси бартариҳои амниятии шумо баста шудааст.

    + ]]>
    + + + Сомона бо муҳтавои номатлуб + + + Сомона дар %1$s ҳамчун cомонаи паҳнкунандаи нармафзори зараровар гузориш дода шудааст ва дар асоси бартариҳои амниятии шумо баста шудааст.

    + ]]>
    + + + Сомона бо муҳтавои зараровар + + + Сомона дар %1$s ҳамчун cомона бо қобилияти зарароварии пинҳонӣ гузориш дода шудааст ва дар асоси бартариҳои амниятии шумо баста шудааст.

    + ]]>
    + + + Сомона бо муҳтавои қалбакӣ + + Ин саҳифаи сомона дар %1$s ҳамчун сомонаи қалбакӣ гузориш дода шудааст ва дар асоси бартариҳои амниятии шумо баста шудааст.

    + ]]>
    + + + Сомонаи бехатар дастрас нест + + %1$s дастрас нест.]]> + + Ба сомонаи HTTP идома диҳед +
    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 @@ + + + + + ลองอีกครั้ง + + + ไม่สามารถทำคำขอให้เสร็จสมบูรณ์ + + + ไม่มีข้อมูลเพิ่มเติมเกี่ยวกับปัญหาหรือข้อผิดพลาดนี้ในปัจจุบัน

    ]]>
    + + + การเชื่อมต่อปลอดภัยล้มเหลว + + +
  • หน้าที่คุณกำลังพยายามจะดูไม่สามารถแสดงได้เนื่องจากไม่สามารถยืนยันความถูกต้องของข้อมูลที่ได้รับ
  • +
  • โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้
  • + ]]>
    + + + การเชื่อมต่อปลอดภัยล้มเหลว + + +
  • สิ่งนี้อาจเป็นปัญหาจากการกำหนดค่าของเซิร์ฟเวอร์ หรืออาจมีใครบางคนกำลังพยายามปลอมแปลงเซิร์ฟเวอร์
  • +
  • หากคุณเคยเชื่อมต่อกับเซิร์ฟเวอร์นี้สำเร็จในอดีต ข้อผิดพลาดอาจเกิดขึ้นเพียงชั่วคราว และคุณสามารถลองอีกครั้งในภายหลัง
  • + ]]>
    + + + ขั้นสูง… + + + อาจมีใครบางคนกำลังพยายามเลียนแบบไซต์และคุณไม่ควรดำเนินการต่อ +

    + ]]>
    + + ย้อนกลับ (แนะนำ) + + ยอมรับความเสี่ยงและดำเนินการต่อ + + + เว็บไซต์นี้ต้องการการเชื่อมต่อที่ปลอดภัย + + + +
  • ไม่สามารถแสดงหน้าที่คุณต้องการเข้าชมได้ เนื่องจากเว็บไซต์นี้บังคับใช้การเชื่อมต่อที่ปลอดภัย
  • +
  • โดยส่วนใหญ่แล้ว ปัญหานี้จะเกิดขึ้นที่ฝั่งเว็บไซต์ และคุณไม่สามารถแก้ปัญหาที่ฝั่งของคุณได้
  • +
  • คุณสามารถแจ้งผู้ดูแลเว็บไซต์ให้ทราบถึงปัญหานี้
  • + + ]]>
    + + + ขั้นสูง… + + + %1$s มีนโยบายการรักษาความปลอดภัยที่เรียกว่า HTTP Strict Transport Security (HSTS) ซึ่งหมายความว่า %2$s สามารถทำการเชื่อมต่อได้อย่างปลอดภัยเท่านั้น คุณไม่สามารถเพิ่มข้อยกเว้นเพื่อเยี่ยมชมไซต์นี้ได้ + ]]> + + ย้อนกลับ + + + การเชื่อมต่อถูกขัดจังหวะ + + เบราว์เซอร์ทำการเชื่อมต่อสำเร็จแล้ว แต่การเชื่อมต่อถูกขัดจังหวะขณะถ่ายโอนข้อมูล โปรดลองอีกครั้ง

    +
      +
    • ไซต์อาจไม่พร้อมใช้งานชั่วคราวหรือยุ่งเกินไป ลองอีกครั้งในอีกสักครู่
    • +
    • หากคุณไม่สามารถโหลดหน้าใด ๆ ได้ ให้ตรวจสอบข้อมูลของอุปกรณ์ของคุณหรือการเชื่อมต่อ Wi-Fi
    • +
    ]]>
    + + + การเชื่อมต่อหมดเวลา + + + ไซต์ที่ร้องขอไม่ตอบสนองต่อคำร้องขอการเชื่อมต่อและเบราว์เซอร์ได้หยุดการรอสำหรับการตอบกลับ

    +
      +
    • เซิร์ฟเวอร์อาจประสบกับความต้องการที่สูงหรือดับไปชั่วคราว? ลองอีกครั้งในภายหลัง
    • +
    • คุณไม่สามารถเรียกดูไซต์อื่น ๆ ได้? ตรวจสอบการเชื่อมต่อเครือข่ายของคอมพิวเตอร์
    • +
    • คอมพิวเตอร์หรือเครือข่ายของคุณถูกปกป้องด้วยไฟร์วอลล์หรือพร็อกซี? การตั้งค่าที่ไม่ถูกต้องสามารถรบกวนการท่องเว็บได้
    • +
    • ยังคงมีปัญหา? ติดต่อผู้ดูแลเครือข่ายหรือผู้ให้บริการอินเทอร์เน็ตของคุณเพื่อขอรับความช่วยเหลือ
    • +
    ]]>
    + + + ไม่สามารถเชื่อมต่อ + + +
  • ไซต์อาจไม่พร้อมใช้งานชั่วคราวหรือกำลังทำงานหนักเกินไป ลองอีกครั้งในอีกสักครู่
  • +
  • หากคุณไม่สามารถโหลดหน้าใด ๆ ได้ ตรวจสอบการเชื่อมต่อข้อมูลหรือ Wi-Fi ของอุปกรณ์ของคุณ
  • + ]]>
    + + + การตอบสนองที่ไม่คาดคิดจากเซิร์ฟเวอร์ + + ไซต์ตอบสนองต่อคำร้องขอเครือข่ายด้วยวิธีที่ไม่คาดคิดและเบราว์เซอร์ไม่สามารถดำเนินการต่อได้

    ]]>
    + + + หน้าไม่ได้เปลี่ยนเส้นทางอย่างถูกต้อง + + เบราว์เซอร์ได้หยุดความพยายามในการเรียกรายการที่ร้องขอ ไซต์กำลังเปลี่ยนเส้นทางคำร้องขอในทางที่ไม่มีวันเสร็จสมบูรณ์

    +
      +
    • คุณได้ปิดใช้งานหรือปิดกั้นคุกกี้ที่ต้องการโดยไซต์นี้หรือไม่?
    • +
    • หากการยอมรับคุกกี้ของไซต์ไม่แก้ปัญหา มันอาจเป็นปัญหาจากการกำหนดค่าเซิร์ฟเวอร์ ไม่ใช่คอมพิวเตอร์ของคุณ
    • +
    ]]>
    + + + โหมดออฟไลน์ + + + เบราว์เซอร์กำลังทำงานในโหมดออฟไลน์และไม่สามารถเชื่อมต่อไปยังรายการที่ร้องขอ

    +
      +
    • คอมพิวเตอร์เชื่อมต่อกับเครือข่ายที่ใช้งานได้หรือไม่?
    • +
    • กด “ลองอีกครั้ง” เพื่อสลับไปยังโหมดออนไลน์และโหลดหน้าใหม่
    • +
    ]]>
    + + + พอร์ตถูกจำกัดด้วยเหตุผลด้านความปลอดภัย + + + ที่อยู่ที่ร้องขอระบุพอร์ต (เช่น mozilla.org:80 สำหรับพอร์ต 80 บน mozilla.org) ซึ่งโดยปกติจะใช้เพื่อวัตถุประสงค์ อื่น ที่ไม่ใช่การเรียกดูเว็บ เบราว์เซอร์ได้ยกเลิกคำร้องขอดังกล่าวเพื่อปกป้องคุณและเพื่อความปลอดภัย

    ]]>
    + + + การเชื่อมต่อถูกตัด + + + การเชื่อมโยงเครือข่ายถูกขัดจังหวะขณะแลกเปลี่ยนข้อมูลการเชื่อมต่อ โปรดลองอีกครั้ง

    +
      +
    • ไซต์อาจไม่พร้อมใช้งานชั่วคราวหรือยุ่งเกินไป ลองอีกครั้งในอีกสักครู่
    • +
    • หากคุณไม่สามารถโหลดหน้าใด ๆ ได้ ให้ตรวจสอบข้อมูลของอุปกรณ์ของคุณหรือการเชื่อมต่อ Wi-Fi
    • +
    ]]>
    + + + ชนิดไฟล์ที่ไม่ปลอดภัย + + +
  • โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้
  • + ]]>
    + + + ข้อผิดพลาดเนื้อหาเสียหาย + + ไม่สามารถแสดงหน้าที่คุณกำลังพยายามจะดูเนื่องจากตรวจพบข้อผิดพลาดในการส่งผ่านข้อมูล

    +
      +
    • โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้
    • +
    ]]>
    + + + เนื้อหาขัดข้อง + ไม่สามารถแสดงหน้าที่คุณกำลังพยายามจะดูเนื่องจากตรวจพบข้อผิดพลาดในการส่งผ่านข้อมูล

    +
      +
    • โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้
    • +
    ]]>
    + + + ข้อผิดพลาดการเข้ารหัสเนื้อหา + ไม่สามารถแสดงหน้าที่คุณกำลังพยายามจะดูเนื่องจากหน้าใช้รูปแบบการบีบอัดที่ไม่ถูกต้องหรือไม่รองรับ

    +
      +
    • โปรดติดต่อเจ้าของเว็บไซต์เพื่อแจ้งพวกเขาให้ทราบถึงปัญหานี้
    • +
    ]]>
    + + + ไม่พบที่อยู่ + + + เบราว์เซอร์ไม่พบเซิร์ฟเวอร์โฮสต์สำหรับที่อยู่ที่ระบุ

    +
      +
    • ตรวจสอบข้อผิดพลาดในการพิมพ์ที่อยู่ เช่น + ww.example.com แทนที่จะเป็น + www.example.com
    • +
    • หากคุณไม่สามารถโหลดหน้าใด ๆ ได้ ให้ตรวจสอบข้อมูลของอุปกรณ์หรือการเชื่อมต่อ Wi-Fi ของคุณ
    • +
    ]]>
    + + + ไม่มีการเชื่อมต่ออินเทอร์เน็ต + + ตรวจสอบการเชื่อมต่อเครือข่ายของคุณหรือลองโหลดหน้านี้อีกครั้งในอีกสักครู่ + + เรียกใหม่ + + + ที่อยู่ไม่ถูกต้อง + + ที่อยู่ที่ให้มาไม่อยู่ในรูปแบบที่รู้จัก โปรดตรวจสอบที่อยู่ในแถบที่ตั้งเพื่อหาข้อผิดพลาดและลองใหม่อีกครั้ง

    ]]>
    + + ที่อยู่ไม่ถูกต้อง + + +
  • ที่อยู่เว็บมักจะเขียนเป็น http://www.example.com/
  • +
  • ตรวจสอบให้แน่ใจว่าคุณใช้เครื่องหมายทับไปข้างหน้า (กล่าวคือ /)
  • + ]]>
    + + + โปรโตคอลที่ไม่รู้จัก + + ที่อยู่ระบุโปรโตคอล (เช่น wxyz://) ที่เบราว์เซอร์ไม่รู้จัก เบราว์เซอร์จึงไม่สามารถเชื่อมต่อไปยังไซต์ได้อย่างเหมาะสม

    +
      +
    • คุณกำลังพยายามเข้าถึงมัลติมีเดียหรือบริการอื่น ๆ ที่ไม่ใช่ตัวอักษร? ตรวจสอบไซต์สำหรับข้อกำหนดพิเศษ
    • +
    • บางโปรโตคอลอาจต้องการซอฟต์แวร์หรือปลั๊กอินจากบุคคลที่สามก่อนที่เบราว์เซอร์จะสามารถรู้จัก
    • +
    ]]>
    + + + ไม่พบไฟล์ + +
  • รายการดังกล่าวอาจถูกเปลี่ยนชื่อ เอาออก หรือย้ายตำแหน่งที่ตั้งแล้วหรือไม่?
  • +
  • ที่อยู่สะกดผิด พิมพ์ตัวพิมพ์ใหญ่-เล็กไม่ตรง หรือตกหล่นหรือไม่?
  • +
  • คุณมีสิทธิอนุญาตการเข้าถึงรายการที่ร้องขอเพียงพอหรือไม่?
  • + ]]>
    + + + การเข้าถึงไฟล์ถูกปฏิเสธ + +
  • ไฟล์อาจถูกเอาออก ย้าย หรือสิทธิอนุญาตของไฟล์อาจป้องกันการเข้าถึง
  • + ]]>
    + + + เซิร์ฟเวอร์พร็อกซีปฏิเสธการเชื่อมต่อ + เบราว์เซอร์ถูกกำหนดค่าให้ใช้เซิร์ฟเวอร์พร็อกซี

    +
      +
    • การกำหนดค่าพร็อกซีของเบราว์เซอร์ถูกต้องหรือไม่? ตรวจสอบการตั้งค่าและลองอีกครั้ง
    • +
    • บริการพร็อกซีอนุญาตการเชื่อมต่อจากเครือข่ายนี้หรือไม่?
    • +
    • ยังคงมีปัญหา? ติดต่อผู้ดูแลเครือข่ายหรือผู้ให้บริการอินเทอร์เน็ตของคุณเพื่อขอรับความช่วยเหลือ
    • +
    ]]>
    + + + ไม่พบเซิร์ฟเวอร์พร็อกซี + เบราว์เซอร์ถูกกำหนดค่าให้ใช้เซิร์ฟเวอร์พร็อกซี แต่พร็อกซีไม่สามารถหาพบ

    +
      +
    • การกำหนดค่าพร็อกซีของเบราว์เซอร์ถูกต้องหรือไม่? ตรวจสอบการตั้งค่าและลองอีกครั้ง
    • +
    • คอมพิวเตอร์เชื่อมต่อกับเครือข่ายที่ใช้งานได้หรือไม่?
    • +
    • ยังคงมีปัญหา? ติดต่อผู้ดูแลเครือข่ายหรือผู้ให้บริการอินเทอร์เน็ตของคุณเพื่อขอรับความช่วยเหลือ
    • +
    ]]>
    + + + ปัญหาไซต์มัลแวร์ + + ไซต์ที่ %1$s ถูกรายงานว่าเป็นไซต์รุกรานและถูกปิดกั้นตามค่ากำหนดความปลอดภัยของคุณ

    ]]>
    + + + ปัญหาไซต์ที่ไม่พึงประสงค์ + + ไซต์ที่ %1$s ถูกรายงานว่าแจกจ่ายซอฟต์แวร์ไม่พึงประสงค์และถูกปิดกั้นตามค่ากำหนดความปลอดภัยของคุณ

    ]]>
    + + + ปัญหาไซต์ที่เป็นอันตราย + + ไซต์ที่ %1$s ถูกรายงานว่าเป็นไซต์ที่อาจเป็นอันตรายและถูกปิดกั้นตามค่ากำหนดความปลอดภัยของคุณ

    ]]>
    + + + ปัญหาไซต์หลอกลวง + + หน้าเว็บนี้ที่ %1$s ถูกรายงานว่าเป็นไซต์หลอกลวงและถูกปิดกั้นตามค่ากำหนดความปลอดภัยของคุณ

    ]]>
    + + + ไซต์ที่ปลอดภัยไม่พร้อมใช้งาน + + %1$s ไม่พร้อมใช้งาน]]> + + ดำเนินการต่อไปยังไซต์ HTTP +
    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 @@ + + + + + Subukan Uli + + + Hindi Makumpleto ang Kahilingan + + + Karagdagang impormasyon tungkol sa problema o error na ito ay hindi puwede sa kasalukuyan.

    + ]]>
    + + + Nabigo ang Ligtas na Koneksyon + + + +
  • Hindi maipakita ang pahinang sinusubukan mong buksan dahil hindi masigurong katiwa-tiwala ang natanggap na data.
  • +
  • Mangyaring ipagbigay-alam ang problemang ito sa mga may-ari ng website.
  • + + ]]>
    + + + Nabigo ang Ligtas na Koneksyon + + +
  • Ang problemang ito’y maaaring may kinalaman sa server configuration, o maaaring mayroong nagpapanggap bilang server na ito.
  • +
  • Kung nakakonekta ka na sa server na ito dati, maaaring pansamantala lang itong problema, at pwede mo uli subukan mamaya.
  • + + ]]>
    + + + Advanced… + + Maaaring mayroong may nagpapanggap bilang site na ito at hindi ka dapat magpatuloy. +

    + + ]]>
    + + Bumalik (Inirerekomenda) + + Tanggapin ang Panganib at Magpatuloy + + + Ang Website na ito ay na ngangailangan ng sekyur na koneksyon + + + Pumaroon… + + + Bumalik + + + Ang koneksyon ay naantala + + + Tagumpay na nakakonekta ang browser, pero nagambala ang koneksyon habang nagpapadala ng impormasyon. Pakisubukan uli.

    +
      +
    • Maaaring maraming ginagawa o pansamantalang hindi maabot ang site. Subukan uli sa loob ng ilang sandali.
    • +
    • Kung wala ka talagang mabuksang mga pahina, pakisuri ang iyong device data o koneksyon sa Wi-Fi.
    • +
    + ]]>
    + + + Ang koneksyon ay nag-time out + + Ang site na hiningi ay hindi nagbigay-tugon sa isang connection request at tumigil nang maghintay sa reply ang browser.

    +
      +
    • Baka naman maraming kumokonekta sa server o kaya pansamantalang pagtigil? Subukan uli mamaya.
    • +
    • Nakakapag-browse ka ba ng ibang mga site? Tingnan din ang network connection ng device.
    • +
    • Ang device mo ba o network ay protektado ng firewall o proxy? Nakakahambalang sa Web browsing ang mga maling setting.
    • +
    • Nagkakaproblema pa rin? Sumangguni sa iyong network administrator o Internet provider para makahingi ng tulong.
    • +
    + ]]>
    + + + Hindi makakonekta + + + +
  • Ang site ay maaaring pansamantalang hindi magagamit o masyadong abala. Subukan muli sa ilang sandali.
  • +
  • Kung hindi ka talaga makapag-load ng mga pahina, suriin ang iyong device data o koneksyon sa Wi-Fi.
  • + + ]]>
    + + + Hindi inaasahang tugon mula sa server + + + Nagbigay-tugon ang site sa network request sa di-inaasahang paraan at hindi na maaaring magpatuloy ang browser.

    + ]]>
    + + + Ang pahina ay hindi nagdidirekta nang maayos + + + Offline Mode + + + The browser is operating in its offline mode and cannot connect to the requested item.

    +
      +
    • Is the device connected to an active network?
    • +
    • Press “Try Again” to switch to online mode and reload the page.
    • +
    + ]]>
    + + + Hinarang ang Port dahil sa mga kadahilanang pangseguridad + + + Ang koneksyon ay na-reset + + + The network link was interrupted while negotiating a connection. Please try again.

    +
      +
    • The site could be temporarily unavailable or too busy. Try again in a few moments.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + Hindi Ligtas na File Type + + + +
  • Please contact the website owners to inform them of this problem.
  • + + ]]>
    + + + Corrupted Content Error + + + The page you are trying to view cannot be shown because an error in the data transmission was detected.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Bumagsak ang nilalaman + + The page you are trying to view cannot be shown because an error in the data transmission was detected.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Error sa Pag-encode ng Nilalaman + + The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Hindi Natagpuan ang Address + + + Hindi matagpuan ng browser ang host server para sa nabanggit na address.

    +
      +
    • Suriin ang address para sa mga maling pagkakatype gaya ng + ww.example.com sa halip na + www.example.com
    • +
    • Kung hindi ka talaga makapag-load ng kahit anong pahina, tingnan mo ang iyong device data o koneksyon sa Wi-Fi.
    • +
    + ]]>
    + + + Walang koneksyon sa Internet + + Suriin ang iyong koneksyon sa network o subukang i-reload ang pahina sa ilang sandali. + + Mag-reload + + + Di-wastong Address + Ang nabanggit na address ay nasa di-kilalang format. Pakisuri ang location bar sa mga pagkakamali at subukan uli.

    + ]]>
    + + Ang address ay hindi wasto + + + +
  • Web addresses are usually written like http://www.example.com/
  • +
  • Make sure that you’re using forward slashes (i.e. /).
  • + + ]]>
    + + + Hindi kilalang Protocol + + The address specifies a protocol (e.g., wxyz://) the browser does not recognize, so the browser cannot properly connect to the site.

    +
      +
    • Are you trying to access multimedia or other non-text services? Check the site for extra requirements.
    • +
    • Some protocols may require third-party software or plugins before the browser can recognize them.
    • +
    + ]]>
    + + + Hindi Natagpuan ang File + + +
  • Could the item have been renamed, removed, or relocated?
  • +
  • Is there a spelling, capitalization, or other typographical error in the address?
  • +
  • Do you have sufficient access permissions to the requested item?
  • + + ]]>
    + + + Pinigilan ang pag-access sa file + + +
  • Maaaring ito ay tinanggal, inilipat, o may mga permiso sa file na pumipigil sa access.
  • + + ]]>
    + + + Tumanggi sa Koneksyon ng Proxy Server + Ang browser ay naka-configure na gumamit ng proxy server, pero tumangging magkonekta ang proxy.

    +
      +
    • Tama ba ang proxy configuration ng browser? Tingnan ang mga setting at subukan uli.
    • +
    • Pinapayagan ba ng proxy service ang mga koneksyon mula sa network na ito?
    • +
    • Nagkakaproblema pa rin? Sumangguni sa iyong network administrator o Internet provider para sa karagdagang tulong.
    • +
    + ]]>
    + + + Hindi Matagpuan ang Proxy Server + + The browser is configured to use a proxy server, but the proxy could not be found.

    +
      +
    • Is the browser’s proxy configuration correct? Check the settings and try again.
    • +
    • Is the device connected to an active network?
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    + ]]>
    + + + Isyu ng site sa malware + + + The site at %1$s has been reported as an attack site and has been blocked based on your security preferences.

    + ]]>
    + + + Hindi ginustong isyu ng site + + + The site at %1$s has been reported as serving unwanted software and has been blocked based on your security preferences.

    + ]]>
    + + + Mapanganib na isyu sa site + + + The site at %1$s has been reported as a potentially harmful site and has been blocked based on your security preferences.

    + ]]>
    + + + May mapanlinlang na site + + This web page at %1$s has been reported as a deceptive site and has been blocked based on your security preferences.

    + ]]>
    + + + Tumuloy sa HTTP Site +
    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 @@ + + + + o sin + + + mi ken ala pini e kama jo + + sona namako pi pakala ni li lon ala.

    +]]>
    + + + linja len li pakala + + + linja len li pakala + + + namako… + + + ken la jan pi lipu ni li lon ala, li wile ike e sina kepeken nasin len. ni la o tawa ala. +

    + +]]>
    + + o tawa pona (sina pona tan ni) + + o awen tawa lipu + + + linja li pakala + + + tenpo mute la linja li pakala + + + mi ken ala tawa lipu + + + lipu li pana e pakala. nasin la mi sona ala + + + lipu li toki e ni: o tawa ni. taso tawa ni li pakala + + + nasin pi linja ala + + + lipu pi nasin ike + + +
  • o toki e pakala tawa jan lawa pi lipu ni.
  • + +]]>
    + + + linja li lon ala + + o lukin e linja sina. o sin e lipu lon tenpo kama. + + o sin + + + nimi nasin pakala + + nimi nasin li pakala. + + + lipu li lon ala + + + sina ken ala lukin e lipu + + + lipu li wile ike e sina + + + o tawa lipu pi len ala +
    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 @@ + + + + + Tekrar dene + + + İstek tamamlanamadı + + + Bu sorun veya hata hakkında ek bilgi mevcut değil.

    ]]>
    + + + Güvenli bağlantı kurulamadı + + +
  • Görüntülemeye çalıştığınız sayfa, alınan verilerin yetkinliği doğrulanamadığı için gösterilemiyor.
  • Lütfen site yöneticisi ile irtibata geçip durumu bildirin.
  • ]]>
    + + + Güvenli bağlantı kurulamadı + + +
  • Sunucunun yapılandırmasıyla ilgili bir sorun olabilir veya birisi sunucuyu taklit etmeye çalışıyor olabilir.
  • Daha önce bu sunucuya sorunsuz bağlandıysanız sorun geçici olabilir ve daha sonra yeniden bağlanmayı deneyebilirsiniz.
  • ]]>
    + + + Gelişmiş… + + Birisi bu siteyi taklit etmeye çalışıyor olabilir. Devam etmemenizi öneririz. +

    + + ]]>
    + + Geri dön (Önerilir) + + Riski kabul ederek devam et + + + Bu web sitesi güvenli bir bağlantı gerektiriyor. + + + +
  • Bu web sitesi güvenli bir bağlantı gerektirdiği için, görüntülemeye çalıştığınızı sayfa gösterilemiyor.
  • +
  • Sorun büyük ihtimalle siteden kaynaklanıyor ve sorunu çözmek için sizin yapabileceğiniz bir şey yok.
  • +
  • Sorun hakkında site yöneticisini bilgilendirmeyi deneyebilirsiniz.
  • + + ]]>
    + + + Gelişmiş… + + + %1$s HTTP Sıkı Aktarım Güvenliği (HSTS) denilen bir güvenlik ilkesi uyguluyor. Bu nedenle %2$s bu siteye yalnızca güvenli bir şekilde bağlanabilir. Bu siteye ayrıcalık tanıyarak siteyi ziyaret edemezsiniz.]]> + + Geri dön + + + Bağlantı kesintiye uğradı + + + Tarayıcı başarıyla bağlandı ama bilgi aktarılırken bağlantı kesildi. Lütfen tekrar deneyin.

    +      
      +        
    • Site geçici olarak kullanılamıyor veya çok meşgul olabilir. Birkaç dakika sonra tekrar deneyin.
    • +        
    • Hiçbir sayfa açılmıyorsa cihazınızın mobil internet veya Wi-Fi bağlantısını kontrol edin.
    • +      
    +    ]]>
    + + + Bağlantı zaman aşımına uğradı + + + İstenen site bağlantı isteğine yanıt vermeyince tarayıcı da beklemeyi bıraktı.

    • Sunucu, yoğun talep veya geçici bir kesinti nedeniyle hizmet veremiyor olabilir. Daha sonra yeniden deneyin.
    • Diğer siteler de mi açılmıyor? Öyleyse cihazınızın ağ bağlantısını kontrol edin.
    • Cihazınız veya ağınız bir güvenlik duvarıyla veya vekil sunucuyla korunuyorsa hatalı ayarlar internete erişmenizi engelleyebilir.
    • 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.
    ]]>
    + + + Bağlanılamadı + + + +
  • Site geçici olarak kapalı veya çok meşgul olabilir. Birkaç dakika sonra yeniden deneyin.
  • +
  • Hiçbir sayfa açılmıyorsa cihazınızın mobil internet veya Wi-Fi bağlantısını kontrol edin.
  • + ]]>
    + + + Sunucudan beklenmeyen yanıt + + + Site, ağ isteğine beklenmeyen bir biçimde karşılık verdi. Tarayıcı bu işlemi sürdüremiyor.

    ]]>
    + + + Sayfa doğru bir şekilde yönlendirilmiyor + + + 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.

    • Site tarafından bırakılmak istenen çerezleri engellemiş ya da etkisizleştirmiş olabilirsiniz.
    • Ç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.
    ]]>
    + + + Çevrimdışı kip + + + Tarayıcı şu an çevrimdışı kipte çalışıyor ve istenen öğeye bağlanamaz.

    • Cihazınız etkin bir ağa bağlı mı?
    • Çevrimiçi kipe geçerek sayfayı tazelemek için “Yeniden dene” düğmesine tıklayın.
    ]]>
    + + + Güvenlik nedeniyle bağlantı noktası kısıtlanmış + + İstenen adres normalde web gezintisi dışında amaçlar için kullanılan bir bağlantı noktası (örn. mozilla.org üzerinde 80. port için mozilla.org:80) içeriyor. Tarayıcı sizi korumak ve güvenliğini sağlamak amacıyla isteği iptal etti.

    ]]>
    + + + Bağlantı sıfırlandı + + + Bağlantı anlaşması yapılırken ağ bağlantısı kesildi. Lütfen tekrar deneyin.

    +      
      +        
    • Site geçici olarak kullanılamıyor veya çok meşgul olabilir. Birkaç dakika sonra tekrar deneyin.
    • +        
    • Hiçbir sayfa açılmıyorsa cihazınızın mobil internet veya Wi-Fi bağlantısını kontrol edin.
    • +      
    +    ]]>
    + + + Güvensiz dosya türü + + + +
  • Site sahipleriyle iletişim kurarak bu sorunu onlara bildirmeyi düşünebilirsiniz.
  • + +]]>
    + + + Hasarlı İçerik Hatası + + Veri aktarımında bir hata tespit edildiği için bakmak istediğiniz sayfa gösterilemiyor.

    • Site sahipleriyle iletişim kurup bu sorunu onlara bildirmeyi düşünebilirsiniz.
    ]]>
    + + + İçerik çöktü + + Veri aktarımında bir hata tespit edildiği için bakmak istediğiniz sayfa gösterilemiyor.

    • Site sahipleriyle iletişim kurup bu sorunu onlara bildirmeyi düşünebilirsiniz.
    ]]>
    + + + İçerik kodlama hatası + + 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.

    +      
      +        
    • Bu sorunu bildirmek için lütfen web sitesi sahipleriyle iletişime geçin.
    • +      
    +    ]]>
    + + + Adres bulunamadı + + + Tarayıcı girilen adresin sunucusunu bulamadı.

    +
      +
    • Adreste olabilecek yazım hatalarını kontrol edin. + Örneğin www.example.com yerine + ww.example.com yazmış olabilir misiniz?
    • +
    • Hiçbir sayfa açılmıyorsa cihazınızın mobil internet veya Wi-Fi bağlantısını kontrol edin.
    • +
    ]]>
    + + + İnternet bağlantısı yok + + Ağ bağlantınızı kontrol edin veya birkaç dakika sonra sayfayı tazelemeyi deneyin. + + Tazele + + + Geçersiz adres + Girilen adres tanınan bir biçimde değil. Konum çubuğuna bakıp olası hataları giderdikten sonra yeniden deneyin.

    ]]>
    + + Adres geçersiz + + + +
  • Web adresleri genelde http://www.example.com/ biçiminde yazılır.
  • +
  • Adreste sağa yatık bölü işareti (/) bulunduğundan emin olun.
  • +]]>
    + + + Bilinmeyen protokol + Girilen adres tarayıcı tarafından tanınmayan bir iletişim kuralına (ör. abcd://) işaret ettiğinden tarayıcı siteye düzgünce bağlanamıyor.

    • Çoklu ortam barındıran veya metin içermeyen bir hizmete bağlanmak istiyor olabilirsiniz.
    • 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.
    ]]>
    + + + Dosya bulunamadı + +
  • Dosya taşınmış, silinmiş ya da adı değiştirilmiş olabilir.
  • Adreste yazım hatası yapılmış olabilir.
  • İstenen öğeye erişmek için gerekli izniniz olmayabilir.
  • ]]>
    + + + Dosyaya erişim reddedildi + + +
  • Silinmiş, taşınmış veya dosya izinleri nedeniyle erişilemiyor olabilir.
  • + +]]>
    + + + Vekil sunucu bağlantıyı reddetti + + Tarayıcı vekil sunucu kullanmak üzere yapılandırılmış, ancak vekil sunucu bağlantı isteğini geri çevirdi.

    • Vekil sunucu ayarları düzgün yapılmamış olabilir. Ayarlarınızı gözden geçirip yeniden deneyin.
    • Belirtilen vekil sunucu ağınız üzerinden kurulan bağlantılara izin vermiyor olabilir.
    • Hâlâ sorun yaşıyorsanız ağ yöneticinize ya da internet servis sağlayıcınıza başvurun.
    ]]>
    + + + Vekil sunucu bulunamadı + + Tarayıcı vekil sunucu kullanmak üzere yapılandırılmış, ancak vekil sunucu bulunamadı.

    • Vekil sunucu ayarları düzgün yapılmamış olabilir. Ayarlarınızı gözden geçirip yeniden deneyin.
    • Cihazınız etkin bir ağ bağlantısına sahip olmayabilir.
    • Hâlâ sorun yaşıyorsanız ağ yöneticinize ya da internet servis sağlayıcınıza başvurun.
    ]]>
    + + + Kötü amaçlı yazılım sitesi sorunu + + + %1$s konumundaki sitenin saldırı sitesi olduğu bildirildi ve güvenlik tercihlerinize dayanılarak site engellendi.

    + ]]>
    + + + İstenmeyen site sorunu + + + %1$s konumundaki sitenin istenmeyen yazılım dağıttığı bildirildi ve güvenlik tercihlerinize dayanılarak site engellendi.

    + ]]>
    + + + Zararlı site sorunu + + + %1$s konumundaki sitenin potansiyel olarak zararlı site olduğu bildirildi ve güvenlik tercihlerinize dayanılarak site engellendi.

    + ]]>
    + + + Aldatıcı site sorunu + + %1$s web sayfasının aldatıcı bir sayfa olduğu ihbar edilmiştir. Sayfa, güvenlik tercihlerinize dayanılarak engellendi.

    ]]>
    + + + Güvenli site mevcut değil + + %1$s sitesinin güvenli bir HTTPS sürümü mevcut değil.]]> + + HTTP siteye devam et +
    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 @@ + + + + + A\'ngô ñû + + + Na\'ue gārayinaj sa achín nì\'iát + + Nitāj ni\'īn nùhuin saj gire\' ma.

    ]]>
    + + + Gire\' koneksiûn dugumî sò\' + + + +
  • \'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.
  • +
  • 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.
  • + ]]>
    + + + Gire\' koneksiûn dugumî sò\' + + + +
  • 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.
  • +
  • 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.
  • + ]]>
    + + + Sa tàj ñā doj… + + + Huā sa huin ruhuâ dīga\'ñūn\'unj sò\' sisī huê sitiô nan huin nī da\'uît nānikàj rūkût. +

    + ]]>
    + + Nānikàj ne\' rūkùu (sa sà\'a huin) + + Gārāyino\' si ahī hua man nī gun\' ne\' ñāan + + + Ni’ñānj sitiô nan ‘ngō koneksiûn dūgumîn ñù’. + + + +
  • Nā’hue nāyi’nïn pajinâ ruhuât ni’hiājt dadin’ ni’ñānj web nan ‘ngō kōneksiûn hue’ê.
  • +
  • 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.
  • +
  • Gā’hue gātāj nan’ānjt riña administrador sitio nan rayi’î sa ahui a’nan’ nan.
  • ]]>
    + + + Sa huāj ñā doj… + + + %1$s nīka ‘ngō chrēj dugumîn ñùn’ gū’nàj HTTP Strict Transport Security (HSTS), ô’ nī gāta ruhuâj sisī %2$s 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. ]]> + + Nānīkāj ne\’ Rūkùu + + + Giyichin\' ngè kōneksiûn + + + 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 ñû.

    +
      +
    • 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.
    • +
    • Sisī na\'ue nāyi\'nïn riña à\'ngō pâjina, ni’iāj sisī huā hue\'ê internet riña si āgâ\'t.
    • +
    ]]>
    + + + Ganahuij diû ana\'uìjt + + + Nu ga\'ue nāyi\'nïn riña sitiô gachrûnt yi\'ì dan ga\'nïn\' ruhuâ riña sā nana\'uî\'t.

    +
      +
    • Ûta hua chruj riña servidor asi gire\' internet aj. Ginù huin nanâ doj.
    • +
    • Na\'ue gātūt riña a\'ngô nej sitio a\'. Gīni\'iaj si huā internet riña si āgâ\' ra.
    • +
    • Dugumîn \'ngō frirewall asi \'ngō proxy si āgâ\' raj. Ni\'iāj si huā konfiguradô hue\'ej dadin\' gā\'ue sisī huej \'iaj.
    • +
    • 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ò\'.
    • +
    ]]>
    + + + Na\'ue gātu riña internet + + + +
  • Ga\'ue si huā chruj riña sitiô nana si huā rán akuan\' riñanj. Ginùn huin ñû nanâ doj.
  • +
  • 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.
  • + ]]>
    + + + Sê \'ngō nuguan\' hue\'ê narikî servidor + + + 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.

    ]]>
    + + + Nitāj si \'iaj redireksionando hue\'ê pajinâ nan + + + 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.

    +
      +
    • Huā rán riña nej kokî arâj sun sitiô nan \'iá raj.
    • +
    • 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.
    • +
    ]]>
    + + + Sī gida\'aj internet + + + 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.

    +
      +
    • \'Iaj sun hue\'ê internet riña si āgâ\' raj.
    • +
    • Gūru\'man ra\'a “Ginùn huin ñû” da\' nānùn internet nī nāyi\'nïn si pajinât.
    • +
    ]]>
    + + + Nitāj si hūaj gātūt dadin\' huā āhī man + + + Dīreksiûn gachrûnt nī nīka \'ngō puerto (dàj rû\' mozilla.org:80 guendâ puerto 80 nīkāj mozilla.org) sê guendâ gāchē nun\' riña internet huin man dadin\' nīnïn 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.

    ]]>
    + + + Nayi\'ì nākà ñû conexión + + + Hīaj nûn huin gā kōneksiûn sanī giyichin\' ngè si enlace red. Gī\'iaj \'ngō sunūnj un, ginù huin ñû.

    +
      +
    • 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.
    • +
    • 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.
    • +
    ]]>
    + + + \'Ngō archivo huā āhīi huin + + +
  • 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.
  • + ]]>
    + + + Gire\'ej dadin\' sa nūn riñanj hua a\'nan\' + + + \'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.

    +
      +
    • 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.
    • +
    ]]>
    + + + Ganarán riña kōntenîdo + \'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.

    +
      +
    • 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.
    • +
    ]]>
    + + + Gire\' si kodigô kōntenîdo + \'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.

    +
      +
    • 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.
    • +
    ]]>
    + + + Nu nārì\'ìj dīreksiûn + + + Nu ga\'ue narì\' riña sā nana\'uî\'t \'ngō servidor guendâ direksiûn gachrûnt.

    +
      +
    • Gīni\'iāj si nu gāchrūn hue\'êt, dàj rû\': + ww.example.com luguâ gāchrūnt + www.example.com.
    • +
    • 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.
    • +
    ]]>
    + + + Nitāj internet hua + + Nātsij ni\'iājt sisī huā internet asi ginù huin ñû nā\'nïnt riña nanâ doj sínj. + + Nā\'nïn ñû + + + Nitāj si huā hue\'ê direksiûn gachrûnt + 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.

    ]]>
    + + Nitāj si huā hue\'ê direksiûn gachrûnt + + + +
  • Huā da\'āj nī achrûn\' \'ngō dīreksiûn dànanj http://www.example.com/
  • +
  • Ni\'iāj hue\'ê sisī arâj sunt huìj chrun li nīkïn\' nītïn dan ân (dàj rû\' /).
  • + ]]>
    + + + Sê dànanj gāchrūn\' + + Dàj gāchrūn\' \'ngō dīreksiûn (dàj rû\', wxyz://) 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.

    +
      +
    • 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.
    • +
    • 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.
    • +
    ]]>
    + + + Nu nārì\'ij archîbo + +
  • Nadunâ si yūgui man, nare\' man asi nadunâ man riña hūa aj.
  • +
  • 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.
  • +
  • Giri\' nì\'iát da\' gātūt hiūj nan bè\'ej.
  • + ]]>
    + + + Nu ga\'ue gā\'nïn gātūt riña archîbo + +
  • 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.
  • + ]]>
    + + + Nu gā\'nïn servidor proxy gātūt + + 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.

    +
      +
    • Huā hue\'ê proxy riña sa riñā nana\'uî\'t bè\'ej. Nātsij ni\'iājt nī ginù huin ñû gātūt.
    • +
    • Ga\'ue gātūt hiūj nan tāj proxy bè\'ej.
    • +
    • 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ò\'.
    • +
    ]]>
    + + + Nu nārì\'ij servidor proxy + + Huā nî\'nïn riña sā nana\'uî\'t dan da\' gārāsunj \'ngō servidor proxy, sanī nu nārì\'ij proxy.

    +
      +
    • Huā hue\'ê proxy riña sa riñā nana\'uî\'t bè\'ej. Nātsij ni\'iājt nī ginù huin ñû gātūt.
    • +
    • Huā kōnektadô si āgâ\'t riña \'ngō red \'iaj sun anj.
    • +
    • 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ò\'.
    • +
    ]]>
    + + + Sa huā a\'nan\' riña sitiô yī\'ì nan + + + 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ï.

    ]]>
    + + + Sa huā a\'nan\' riña sitiô nāsinùnj nan + + + 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ï.

    ]]>
    + + + Sa huā a\'nan\' riña sitiô yī\'ìi + + 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ï.

    ]]>
    + + + Sa huā a\'nan\' riña sitiô diga\'ñu\'ūnj un + + 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ï.

    ]]>
    + + + Nitāj si huā akuan’ sitiô hue’è nan + + %1$s hua.]]> + + Gīnu ngè riña HTTP +
    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 @@ + + + + Янәдән тырышып карау + + + Сорауны тәмамлап булмый + + + Бу проблема яки хата турында өстәмә мәгълүмат әлегә юк.

    + ]]>
    + + + Хәвефсез бәйләнеш кору хатасы + + + +
  • Күрәсегез килгән сәхифә күрсәтелә алмый, чөнки алынган мәгълүматларның чынлыгын раслау мөмкин түгел
  • +
  • Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.
  • + + ]]>
    + + + Хәвефсез бәйләнеш кору хатасы + + +
  • Бу сервер көйләүләрендәге хата белән бәйле булырга мөмкин, яисә кемдер Сезгә кирәкле серверны икенче берәве белән алыштырырга тырыша.
  • +
  • Моңа кадәр бу серверга уңышлы тоташа торган булсагыз, бу хата вакытлыча гына булырга мөмкин. Берәздән янәдән тырышып карагыз.
  • + + + ]]>
    + + + Киңәйтелгән… + + + Кемдер үзенең сайтын бу сайт дип күрсәтергә тырыша кебек, шунлыктан дәвам итмәвегез хәерле. +

    + + ]]>
    + + Кире кайту (киңәш ителә) + + + Рискны кабул итү һәм дәвам итү + + + Бу вебсайт хәвефсез бәйләнеш таләп итә. + + + +
  • Күрәсегез килгән сәхифә күрсәтелә алмый, чөнки бу вебсайт хәвефсез бәйләнеш таләп итә.
  • +
  • Проблема сайтта булырга тиеш һәм аны чишү өчен Сез эшли алырлык бернәрсә дә юк.
  • +
  • Вебсайтның администраторына проблема турында хәбәр итә аласыз.
  • + + ]]>
    + + + Киңәйтелгән… + + + %1$s сәхифәсенең HTTP Strict Transport Security (HSTS) дип аталган хәвефсезлек сәясәте бар, шуңа күрә %2$s аңа хәвефсез рәвештә генә тоташа ала. Сез бу сайтка керү өчен чыгарма өсти алмыйсыз. + ]]> + + Кире кайту + + + Бәйләнеш өзелде + + + Браузер уңышлы тоташты, ләкин мәгълүмат тапшырылганда бәйләнеш өзелеп китте. Янәдән тырышып карагыз.

    +
      +
    • Сайт вакытлыча я бөтенләй эшләми, я бик мәшгуль. Берәздән янәдән ачып карагыз.
    • +
    • Әгәр һичбер сайт та ачылмаса, җиһазыгызның мобиль интернет я Wi-Fi бәйләнешләрен тикшерегез.
    • +
    + ]]>
    + + + Бәйләнешне көтү вакыты чыкты + + + Соралган сайт тоташу соравына җавап бирмәде һәм браузер җавап көтүдән туктады.

    +
      +
    • Сайт серверы вакытлыча сүнгән яки артык йөкләнгән булырга мөмкинме? Соңрак янәдән тырышып карагыз.
    • +
    • Башка сайтлар да ачылмаса, җиһазыгызның интернетка бәйләнешен тикшерегез.
    • +
    • Җиһазыгыз яки челтәрегез иминлек дивары яисә прокси сервер белән сакланган булса, аларның көйләүләрен тикшерегез, чөнки ялгыш көйләүләр вебсайтларның ачылуына киртә була ал.
    • +
    • Проблема чишелмәсә, челтәр администраторыгыз яки интернет-провайдерыгыз белән элемтәгә керегез.
    • +
    + ]]>
    + + + Тоташып булмады + + +
  • Сайт вакытлыча сүнгән я бик мәшгуль булырга мөмкин. Берничә минуттан янәдән тырышып карагыз.
  • +
  • Һичбер сәхифә дә ачылмаса җиһазыгызның мобиль Интернет яисә Wi-Fi бәйләнешен тикшерегез.
  • + + ]]>
    + + + Сервердан көтелмәгән җавап + + + Сайт сорауга көтелмәгәнчә җавап бирде һәм браузер дәвам итә алмый.

    + ]]>
    + + + Сәхифә икенче сәхифәгә дөрес юнәлтми + + + Автоном эш режимы + + + Порт куркынычсызлык саклау максатыннан ябык + + + Бәйләнеш ташланды + + + Тоташкан вакытта челтәр аша бәйләнеш өзелеп китте. Зинһар янәдән тырышып карагыз.

    +
      +
    • Сайт я вакытлыча эшләми, я бик мәшгуль. Берәздән янәдән ачып карагыз.
    • +
    • Әгәр һичбер сайт та ачылмаса, җиһазыгызның мобиль интернет яки Wi-Fi бәйләнешен тикшерегез.
    • +
    + ]]>
    + + + Куркыныч файл төре + + + +
  • Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.
  • + + ]]>
    + + + Бозык эчтәлек хатасы + + + Күрәсегез килгән сәхифә күрсәтелә алмый, чөнки мәгълүмат тапшыруда хата табылды.

    +
      +
    • Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.
    • +
    + ]]>
    + + + Эчтәлеге ватык + + Күрәсегез килгән сәхифә күрсәтелә алмый, чөнки мәгълүмат тапшыруда хата табылды.

    +
      +
    • Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.
    • +
    + ]]>
    + + + Эчтәлекне кодлау хатаcы + + Күрәсегез килгән сәхифәне күрсәтеп булмый, чөнки ул кулланган компрессия ысулы я хаталы, я браузер аны танымый.

    +
      +
    • Зинһар сайт ияләре белән элемтәгә кереп, аларга бу проблема турында сөйләгез.
    • +
    + ]]>
    + + + Адрес табылмады + + + Интернетка тоташу юк + + Челтәргә бәйләнешегезне тикшерегез яки бераздан битне яңадан йөкләп карагыз. + + Яңарту + + + Яраксыз адрес + + Адрес хаталы + + +
  • Веб адреслары гадәттә түбәндәгечә языла — http://www.example.com/
  • +
  • Адресларда сулга түгел, ә бәлки уңга авыш бүлү билгесен кулланасы (ягъни бу билге: /).
  • + + ]]>
    + + + Билгесез протокол + + + Файл табылмады + + + Файлдан файдалануга рөхсәт бирелмәде + + + Прокси-сервер бәйләнештән бар тартты + + + Прокси-сервер табылмады + + + Зыянлы программалы сайт + + + Кирәкмәс сайт + + + Зыянлы сайт + + + Ялган сайт + + + Хәвефсез сайт юк + + HTTP сайтка үтү +
    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 @@ + + + + + Arem daɣ + + + Ur teẓḍired ad tsemded asuter + + + Ur tettyafa tansa + + + Smiren + + + Abrutukul arussin + + + Ur ittyafa ufaylu + + + Anekcum ɣer ufaylu yegdel + + 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 @@ + + + + قايتا سىناڭ + + + تەلەپنى تاماملىيالمايدۇ + + ھازىرچە بۇ مەسىلە ياكى خاتالىق ھەققىدە تەپسىلىي ھەل قىلىش چارىسى يوق

    + + ]]>
    + + + بىخەتەر ئۇلىنىش مەغلۇپ بولدى + + + +
  • سىز كۆرمەكچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ ، چۈنكى تاپشۇرۇۋالغان سانلىق مەلۇماتلارنىڭ راست-يالغانلىقىنى تەكشۈرگىلى بولمايدۇ.
  • +
  • توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ، ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.
  • + + ]]>
    + + + بىخەتەر ئۇلىنىش مەغلۇپ بولدى + + +
  • بۇ مۇلازىمىتىرنىڭ سەپلىمىسىدە مەسىلە بولغانلىقى، ياكى نامەلۈم كىشلەرنىڭ مۇلازىمىتىرنى دوراشقا ئۇرۇنغانلىق سەۋەبلىك بولۇشى مۇمكىن.
  • +
  • ئەگەر سىز بۇرۇن بۇ مۇلازىمېتىرغا مۇۋەپپەقىيەتلىك ئۇلىنالىغان بولسىڭىز، خاتالىق ۋاقىتلىق بولۇشى مۇمكىن، شۇنداقلا قايتا سىناپ باقسىڭىز بولىدۇ.
  • + + ]]>
    + + + ئالىي… + + بەزىلەر تور بېكەتنى تەقلىد قىلىشقا ئۇرۇنغان بولۇشى مۇمكىن، سىز داۋاملاشتۇرماسلىقىڭىز كېرەك. +

    + + ]]>
    + + قايتىش (تەۋسىيە) + + + خەتەرنى قوبۇل قىلىپ داۋاملاشتۇرۇش + + + بۇ تور بېكەت بىخەتەر ئۇلىنىشنى تەلەپ قىلىدۇ. + + + +
  • سىز زىيارەت قىلماقچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ، چۈنكى بۇ تور بېكەت بىخەتەر ئۇلىنىشنى تەلەپ قىلىدۇ.
  • +
  • مەسىلە تور بېكەتتە بولۇشى مۇمكىن، ئۇنى سىز ھەل قىلالمايسىز.
  • +
  • مەسىلىنى تور بېكەت باشقۇرغۇچىسىغا ئۇقتۇرۇپ قويالايسىز.
  • + + ]]>
    + + + ئالىي… + + + %1$sنىڭ HTTP قاتتىق قاتناش بىخەتەرلىكى (HSTS) دەپ ئاتىلىدىغان بىخەتەرلىك سىياسىتى بار ، يەنى %2$s پەقەت بىخەتەر ئۇلىنالايدۇ. بۇ تور بېكەتنى زىيارەت قىلىش ئۈچۈن مۇستەسنا قوشالمايسىز. + + ]]> + + قايتىش + + + ئۇلىنىش ئۈزۈلۈپ قالدى + + + توركۆرگۈچ مۇۋەپپەقىيەتلىك ئۇلاندى ، ئەمما ئۇچۇر يوللىغاندا ئۇلىنىش ئۈزۈلۈپ قالدى. قايتا سىناڭ.

    +
      +
    • تور بېكەتنى ۋاقتىنچە زىيارەت قىلغىلى بولمايدۇ ياكى مۇلازىمىتىر ئالدىراش. بىر نەچچە مىنۇتتىن كېيىن قايتا سىناڭ.
    • +
    • ئەگەر سىز ھېچقانداق بەتنى يۈكلىيەلمىسىڭىز ، ئۈسكۈنىڭىزنىڭ تور ئۇلىنىشىنى تەكشۈرۈپ بىقىڭ.
    • +
    + ]]>
    + + + ۋاقتىدا ئۇلىنالمىدى + + + ئىلتىماس قىلىنغان تور بېكەت ئۇلىنىش تەلىپىگە جاۋاب قايتۇرمىغاچقا توركۆرگۈ جاۋاب كۈتۈشنى توختاتتى.

    +
      +
    • تور بېكەتنىڭ زىيارەتچىسى زىيادە كۆپ بولۇپ مۇلازىمېتىردا توسۇلۇش يۈز بەرگەن بولۇشى مۇمكىنمۇ؟ سەل تۇرۇپ قايتا سىناڭ.
    • +
    • باشقا تور بېكەتلەرنىمۇ كۆرەلمەيۋاتامسىز؟ ئۈسكۈنىنىڭ تور ئۇلىنىشىنى تەكشۈرۈڭ.
    • +
    • ئۈسكۈنىڭىز ياكى تورىڭىز مۇداپىئە تام ياكى ۋاكالەتچى تەرىپىدىن قوغدىلامدۇ؟ خاتا تەڭشەكلەر تور زىيارىتىگە دەخلى قىلىدۇ.
    • +
    • يەنىلا ھەل بولمىدىمۇ؟ تور باشقۇرغۇچى ياكى ئىنتېرنېت مۇلازىمىتى تەمىنلىگۈچى بىلەن ئالاقىلىشىپ ياردەم سوراڭ.
    • +
    + ]]>
    + + + ئۇلىنالمىدى + + + +
  • تور بېكەتنى ۋاقتىنچە ئىشلەتكىلى بولمايدۇ ياكى مۇلازىمىتىر ئالدىراش بولۇشى مۇمكىن. سەل تۇرۇپ قايتا سىناڭ.
  • +
  • ئەگەر ھېچقانداق بەتنى يۈكلىيەلمىسىڭىز، ئۈسكۈنىڭىزنىڭ سانلىق مەلۇمات تورى ياكى Wi-Fi ئۇلىنىشىنى تەكشۈرۈڭ.
  • + + ]]>
    + + + كۈتۈلمىگەن مۇلازىمېتىر ئىنكاسى + + تور بېكەتنىڭ تور تەلىپىگە قايتۇرغان ئىنكاسى مۆلچەردىكىگە ئۇيغۇن ئەمەس، تور كۆرگۈچ داۋاملاشتۇرۇشقا ئامالسىز.

    + ]]>
    + + + بۇ بەت توغرا قايتا نىشانلىيالمىدى + + + توركۆرگۈ تەلەپ قىلىنغان تۈرنى ئەسلىگە كەلتۈرۈشنى توختاتتى. تور بېكەت تەلەپنى ھەرگىز تاماملىمايدىغان ئۇسۇلدا قايتا نىشانلايدۇ.

    +
      +
    • بۇ تور بېكەت تەلەپ قىلغان ساقلانمىلارنى چەكلەپ ياكى توستىڭىزمۇ؟
    • +
    • ئەگەر تور بېكەتنىڭ ساقلانما تەلىپىگە قوشۇلۇپمۇ مەسىلە ھەل بولمىسا، ئۇ بەلكىم مۇلازىمېتىر سەپلەش مەسىلىسى بولۇشى مۇمكىن ، ئۈسكۈنىڭىز ئەمەس.
    • +
    + ]]>
    + + + تورسىز ھالەت + + + تور كۆرگۈچ تورسىز ھالەتتە، تەلەپ قىلغان تۈرگە ئۇلىنالمايدۇ.

    +
      +
    • ئۈسكۈنە ئاكتىپ تورغا ئۇلانغانمۇ؟
    • +
    • “قايتا سىناش” نى بېسىپ ئاكتىپ ھالەتكە ئالماشتۇرۇڭ ۋە بەتنى قايتا يۈكلەڭ.
    • +
    + ]]>
    + + + بىخەتەرلىك سەۋەبىدىن پورت چەكلەنگەن + + + تەلەپ قىلىنغان ئادرېس تورنى زىيارەت قىلىشتىن باشقا مەقسەتلەردە ئىشلىتىلىدىغان ئېغىزنى (مەسىلەن، mozilla.org دىكى 80-نومۇرلۇق ئېغىز mozilla.org:80) ئۈچۈن بەلگىلىدى. توركۆرگۈ بىخەتەرلىكىڭىزنى قوغداش ئۈچۈن ئىلتىماسنى بىكار قىلدى.

    + ]]>
    + + + ئۇلىنىش ئەسلىگە كەلتۈرۈلدى + + + باغلىنىش ھەققىدە سۆھبەتلىشىۋاتقاندا تور ئۇلىنىشى ئۈزۈلۈپ قالدى. قايتا سىناڭ.

    +
      +
    • تور بېكەتنى ۋاقتىنچە زىيارەت قىلغىلى بولمايدۇ ياكى مۇلازىمىتىر ئالدىراش. بىر نەچچە مىنۇتتىن كېيىن قايتا سىناڭ.
    • +
    • ئەگەر سىز ھېچقانداق بەتنى يۈكلىيەلمىسىڭىز، ئۈسكۈنىڭىزنىڭ كۆچمە تورى ياكى Wi-Fi باغلىنىشىنى تەكشۈرۈپ بىقىڭ.
    • +
    + ]]>
    + + + خەتەرلىك ھۆججەت تىپى + + + +
  • توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ، ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.
  • + + ]]>
    + + + بۇزۇلغان مەزمۇن خاتالىقى + + + سىز كۆرمەكچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ چۈنكى سانلىق مەلۇمات يوللاشتا خاتالىق بايقالدى.

    +
      +
    • توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.
    • +
    + ]]>
    + + + مەزمۇن بۇزۇلغان + + سىز كۆرمەكچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ چۈنكى سانلىق مەلۇمات يوللاشتا خاتالىق بايقالدى.

    +
      +
    • توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.
    • +
    + ]]>
    + + + مەزمۇن كودلاش خاتالىقى + + سىز كۆرمەكچى بولغان بەتنى كۆرسەتكىلى بولمايدۇ ، چۈنكى ئۇ ئىناۋەتسىز ياكى قوللىمايدىغان پىرىسلاش شەكلىنى قوللىنىدۇ.

    +
      +
    • توربېكەت ئىگىلىرى بىلەن ئالاقىلىشىپ ، ئۇلارغا بۇ مەسىلىنى ئۇقتۇرۇڭ.
    • +
    + ]]>
    + + + ئادرېس تېپىلمىدى + + + توركۆرگۈ تەمىنلەنگەن ئادرېسنىڭ مۇلازىمېتىرىنى تاپالمىدى.

    +
      +
    • خاتا كىرگۈزگەن ئادرېس + ww .example.com نىڭ ئورنىغا + www .example.com.
    • نى كىرگۈزۈڭ +
    • ئەگەر ھېچقانداق بەتنى يۈكلىيەلمىسىڭىز، ئۈسكۈنىڭىزنىڭ كۆچمە تورى ياكى Wi-Fi باغلىنىشىنى تەكشۈرۈڭ.
    • +
    + ]]>
    + + + تورغا ئۇلانمىغان + + + تور ئۇلىنىشىڭىزنى تەكشۈرۈڭ ياكى بىر نەچچە مىنۇتتىن كېيىن بەتنى قايتا يۈكلەڭ. + + قايتا يۈكلە + + + ئىناۋەتسىز ئادرېس + تەمىنلەنگەن ئادرېس تونۇيالايدىغان پىچىمدا ئەمەس. ئورۇن بالداقتىن خاتالىقنى تەكشۈرۈپ ئاندىن قايتا سىناڭ.

    + ]]>
    + + ئادرېس ئىناۋەتسىز + + + +
  • تور ئادرېسى ئادەتتە مىسالدىكىدەك يېزىلىدۇ، مىسال- http://www.example.com/
  • +
  • ئىشلەتكىنىڭىزنىڭ يانتۇ سېزىق ئىكەنلىكىنى جەزىملەشتۈرۈڭ.(مىسال. /).
  • + + ]]>
    + + + نامەلۇم كېلىشىم + + ئادرېس كېلىشىمنامە (مەسىلەن، wxyz://) سىدە بەلگىلەنگەننى توركۆرگۈ تونۇمىدى، شۇڭا توركۆرگۈ تور بېكەتكە توغرا ئۇلىنالمايدۇ.

    +
      +
    • كۆپ ۋاسىتە ياكى باشقا تېكىستسىز مۇلازىمەتنى زىيارەت قىلامسىز؟ تور بېكەتنىڭ قوشۇمچە تەلىپىنى تەكشۈرۈڭ.
    • +
    • بەزى كېلىشىملەر توركۆرگۈ ئۇلارنى تونۇشتىن بۇرۇن ئۈچىنچى تەرەپ يۇمشاق دېتالى ياكى قىستۇرما تەلەپ قىلىشى مۇمكىن.
    • +
    + ]]>
    + + + ھۆججەت تېپىلمىدى + + +
  • بۇ تۈر نامى ئۆزگەرتىلگەن، ئۆچۈرۈلگەن ياكى كۆچۈرۈلگەنمۇ؟
  • +
  • ئادرېسنىڭ ئىملاسىدا ياكى چوڭ-كىچىك يېزىلىشى ياكى باشقا يېزىق خاتالىقى بارمۇ؟
  • +
  • تەلەپ قىلىنغان تۈرنى زىيارەت قىلىش ئۈچۈن يېتەرلىك ھوقۇقىڭىز بارمۇ؟
  • + + ]]>
    + + + ھۆججەتنى زىيارەت قىلىش رەت قىلىندى + + +
  • ھۆججەت يۇيۇلغان ياكى يۆتكەلگەن ۋە ياكى زىيارەت ھوقۇقى يوق بولۇشى مۇمكىن
  • + + ]]>
    + + + ۋاكالەتچى مۇلازىمېتىر ئۇلىنىشنى رەت قىلدى + + توركۆرگۈ ۋاكالەتچى مۇلازىمېتىر ئىشلىتىشكە سەپلەنگەن، ئەمما ۋاكالەتچى ئۇلىنىشنى رەت قىلدى.

    +
      +
    • توركۆرگۈنىڭ ۋاكالەتچى سەپلىمىسى توغرىمۇ؟ تەڭشەكلەرنى تەكشۈرۈپ قايتا سىناڭ.
    • +
    • ۋاكالەتچى مۇلازىمىتى بۇ توردىن ئۇلىنىشقا يول قويامدۇ؟
    • +
    • يەنىلا مەسىلە بارمۇ؟ ياردەمگە ئېرىشىش ئۈچۈن تور باشقۇرغۇچىڭىز ياكى تور مۇلازىمىتى تەمىنلىگۈچى بىلەن ئالاقىلىشىڭ.
    • +
    + ]]>
    + + + ۋاكالەتچى مۇلازىمېتىر تېپىلمىدى + + توركۆرگۈ ۋاكالەتچى مۇلازىمېتىر ئىشلىتىشكە سەپلەنگەن، ئەمما ۋاكالەتچىنى تاپالمىدى.

    +
      +
    • توركۆرگۈنىڭ ۋاكالەتچى سەپلىمىسى توغرىمۇ؟ تەڭشەكلەرنى تەكشۈرۈپ قايتا سىناڭ.
    • +
    • ئۈسكۈنە ئاكتىپ تورغا ئۇلانغانمۇ؟
    • +
    • يەنىلا مەسىلە بارمۇ؟ ياردەمگە ئېرىشىش ئۈچۈن تور باشقۇرغۇچىڭىز ياكى تور مۇلازىمىتى تەمىنلىگۈچى بىلەن ئالاقىلىشىڭ.
    • +
    + ]]>
    + + + يامان غەرەزلىك تور بېكەت مەسىلىسى + + + %1$s دىكى تور بېكەت ھۇجۇم تور بېكىتى دەپ خەۋەر قىلىنغان ۋە بىخەتەرلىك مايىللىقىڭىزغا ئاساسەن چەكلەنگەن.

    ]]>
    + + + كېرەكسىز بېكەت مەسىلىسى + + + %1$s دىكى تور بېكەتنىڭ لۈكچەك يۇمتاللارغا مۇلازىمەت قىلىدىغانلىقى مەلۇم بولدى ۋە بىخەتەرلىك مايىللىقىڭىزغا ئاساسەن توسۇۋېتىلدى.

    + ]]>
    + + + زىيانلىق تور بېكەت مەسىلىسى + + + %1$s دىكى تور بېكەتنىڭ يۇشۇرۇن زىيانلىق تور بېكەت ئىكەنلىكى مەلۇم قىلىنغانلىقى ئۈچۈن بىخەتەرلىك مايىللىقىڭىزغا ئاساسەن توسۇۋېتىلدى.

    + ]]>
    + + + ئالدامچىلىق تور بېكىتى مەسىلىسى + + + %1$s دىكى بۇ تور بېكەت ئالدامچىلىق تور بېكىتى دەپ دوكلات قىلىنغان ۋە بىخەتەرلىك مايىللىقىڭىزغا ئاساسەن چەكلەنگەن.

    ]]>
    + + + بىخەتەر تور بېكەتنى ئىشلەتكىلى بولمايدۇ + + %1$s نىڭ HTTPS نەشرىنى ئىشلەتكىلى بولمايدۇ.]]> + + HTTP تور بېتىنى داۋاملاشتۇرۇش +
    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 @@ + + + + + Спробувати знову + + + Неможливо виконати запит + + Подробиці про цю проблему чи помилку наразі недоступні.

    + ]]>
    + + + Не вдалося встановити безпечне з’єднання + + +
  • Неможливо відобразити сторінку, яку ви намагаєтесь переглянути, тому що не вдається перевірити справжність отриманих даних.
  • +
  • Будь ласка, зв’яжіться з власниками вебсайту, щоб повідомити їх про цю проблему.
  • + + ]]>
    + + + Не вдалося встановити безпечне з’єднання + + +
  • Це може бути проблемою конфігурації сервера, або, можливо, хтось намагається імітувати сервер.
  • +
  • Якщо раніше ви успішно отримували доступ до цього сервера, то це може бути тимчасовою помилкою і ви можете спробувати ще раз пізніше.
  • + + ]]>
    + + + Додатково… + + Хтось може намагатися видати себе за сайт, тому вас не слід продовжувати. +

    + + ]]>
    + + Назад (Рекомендовано) + + + Погодитись на ризик і продовжити + + + Цей вебсайт вимагає захищеного з’єднання. + + +
  • Сторінку, яку ви намагаєтеся переглянути, неможливо показати, оскільки для цього вебсайту потрібне захищене з’єднання.
  • +
  • Імовірно, проблема пов’язана з вебсайтом, і ви нічого не можете зробити, щоб усунути ваду.
  • +
  • Ви можете повідомити адміністратора вебсайту про проблему.
  • + + ]]>
    + + + Додатково… + + + %1$s має політику безпеки під назвою HTTP Strict Transport Security (HSTS), що означає, що %2$s може підключитися до нього лише безпечно. Ви не можете додати виняток для відвідування цього сайту. + ]]> + + Назад + + + З’єднання перервано + + Браузер успішно підключився, але при передачі інформації з’єднання було перервано. Будь ласка, спробуйте знову.

    +
      +
    • Сайт може бути тимчасово недоступним, або перевантаженим запитами. Спробуйте знову через кілька хвилин.
    • +
    • Якщо вам не вдається завантажити жодну сторінку, перевірте з’єднання з Інтернетом свого пристрою.
    • +
    + ]]>
    + + + Перевищено термін очікування з’єднання + + Завершився час очікування відповіді під час спроби з’єднання з сайтом.

    +
      +
    • Можливо, сайт тимчасово недоступний, або перевантажений запитами. Спробуйте знову пізніше.
    • +
    • Якщо жодна сторінка не завантажується, перевірте з’єднання свого пристрою з Інтернетом.
    • +
    • Якщо ваш комп’ютер чи мережа захищені мережевим екраном чи проксі-сервером, переконайтеся, що для браузера дозволено доступ до Інтернету.
    • +
    • Якщо проблема не зникає, проконсультуйтесь зі своїм системним адміністратором чи інтернет-провайдером.
    • +
    + ]]>
    + + + Неможливо з’єднатися + + +
  • Сайт може бути тимчасово недоступний або перевантажений. Спробуйте знову через деякий час.
  • +
  • Якщо ви не можете завантажити жодної сторінки, перевірте з’єднання вашого пристрою з мобільною або Wi-Fi мережею.
  • + + ]]>
    + + + Неочікувана відповідь сервера + + Сайт надав неочікувану відповідь на мережевий запит і браузер не може його обробити.

    + ]]>
    + + + Неналежне перенаправлення на сторінці + + Браузер припинив спроби завантажити запитаний елемент. Сайт переспрямовує запит так, що він ніколи не завершиться.

    +
      +
    • Можливо, ви вимкнули чи заблокували файли cookie для цього сайту.
    • +
    • Якщо дозвіл на прийняття файлів cookie сайту не розв’язує проблему, ймовірно, це спричинено конфігурацією сервера, а не вашим пристроєм.
    • +
    + ]]>
    + + + Автономний режим + + + Браузер працює в автономному режимі і не може зв’язатись із запитаним ресурсом.

    +
      +
    • Перевірте, чи ваш комп’ютер підключений до активної мережі.
    • +
    • Натисніть “Спробувати знову”, щоб перемкнутись в онлайновий режим і перезавантажити сторінку.
    • +
    + ]]>
    + + + Порт заборонений з міркувань безпеки + + Для запитаної адреси вказано порт (наприклад, mozilla.org:80 означає порт 80 на mozilla.org), який зазвичай не використовується для роботи з вебсайтами. Браузер скасував запит для вашої безпеки.

    + ]]>
    + + + З’єднання скинуто + + Зв’язок з сайтом був перерваний при встановленні з’єднання. Будь ласка, спробуйте знову.

    +
      +
    • Сайт може бути тимчасово недоступним, або перевантаженим запитами. Спробуйте знову через кілька хвилин.
    • +
    • Якщо вам не вдається завантажити жодну сторінку, перевірте з’єднання з Інтернетом свого пристрою.
    • +
    + + ]]>
    + + + Небезпечний тип файлу + + +
  • Будь ласка, зв’яжіться з власниками вебсайту і повідомте їх про цю проблему.
  • + + ]]>
    + + + Помилка пошкодженого вмісту + + Не вдається відобразити сторінку, яку ви намагаєтесь переглянути, тому що було виявлено помилку в передачі даних.

    +
      +
    • Будь ласка, зв’яжіться з власниками вебсайту та повідомте їх про цю проблему.
    • +
    + ]]>
    + + + Вміст пошкоджено + Не вдається відобразити сторінку, яку ви намагаєтесь переглянути, тому що було виявлено помилку в передачі даних.

    +
      +
    • Будь ласка, зв’яжіться з власниками вебсайту та повідомте їх про цю проблему.
    • +
    + ]]>
    + + + Помилка кодування вмісту + + Не вдається показати сторінку, яку ви намагаєтеся переглянути, оскільки вона використовує неправильну чи непідтримувану форму стиснення даних.

    +
      +
    • Будь ласка, повідомте власника сайту про цю проблему.
    • +
    ]]>
    + + + Адресу не знайдено + + Браузер не зміг знайти сервер за вказаною адресою.

    +
      +
    • Перевірте правильність введення адреси, наприклад, + ww.example.com замість + www.example.com.
    • +
    • Якщо вам не вдається завантажити жодну сторінку, перевірте з’єднання вашого пристрою з Інтернетом.
    • +
    + ]]>
    + + + Немає з’єднання з Інтернетом + + Перевірте з’єднання з Інтернетом або спробуйте перезавантажити сторінку через кілька хвилин. + + Перезавантажити + + + Недійсна адреса + Неможливо розпізнати формат вказаної адреси. Перевірте правильність введення адреси в панелі та спробуйте знову.

    + ]]>
    + + Недійсна адреса + + +
  • Зазвичай, веб-адреса має такий вигляд http://www.example.com/
  • +
  • Переконайтеся, що ви використовуєте дробову риску (тобто /).
  • + + ]]>
    + + + Невідомий протокол + + Адреса містить невідомий браузеру протокол, (наприклад, wxyz://), тому браузер не може встановити з’єднання з сайтом.

    +
      +
    • Можливо, ви намагаєтесь отримати доступ до мультимедіа чи інших сервісів? Перевірте сайт на наявність особливих вимог.
    • +
    • Деякі протоколи можуть потребувати додаткових сторонніх програм або плагінів, перш ніж браузер зможе з ними працювати.
    • +
    + ]]>
    + + + Файл не знайдено + +
  • Перевірте, чи файл не був перейменований, видалений чи переміщений.
  • +
  • Переконайтеся, що адреса не містить помилок введення.
  • +
  • Переконайтеся, що у вас є дозвіл на доступ до запитаного елемента.
  • + + ]]>
    + + + Доступ до файлу було заборонено + +
  • Він міг бути вилучений, переміщений або заборонено доступ до файлу.
  • + + ]]>
    + + + Проксі-сервер відмовляється приймати з’єднання + Браузер налаштовано на використання проксі-сервера, але проксі відхилив з’єднання.

    +
      +
    • Перевірте правильність конфігурації проксі та спробуйте знову.
    • +
    • Перевірте, чи дозволяє сервіс проксі з’єднання з цієї мережі.
    • +
    • Все ще є проблеми? Проконсультуйтесь зі своїм системним адміністратором чи інтернет-провайдером.
    • +
    +]]>
    + + + Проксі-сервер не знайдено + + Браузер налаштований для використання сервера проксі, але проксі не вдалося знайти.

    +
      +
    • Перевірте правильність конфігурації проксі та спробуйте знову.
    • +
    • Перевірте, чи комп’ютер підключений до активної мережі.
    • +
    • Все ще є проблеми? Проконсультуйтесь зі своїм системним адміністратором чи Інтернет-провайдером для отримання допомоги.
    • +
    + ]]>
    + + + Зловмисний сайт + + Сайт %1$s відомий як зловмисний, і тому був заблокований згідно з вашими налаштуваннями безпеки.

    + ]]>
    + + + Небажаний сайт + + Сайт %1$s відомий як такий, що розповсюджує небажане програмне забезпечення, і тому був заблокований згідно з вашими налаштуваннями безпеки.

    + ]]>
    + + + Небезпечний сайт + + + Сайт %1$s відомий, як потенційно небезпечний, і був заблокований згідно з вашими налаштуваннями безпеки.

    + ]]>
    + + + Шахрайський сайт + + Сайт %1$s відомий, як шахрайський, і був заблокований згідно з вашими налаштуваннями безпеки.

    + ]]>
    + + + Захищений сайт недоступний + + %1$s недоступна.]]> + + Продовжити на HTTP-сайті +
    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 @@ + + + + + دوبارہ کوشش کریں + + + درخواست مکمل نہیں کی جا سکتی + + + اس مسئلے کے متعلق اضافی معلومات فی الحال دستیاب نہیں ہے۔

    ]]>
    + + + قابل بھروسا کنکشن ناکام ہو گیا ہے + + + +
  • جو صفحہ آپ دیکھنا چاہ رہے ہیں، دکھایا نہیں جا سکتا کیونکہ موصول قوائف کی اصلیت کی تصدیق نہیں ہو سکی۔
  • +
  • ویب سائٹ کے مالک کو اس مسلے کا بتانے کے لیے ان سے رابطہ کریں۔
  • + + ]]>
    + + + قابل بھروسا کنکشن ناکام ہو گیا ہے + + + +
  • یہ سرور کانفیگوریشن کا کوئی مسلہ ہو سکتا ہے یا ہو سکتا ہے کی کوئی سرور کی نکل کرنے کی کوشش کر رہا ہو۔
  • +
  • اگر آپ ماضی میں اس سرور سے جڑیں ہیں تو ہو سکتا ہے خرابی کچھ وقت کے لئے ہو اور آپ کچھ دیر میں پھر سے کوشش کر سکتے ہیں۔
  • + ]]>
    + + + اعلٰی… + + ہو سکتا ہے کی کوئی سائٹ کی نقل بنا رہا ہو اور آپ کو آگے نہیں بڑھنی چاہیے۔ +

    + ]]>
    + + واپس جائیں (تجویز شدہ) + + خطرے کو قبول کریں اور جاری رکھیں + + + کنکشن خراب ہو گیا + + + براؤزر کامیابی کے ساتھ جڑ چکا ہے، لیکن معلومات کی منتقلی کے دوران کنکشن میں رکاوٹ پیدا ہوئی۔ براہ کرمپھر سے کوشش کریں۔

    +
      +
    • سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف ہو سکتی ہے۔ کچھ دیر بعد پھر سے کوشش کریں۔
    • +
    • اگر آپ کوئی بھی صفحات لوڈ نہیں کر پا رہے ہیں، تو اپنے آلے کی ڈیٹا یا وائی-فائی کنیکشن کی جانچ کریں۔
    • +
    ]]>
    + + + کنکشن ٹائم آوٹ ہو گیا ہے + + + درخواست کی گئی سائٹ نے کنکشن کی التجا کا جواب نہیں دیا اور براؤزر نے جواب کا انتظار کرنا بند کر دیا ہے۔

    +
      +
    • ہو سکتا ہے کی سرور زیادہ مانگ یا عارضی وقفے کا سامنا کر رہا ہو؟ بعد میں پھر سے کوشش کریں۔
    • +
    • کیا آپ دوسرے سائٹ بھی براؤز نہیں کر پا رہے ہیں؟ آلے کی نیٹ ورک کنکشن کی جانچ کریں۔
    • +
    • کیا آپ کا آلہ یا نیٹ ورک کسی فائروال یا پراکسی کے ذریعے محفوظ ہے؟غلط سیٹنگ ویب براؤز کرنے میں مداخلت کر سکتی ہے۔
    • +
    • ابھی بھی مسئلہ درپیش ہے؟ مدد کے لئے اپنے نیٹ ورک کے نگراں یا انٹرنیٹ مہیا کار سے رابطہ کریں۔
    • +
    ]]>
    + + + جڑنے میں ناکام رہا + + + +
  • سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف ہو سکتی ہے۔ کچھ دیر بعد پھر سے کوشش کریں۔
  • +
  • اگر آپ کوئی بھی صفحات لوڈ نہیں کر پا رہے ہیں، تو اپنے آلے کی ڈیٹا یا وائی-فائی کنکشن کی جانچ کریں۔
  • + ]]>
    + + + سرور کی طرف سے غیر متوقع جواب + + + سائٹ نے نیٹ ورک فرمائش کا ایک غیر متوقع طریقے سے جواب دیا اور براؤزر جاری نہیں رہ سکتا۔

    + ]]>
    + + + صفحہ ٹھیک طرح ری ڈائریکٹ نہیں ہو رہا + + + براؤزر نے درخواست کی گئی چیز کو پانے کی کوشش روک کر دی ہے۔ سائٹ اس درخواست کو اس طرح سے دوسری سمت بھیج رہا ہے جو کبھی مکمل نہیں ہوگی۔

    +
      +
    • کیا اپ نے سائٹ کے لئے ضروری کوکیز کو معزور یا نا اہل کی ہے؟
    • +
    • اگر سائٹ کی کوکیز کو قبول کرنے سے بھی مسئلہ ٹھیک نہیں ہوتا، تو ممکن ہے کہ یہ سرور کنفگریشن کا کوئی مسئلہ ہو نا کہ آپکے آلے کا۔
    • +
    ]]>
    + + + آف لائن موڈ + + + براؤزر اپنے آف لائن موڈ میں عمل کاری ہے اور درخواست کی گئی چیز سے نہیں جڑ سکتا۔ +

    +
      +
    • کیا آلہ ایک زیر عمل نیٹ ورک سے جڑا ہوا ہے؟
    • +
    • آن لائن موڈ میں لانے کے لئے “دوبارہ کوشش کریں” دبایں اور صفہ کو فر سے لوڈ کریں۔
    • +
    ]]>
    + + + سلامتی وجوہات کے لیے دہانہ محدود کر دیا گیا + + + درخواست کیا گیا پتا ایک خاص پورٹ (مثلاً، mozilla.org پر پورٹ 80 کے لئے mozilla.org:80) + کو مخصوص کرتا ہے جو عام طور پر ویب براؤز کرنے کے علاوہ کچھ اور مقاصد کے لئے استعمال ہوتا ہے۔ آپ کی حفاظت اور سلامتی کے لئے براؤزر نے درخواست کو منسوخ کر دی ہے۔

    ]]>
    + + + کنکشن ریسٹ ہو گیا + + + کنکشن کو قائم کرنے کے دوران نیٹ ورک لنک میں رکاوٹ پیدا ہوئی۔ براہ کرم دوبارہ کوشش کریں۔

    +
      +
    • سائٹ عارضی طور پر غیر دستیاب یا کافی مصروف ہو سکتی ہے۔ کچھ دیر بعد دوبارہ کوشش کریں۔
    • +
    • اگر آپ کوئی بھی صفحات لوڈ نہیں کر پا رہے ہیں، تو اپنے آلے کی ڈیٹا یا وائی-فائی کنکشن کی جانچ کریں۔
    • +
    ]]>
    + + + غیر محفوظ فائل قسم + + + +
  • ویب سائٹ کے مالکین کو اس مسلے کا بتانے کے لیے رابطہ کریں۔
  • + ]]>
    + + + خراب مواد نقص + + + آپ جس صفہ کو دیکھنا چاہتے ہیں اسے دکھایا نہیں جا سکتا کیونکہ ڈیٹا ترسیل میں ایک خرابی کی جانکاری ملی ہے۔

    +
      +
    • براہ کرم ویب سائٹ کے مالکان کو اس مسلے کا بتانے کے لیے ان سے رابطہ کریں۔
    • +
    ]]>
    + + + مواد تباہ ہو گیا + + آپ جس صفہ کو دیکھنا چاہتے ہیں اسے دکھایا نہیں جا سکتا کیونکہ ڈیٹا ترسیل میں ایک خرابی کی جانکاری ملی ہے۔

    +
      +
    • براہ کرم ویب سائٹ کے مالکان کو اس مسلے کا بتانے کے لیے ان سے رابطہ کریں۔
    • +
    ]]>
    + + + مواد انکوڈنگ نقص + + آپ جس صفہ کو دیکھنا چاہتے ہیں اسے دکھایا نہیں جا سکتا کیونکہ یہ ایک غیر موثر یا غیر معاون قسم کی کمپریشن کا استعمال کرتا ہے۔

    +
      +
    • براہ کرم ویب سائٹ کے مالکان کو اس مسلے کا بتانے کے لیے ان سے رابطہ کریں۔
    • +
    ]]>
    + + + پتہ نہیں ملا + + + کوئی انٹرنیٹ کنکشن نہیں + + اپنے نیٹ ورک کنکشن کی پڑتال کریں یا چند لمحات بعد دوبارہ لوڈ کرنے کی کوشش کریں۔ + + پھر لوڈ کریں + + + ناجائز پتہ + مہیا کیا گیا پتہ ایک پہچان کردہ وضع میں نہیں ہے۔ برائے مہربانى غلطیوں کے لئے محل وقوع بار کی پڑتال کریں اور دوبارہ کوشش کریں۔

    ]]>
    + + پتہ جائز نہیں ہے + + + +
  • ویب پتے عمومی طور پر ایسے ہی http://www.example.com/ لکھے جاتے ہیں
  • +
  • یقینی بنائیں کہ آپ فارورڈ سلیشیز (جیسے /) کا استعمال کر رہے ہیں۔
  • + ]]>
    + + + نامعلوم پروٹوکول + + + مسل نہیں ملی + + + فائل تک رسائی مسترد کردی گئی ہے + + + پراکسی سرور نے کنکشن سے انکار کردیا + + + پراکسی سرور نہیں ملا + + + میلویئر سائٹ مسئلہ + + + ناپسندیدہ سائٹ مسئلہ + + + نقصان دہ سائٹ کا مسئلہ + + + فریب دہ سائٹ کا مسئلہ + +
    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 @@ + + + + + Qayta urining + + + Soʻrov bajarilmadi + + + Bu muammo yoki xato haqida qoʻshimcha maʼlumot hozirda mavjud emas.

    ]]>
    + + + Xavfsiz ulanmadi + + + +
  • Siz koʻrmoqchi boʻlgan sahifani koʻrsatib boʻlmaydi, chunki olingan maʼlumotlarning haqiqiyligini tekshirib boʻlmadi.
  • +
  • Iltimos, ushbu muammo haqida veb-sayt egalariga murojaat qiling.
  • + + ]]>
    + + + Xavfsiz ulanish amalga oshmadi + + +
  • Bu server konfiguratsiyasida muammo boʻlishi mumkin yoki kimdir oʻzini serverga oʻxshatmoqchi boʻlgan boʻlishi mumkin.
  • +
  • Agar ilgari ushbu serverga muvaffaqiyatli ulangan boʻlsangiz, xato vaqtinchalik boʻlishi mumkin va keyinroq qayta urinib koʻrishingiz mumkin.
  • + + ]]>
    + + + Qoʻshimcha… + + + Kimdir saytdan notoʻgʻri maqsadda foydalanmoqchi boʻlishi mumkin va siz davom ettirmasligingiz kerak. +

    + + ]]>
    + + Orqaga qaytish (Tavsiya etiladi) + + + Xavfni oʻz zimmamga olaman va davom etaman + + + Bu sayt xavfsiz ulanishni talab qiladi. + + +
  • Siz ochmoqchi boʻlgan sayt ochilmaydi, chunki bu sayt xavfsiz ulanishni talab qiladi.
  • +
  • Muammo katta ehtimollik bilan saytda. Uni siz tuzata olmaysiz.
  • +
  • Muammo haqida sayt administratoriga xabar berishingiz mumkin.
  • + + ]]>
    + + + Qoʻshimcha… + + + %1$s HTTP Strict Transport Security (HSTS) deb nomlangan xavfsizlik siyosatiga ega, yaʼni %2$s unga faqat xavfsiz ulanishi mumkin. Bu saytga tashrif buyurish uchun istisno qoʻsha olmaysiz. + ]]> + + Orqaga qaytish + + + Aloqa uzilib qoldi + + Brauzer muvaffaqiyatli bogʻlandi, lekin maʼlumot uzatish vaqtida ulanish toʻxtatildi. Iltimos, qayta urinib koʻring.

    +
      +
    • Sayt vaqtincha ishlamay qolishi yoki band boʻlishi mumkin. Oz vaqtdan soʻng yana urinib koʻring.
    • +
    • Agar siz biron bir sahifani yuklay olmasangiz, qurilmangiz maʼlumotlarini yoki Wi-Fi ulanishini tekshiring.
    • +
    + ]]>
    + + + Ulanish muddati tugadi + + + Soʻralgan sayt ulanish soʻroviga javob bermadi va brauzer javob uchun kutishni toʻxtatdi.

    +
      +
    • Server band yoki vaqtincha ishlamay qolishi mumkinmi? Keyinroq yana urinib koʻring.
    • +
    • Boshqa saytlarni koʻrib chiqa olmaysizmi? Qurilmaning tarmoqqa ulanishini tekshiring.
    • +
    • Qurilmangiz yoki tarmogʻingiz xavfsizlik devori yoki ishonchli server bilan himoyalanganmi? Notoʻgʻri sozlamalar veb-brauzerga xalaqit berishi mumkin.
    • +
    • Hali ham muammo bormi? Yordam uchun tarmoq administratori yoki Internet-provayderingiz bilan maslahatlashing.
    • +
    + ]]>
    + + + Ulana olmadi + + + +
  • Sayt vaqtincha ishlamay qolgan yoki juda band boʻlishi mumkin. Birozdan soʻng yana urinib koʻring.
  • +
  • Agar siz biron bir sahifani yuklay olmasangiz, qurilmangiz maʼlumotlarini yoki Wi-Fi ulanishini tekshiring
  • + + ]]>
    + + + Serverdan kutilmagan javob + + Sayt tarmoq soʻroviga kutilmagan usulda javob berdi, shuning uchun brauzer ishni davom ettira olmaydi.

    + ]]>
    + + + Sahifa toʻgʻri yoʻnaltirilmagan + + Brauzer soʻralgan elementni qayta tiklashga urinishni toʻxtatdi. Sayt soʻrovni hech qachon bajarilmaydigan tarzda yoʻnaltirmoqda.

    +
      +
    • Ushbu sayt talab qiladigan kukilarni oʻchirib qoʻydingizmi yoki blokladingizmi?
    • +
    • Agar sayt kukilarini qabul qilish muammoni hal qilmasa, ehtimol bu sizning qurilmangiz emas, balki server konfiguratsiyasi muammosi.
    • +
    + ]]>
    + + + Oflayn rejim + + Brauzer oflayn rejimida ishlaydi va soʻralgan elementga ulana olmaydi.

    +
      +
    • Qurilma faol tarmoqqa ulanganmi?
    • +
    • Onlayn rejimga oʻtish va sahifani qayta yuklash uchun "Qayta urinib koʻrish" tugmasini bosing.
    • +
    + ]]>
    + + + Port xavfsizlik sabablariga koʻra cheklangan + + Soʻralgan manzil uchun (masalan, mozilla.orgda 80 porti uchun mozilla.org:80) internetni koʻrishdan boshqa maqsadlar uchun foydalaniladigan port koʻrsatilgan. Brauzer himoya va xavfsizligingiz uchun soʻrovni bekor qildi.

    +]]>
    + + + Ulanish uzilib qolgan + + Tarmoq havolasi bilan ulanish aloqasi uzilib qoldi. Iltimos, qayta urinib koʻring.

    +
      +
    • Sayt vaqtincha ishlamay qolishi yoki juda band boʻlishi mumkin. Birozdan soʻng qayta urinib koʻring
    • +
    • Agar siz biron bir sahifafni yuklay olmasangiz, qurilmangiz maʼlumotlarini yoki Wi-Fi ulanishini tekshiring.
    • +
    + ]]>
    + + + Xavfli fayl turi + + +
  • Bu muammo haqida xabar berish uchun sayt egalari bilan bogʻlaning.
  • + ]]>
    + + + Tarkibi buzilgan + + Koʻrish uchun urinilayotgan sayt ochilmasligi mumkin, chunki maʼlumotlarni uzatishda xatolik aniqlandi.

    +
      +
    • Bu muammo haqida xabar berish uchun sayt egalari bilan bogʻlaning.
    • +
    ]]>
    + + + Tarkib buzildi + Siz koʻrmoqchi bo‘lgan sahifani koʻrsatib boʻlmaydi, chunki maʼlumotlarni uzatishda xatolik aniqlandi.

    +
      +
    • Bu muammo haqida xabar berish uchun sayt egasi bilan bogʻlaning.
    • +
    ]]>
    + + + Xato siqish algoritmi + Siz koʻrmoqchi boʻlgan sahifani yuklab boʻlmaydi, chunki u xato yoki mos kelmaydigan siqish usulidan foydalanadi.

    +
      +
    • Bu muammo haqida xabar berish uchun veb sayt egalari bilan bogʻlaning.
    • +
    ]]>
    + + + Manzil topilmadi + + + Brauzer kiritilgan manzil uchun hosting serverini topa olmadi.

    +
      +
    • Kiritilgan manzilni tekshiring, xato yoʻqmi? Masalan, + ww.example.com kiritilgan boʻlishi mumkin. + Aslida, www.example.com.
    • kiritilishi lozim. +
    • Agar birorta ham sahifa yuklanmasa, mobil internetni yoki Wi-Fi ulanishini tekshiring.
    • +
    ]]>
    + + + Internetga ulanmagan + + Tarmoqqa ulanishni tekshiring yoki bir necha daqiqadan soʻng sahifani yangilang. + + Qayta yuklash + + + Xato manzil + Kiritilgan manzil aniqlab boʻlmaydigan formatda. Manzilni xatosiz kiritilganligini tekshiring va qaytadan urinib koʻring.

    ]]>
    + + Manzil xato + + +
  • Sayt manzillari odatda quyidagicha yoziladi: http://www.example.com/
  • +
  • Yoʻnaltiruvchi qiya chiziq (slesh) toʻgʻri kiritilganligini tekshiring (masalan, /).
  • + ]]>
    + + + Nomaʼlum protokol + + Manzil brauzerga notanish boʻlgan protokol (masalan, wxyz://) bilan boshlanmoqda, shuning uchun brauzer saytga ulana olmaydi.

    +
      +
    • Multimedia yoki matn boʻlmagan xizmatlari mavjud saytga ulanishga urinyapsizmi? Saytning qoʻshimcha dasturiy taʼminot boʻyicha talablari bilan tanishing.
    • +
    • Ayrim protokollar brauzer tanishi uchun begona dastur yoki plaginlarni talab qilishi mumkin.
    • +
    ]]>
    + + + Fayl topilmadi + +
  • Faylning nomi oʻzgarmadimi, koʻchirilgani yoki oʻchirilgani yoʻqmi?
  • +
  • Manzilni yozishda imlo yoki katta-kichik harfda yozishda xato qilmadingizmi?
  • +
  • Soʻralgan fayldan foydalanish uchun sizga yetarlicha huquq berilganmi?
  • + ]]>
    + + + Fayldan foydalanishga ruxsat berilmadi + + +
  • U oʻchirilgan, koʻchirilgan yoki ruxsat olib tashlangan boʻlishi mumkin.
  • + ]]>
    + + + Proksi server ulanishni rad etdi + Brauzer proksi serverdan foydalanish uchun sozlangan, ammo brauzer ulanishni rad qildi.

    +
      +
    • Brauzer proksi sozlamalari toʻgʻrimi? Tekshiring va qaytadan urinib koʻring.
    • +
    • Proksi xizmatida shu tarmoqdan ulanishga ruxsat berilganmi?
    • +
    • Hali ham muammo mavjudmi? Maslahat olish uchun internet provayderingiz xodimlari yoki tarmoq administratori bilan bogʻlaning.
    • +
    ]]>
    + + + Proksi server topilmadi + + Brauzer proksi-serverdan foydalanish uchun sozlangan, ammo proksi-server topilmadi.

    +
      +
    • Brauzerning proksi-server konfiguratsiyasi toʻgʻrimi? Sozlamalarni tekshiring va qayta urinib koʻring.
    • +
    • Qurilma faol tarmoqqa ulanganmi?
    • +
    • Hali ham muammo mavjudmi? Maslahat olish uchun internet provayderingiz xodimlari yoki tarmoq administratorlari bilan bogʻlaning.
    • +
    + ]]>
    + + + “Zararli sayt” muammosi + + sayti %1$s hujum qiluvchi sayt sifatida maʼlumot berilgan va xavfsizlik moslamalaringiz asosida u bloklandi.

    ]]>
    + + + Kutilmagan sayt muammosi + + Saytida%1$s zararli dastur bor deb xabar berilgan, shuning uchun xavfsizlik moslamalaringizga asoslanib bloklandi.

    + ]]>
    + + + Zararli sayt muammosi + + sayti%1$s jiddiy xavfli sayt sifatida maʼlumot berilgan va xavfsizlik parametrlariga asoslanib bloklangan.

    + ]]>
    + + + Yolgʻon sayt muammosi + + %1$s veb sahifasi aldamchi sayt ekanligi xabar etilgan va sizning xavfsizlik sozlamalaringizga asoslangan holda bloklandi.

    + ]]>
    + + + Xavfsiz sayt mavjud emas + + %1$s HTTPS versiyasi mavjud emas.]]> + + HTTP saytga kirishda davom etish +
    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 @@ + + + + + Riprova + + + Inposibiƚe conpletare ƚa dimanda + + Dèso no xe mìa disponibili altre informasioni che ƚe riguarda sto problema o erore.

    ]]>
    + + + Ƚa conesion sicura no ƚa xe riusìa + + + +
  • Ƚ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.
  • +
  • Contatare el responsabiƚe de el sito web par informarƚo de’l problema.
  • + ]]>
    + + + Ƚa conesion sicura no ƚa xe riusìa + + + +
  • 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.
  • +
  • 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.
  • + ]]>
    + + + Avansate… + + + Poderìa tratarse de on tentativo de sostituirse a’l sito originaƚe. Xe sconsijà proseguire. +

    + ]]>
    + + Torna indrio (racomandà) + + Aceta el ris-cio e continua + + + Par ’sto sito serve na conesion segura. + + + +
  • Ł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.
  • +
  • Ze probàbiłe che’l problema el rive da’l sito e che no te posi far gnente par resòlvarło.
  • +
  • Te połi segnałarghe el problema a l’aministrador de’l sito.
  • + +]]>
    + + + Vansà… + + + %1$s el gà na połìtega de seguresa ciamada HTTP Strict Transport Security (HSTS), che vołe dir che %2$s el połe conétarse soło en magnera segura. No te połi zontar nissuna ecesion par vardar ’sto sito. + ]]> + + Indrìo + + + Ƚa conesion ƚa xe stà interota + + + El browser el xe stà coneso coretamente, però ƚa conesion ƚa xe stà interota durante el trasferimento de ƚe informasioni. Riprovare.

    +
      +
    • El sito el poderìa èsare tenporaneamente no disponibiƚe o masa ocupà. Riprovare tra cualche istante.
    • +
    • Se no se rièse a cargare nisuna pàgina, controƚare i dati de el dispoxitivo o ƚa conesion Wi-Fi.
    • +
    ]]>
    + + + El tenpo par ƚa conesion el xe exaurìo + + + El sito richiesto no ga risposto a na richiesta de conesion e el browser el ga smesso de spetare na risposta.

    +
      +
    • Xe posibiƚe che el server el sia sogeto a n’eƚevata dimanda o n’interusion tenporanea. Riprovare pì tardi.
    • +
    • Se riese a navigare so altri siti? Controƚare ƚa conesion de rete de el computer.
    • +
    • 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.
    • +
    • Se riscontra ancora problemi? Consultare l’amministradore de rete o el provider Internet par risevere asistensa.
    • +
    ]]>
    + + + Conesion no riusìa + + + +
  • El sito el poderìa èsare tenporaneamente no disponibiƚe o masa ocupà. Riprovare tra poco.
  • +
  • Se non si riese a cargare nisuna pàgina, controƚare i dati de el dispoxitivo o ƚa conesion Wi-Fi.
  • + ]]>
    + + + Resposta inaspetà de el server + + El sito el ga resposto a ƚa dimanda de rete en modo no previsto, cuindi el browser no el poƚe continuare.

    ]]>
    + + + Ƚa pàgina no ƚa reinderisa en modo coreto + + + 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à.

    +
      +
    • Xe sta dixabiƚità o blocà i cookie dimandà da sto sito?
    • +
    • 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.
    • +
    ]]>
    + + + No en ƚinea + + + El browser el xe en modaƚità no en ƚinea e no xe posibiƚe conetarse co’l eƚemento dimandà.

    +
      +
    • El computer xeƚo coƚegà a na rete ativa?
    • +
    • Seƚesionare “Riprova” par pasare a ƚa modaƚità en ƚinea e ricargare ƚa pàgina.
    • +
    ]]>
    + + + Porta blocà par motivi de sicuresa + + + L’indiriso dimandà spesificava na porta (par es. Mozilla.org:80 par ƚa porta 80 su mozilla.org) normalmente utilixà par scopi diversi da ƚa navigasion so’l web. El browser el ga anuƚà ƚa dimandà par garantire ƚa protesion e ƚa sicuresa de’l utente.

    ]]>
    + + + Ƚa conesion ƚa xe stà anuƚà + + + El coƚegamento de rete el xe stà interroto durante ƚa negosiasion de na conesion. Riprovare.

    +
      +
    • El sito el poderìa èsare tenporaneamente no disponibiƚe o masa ocupà. Riprovare tra cualche istante.
    • +
    • Se no se riese a cargare nisuna pàgina, controƚare i dati de el dispoxitivo o ƚa conesion Wi-Fi.
    • +
    ]]>
    + + + Tipo de file no sicuro + + + +
  • Contatare el proprietario de el sito web par informarƚo de el problema.
  • + + ]]>
    + + + Erore: contenudo danegià + + + Ƚa pàgina che se sta sercando de vixualixare no ƚa pole èsare mostrà par colpa de on erore de trasmision dati.

    +
      +
    • Contatare l’aministradore de’l sito par segnaƚare el problema.
    • +
    ]]>
    + + + Aresto anomaƚo de el contenudo + + Ƚa pàgina che se sta sercando de vixualixare no ƚa poƚe èsare mostrà par colpa de on erore de trasmision dati.

    +
      +
    • Contatare l’aministradore de’l sito par segnaƚare el problema.
    • +
    ]]>
    + + + Erore de codifega de el contenudo + + Ƚ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à.

    +
      +
    • Contatare el paron de’l sito web par informarƚo de el problema.
    • +
    ]]>
    + + + Indiriso no catà + + + El browser no el xe riusìo a catare el server host par el indiriso fornìo.

    +
      +
    • Verificare se l’indiriso contien erori de batitura de el tipo + ww.example.com invese de + www.example.com
    • +
    • Se no xe posibiƚe cargare nisuna pàgina, controƚare ƚa conesione dati o Wi-Fi de el dispoxitivo.
    • +
    + ]]>
    + + + Nisuna conesion a Internet + + Verifega ƚa to conesion de rete o prova a recargare ƚa pàgina tra cualche istante. + + Recarga + + + Indiriso mìa vaƚido + 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.

    + ]]>
    + + El indiriso no el xe vaƚido + + + +
  • I indirisi internet de soƚito i se scìve ne ƚa forma http://www.example.com/
  • +
  • Verifegare se xe drio utilixare ƚe bare corete (a exempio /).
  • + + ]]>
    + + + Protocoƚo no conosùo + + El indiriso dimanda on protocoƚo (a ex. wxyz://) che el browser no riconose, indi non poƚe coƚegarse coretamente a el sito.

    +
      +
    • Se stà asedendo a servisi multimediali o no testuaƚi? Verificare so el sito i recuisiti necesari.
    • +
    • Alcuni protocoli i dimanda software esterni o plugin en modo che el browser li possa riconosere.
    • +
    ]]>
    + + + File no catà + + +
  • L’ogeto el poderìa èsare stà rinominà, rimoso o spostà.
  • +
  • Poderìa eserghe on erore de ortografia ne el indiriso.
  • +
  • Se gà i parmesi par asedere a el ogeto specificà?
  • + + ]]>
    + + + Aceso a el file no consentìo + + +
  • El file el poderìa èsare stà spostà o scanceƚà opure i parmesi so el file i poderìa inpedirne l’aceso.
  • + + ]]>
    + + + Conesion rifiutà da el server proxy + + El browser el xe configurà par utilixare on server proxy, ma el proxy el ga rifiutà na conesion.

    +
      +
    • Ƚa configurasione proxy de el browser ƚa xe coreta? Controƚare ƚe inpostasioni e riprovare.
    • +
    • el servisio proxy el parmete ƚe conesioni da sta rete?
    • +
    • Se riscontra ancora problemi? Consultare ’l aministradore de rete o el provider Internet par risevere asistensa.
    • +
    ]]>
    + + + Inposibiƚe contatare el server proxy + + El browser el xe configurà par uxare on server proxy, ma el proxy no el xe stà riƚevà.

    +
      +
    • Ƚa configurasion proxy de el browser ƚa xe coreta? Controƚare ƚe inpostasioni e riprovare.
    • +
    • El computer el xe coƚegà a na rete funsionante?
    • +
    • Se riscontra ancora problemi? Consultare l’aministradore de rete o el provider Internet par risevere asistensa.
    • +
    ]]>
    + + + Problema de sito co malware + + + 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.

    ]]>
    + + + Problema de sito no desiderà + + + 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.

    ]]>
    + + + Problema de sito pericoƚoso + + + 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.

    + ]]>
    + + + Problema co sito inganevoƚe + + 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.

    ]]>
    + + + Sito seguro mìa disponìbiłe + + %1$s no ła ze mìa disponìbiłe.]]> + + Và vanti so’l sito HTTP +
    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 @@ + + + + + Thử lại + + + Không thể hoàn tất yêu cầu + + Hiện không có thông tin bổ sung về sự cố hay lỗi này.

    ]]>
    + + + Kết nối bảo mật đã thất bại + + +
  • 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.
  • +
  • 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ể.
  • + + ]]>
    + + + Kết nối bảo mật đã thất bại + + +
  • Đâ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.
  • +
  • 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.
  • + + ]]>
    + + + Nâng cao… + + Ai đó có thể đang cố gắng mạo danh trang web và bạn không nên tiếp tục. +

    + + ]]>
    + + Quay lại (Khuyến nghị) + + Chấp nhận rủi ro và tiếp tục + + + Trang web này yêu cầu kết nối an toàn. + + +
  • 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.
  • +
  • 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ó.
  • +
  • Bạn có thể thông báo cho quản trị viên của trang web về sự cố.
  • + + ]]>
    + + + Nâng cao… + + + %1$s 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à %2$s 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. + ]]> + + Quay lại + + + Kết nối đã bị gián đoạn + + 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.

    +
      +
    • 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.
    • +
    • 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.
    • +
    + ]]>
    + + + Kết nối đã quá hạn + + 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.

    +
      +
    • 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.
    • +
    • 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.
    • +
    • 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.
    • +
    • 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ợ.
    • +
    + ]]>
    + + + Không thể kết nối + + + +
  • Trang web có thể tạm thời không có hoặc quá tải. Hãy thử lại sau.
  • +
  • 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.
  • + + ]]>
    + + + Phản hồi không mong muốn từ máy chủ + + 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.

    + ]]>
    + + + Trang này chuyển hướng không đúng cách + + + 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.

    +
      +
    • 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?
    • +
    • 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 đề .
    • +
    + ]]>
    + + + Chế độ ngoại tuyến + + + 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.

    +
      +
    • Máy tính có được kết nối với mạng đang hoạt động không?
    • +
    • Nhấp “Thử lại” để chuyển sang chế độ trực tuyến và tải lại trang.
    • +
    + ]]>
    + + + Port bị hạn chế vì lý do an ninh + + Địa chỉ được yêu cầu chỉ định một cổng (ví dụ: mozilla.org:80 cho port 80 trên mozilla.org) thường được sử dụng cho những mục đích khác 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.

    + ]]>
    + + + Kết nối đã được thiết lập lại + + 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.

    +
      +
    • Trang web có thể tạm thời không có hoặc quá bận rộn. Hãy thử lại sau.
    • +
    • 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.
    • +
    + ]]>
    + + + Loại tập tin không an toàn + + +
  • 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.
  • + + ]]>
    + + + Lỗi nội dung bị hỏng + + 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.

    +
      +
    • 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.
    • +
    + ]]>
    + + + Nội dung bị lỗi + 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.

    +
      +
    • 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.
    • +
    + ]]>
    + + + Lỗi mã hóa nội dung + 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ợ.

    +
      +
    • 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.
    • +
    + ]]>
    + + + Không tìm thấy địa chỉ + + Trình duyệt không thể tìm thấy máy chủ lưu trữ cho địa chỉ được cung cấp.

    +
      +
    • Kiểm tra địa chỉ cho các lỗi đánh máy như + ww.example.com thay cho + www.example.com.
    • +
    • 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.
    • +
    + ]]>
    + + + Không có kết nối mạng + + 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. + + Tải lại + + + Địa chỉ không hợp lệ + Đị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.

    + ]]>
    + + Địa chỉ không hợp lệ + + +
  • Địa chỉ web thường được viết như http://www.example.com/
  • +
  • Đảm bảo rằng bạn sử dụng dấu gạch chéo về phía trước (như /).
  • + + ]]>
    + + + Giao thức không xác định + Địa chỉ chỉ định một giao thức (e.g., wxyz://) 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.

    +
      +
    • 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.
    • +
    • 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.
    • +
    + ]]>
    + + + Không tìm thấy tập tin + + +
  • Mục này đã được đổi tên, loại bỏ hoặc di chuyển không?
  • +
  • Có lỗi chính tả, viết hoa hoặc lỗi đánh máy khác trong địa chỉ không?
  • +
  • Bạn có đủ quyền truy cập vào mục được yêu cầu không?
  • + + ]]>
    + + + Quyền truy cập tập tin đã bị từ chối + + +
  • 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.
  • + + ]]>
    + + + Máy chủ proxy từ chối kết nối + + 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.

    +
      +
    • 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.
    • +
    • Dịch vụ proxy có cho phép kết nối từ mạng này không?
    • +
    • 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ợ.
    • +
    + ]]>
    + + + Không tìm thấy máy chủ proxy + + 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.

    +
      +
    • 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.
    • +
    • Máy tính có được kết nối với mạng đang hoạt động không?
    • +
    • 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ợ.
    • +
    + ]]>
    + + + Sự cố với trang web có mã độc + + + 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.

    + ]]>
    + + + Sự cố với trang web không mong muốn + + + 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.

    + ]]>
    + + + Sự cố trang web gây hại + + + 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.

    + ]]>
    + + + Sự cố với trang web lừa đảo + + 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.

    +    ]]>
    + + + Trang web an toàn không khả dụng + + %1$s không khả dụng.]]> + + Tiếp tục đến trang web HTTP +
    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 @@ + + + + Gbìyànjú lẹ́ẹ̀kan sí i + + + A kò lè parí ìbéèrè rẹ + + + Àfikún ìfitónilétí nípa ìsòro tàbí àsìṣe yìí kò sí lọ́wọ́ lọ́wọ́ báyìí

    + ]]>
    + + + Ìsomọ́ra Onífọ̀kànbalẹ̀ Kùnà + + + +
  • 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ẹ̀.
  • +
  • Jọ̀wọ́ kàn sí ẹni tí ó ni ìkànnì náà láti fi ìsòro yìí tó wọn létí.
  • + + ]]>
    + + + Ìsomọ́ra onífọkànbalẹ̀ kùnà + + + +
  • Ìsòro yìí lè jẹ́ ìfisáfà ṣiṣẹ́ tàbí kí ẹlòmíràn fẹ́ gbáwọ̀ sáfà wọ̀.
  • +
  • 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á .
  • + + ]]>
    + + + Ìjìnlẹ̀… + + Ẹnìkan le máa gbìyànjú àti gbáwọ̀ ìkànnì náà wọ̀, má tẹ̀síwájú. +

    + + ]]>
    + + + Padà Sẹ́yìn (Ìmọ̀ràn tó dára) + + Gba Ewu náà kí o sì tẹ̀síwájú + + + Ìkọ̀nì yìí nílò ìsopọ̀ tó pamọ́. + + +
  • O kò lè rí abala tí ò ń gbìyànjú láti ṣí yìí nítorí pé ìkànní yìí nílò ààbò.
  • Ìṣò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.
  • O sì lè pe àkíyèsí alámòjútó ìkànnì náà sí ìṣòro yíì.
  • ]]>
    + + + Ní ìlọsíwájú… + + + %1$s gẹ́gẹ́ bíi ètò ààbò tí a pè ní HTTP ààbò ìlọmabọ̀ tó le (HSTS), tó túmọ̀ sí pé %2$s ó 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ìí. +]]> + + Padà Sẹ́yìn + + + Akùdé bá ìsomọ́ra náà + + + ì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.

    +
      +
    • Ì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ẹ́.
    • +
    • 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ẹ.
    • +
    + ]]>
    + + + Àkókò ìsomọ́ ti kọjá + + + Ì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ì.

    +
      +
    • Ǹ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á.
    • +
    • Ǹjẹ́ o ní ìṣòro àti sàwárí àwọn ìkànnì mìíràn? Yẹ ohun tí o fi ṣe àsomọ́ wò.
    • +
    • Ǹ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ì.
    • +
    • 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ọ́.
    • +
    + ]]>
    + + + Kùnà láti somọ́ra + + +
  • Ì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ẹ́.
  • +
  • Bí o kò bá lè jẹ́ kí ojú-ìwé siṣẹ́, yẹ ohun èlò data rẹ wò tàbí àsomọ́ Wi-Fi.
  • + + ]]>
    + + + Èsì tí a kò retí láti ọ̀dọ̀ sáfà + + + Ì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ú.

    + ]]>
    + + + Ojú-ìwé náà kò ṣe atọ̀nà dáadáa + + ì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áé.

    +
      +
    • Ǹjẹ́ o ti ṣo ọ́ di aláìlágbára tàbí dínà àwọn adásiṣẹ́ tí ìkànnì yìí nílò?
    • +
    • 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ẹ.
    • +
    + ]]>
    + + + Ipò àìsí lórí íntánẹ́ẹ̀tì + + + Ì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.

    +
      +
    • Ǹjẹ́ ohun èlò náà wà ní sísomọ́ nẹ́tíwọ̀kì tó ń ṣiṣẹ́?
    • +
    • Tẹ “Gbìyànjú sí i” láti bọ́ sí ipo íntánẹ́ẹ̀tì, kí o si ojú-ìwé náà siṣẹ́ lẹ́ẹ̀kan sí.
    • +
    + ]]>
    + + + Ojú yìí kò ṣiṣẹ́ nítorí ààbò + + Àdírẹ́sì tí o bèèrè fún nílò ojú kan pàtó (e.g., mozilla.org:80 fún ojú 80 lórí mozilla.org) ni a máa ń sábà lò fún àwọn ìdí yàtọ̀ 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ẹ.

    + ]]>
    + + + Ìsomọ́ra di àtúntò + + + 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.

    +
      +
    • Ì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íẹ̀.
    • +
    • Bí o kò bá lè jẹ́ kí ojú ìwé kankan siṣẹ́, yẹ ohun èlò data rẹ wò tàbí ìsomọ́ Wi-Fi.
    • +
    + ]]>
    + + + Ẹ̀yà fáìlì tó léwu + + +
  • Jọ̀wọ́ kàn sí àwọn tí ó ni ìkànnì láti fi ìsòro yìí tó wọn létí.
  • + + ]]>
    + + + Àṣìṣe ìbàjẹ́ àkóónú + + + 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ṣẹ́.

    +
      +
    • Jọ̀wọ́ kan sí àwọn tí ó ni ìkànnì láti fi ìsòro yìí tó wọn létí.
    • +
    + ]]>
    + + + Àkóónú ti bàjẹ́ + 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ẹ́.

    +
      +
    • Jọ̀wọ́ kàn sí àwọn tó ni ìkànnì láti fi ìsòro yìí tó wọn létí.
    • +
    + ]]>
    + + + Àṣìṣe ìṣàrokò àkóónú + + 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í.

    +
      +
    • Jọ̀wọ́ kàn sí àwọn tí wọ́n ni ìkànnì láti fi ìsòro yìí tó wọn létí.
    • +
    + ]]>
    + + + A kò rí àdírẹ́sì + + Ìtàkùn-àgbáyé kò rí agbàlejò sáfà fún àdírẹ́sì.

    +
      +
    • yẹ àdírẹ́sì náà wò fún àṣìtẹ̀ gẹ́gẹ́ bí i + ww.example.com dípò + www.example.com.
    • +
    • Bí o kò bá lè mú kí ojú ìwé kankan ṣiṣẹ́, yẹ ohun èlò data rẹ wò tàbí ìsomọ́ Wi-Fi.
    • +
    + ]]>
    + + + Kò sí ìsomọ́ íntánẹ́ẹ̀tì + + Yẹ àsomọ́ nẹ́tiwọ̀kì rẹ wò tàbí gbìyànjú àti mú ojú-ìwé ṣiṣẹ́ lẹ́yìn ìgbà díẹ̀ + + + Tún mú un ṣiṣẹ́ + + + Àdírẹ́sì aláìṣeégbà + + À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.

    + ]]>
    + + Àdírẹ́sì náà kò ṣe é gbà + + +
  • Àdírẹ́sì ìkànnì ni a sábà máa ń kọ bí i http://www.example.com/
  • +
  • Rí i dájú pé àkámọ́ asùnsọ́wọ́-wájú ni ò ń lò (b.a. /).
  • + + ]]>
    + + + Ìlànà tí a kò mọ̀ + + Àdírẹ́sì náà sọ ìlànà kan (e.g., wxyz://) ìtàkùn-àgbáyé kò dá a mọ̀, nítorí náà àtàkùn-àgbáyé kò lè so ó mọ́ ìkànnì.

    +
      +
    • Ǹ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ù.
    • +
    • À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ọ̀.
    • +
    + ]]>
    + + + A kò rí fáìlì + + +
  • Ǹjẹ́ a lè ti pa orúkọ nǹkan náà dà, yọ ọ́ kúrò tàbí mú un lọ síbòmíràn?
  • +
  • Ǹjẹ́ àṣìṣe wa lè wà níbi sípẹ́lì, ìlo-lẹ́tà-ńlá, tàbí àṣìtẹ̀ níbi àdírẹ́sì bí?
  • +
  • Ǹjẹ́ o ní ìyọ́nda tótó fún ohun tí ò ń bèèrè?
  • + + ]]>
    + + + Ìdènà wà sí àṣe sí fáìlì náà + +
  • Ó ṣe é ṣe kí wọ́n ti yọ ọ́, gbé e tàbí àṣẹ sí fáìlì lè máa dènà àti wọlé.
  • + + ]]>
    + + + Asojú sáfà kọ asomọ́ra + + Ìtàkùn-àgbáyé ni a ṣe lanà tí ó gbọ́dọ̀ lo asojú fáfà, ṣùgbọ́n asojú náà kọ ìsomọ́ra.

    +
      +
    • Ǹjẹ́ ìfiṣiṣẹ́ asojú ìtàkùn-àgbáyé náà tọ̀nà? Yẹ ààtò wò kí o sì gbìyànjú sí i.
    • +
    • Ǹjẹ́ iṣẹ́ asojú fàyè gba ìsomọ́ra láti ọ̀dọ̀ nẹ́tíwọ̀kì yìí?
    • +
    • 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ọ́.
    • +
    + ]]>
    + + + A kò sí asokú sáfà + A ṣe ìtànkùn-àgbáyé láti ṣiṣẹ́ pẹ̀lú asojú sáfà ṣùgbọ́n a kò rí asojú.

    +
      +
    • Ǹjẹ́ ìfiṣiṣẹ́ ìtàkùn-àgbáyé tọ̀nà? Yẹ ààtò wò kí o sì gbìyànjú sí i.
    • +
    • Ǹjẹ́ ohun èlò rẹ wà ní ìsomọ́ nẹ́tíwọ̀kì tó ń ṣiṣẹ́?
    • +
    • Ǹ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ọ́.
    • +
    + ]]>
    + + + Ìṣòro ìkànnì mẹ́rọṣisẹ́-onísùtá + + + Ì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.

    + ]]>
    + + + Ìṣòro ìkànnì tí a kò fẹ́ + + Ì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.

    + ]]>
    + + + Ìṣòro ìkànnì tó léwu + + Ì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.

    + ]]>
    + + + Ìṣòro ìkànnì atànnijẹ + + + 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.

    + ]]>
    + + + Ìkànnì adánilójú kò sí + + %1$s kò sí.]]> + + Tẹ̀síwájú sí ìkànnì HTTP +
    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 @@ + + + + + 重试 + + + 无法完成请求 + + 暂无此问题或错误的详细解释信息。

    + ]]>
    + + + 安全连接失败 + + +
  • 您尝试查看的网页无法显示,因为接收到的数据的真实性无法验证。
  • +
  • 建议联系向这个网站的拥有者反馈此问题。
  • + + ]]>
    + + + 安全连接失败 + + +
  • 这可能是服务器配置的问题,或是有人尝试冒充该服务器所致。
  • +
  • 如果您之前曾成功连接该服务器,错误可能是暂时的,您可以稍后再尝试。
  • + + ]]>
    + + + 高级… + + 可能有人试图冒充该网站,您不应该继续访问。 +

    + + ]]>
    + + 返回上一页(推荐) + + 接受风险并继续 + + + 此网站要求使用安全连接。 + + + +
  • 由于此网站要求使用安全连接,您尝试查看的页面无法显示。
  • +
  • 这个问题大多与网站有关,无法通过您的操作解决。
  • +
  • 您可以向此网站的管理者反馈该问题。
  • + + ]]>
    + + + 高级… + + + %1$s 启用了被称为 HTTP 严格传输安全(HSTS)的安全策略,%2$s 只能与其建立安全连接。您无法为此网站添加例外,以访问此网站。 + ]]> + + 返回 + + + 连接被中断 + + 浏览器连接成功,但连接在传输数据时中断了。请稍后重试。

    +
      +
    • 该网站可能暂时无法使用或太忙。请稍后再试一次。
    • +
    • 如果您无法加载任何页面,请检查设备的数据或 Wi-Fi 连接。
    • +
    + ]]>
    + + + 连接超时 + + 指定的网站一直没有回应,浏览器停止了等待。

    +
      +
    • 该网站可能目前访问人数过多?请稍后重试。
    • +
    • 您是否也无法浏览其他网站?请检查您网络连接。
    • +
    • 设备或网络是否受防火墙或代理保护?请确定这些设置是否正确。
    • +
    • 仍然不行?请联系您的网络管理员或者电信运营商以寻求协助。
    • +
    + + ]]>
    + + + 无法连接 + + +
  • 该网站可能暂时无法使用或太忙。请稍后再试一次。
  • +
  • 如果您无法加载任何页面,请检查设备的数据或 Wi-Fi 连接。
  • + + ]]>
    + + + 意外的服务器响应 + + 该网站响应网络请求的方式与预期不符,浏览器无法继续。

    + ]]>
    + + + 页面未正确重定向 + + 浏览器已停止尝试获取请求的项。网站正在将请求无限循环重定向。

    +
      +
    • 您是否禁用或拦截了该网站必须的 Cookie?
    • +
    • 如果接受该网站的 Cookie 仍然不能解决问题,则很可能是该网站服务器配置的问题而不是您计算机的问题。
    • +
    + ]]>
    + + + 脱机模式 + + 浏览器当前处于脱机模式,无法连接请求的项。

    +
      +
    • 计算机是否连接了可用的网络?
    • +
    • 按“重试”切换到联机模式并重新加载此页面。
    • +
    + ]]>
    + + + 安全原因导致访问端口受限 + + 请求的网址指定的端口(例如 mozilla.org:80 表示使用 mozilla.org 的 80 端口)通常不是用于网络浏览。为了保护您的安全,浏览器已取消请求。

    + ]]>
    + + + 连接被重置 + + 协商连接时网络链路中断。请重试。

    +
      +
    • 该网站可能暂时无法使用或太忙。请稍后再试一次。
    • +
    • 如果您无法加载任何页面,请检查设备的数据或 Wi-Fi 连接。
    • +
    + ]]>
    + + + 不安全的文件类型 + + +
  • 请联系网站所有者,向其通报这一问题。
  • + + ]]>
    + + + 内容损坏错误 + + 您尝试查看的页面无法显示,因为检测到传输的数据含有错误。

    +
      +
    • 请联系网站所有者,向其通报这一问题。
    • +
    + ]]>
    + + + 内容已崩溃 + 您尝试查看的页面无法显示,因为检测到传输的数据含有错误。

    +
      +
    • 请联系网站所有者,向其通报这一问题。
    • +
    + ]]>
    + + + 内容编码错误 + 您尝试查看的页面无法显示,因为它使用了无效的或者不支持的压缩格式。

    +
      +
    • 请联系网站所有者,向其通报这一问题。
    • +
    + ]]>
    + + + 找不到网址 + + 浏览器无法找到该网址对应的服务器。

    +
      +
    • 检查网址有没有输入错误,比如把 + www.example.com 输入成了 + ww.example.com。
    • +
    • 如果您无法加载任何页面,请检查设备的数据或 Wi-Fi 连接。
    • +
    + ]]>
    + + + 无互联网连接 + + 请检查您的网络连接,或在稍后尝试重新加载页面。 + + 重新加载 + + + 不正确的网址 + 无法识别这种格式的网址。请检查网址是否有误并重试。

    + ]]>
    + + 网址无效 + + +
  • 网址的格式通常是:http://www.example.com/
  • +
  • 请确保您使用的是正斜杠(即 /)。
  • + + ]]>
    + + + 未知协议 + 浏览器无法识别该网址的协议(例如 wxyz://),因此浏览器无法连接至该网站。

    +
      +
    • 您是否在尝试访问多媒体或其他非文字服务?请检查该站点是否有其他要求。
    • +
    • 某些协议可能需要安装第三方软件或插件后浏览器才能识别。
    • +
    + ]]>
    + + + 未找到文件 + +
  • 或许该项目已经被改名、移除或者移走?
  • +
  • 网址是否有拼写错误、大小写错误或其他笔误?
  • +
  • 您是否有访问该项目的权限?
  • + + ]]>
    + + + 访问该文件被拒绝 + +
  • 文件可能已被移走、移除,或者没有访问该文件的权限。
  • + + ]]>
    + + + 代理服务器拒绝连接 + 浏览器被设置为使用代理服务器,但是该代理服务器拒绝了该连接。

    +
      +
    • 浏览器的代理服务器设置是否正确?请检查并重试。
    • +
    • 该代理服务是否允许来自此网络的连接?
    • +
    • 仍然不行?请联系您的网络管理员或者电信运营商以寻求协助。
    • +
    + ]]>
    + + + 无法找到代理服务器 + 浏览器被设置为使用代理服务器,但是无法找到该代理服务器。

    +
      +
    • 浏览器的代理服务器设置是否正确?请检查并重试。
    • +
    • 计算机是否连接了可用的网络?
    • +
    • 仍然不行?请联系您的网络管理员或者互联网服务提供商以寻求协助。
    • +
    + ]]>
    + + + 有恶意软件网站问题 + + 根据举报,位于 %1$s 的此网站有攻击行为。现已根据您的安全选项予以拦截。

    + ]]>
    + + + 有流氓软件网站问题 + + 根据举报,位于 %1$s 的此网站提供流氓软件。现已根据您的安全选项予以拦截。

    + ]]>
    + + + 有恶意网站问题 + + 根据举报,位于 %1$s 的此网站可能有攻击行为。现已根据您的安全选项予以拦截。

    + ]]>
    + + + 有欺诈网站问题 + + 根据举报,位于 %1$s 的此网站为欺诈网站。现已根据您的安全选项予以拦截。

    + ]]>
    + + + 安全网站不可用 + + %1$s 的 HTTPS 版本不可用。]]> + + 继续前往 HTTP 网站 +
    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 @@ + + + + + 重試 + + + 無法完成要求 + + 沒有關於此問題或錯誤的詳細解說資訊。

    + ]]>
    + + + 安全連線失敗 + + +
  • 因為無法驗證已接收資料的真實性,無法顯示您嘗試檢視的頁面。
  • +
  • 請向網站擁有者回報此問題。
  • + + ]]>
    + + + 安全連線失敗 + + +
  • 可能是伺服器設定問題造成,或是有人嘗試偽裝成該伺服器。
  • +
  • 若您以前可以與該伺服器正常連線,那麼這個錯誤可能只是暫時的,請稍候再試試看。
  • + + ]]>
    + + + 進階… + + 有心人士可能正在嘗試將別的網站偽裝成您想造訪的網站,不應繼續開啟。 +

    + ]]>
    + + 返回上一頁(建議) + + 接受風險並繼續 + + + 此網站要求必須使用安全連線。 + + +
  • 由於此網站必須使用安全性連線,無法顯示您嘗試檢視的頁面。
  • +
  • 這個問題最有可能是由於網站端的設定不正確,無法由您調整設定解決。
  • +
  • 可以通知網站管理員處理。
  • + ]]>
    + + + 進階… + + + %1$s 有一條稱為 HTTP Strict Transport Security (HSTS) 的安全性政策,讓 %2$s 僅能與其進行安全連線。您無法加入例外,手動排除此政策。]]> + + 回上一頁 + + + 連線中斷 + + 瀏覽器連線成功,但傳輸被中斷了;請稍候重試。

    +
      +
    • 網站可能太忙碌或暫時無法使用,請稍候再試一次。
    • +
    • 若您無法載入任何頁面,請檢查裝置的數據或 Wi-Fi 連線。
    • +
    ]]>
    + + + 網路連線逾時 + + 指定的網站一直沒有回應,瀏覽器已停止等待。

    +
      +
    • 該網站可能暫時流量過高?請稍候再試。
    • +
    • 無法瀏覽其它網站?請檢查裝置的網路連線。
    • +
    • 裝置需經過防火牆或 Proxy 才能連線?請確定這些設定是否正確。
    • +
    • 仍有其它問題?請洽詢您的網路管理員或網路業者。
    • +
    ]]>
    + + + 連線失敗 + + +
  • 可能是網站暫停服務或忙碌中?請稍候重試。
  • +
  • 若您無法載入任何頁面,請檢查裝置的數據或 Wi-Fi 連線。
  • + ]]>
    + + + 錯誤的回應 + + 網站回應錯誤,瀏覽器無法繼續。

    + ]]>
    + + + 頁面重新導向不正確 + + 瀏覽器已停止試圖取得資料。因為網站似乎在無止盡的重新導向。

    +
      +
    • 您是否關掉或封鎖此網站運作必需的 Cookie?
    • +
    • 若接受此網站的 Cookie 也無法解決此問題,通常是伺服器設定錯誤而非您的裝置的問題。
    • +
    ]]>
    + + + 離線模式 + + 瀏覽器正在離線模式,無法連線到指定位置。

    +
      +
    • 目前的網路連線正常嗎?
    • +
    • 請按下「重試」以切換到連線模式並重新載入頁面。
    • +
    + ]]>
    + + + 因安全考量禁止使用的 Port + + 網址所指定的 Port(例如 mozilla.org:80 表示使用 Port 80)通常不是給正常網站所使用的。為了安全考量,已取消對該網址的連線。

    + ]]>
    + + + 連線被重設 + + 在嘗試連線時,此網路連線被中斷。請再試一次。

    +
      +
    • 網站可能太忙碌或暫時無法使用,請稍候再試一次。
    • +
    • 若您無法載入任何頁面,請檢查裝置的數據或 Wi-Fi 連線。
    • +
    ]]>
    + + + 不安全的檔案格式 + + +
  • 請向網站擁有者回報此問題。
  • + + ]]>
    + + + 內容毀損錯誤 + + 因為在資料傳輸過程當中偵測到錯誤,無法顯示您正要檢視的頁面。

    +
      +
    • 請通知網站管理者以讓他們知道這個問題。
    • +
    + ]]>
    + + + 內容發生錯誤 + 因為在資料傳輸過程當中偵測到錯誤,無法顯示您正要檢視的頁面。

    +
      +
    • 請通知網站管理者以讓他們知道這個問題。
    • +
    + ]]>
    + + + 內容編碼錯誤 + 您嘗試檢視的頁面無法顯示,因為其中使用了無效或不支援的壓縮類型。

    +
      +
    • 請向網站擁有者回報此問題。
    • +
    + ]]>
    + + + 找不到網址 + + 瀏覽器找不到網址指定的伺服器主機。

    +
      +
    • 請檢查網址是否有打錯?例如把 + www.example.com 打成 + ww.example.com
    • +
    • 若無法載入任何網站,請檢查裝置的網路連線狀態。
    • +
    ]]>
    + + + 沒有網路連線 + + 請檢查您的網路連線是否正常,或者稍後再重新載入頁面。 + + 重新載入 + + + 不正確的網址 + 網址不正確。請檢查網址是否有誤後再重試。

    + ]]>
    + + 不正確的網址 + + +
  • 網址通常長得像 http://www.example.com/
  • +
  • 請確定您用的是斜線(例: /)。
  • + + ]]>
    + + + 未知的通訊協定 + 指定的網址使用了瀏覽器無法辨識的通訊協定(如 wxyz://),無法正確連線。

    +
      +
    • 您是要存取非文字的影音多媒體服務嗎?請檢查網站上是否有更多需求。
    • +
    • 某些通訊協定需要另外安裝其它軟體或外掛程式才能使用。
    • +
    + ]]>
    + + + 找不到檔案 + +
  • 或許此項目已被改名或刪除了?
  • +
  • 是否有拼錯字、大小寫錯誤?
  • +
  • 您有存取該項目的權限嗎?
  • + + ]]>
    + + + 對檔案的存取要求已被拒絕 + +
  • 可能是檔案被刪除、移動了,或存取權限不足。
  • + + ]]>
    + + + Proxy 伺服器拒絕連線 + 瀏覽器被設定為使用 Proxy 代理伺服器,但該伺服器拒絕連線。

    +
      +
    • 瀏覽器的 Proxy 設定正確嗎?請檢查並重試。
    • +
    • 該 Proxy 允許您從這個區域的網路連線嗎?
    • +
    • 仍有問題?請洽詢您的網路管理員或網路業者。
    • +
    ]]>
    + + + 找不到 Proxy 伺服器 + 有設定瀏覽器使用 Proxy 代理伺服器,但找不到該伺服器。

    +
      +
    • 瀏覽器的 Proxy 設定正確嗎?請檢查並重試。
    • +
    • 裝置的網路連線正常嗎?
    • +
    • 仍有問題?請洽詢您的網路管理員或網路業者。
    • +
    ]]>
    + + + 惡意軟體網站問題 + + %1$s 這個網站被回報為有害網站,已依照您的安全性偏好設定予以封鎖。

    + ]]>
    + + + 不安全網站問題 + + %1$s 這個網站被回報為提供不安全的軟體,已依照您的安全性偏好設定予以封鎖。

    + ]]>
    + + + 危險網站問題 + + %1$s 這個網站被回報為危險網站,已依照您的安全性偏好設定予以封鎖。

    + ]]>
    + + + 詐騙網站問題 + + %1$s 這個網站被回報為詐騙網站,已依照您的安全性偏好設定予以封鎖。

    + ]]>
    + + + 無法使用安全網站 + + %1$s 的 HTTPS 版本無法使用。]]> + + 繼續前往 HTTP 網站 +
    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 @@ + + + + + Try Again + + + Cannot Complete Request + + Additional information about this problem or error is currently unavailable.

    + ]]>
    + + + Secure Connection Failed + + +
  • The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.
  • +
  • Please contact the website owners to inform them of this problem.
  • + + ]]>
    + + + Secure Connection Failed + + +
  • This could be a problem with the server’s configuration, or it could be someone trying to impersonate the server.
  • +
  • If you have connected to this server successfully in the past, the error may be temporary, and you can try again later.
  • + + ]]>
    + + + Advanced… + + Someone could be trying to impersonate the site and you should not continue. +

    + + ]]>
    + + Go Back (Recommended) + + Accept the Risk and Continue + + + This website requires a secure connection. + + +
  • The page you are trying to view cannot be shown because this website requires a secure connection.
  • +
  • The issue is most likely with the website, and there is nothing you can do to resolve it.
  • +
  • You can notify the website’s administrator about the problem.
  • + + ]]>
    + + + Advanced… + + + %1$s has a security policy called HTTP Strict Transport Security (HSTS), which means that %2$s can only connect to it securely. You can’t add an exception to visit this site. + ]]> + + Go Back + + + The connection was interrupted + + The browser connected successfully, but the connection was interrupted while transferring information. Please try again.

    +
      +
    • The site could be temporarily unavailable or too busy. Try again in a few moments.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + The connection has timed out + + The requested site did not respond to a connection request and the browser has stopped waiting for a reply.

    +
      +
    • Could the server be experiencing high demand or a temporary outage? Try again later.
    • +
    • Are you unable to browse other sites? Check the device’s network connection.
    • +
    • Is your device or network protected by a firewall or proxy? Incorrect settings can interfere with Web browsing.
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    + ]]>
    + + + Unable to connect + + +
  • The site could be temporarily unavailable or too busy. Try again in a few moments.
  • +
  • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
  • + + ]]>
    + + + Unexpected response from server + + The site responded to the network request in an unexpected way and the browser cannot continue.

    + ]]>
    + + + The page isn’t redirecting properly + + The browser has stopped trying to retrieve the requested item. The site is redirecting the request in a way that will never complete.

    +
      +
    • Have you disabled or blocked cookies required by this site?
    • +
    • If accepting the site’s cookies does not resolve the problem, it is likely a server configuration issue and not your device.
    • +
    + ]]>
    + + + Offline Mode + + The browser is operating in its offline mode and cannot connect to the requested item.

    +
      +
    • Is the device connected to an active network?
    • +
    • Press “Try Again” to switch to online mode and reload the page.
    • +
    + ]]>
    + + + Port restricted for security reasons + + The requested address specified a port (e.g., mozilla.org:80 for port 80 on mozilla.org) normally used for purposes other than Web browsing. The browser has canceled the request for your protection and security.

    + ]]>
    + + + The connection was reset + + The network link was interrupted while negotiating a connection. Please try again.

    +
      +
    • The site could be temporarily unavailable or too busy. Try again in a few moments.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + Unsafe File Type + + +
  • Please contact the website owners to inform them of this problem.
  • + + ]]>
    + + + Corrupted Content Error + + The page you are trying to view cannot be shown because an error in the data transmission was detected.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Content crashed + The page you are trying to view cannot be shown because an error in the data transmission was detected.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Content Encoding Error + The page you are trying to view cannot be shown because it uses an invalid or unsupported form of compression.

    +
      +
    • Please contact the website owners to inform them of this problem.
    • +
    + ]]>
    + + + Address Not Found + + The browser could not find the host server for the provided address.

    +
      +
    • Check the address for typing errors such as + ww.example.com instead of + www.example.com.
    • +
    • If you are unable to load any pages, check your device’s data or Wi-Fi connection.
    • +
    + ]]>
    + + + No internet connection + + Check your network connection or try reloading the page in a few moments. + + Reload + + + Invalid Address + The provided address is not in a recognized format. Please check the location bar for mistakes and try again.

    + ]]>
    + + The address isn’t valid + + +
  • Web addresses are usually written like http://www.example.com/
  • +
  • Make sure that you’re using forward slashes (i.e. /).
  • + + ]]>
    + + + Unknown Protocol + The address specifies a protocol (e.g., wxyz://) the browser does not recognize, so the browser cannot properly connect to the site.

    +
      +
    • Are you trying to access multimedia or other non-text services? Check the site for extra requirements.
    • +
    • Some protocols may require third-party software or plugins before the browser can recognize them.
    • +
    + ]]>
    + + + File Not Found + +
  • Could the item have been renamed, removed, or relocated?
  • +
  • Is there a spelling, capitalization, or other typographical error in the address?
  • +
  • Do you have sufficient access permissions to the requested item?
  • + + ]]>
    + + + Access to the file was denied + +
  • It may have been removed, moved, or file permissions may be preventing access.
  • + + ]]>
    + + + Proxy Server Refused Connection + The browser is configured to use a proxy server, but the proxy refused a connection.

    +
      +
    • Is the browser’s proxy configuration correct? Check the settings and try again.
    • +
    • Does the proxy service allow connections from this network?
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    + ]]>
    + + + Proxy Server Not Found + The browser is configured to use a proxy server, but the proxy could not be found.

    +
      +
    • Is the browser’s proxy configuration correct? Check the settings and try again.
    • +
    • Is the device connected to an active network?
    • +
    • Still having trouble? Consult your network administrator or Internet provider for assistance.
    • +
    + ]]>
    + + + Malware site issue + + The site at %1$s has been reported as an attack site and has been blocked based on your security preferences.

    + ]]>
    + + + Unwanted site issue + + The site at %1$s has been reported as serving unwanted software and has been blocked based on your security preferences.

    + ]]>
    + + + Harmful site issue + + The site at %1$s has been reported as a potentially harmful site and has been blocked based on your security preferences.

    + ]]>
    + + + Deceptive site issue + + This web page at %1$s has been reported as a deceptive site and has been blocked based on your security preferences.

    + ]]>
    + + + Secure Site Not Available + + %1$s is not available.]]> + + Continue to HTTP Site +
    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("
      ", "
        ") + + 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 @@ + + 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 = listOf( + TippyTopIconPreparer(context.assets), + MemoryIconPreparer(sharedMemoryCache), + DiskIconPreparer(sharedDiskCache), + ), + internal var loaders: List = listOf( + MemoryIconLoader(sharedMemoryCache), + DiskIconLoader(sharedDiskCache), + HttpIconLoader(httpClient), + DataUriIconLoader(), + ), + private val decoders: List = listOf( + AndroidImageDecoder(), + ICOIconDecoder(), + SvgIconDecoder(), + ), + private val processors: List = 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 = 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 = 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, + 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, + 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, request: IconRequest): IconRequest = + preparers.fold(request) { preparedRequest, preparer -> + preparer.prepare(context, preparedRequest) + } + +private fun load( + context: Context, + request: IconRequest, + loaders: List, + decoders: List, + desiredSize: DesiredSize, +): Pair? { + 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, + 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, + 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, + 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 = 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 = 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 +} + +/** + * 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 = 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 { + + 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 { + // 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() + + 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 = 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.reverseLookup(type: IconRequest.Resource.Type): String { + forEach { (value, currentType) -> + if (currentType == type) { + return value + } + } + + throw IllegalArgumentException("Unknown type: $type") +} + +internal fun List.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 { + 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 { + 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(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 { + + /** + * 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 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 + } + + 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 + } + + 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 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 = 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 { + 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>(MAXIMUM_CACHE_URLS) + private val iconBitmapCache = object : LruCache(MAXIMUM_CACHE_BITMAP_BYTES) { + override fun sizeOf(key: String, value: Bitmap): Int { + return value.byteCount + } + } + + override fun getResources(request: IconRequest): List { + 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>.findBestSize(targetSize: Int, maxSize: Int, maxScaleFactor: Float): Pair? { + // 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 @@ + + + + @android:color/white + + + #9A4C00 + #AB008D + #4C009C + #002E9C + #009EC2 + #009D02 + #51AB00 + #36385A + + 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 @@ + + + + 32dp + 48dp + 102dp + + 1024px + 32px + 2dp + 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 @@ + + + + + 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() + 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() + 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() + 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 = """""".toByteArray() + + // SVGParseException for unbalancedClose + private val invalidSvg: ByteArray = """""".toByteArray() + + // IllegalArgumentException: SVG document is empty + private val illegalArgumentSvg: ByteArray = """""".toByteArray() + + // NullPointerException: Attempt to read from field on a null object reference in method + private val nullPointerSvg: ByteArray = """""".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() + 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() + 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() + 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() + 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() + 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 Binary files /dev/null and b/mobile/android/android-components/components/browser/icons/src/test/resources/bmp/test.bmp 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 Binary files /dev/null and b/mobile/android/android-components/components/browser/icons/src/test/resources/gif/cat.gif 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 Binary files /dev/null and b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/golem_favicon.ico 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 Binary files /dev/null and b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/microsoft_favicon.ico 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 Binary files /dev/null and b/mobile/android/android-components/components/browser/icons/src/test/resources/ico/nvidia_favicon.ico 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 Binary files /dev/null and b/mobile/android/android-components/components/browser/icons/src/test/resources/jpg/tonys.jpg 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 Binary files /dev/null and b/mobile/android/android-components/components/browser/icons/src/test/resources/png/mozac.png 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 Binary files /dev/null and b/mobile/android/android-components/components/browser/icons/src/test/resources/webp/test.webp 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 + + + + 4dp + + + 4dp + + + 250dp + + + 200dp + 300dp + + + 8dp + + +``` +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 + YOUR_HEIGHT +``` +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 + +``` + +### 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 + + + 16sp + + + + + 24dp + + + 24dp + + + + + + 20dp + + +``` + +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 + + + + + + +``` + +## 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 @@ + + + + + \ 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 @@ + + + + \ 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(R.id.mozac_browser_menu_recyclerView).apply { + layoutManager = StickyItemsLinearLayoutManager.get( + 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( + 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, +) : RecyclerView.Adapter(), 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, + val extras: Map = 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 { + 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() + internal val webExtensionPageActions = HashMap() + + internal fun getOrUpdateWebExtensionMenuItems( + state: BrowserState, + tab: SessionState? = null, + ): List { + val menuItems = ArrayList() + 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, + 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, + extras: Map = 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, + 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, + filteredExtensionMenuItems: List, + ): List { + 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.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.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? = 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(R.id.end_image) + endImageView.setTintResource(iconTintColorResource) + + val highlightedTextView = view.findViewById(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(R.id.image) + val endImageView = view.findViewById(R.id.end_image) + val notificationDotView = view.findViewById(R.id.notification_dot) + val textView = view.findViewById(R.id.text) + val highlightedTextView = view.findViewById(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(R.id.switch_widget)) + setTints(view) + + val notificationDotView = view.findViewById(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(R.id.switch_widget) + switch.setColorResource(textColorResource) + + val imageView = view.findViewById(R.id.image) + imageView.setImageResource(startImageResource) + imageView.setTintResource(iconTintColorResource) + } + + private fun updateHighlight(view: View, isHighlighted: Boolean) { + val notificationDotView = view.findViewById(R.id.notification_dot) + val switch = view.findViewById(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(R.id.text) + textView.text = label + textView.setColorResource(textColorResource) + textView.isEnabled = enabled + } + + private fun bindImage(view: View) { + val imageView = view.findViewById(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(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