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