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 --- mobile/android/focus-android/.buildconfig.yml | 165 + mobile/android/focus-android/.editorconfig | 5 + mobile/android/focus-android/CODEOWNERS | 5 + mobile/android/focus-android/CONTRIBUTING.md | 4 + mobile/android/focus-android/README.md | 119 + mobile/android/focus-android/Screengrabfile | 22 + .../android/focus-android/app/.experimenter.yaml | 23 + mobile/android/focus-android/app/.gitignore | 3 + mobile/android/focus-android/app/build.gradle | 788 +++ mobile/android/focus-android/app/lint-baseline.xml | 7196 ++++++++++++++++++++ mobile/android/focus-android/app/lint.xml | 27 + mobile/android/focus-android/app/metrics.yaml | 2458 +++++++ mobile/android/focus-android/app/nimbus.fml.yaml | 48 + mobile/android/focus-android/app/pings.yaml | 31 + .../android/focus-android/app/proguard-rules.pro | 154 + .../app/src/androidTest/assets/audioPage.html | 37 + .../src/androidTest/assets/cross-site-cookies.html | 12 + .../app/src/androidTest/assets/download.jpg | Bin 0 -> 9375 bytes .../androidTest/assets/etpPages/adsTrackers.html | 21 + .../assets/etpPages/analyticsTrackers.html | 21 + .../androidTest/assets/etpPages/otherTrackers.html | 22 + .../assets/etpPages/socialTrackers.html | 21 + .../app/src/androidTest/assets/genericPage.html | 15 + .../androidTest/assets/global_privacy_control.html | 15 + .../app/src/androidTest/assets/htmlControls.html | 66 + .../app/src/androidTest/assets/image_test.html | 20 + .../app/src/androidTest/assets/mutedVideoPage.html | 53 + .../app/src/androidTest/assets/rabbit.jpg | Bin 0 -> 5038 bytes .../androidTest/assets/resources/audioSample.mp3 | Bin 0 -> 5517 bytes .../app/src/androidTest/assets/resources/clip.mp4 | Bin 0 -> 39160 bytes .../src/androidTest/assets/same-site-cookies.html | 125 + .../app/src/androidTest/assets/service-worker.js | 2 + .../app/src/androidTest/assets/storage_check.html | 23 + .../app/src/androidTest/assets/storage_start.html | 28 + .../app/src/androidTest/assets/tab1.html | 29 + .../app/src/androidTest/assets/tab2.html | 16 + .../app/src/androidTest/assets/tab3.html | 20 + .../app/src/androidTest/assets/test.html | 38 + .../app/src/androidTest/assets/videoPage.html | 53 + .../mozilla/focus/activity/AddToHomescreenTest.kt | 89 + .../org/mozilla/focus/activity/ContextMenusTest.kt | 187 + .../org/mozilla/focus/activity/CustomTabTest.kt | 133 + .../org/mozilla/focus/activity/DownloadFileTest.kt | 200 + .../EnhancedTrackingProtectionSettingsTest.kt | 351 + .../focus/activity/EraseBrowsingDataTest.kt | 160 + .../org/mozilla/focus/activity/ErrorPagesTest.kt | 60 + .../org/mozilla/focus/activity/FirstRunTest.kt | 57 + .../mozilla/focus/activity/MediaPlaybackTest.kt | 101 + .../focus/activity/MozillaSupportPagesTest.kt | 128 + .../org/mozilla/focus/activity/MultitaskingTest.kt | 147 + .../org/mozilla/focus/activity/OldFirstRunTest.kt | 67 + .../activity/OpenInExternalBrowserDialogueTest.kt | 61 + .../org/mozilla/focus/activity/SafeBrowsingTest.kt | 139 + .../java/org/mozilla/focus/activity/SearchTest.kt | 184 + .../mozilla/focus/activity/SettingsAdvancedTest.kt | 82 + .../mozilla/focus/activity/SettingsGeneralTest.kt | 178 + .../mozilla/focus/activity/SettingsPrivacyTest.kt | 135 + .../org/mozilla/focus/activity/SettingsTest.kt | 85 + .../org/mozilla/focus/activity/ShortcutsTest.kt | 124 + .../mozilla/focus/activity/SitePermissionsTest.kt | 272 + .../mozilla/focus/activity/SwitchContextTest.kt | 132 + .../mozilla/focus/activity/ThreeDotMainMenuTest.kt | 137 + .../mozilla/focus/activity/URLAutocompleteTest.kt | 147 + .../org/mozilla/focus/activity/WebControlsTest.kt | 174 + .../focus/activity/robots/AddToHomeScreenRobot.kt | 71 + .../mozilla/focus/activity/robots/BrowserRobot.kt | 694 ++ .../focus/activity/robots/CustomTabRobot.kt | 132 + .../mozilla/focus/activity/robots/DownloadRobot.kt | 96 + .../focus/activity/robots/HomeScreenRobot.kt | 235 + .../focus/activity/robots/NotificationRobot.kt | 159 + .../mozilla/focus/activity/robots/SearchRobot.kt | 166 + .../activity/robots/SettingsAdvancedMenuRobot.kt | 109 + .../activity/robots/SettingsGeneralMenuRobot.kt | 197 + .../activity/robots/SettingsMozillaMenuRobot.kt | 187 + .../activity/robots/SettingsPrivacyMenuRobot.kt | 741 ++ .../mozilla/focus/activity/robots/SettingsRobot.kt | 114 + .../activity/robots/SettingsSearchMenuRobot.kt | 169 + .../robots/SettingsSitePermissionsRobot.kt | 166 + .../activity/robots/SiteSecurityInfoSheetRobot.kt | 75 + .../mozilla/focus/activity/robots/TabsTrayRobot.kt | 69 + .../focus/activity/robots/ThreeDotMainMenuRobot.kt | 238 + .../java/org/mozilla/focus/helpers/Constants.kt | 9 + .../org/mozilla/focus/helpers/DeleteFilesHelper.kt | 52 + .../org/mozilla/focus/helpers/EspressoHelper.kt | 84 + .../mozilla/focus/helpers/FeatureSettingsHelper.kt | 51 + .../helpers/HostScreencapScreenshotStrategy.java | 116 + .../mozilla/focus/helpers/MainActivityTestRule.kt | 151 + .../focus/helpers/MockLocationUpdatesRule.kt | 113 + .../mozilla/focus/helpers/MockWebServerHelper.kt | 76 + .../org/mozilla/focus/helpers/RetryTestRule.kt | 44 + .../org/mozilla/focus/helpers/StringsHelper.kt | 27 + .../org/mozilla/focus/helpers/TestAssetHelper.kt | 71 + .../java/org/mozilla/focus/helpers/TestHelper.kt | 379 ++ .../org/mozilla/focus/helpers/ext/WaitNotNull.kt | 20 + .../idlingResources/RecyclerViewIdlingResource.kt | 32 + .../idlingResources/SessionLoadedIdlingResource.kt | 48 + .../focus/privacy/GlobalPrivacyControlTest.kt | 60 + .../focus/privacy/LocalSessionStorageTest.kt | 96 + .../focus/screenshots/AllowListScreenshots.java | 123 + .../screenshots/BrowserScreenScreenshots.java | 301 + .../focus/screenshots/ErrorPagesScreenshots.java | 96 + .../focus/screenshots/FirstRunScreenshots.kt | 55 + .../focus/screenshots/HomeScreenScreenshots.kt | 129 + .../focus/screenshots/NotificationScreenshots.java | 104 + .../mozilla/focus/screenshots/ScreenshotTest.java | 94 + .../focus/screenshots/SettingsScreenshots.kt | 71 + .../org/mozilla/focus/testAnnotations/SmokeTest.kt | 13 + .../src/beta/res/drawable-v24/ic_splash_screen.xml | 263 + .../app/src/beta/res/drawable/ic_splash_screen.png | Bin 0 -> 6437 bytes .../app/src/beta/res/values-night/colors.xml | 7 + .../app/src/debug/AndroidManifest.xml | 23 + .../org/mozilla/focus/DebugFocusApplication.kt | 40 + .../java/org/mozilla/focus/utils/AdjustHelper.java | 14 + .../src/debug/java/org/mozilla/focus/web/Config.kt | 10 + .../debug/res/mipmap-anydpi-v26/ic_launcher.xml | 9 + .../app/src/debug/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 14925 bytes .../res/mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 14454 bytes .../app/src/debug/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 16819 bytes .../res/mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 18016 bytes .../src/debug/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 21584 bytes .../res/mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 25726 bytes .../src/debug/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 24098 bytes .../res/mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 29236 bytes .../app/src/focusBeta/ic_launcher-playstore.png | Bin 0 -> 115595 bytes .../java/org/mozilla/focus/utils/AdjustHelper.java | 14 + .../res/drawable-land/dark_background.xml | 29 + .../res/drawable-v24/ic_launcher_foreground.xml | 262 + .../focusBeta/res/drawable-v24/icon_foreground.xml | 253 + .../src/focusBeta/res/drawable/dark_background.xml | 29 + .../res/drawable/ic_launcher_background.xml | 78 + .../src/focusBeta/res/drawable/onboarding_logo.xml | 263 + .../app/src/focusBeta/res/drawable/wordmark2.xml | 272 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 9 + .../src/focusBeta/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 5962 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 9028 bytes .../src/focusBeta/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3309 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 4965 bytes .../src/focusBeta/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 8920 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 13502 bytes .../focusBeta/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 15094 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 22454 bytes .../focusBeta/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 21909 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 33006 bytes .../app/src/focusBeta/res/values/app.xml | 7 + .../app/src/focusBeta/res/xml-v25/shortcuts.xml | 31 + .../app/src/focusDebug/res/xml-v25/shortcuts.xml | 31 + .../app/src/focusNightly/res/values/app.xml | 8 + .../app/src/focusNightly/res/xml-v25/shortcuts.xml | 31 + .../app/src/focusRelease/AndroidManifest.xml | 24 + .../java/org/mozilla/focus/utils/AdjustHelper.java | 72 + .../java/org/mozilla/focus/web/Config.kt | 10 + .../app/src/focusRelease/res/xml-v25/shortcuts.xml | 31 + .../klar/res/drawable/background_gradient_dark.xml | 11 + .../app/src/klar/res/drawable/wordmark2.xml | 357 + .../focus-android/app/src/klar/res/values/app.xml | 11 + .../java/org/mozilla/focus/utils/AdjustHelper.java | 14 + .../src/klarBeta/res/drawable/onboarding_logo.xml | 263 + .../app/src/klarBeta/res/xml-v25/shortcuts.xml | 31 + .../app/src/klarDebug/res/xml-v25/shortcuts.xml | 31 + .../klarNightly/drawable-v24/icon_foreground.xml | 253 + .../drawable/background_gradient_dark.xml | 11 + .../src/klarNightly/drawable/icon_background.xml | 18 + .../drawable/toolbar_url_background.xml | 8 + .../klarNightly/mipmap-anydpi-v26/ic_launcher.xml | 8 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 8 + .../src/klarNightly/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4230 bytes .../klarNightly/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 6819 bytes .../src/klarNightly/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2521 bytes .../klarNightly/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3997 bytes .../src/klarNightly/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6134 bytes .../klarNightly/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 10083 bytes .../src/klarNightly/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 10218 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 16745 bytes .../src/klarNightly/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 15031 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 24870 bytes .../app/src/klarNightly/res/xml-v25/shortcuts.xml | 31 + .../java/org/mozilla/focus/utils/AdjustHelper.java | 14 + .../java/org/mozilla/focus/web/Config.kt | 10 + .../app/src/klarRelease/res/xml-v25/shortcuts.xml | 31 + .../focus-android/app/src/main/AndroidManifest.xml | 229 + .../app/src/main/assets/error_style.css | 172 + .../focus-android/app/src/main/assets/style.css | 43 + .../app/src/main/ic_launcher-playstore.png | Bin 0 -> 99479 bytes .../src/main/java/org/mozilla/focus/Components.kt | 321 + .../java/org/mozilla/focus/FocusApplication.kt | 238 + .../mozilla/focus/activity/CrashListActivity.kt | 24 + .../mozilla/focus/activity/CustomTabActivity.kt | 95 + .../focus/activity/EraseAndOpenShortcutActivity.kt | 31 + .../focus/activity/EraseShortcutActivity.kt | 23 + .../focus/activity/InstallFirefoxActivity.kt | 93 + .../focus/activity/IntentReceiverActivity.kt | 72 + .../org/mozilla/focus/activity/MainActivity.kt | 473 ++ .../mozilla/focus/activity/TextActionActivity.kt | 48 + .../focus/animation/TransitionDrawableGroup.kt | 26 + .../org/mozilla/focus/appreview/AppReviewStep.kt | 11 + .../org/mozilla/focus/appreview/AppReviewUtils.kt | 151 + .../focus/autocomplete/AutocompleteAddFragment.kt | 120 + .../AutocompleteCustomDomainsPreference.kt | 23 + .../AutocompleteDefaultDomainsPreference.kt | 23 + .../autocomplete/AutocompleteDomainFormatter.kt | 18 + .../focus/autocomplete/AutocompleteListFragment.kt | 349 + .../autocomplete/AutocompleteRemoveFragment.kt | 74 + .../autocomplete/AutocompleteSettingsFragment.kt | 84 + .../biometrics/BiometricAuthenticationFragment.kt | 131 + .../BiometricAuthenticationFragmentCompose.kt | 114 + .../org/mozilla/focus/biometrics/LockObserver.kt | 59 + .../focus/browser/BlockedTrackersMiddleware.kt | 55 + .../org/mozilla/focus/browser/LocalizedContent.kt | 106 + .../browser/integration/BrowserMenuController.kt | 190 + .../integration/BrowserToolbarIntegration.kt | 501 ++ .../browser/integration/FindInPageIntegration.kt | 53 + .../browser/integration/FullScreenIntegration.kt | 133 + .../integration/NavigationButtonsIntegration.kt | 123 + .../java/org/mozilla/focus/cfr/CfrMiddleware.kt | 123 + .../org/mozilla/focus/components/EngineProvider.kt | 59 + .../focus/contextmenu/ContextMenuCandidates.kt | 87 + .../focus/cookiebanner/CookieBannerFragment.kt | 66 + .../focus/cookiebanner/CookieBannerOption.kt | 28 + .../CookieBannerRejectAllPreference.kt | 20 + .../CookieBannerExceptionDetailsSwitch.kt | 32 + .../CookieBannerReducerDetailsPanel.kt | 181 + .../cookiebannerreducer/CookieBannerReducerItem.kt | 158 + .../CookieBannerReducerMiddleware.kt | 230 + .../CookieBannerReducerStatus.kt | 32 + .../CookieBannerReducerStore.kt | 113 + .../DefaultCookieBannerReducerInteractor.kt | 35 + .../mozilla/focus/customtabs/CustomTabsService.kt | 15 + .../org/mozilla/focus/downloads/DownloadService.kt | 17 + .../mozilla/focus/engine/AppContentInterceptor.kt | 111 + .../java/org/mozilla/focus/engine/ClientWrapper.kt | 29 + .../engine/EngineSharedPreferencesListener.kt | 95 + .../mozilla/focus/engine/SanityCheckMiddleware.kt | 36 + .../focus/exceptions/ExceptionsListFragment.kt | 323 + .../focus/exceptions/ExceptionsRemoveFragment.kt | 61 + .../org/mozilla/focus/experiments/NimbusSetup.kt | 116 + .../main/java/org/mozilla/focus/ext/Activity.kt | 41 + .../java/org/mozilla/focus/ext/AndroidViewModel.kt | 12 + .../java/org/mozilla/focus/ext/BrowserStore.kt | 21 + .../java/org/mozilla/focus/ext/BrowserToolbar.kt | 86 + .../java/org/mozilla/focus/ext/ContentState.kt | 16 + .../src/main/java/org/mozilla/focus/ext/Context.kt | 60 + .../main/java/org/mozilla/focus/ext/Fragment.kt | 47 + .../mozilla/focus/ext/PreferenceFragmentCompat.kt | 16 + .../java/org/mozilla/focus/ext/SessionState.kt | 25 + .../src/main/java/org/mozilla/focus/ext/String.kt | 75 + .../app/src/main/java/org/mozilla/focus/ext/Uri.kt | 69 + .../org/mozilla/focus/firstrun/FirstrunCardView.kt | 38 + .../mozilla/focus/firstrun/FirstrunPagerAdapter.kt | 103 + .../fragment/AddToHomescreenDialogFragment.kt | 145 + .../org/mozilla/focus/fragment/BaseFragment.kt | 57 + .../org/mozilla/focus/fragment/BrowserFragment.kt | 1072 +++ .../focus/fragment/CrashReporterFragment.kt | 40 + .../org/mozilla/focus/fragment/FirstrunFragment.kt | 150 + .../org/mozilla/focus/fragment/UrlInputFragment.kt | 632 ++ .../mozilla/focus/fragment/about/AboutFragment.kt | 209 + .../focus/fragment/about/SecretSettingsUnlocker.kt | 59 + .../fragment/onboarding/OnboardingController.kt | 100 + .../fragment/onboarding/OnboardingFirstFragment.kt | 62 + .../onboarding/OnboardingFirstScreenCompose.kt | 165 + .../fragment/onboarding/OnboardingInteractor.kt | 34 + .../onboarding/OnboardingSecondFragment.kt | 88 + .../onboarding/OnboardingSecondScreenCompose.kt | 194 + .../focus/fragment/onboarding/OnboardingStep.kt | 12 + .../focus/fragment/onboarding/OnboardingStorage.kt | 49 + .../mozilla/focus/input/InputToolbarIntegration.kt | 186 + .../java/org/mozilla/focus/locale/LocaleManager.kt | 80 + .../main/java/org/mozilla/focus/locale/Locales.kt | 107 + .../screen/DefaultLanguageScreenInteractor.kt | 17 + .../org/mozilla/focus/locale/screen/Language.kt | 15 + .../focus/locale/screen/LanguageFragment.kt | 115 + .../focus/locale/screen/LanguageListItem.kt | 16 + .../focus/locale/screen/LanguageMiddleware.kt | 69 + .../focus/locale/screen/LanguageScreenStore.kt | 70 + .../mozilla/focus/locale/screen/LanguageStorage.kt | 90 + .../focus/locale/screen/LocaleDescriptor.kt | 125 + .../focus/locale/screen/LocaleFragmentCompose.kt | 175 + .../org/mozilla/focus/media/MediaSessionService.kt | 20 + .../java/org/mozilla/focus/menu/ToolbarMenu.kt | 40 + .../mozilla/focus/menu/browser/CustomTabMenu.kt | 160 + .../focus/menu/browser/DefaultBrowserMenu.kt | 193 + .../java/org/mozilla/focus/menu/home/HomeMenu.kt | 35 + .../org/mozilla/focus/menu/home/HomeMenuItem.kt | 10 + .../focus/navigation/MainActivityNavigation.kt | 315 + .../java/org/mozilla/focus/navigation/Navigator.kt | 56 + .../java/org/mozilla/focus/navigation/StoreLink.kt | 50 + .../main/java/org/mozilla/focus/open/AppAdapter.kt | 114 + .../java/org/mozilla/focus/open/AppViewHolder.kt | 45 + .../mozilla/focus/open/InstallBannerViewHolder.kt | 39 + .../org/mozilla/focus/open/OpenWithFragment.kt | 145 + .../java/org/mozilla/focus/perf/Performance.kt | 52 + .../search/ManualAddSearchEnginePreference.kt | 126 + .../MultiselectSearchEngineListPreference.kt | 66 + .../search/RadioSearchEngineListPreference.kt | 64 + .../focus/search/SearchEngineListPreference.kt | 114 + .../mozilla/focus/search/SearchEnginePreference.kt | 48 + .../mozilla/focus/search/SearchFilterMiddleware.kt | 37 + .../org/mozilla/focus/search/SearchMigration.kt | 71 + .../SearchSuggestionsPreferences.kt | 44 + .../SearchSuggestionsViewModel.kt | 116 + .../focus/searchsuggestions/ui/SearchOverlay.kt | 121 + .../ui/SearchSuggestionsFragment.kt | 127 + .../ui/SearchSuggestionsPreference.kt | 31 + .../focus/searchwidget/ExternalIntentNavigation.kt | 125 + .../PromoteSearchWidgetDialogCompose.kt | 201 + .../focus/searchwidget/SearchWidgetProvider.kt | 73 + .../focus/searchwidget/SearchWidgetUtils.kt | 73 + .../focus/searchwidget/VoiceSearchActivity.kt | 32 + .../org/mozilla/focus/session/IntentProcessor.kt | 203 + .../focus/session/PrivateNotificationFeature.kt | 49 + .../focus/session/SessionNotificationService.kt | 240 + .../focus/session/VisibilityLifeCycleCallback.kt | 81 + .../org/mozilla/focus/session/ui/TabViewHolder.kt | 51 + .../org/mozilla/focus/session/ui/TabsAdapter.kt | 40 + .../java/org/mozilla/focus/session/ui/TabsPopup.kt | 78 + .../focus/settings/AboutLibrariesFragment.kt | 27 + .../mozilla/focus/settings/BaseComposeFragment.kt | 130 + .../mozilla/focus/settings/BaseSettingsFragment.kt | 35 + .../focus/settings/BaseSettingsLikeFragment.kt | 40 + .../focus/settings/GeneralSettingsFragment.kt | 141 + .../focus/settings/HttpsOnlyModePreference.kt | 35 + .../InstalledSearchEnginesSettingsFragment.kt | 138 + .../focus/settings/LearnMoreSwitchPreference.kt | 58 + .../ManualAddSearchEngineSettingsFragment.kt | 262 + .../focus/settings/MozillaSettingsFragment.kt | 97 + .../focus/settings/RadioButtonPreference.kt | 191 + .../RemoveSearchEnginesSettingsFragment.kt | 77 + .../focus/settings/SafeBrowsingSwitchPreference.kt | 19 + .../focus/settings/SearchSettingsFragment.kt | 70 + .../org/mozilla/focus/settings/SettingsFragment.kt | 50 + .../org/mozilla/focus/settings/StatePreference.kt | 47 + .../settings/advanced/AdvancedSettingsFragment.kt | 80 + .../settings/advanced/SecretSettingsFragment.kt | 53 + .../settings/advanced/SharedPreferenceUpdater.kt | 24 + .../settings/permissions/SitePermissionOption.kt | 44 + .../permissions/SitePermissionsFragment.kt | 63 + ...DefaultSitePermissionOptionsScreenInteractor.kt | 22 + .../HardwarePermissionCheckFeature.kt | 20 + .../permissionoptions/SitePermission.kt | 20 + .../SitePermissionOptionListItem.kt | 11 + .../SitePermissionOptionsFragment.kt | 141 + .../SitePermissionOptionsFragmentCompose.kt | 280 + .../SitePermissionOptionsScreenStore.kt | 70 + .../SitePermissionOptionsStorage.kt | 215 + .../SitePermissionOptionsStorageMiddleware.kt | 43 + .../settings/privacy/ConnectionDetailsPanel.kt | 82 + .../focus/settings/privacy/PreferenceSwitch.kt | 68 + .../settings/privacy/PreferenceToolTipCompose.kt | 137 + .../privacy/PrivacySecuritySettingsFragment.kt | 254 + .../settings/privacy/TrackingProtectionPanel.kt | 215 + .../settings/privacy/studies/StudiesAdapter.kt | 67 + .../settings/privacy/studies/StudiesFragment.kt | 158 + .../settings/privacy/studies/StudiesListItem.kt | 16 + .../privacy/studies/StudiesRecyclerView.kt | 21 + .../settings/privacy/studies/StudiesViewHolder.kt | 74 + .../settings/privacy/studies/StudiesViewModel.kt | 65 + .../java/org/mozilla/focus/shortcut/HomeScreen.kt | 122 + .../org/mozilla/focus/shortcut/IconGenerator.kt | 135 + .../main/java/org/mozilla/focus/state/AppAction.kt | 124 + .../java/org/mozilla/focus/state/AppReducer.kt | 311 + .../main/java/org/mozilla/focus/state/AppState.kt | 124 + .../main/java/org/mozilla/focus/state/AppStore.kt | 14 + .../org/mozilla/focus/telemetry/ActivationPing.kt | 84 + .../org/mozilla/focus/telemetry/BrowsersCache.kt | 41 + .../org/mozilla/focus/telemetry/FactsProcessor.kt | 82 + .../focus/telemetry/FenixProductDetector.kt | 49 + .../mozilla/focus/telemetry/GleanMetricsService.kt | 243 + .../org/mozilla/focus/telemetry/MetricsService.kt | 18 + .../focus/telemetry/ProfilerMarkerFactProcessor.kt | 85 + .../mozilla/focus/telemetry/TelemetryMiddleware.kt | 148 + .../startuptelemetry/AppStartReasonProvider.kt | 99 + .../DefaultActivityLifecycleCallbacks.kt | 25 + .../startuptelemetry/StartupActivityLog.kt | 106 + .../startuptelemetry/StartupPathProvider.kt | 98 + .../startuptelemetry/StartupStateProvider.kt | 137 + .../startuptelemetry/StartupTypeTelemetry.kt | 110 + .../src/main/java/org/mozilla/focus/theme/Theme.kt | 14 + .../focus/topsites/DefaultTopSitesStorage.kt | 64 + .../mozilla/focus/topsites/DefaultTopSitesView.kt | 22 + .../mozilla/focus/topsites/RenameTopSiteDialog.kt | 47 + .../java/org/mozilla/focus/topsites/TopSites.kt | 166 + .../org/mozilla/focus/topsites/TopSitesOverlay.kt | 112 + .../org/mozilla/focus/ui/dialog/FocusDialog.kt | 209 + .../mozilla/focus/ui/menu/CustomDropdownMenu.kt | 46 + .../java/org/mozilla/focus/ui/menu/MenuItem.kt | 16 + .../java/org/mozilla/focus/ui/theme/FocusColors.kt | 49 + .../org/mozilla/focus/ui/theme/FocusDimensions.kt | 15 + .../java/org/mozilla/focus/ui/theme/FocusTheme.kt | 138 + .../org/mozilla/focus/ui/theme/FocusTypography.kt | 136 + .../java/org/mozilla/focus/utils/AppConstants.kt | 33 + .../mozilla/focus/utils/ClickableSubstringLink.kt | 112 + .../main/java/org/mozilla/focus/utils/Features.kt | 15 + .../java/org/mozilla/focus/utils/FocusSnackbar.kt | 146 + .../mozilla/focus/utils/FocusSnackbarDelegate.kt | 37 + .../java/org/mozilla/focus/utils/HtmlLoader.kt | 121 + .../java/org/mozilla/focus/utils/IntentUtils.kt | 82 + .../focus/utils/OneShotOnPreDrawListener.kt | 27 + .../java/org/mozilla/focus/utils/SearchUtils.kt | 19 + .../main/java/org/mozilla/focus/utils/Settings.kt | 507 ++ .../java/org/mozilla/focus/utils/SupportUtils.kt | 133 + .../main/java/org/mozilla/focus/utils/ViewUtils.kt | 103 + .../org/mozilla/focus/widget/AboutPreference.kt | 24 + .../org/mozilla/focus/widget/CookiesPreference.kt | 46 + .../focus/widget/DefaultBrowserPreference.kt | 90 + .../mozilla/focus/widget/LocaleListPreference.java | 0 .../org/mozilla/focus/widget/MozillaPreference.kt | 21 + .../widget/ResizableKeyboardCoordinatorLayout.kt | 34 + .../focus/widget/ResizableKeyboardLinearLayout.kt | 29 + .../focus/widget/ResizableKeyboardViewDelegate.kt | 119 + .../mozilla/focus/widget/SwitchWithDescription.kt | 72 + .../focus/widget/TelemetrySwitchPreference.kt | 44 + .../app/src/main/res/anim/erase_animation.xml | 20 + .../app/src/main/res/anim/fab_reveal.xml | 16 + .../app/src/main/res/anim/fade_in.xml | 9 + .../app/src/main/res/anim/fade_out.xml | 9 + .../src/main/res/color/preference_title_text.xml | 9 + .../res/color/selected_search_engine_state.xml | 9 + .../main/res/drawable-hdpi/focus_search_widget.png | Bin 0 -> 17294 bytes .../focus_search_widget_promote_dialog.png | Bin 0 -> 29857 bytes .../drawable-hdpi/focus_snackbar_background.xml | 9 + .../src/main/res/drawable-hdpi/onboarding_img1.png | Bin 0 -> 49768 bytes .../src/main/res/drawable-hdpi/onboarding_img2.png | Bin 0 -> 24031 bytes .../src/main/res/drawable-hdpi/onboarding_img3.png | Bin 0 -> 15524 bytes .../src/main/res/drawable-hdpi/onboarding_img4.png | Bin 0 -> 23796 bytes .../res/drawable-land-night/home_background.xml | 31 + .../src/main/res/drawable-land/dark_background.xml | 46 + .../src/main/res/drawable-land/home_background.xml | 31 + .../drawable-night-hdpi/focus_search_widget.png | Bin 0 -> 16899 bytes .../focus_search_widget_promote_dialog.png | Bin 0 -> 29279 bytes .../main/res/drawable-night/home_background.xml | 31 + .../res/drawable-nodpi/ic_homescreen_shape.png | Bin 0 -> 1283 bytes .../src/main/res/drawable-v24/ic_splash_screen.xml | 213 + .../main/res/drawable-xhdpi/onboarding_img1.png | Bin 0 -> 32761 bytes .../main/res/drawable-xhdpi/onboarding_img2.png | Bin 0 -> 17156 bytes .../main/res/drawable-xhdpi/onboarding_img3.png | Bin 0 -> 11388 bytes .../main/res/drawable-xhdpi/onboarding_img4.png | Bin 0 -> 31166 bytes .../main/res/drawable-xxhdpi/onboarding_img1.png | Bin 0 -> 106485 bytes .../main/res/drawable-xxhdpi/onboarding_img2.png | Bin 0 -> 49218 bytes .../main/res/drawable-xxhdpi/onboarding_img3.png | Bin 0 -> 30170 bytes .../main/res/drawable-xxhdpi/onboarding_img4.png | Bin 0 -> 50377 bytes .../src/main/res/drawable/background_gradient.xml | 11 + .../res/drawable/background_install_banner.xml | 8 + .../background_list_item_current_session.xml | 8 + .../res/drawable/background_list_item_session.xml | 8 + .../main/res/drawable/background_open_in_item.xml | 12 + .../background_search_suggestion_section.xml | 12 + .../src/main/res/drawable/background_snackbar.xml | 19 + .../context_menu_navigation_view_background.xml | 9 + .../app/src/main/res/drawable/dark_background.xml | 43 + .../src/main/res/drawable/dialog_background.xml | 7 + .../res/drawable/dialog_warning_background.xml | 10 + .../main/res/drawable/find_in_page_background.xml | 18 + .../res/drawable/firstrun_button_background.xml | 15 + .../res/drawable/foreground_list_item_erase.xml | 7 + .../app/src/main/res/drawable/highlight_dot.xml | 10 + .../app/src/main/res/drawable/home_background.xml | 32 + .../src/main/res/drawable/ic_arrowhead_down.xml | 13 + .../app/src/main/res/drawable/ic_arrowhead_up.xml | 13 + .../src/main/res/drawable/ic_autoplay_enabled.xml | 17 + .../app/src/main/res/drawable/ic_back_button.xml | 14 + .../src/main/res/drawable/ic_camera_enabled.xml | 14 + .../app/src/main/res/drawable/ic_check.xml | 14 + .../src/main/res/drawable/ic_cookies_disable.xml | 13 + .../app/src/main/res/drawable/ic_developer.xml | 12 + .../app/src/main/res/drawable/ic_download.xml | 16 + .../main/res/drawable/ic_error_session_crashed.xml | 387 ++ .../app/src/main/res/drawable/ic_favorite.xml | 13 + .../app/src/main/res/drawable/ic_fingerprint.xml | 14 + .../app/src/main/res/drawable/ic_firefox.xml | 13 + .../app/src/main/res/drawable/ic_info.xml | 13 + .../app/src/main/res/drawable/ic_internet.xml | 13 + .../app/src/main/res/drawable/ic_language.xml | 13 + .../main/res/drawable/ic_launcher_background.xml | 13 + .../main/res/drawable/ic_launcher_foreground.xml | 213 + .../app/src/main/res/drawable/ic_link.xml | 22 + .../app/src/main/res/drawable/ic_menu.xml | 13 + .../app/src/main/res/drawable/ic_mozilla.xml | 13 + .../app/src/main/res/drawable/ic_notification.xml | 13 + .../app/src/main/res/drawable/ic_reorder.xml | 13 + .../src/main/res/drawable/ic_shortcut_erase.xml | 16 + .../app/src/main/res/drawable/ic_splash_screen.png | Bin 0 -> 5499 bytes .../app/src/main/res/drawable/ic_tab_new.xml | 14 + .../src/main/res/drawable/indicator_onboarding.xml | 12 + .../res/drawable/indicator_onboarding_default.xml | 15 + .../res/drawable/indicator_onboarding_selected.xml | 15 + .../res/drawable/menu_item_dark_background.xml | 8 + .../src/main/res/drawable/mozac_ic_broken_lock.xml | 19 + .../app/src/main/res/drawable/onboarding_logo.xml | 214 + .../res/drawable/onboarding_second_screen_icon.png | Bin 0 -> 39339 bytes .../src/main/res/drawable/photon_progressbar.xml | 21 + .../drawable/preference_foreground_disabled.xml | 7 + ...erence_multiselect_search_engine_foreground.xml | 10 + .../app/src/main/res/drawable/scrollbar_thumb.xml | 8 + .../src/main/res/drawable/tab_number_border.xml | 16 + .../main/res/drawable/toolbar_url_background.xml | 8 + ...top_rounded_corners_bottom_sheet_background.xml | 11 + .../src/main/res/drawable/urlbar_background.xml | 7 + .../app/src/main/res/drawable/wordmark2.xml | 232 + .../app/src/main/res/font/metropolis.xml | 81 + .../app/src/main/res/font/metropolis_black.ttf | Bin 0 -> 74628 bytes .../src/main/res/font/metropolis_blackitalic.ttf | Bin 0 -> 76524 bytes .../app/src/main/res/font/metropolis_bold.ttf | Bin 0 -> 75660 bytes .../src/main/res/font/metropolis_bolditalic.ttf | Bin 0 -> 78120 bytes .../app/src/main/res/font/metropolis_extrabold.ttf | Bin 0 -> 75980 bytes .../main/res/font/metropolis_extrabolditalic.ttf | Bin 0 -> 78284 bytes .../main/res/font/metropolis_extralighitalic.ttf | Bin 0 -> 75624 bytes .../src/main/res/font/metropolis_extralight.ttf | Bin 0 -> 74496 bytes .../app/src/main/res/font/metropolis_light.ttf | Bin 0 -> 74456 bytes .../src/main/res/font/metropolis_lightitalic.ttf | Bin 0 -> 75692 bytes .../app/src/main/res/font/metropolis_medium.ttf | Bin 0 -> 75476 bytes .../src/main/res/font/metropolis_mediumitalic.ttf | Bin 0 -> 77588 bytes .../app/src/main/res/font/metropolis_regular.ttf | Bin 0 -> 76504 bytes .../src/main/res/font/metropolis_regularitalic.ttf | Bin 0 -> 76748 bytes .../app/src/main/res/font/metropolis_semibold.ttf | Bin 0 -> 76016 bytes .../main/res/font/metropolis_semibolditalic.ttf | Bin 0 -> 78180 bytes .../app/src/main/res/font/metropolis_thin.ttf | Bin 0 -> 73292 bytes .../src/main/res/font/metropolis_thinitalic.ttf | Bin 0 -> 74688 bytes .../app/src/main/res/layout/active_study_item.xml | 40 + .../app/src/main/res/layout/activity_customtab.xml | 12 + .../app/src/main/res/layout/activity_info.xml | 19 + .../app/src/main/res/layout/activity_main.xml | 30 + .../app/src/main/res/layout/connection_details.xml | 89 + .../res/layout/cookie_banner_reducer_details.xml | 97 + .../app/src/main/res/layout/cookies_preference.xml | 55 + .../src/main/res/layout/custom_tab_menu_item.xml | 21 + .../main/res/layout/dialog_add_to_homescreen2.xml | 113 + .../res/layout/dialog_full_screen_notification.xml | 38 + .../layout/dialog_tracking_protection_sheet.xml | 152 + .../app/src/main/res/layout/firstrun_page.xml | 98 + .../app/src/main/res/layout/focus_preference.xml | 79 + .../layout/focus_preference_category_no_title.xml | 8 + .../res/layout/focus_preference_compose_layout.xml | 11 + .../res/layout/focus_preference_left_checkbox.xml | 49 + .../main/res/layout/focus_preference_new_tab.xml | 73 + .../main/res/layout/focus_preference_no_icon.xml | 44 + .../app/src/main/res/layout/focus_snackbar.xml | 65 + .../app/src/main/res/layout/fragment_about.xml | 18 + .../main/res/layout/fragment_about_libraries.xml | 18 + .../layout/fragment_autocomplete_add_domain.xml | 44 + .../layout/fragment_autocomplete_customdomains.xml | 12 + .../app/src/main/res/layout/fragment_browser.xml | 93 + .../main/res/layout/fragment_crash_reporter.xml | 85 + .../res/layout/fragment_exceptions_domains.xml | 38 + .../app/src/main/res/layout/fragment_firstrun.xml | 49 + .../app/src/main/res/layout/fragment_info.xml | 19 + .../app/src/main/res/layout/fragment_open_with.xml | 24 + .../res/layout/fragment_search_suggestions.xml | 126 + .../app/src/main/res/layout/fragment_settings.xml | 12 + .../app/src/main/res/layout/fragment_studies.xml | 57 + .../app/src/main/res/layout/fragment_urlinput.xml | 117 + .../src/main/res/layout/item_add_custom_domain.xml | 15 + .../app/src/main/res/layout/item_app.xml | 40 + .../app/src/main/res/layout/item_custom_domain.xml | 41 + .../main/res/layout/item_indicator_menu_button.xml | 25 + .../src/main/res/layout/item_install_banner.xml | 41 + .../app/src/main/res/layout/item_session.xml | 47 + .../app/src/main/res/layout/menu_item.xml | 40 + .../app/src/main/res/layout/menu_navigation.xml | 38 + .../app/src/main/res/layout/popup_tabs.xml | 27 + .../main/res/layout/preference_default_browser.xml | 14 + .../layout/preference_manual_add_search_engine.xml | 56 + .../main/res/layout/preference_radio_button.xml | 57 + .../res/layout/preference_screen_header_layout.xml | 13 + .../layout/preference_search_engine_chooser.xml | 16 + .../layout/preference_section_header_layout.xml | 17 + .../res/layout/preference_switch_learn_more.xml | 64 + .../res/layout/search_engine_checkbox_button.xml | 17 + .../main/res/layout/search_engine_radio_button.xml | 18 + .../src/main/res/layout/studies_section_item.xml | 27 + .../main/res/layout/switch_with_description.xml | 54 + .../app/src/main/res/layout/toolbar.xml | 9 + .../src/main/res/menu/menu_autocomplete_add.xml | 10 + .../src/main/res/menu/menu_autocomplete_list.xml | 10 + .../src/main/res/menu/menu_autocomplete_remove.xml | 12 + .../app/src/main/res/menu/menu_exceptions_list.xml | 10 + .../main/res/menu/menu_remove_search_engines.xml | 12 + .../res/menu/menu_search_engine_manual_add.xml | 19 + .../app/src/main/res/menu/menu_search_engines.xml | 15 + .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 9 + .../app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4076 bytes .../src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 6336 bytes .../app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2137 bytes .../src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3400 bytes .../app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5622 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 8345 bytes .../app/src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 9985 bytes .../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 14316 bytes .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 14478 bytes .../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 20937 bytes .../focus-android/app/src/main/res/raw/about.html | 55 + .../focus-android/app/src/main/res/raw/gpl.html | 713 ++ .../app/src/main/res/raw/initial_experiments.json | 3 + .../app/src/main/res/raw/licenses.html | 948 +++ .../focus-android/app/src/main/res/raw/rights.html | 56 + .../app/src/main/res/transition/firstrun_exit.xml | 7 + .../app/src/main/res/values-ace/strings.xml | 784 +++ .../app/src/main/res/values-af/strings.xml | 246 + .../app/src/main/res/values-am/strings.xml | 1096 +++ .../app/src/main/res/values-an/strings.xml | 311 + .../app/src/main/res/values-anp/strings.xml | 730 ++ .../app/src/main/res/values-ar/strings.xml | 839 +++ .../app/src/main/res/values-ast/strings.xml | 490 ++ .../app/src/main/res/values-ay/strings.xml | 476 ++ .../app/src/main/res/values-az/strings.xml | 403 ++ .../app/src/main/res/values-be/strings.xml | 1086 +++ .../app/src/main/res/values-bg/strings.xml | 1103 +++ .../app/src/main/res/values-bn/strings.xml | 403 ++ .../app/src/main/res/values-bo/strings.xml | 328 + .../app/src/main/res/values-bs/strings.xml | 1102 +++ .../app/src/main/res/values-ca/strings.xml | 1095 +++ .../app/src/main/res/values-cak/strings.xml | 1030 +++ .../app/src/main/res/values-co/strings.xml | 1125 +++ .../app/src/main/res/values-cs/strings.xml | 1113 +++ .../app/src/main/res/values-cy/strings.xml | 1122 +++ .../app/src/main/res/values-da/strings.xml | 1122 +++ .../app/src/main/res/values-de/strings.xml | 1120 +++ .../app/src/main/res/values-dsb/strings.xml | 1119 +++ .../app/src/main/res/values-el/strings.xml | 1128 +++ .../app/src/main/res/values-en-rCA/strings.xml | 1118 +++ .../app/src/main/res/values-en-rGB/strings.xml | 1090 +++ .../app/src/main/res/values-eo/strings.xml | 1118 +++ .../app/src/main/res/values-es-rAR/strings.xml | 1121 +++ .../app/src/main/res/values-es-rCL/strings.xml | 1117 +++ .../app/src/main/res/values-es-rES/strings.xml | 1117 +++ .../app/src/main/res/values-es-rMX/strings.xml | 1111 +++ .../app/src/main/res/values-et/strings.xml | 881 +++ .../app/src/main/res/values-eu/strings.xml | 1117 +++ .../app/src/main/res/values-fa/strings.xml | 857 +++ .../app/src/main/res/values-fi/strings.xml | 1119 +++ .../app/src/main/res/values-fr/strings.xml | 1120 +++ .../app/src/main/res/values-fur/strings.xml | 1080 +++ .../app/src/main/res/values-fy-rNL/strings.xml | 1121 +++ .../app/src/main/res/values-ga-rIE/strings.xml | 257 + .../app/src/main/res/values-gl/strings.xml | 1098 +++ .../app/src/main/res/values-gu-rIN/strings.xml | 403 ++ .../app/src/main/res/values-hi-rIN/strings.xml | 749 ++ .../app/src/main/res/values-hr/strings.xml | 1035 +++ .../app/src/main/res/values-hsb/strings.xml | 1119 +++ .../app/src/main/res/values-hu/strings.xml | 1121 +++ .../app/src/main/res/values-hus/strings.xml | 361 + .../app/src/main/res/values-hy-rAM/strings.xml | 634 ++ .../app/src/main/res/values-ia/strings.xml | 1123 +++ .../app/src/main/res/values-in/strings.xml | 1108 +++ .../app/src/main/res/values-is/strings.xml | 1091 +++ .../app/src/main/res/values-it/strings.xml | 1122 +++ .../app/src/main/res/values-iw/strings.xml | 1093 +++ .../app/src/main/res/values-ixl/strings.xml | 1103 +++ .../app/src/main/res/values-ja/strings.xml | 1120 +++ .../app/src/main/res/values-jv/strings.xml | 152 + .../app/src/main/res/values-ka/strings.xml | 1116 +++ .../app/src/main/res/values-kaa/strings.xml | 795 +++ .../app/src/main/res/values-kab/strings.xml | 1123 +++ .../app/src/main/res/values-kk/strings.xml | 1117 +++ .../app/src/main/res/values-ko/strings.xml | 1117 +++ .../app/src/main/res/values-kw/strings.xml | 1097 +++ .../app/src/main/res/values-lo/strings.xml | 1121 +++ .../app/src/main/res/values-lt/strings.xml | 841 +++ .../app/src/main/res/values-meh/strings.xml | 1105 +++ .../app/src/main/res/values-mix/strings.xml | 1072 +++ .../app/src/main/res/values-mr/strings.xml | 680 ++ .../app/src/main/res/values-ms/strings.xml | 403 ++ .../app/src/main/res/values-my/strings.xml | 634 ++ .../app/src/main/res/values-nb-rNO/strings.xml | 1117 +++ .../app/src/main/res/values-ne-rNP/strings.xml | 574 ++ .../app/src/main/res/values-night/colors.xml | 146 + .../app/src/main/res/values-nl/strings.xml | 1121 +++ .../app/src/main/res/values-nn-rNO/strings.xml | 1127 +++ .../app/src/main/res/values-nv/strings.xml | 32 + .../app/src/main/res/values-oc/strings.xml | 1114 +++ .../app/src/main/res/values-pa-rIN/strings.xml | 1118 +++ .../app/src/main/res/values-pai/strings.xml | 354 + .../app/src/main/res/values-pl/strings.xml | 1119 +++ .../app/src/main/res/values-ppl/strings.xml | 1072 +++ .../app/src/main/res/values-pt-rBR/strings.xml | 1122 +++ .../app/src/main/res/values-quc/strings.xml | 709 ++ .../app/src/main/res/values-quy/strings.xml | 302 + .../app/src/main/res/values-ro/strings.xml | 661 ++ .../app/src/main/res/values-ru/strings.xml | 1122 +++ .../app/src/main/res/values-si/strings.xml | 1078 +++ .../app/src/main/res/values-sk/strings.xml | 1121 +++ .../app/src/main/res/values-skr/strings.xml | 1107 +++ .../app/src/main/res/values-sl/strings.xml | 1123 +++ .../app/src/main/res/values-sn/strings.xml | 403 ++ .../app/src/main/res/values-sq/strings.xml | 1127 +++ .../app/src/main/res/values-sr/strings.xml | 1114 +++ .../app/src/main/res/values-su/strings.xml | 1115 +++ .../app/src/main/res/values-sv-rSE/strings.xml | 1121 +++ .../app/src/main/res/values-sw480dp/dimens.xml | 9 + .../app/src/main/res/values-sw600dp/dimens.xml | 9 + .../app/src/main/res/values-ta/strings.xml | 552 ++ .../app/src/main/res/values-te/strings.xml | 666 ++ .../app/src/main/res/values-tg/strings.xml | 1086 +++ .../app/src/main/res/values-th/strings.xml | 1119 +++ .../app/src/main/res/values-tr/strings.xml | 1127 +++ .../app/src/main/res/values-trs/strings.xml | 1090 +++ .../app/src/main/res/values-tsz/strings.xml | 947 +++ .../app/src/main/res/values-tt/strings.xml | 1103 +++ .../app/src/main/res/values-uk/strings.xml | 1120 +++ .../app/src/main/res/values-ur/strings.xml | 818 +++ .../app/src/main/res/values-vi/strings.xml | 1120 +++ .../app/src/main/res/values-wo/strings.xml | 241 + .../app/src/main/res/values-yua/strings.xml | 479 ++ .../app/src/main/res/values-zam/strings.xml | 443 ++ .../app/src/main/res/values-zh-rCN/strings.xml | 1127 +++ .../app/src/main/res/values-zh-rHK/strings.xml | 933 +++ .../app/src/main/res/values-zh-rTW/strings.xml | 1121 +++ .../focus-android/app/src/main/res/values/app.xml | 13 + .../app/src/main/res/values/attrs.xml | 39 + .../app/src/main/res/values/colors.xml | 152 + .../app/src/main/res/values/configuration.xml | 14 + .../app/src/main/res/values/dimens.xml | 91 + .../app/src/main/res/values/fonts.xml | 8 + .../focus-android/app/src/main/res/values/ids.xml | 21 + .../app/src/main/res/values/preference_keys.xml | 125 + .../app/src/main/res/values/static_strings.xml | 26 + .../app/src/main/res/values/strings.xml | 1069 +++ .../app/src/main/res/values/strings_references.xml | 35 + .../app/src/main/res/values/styles.xml | 326 + .../app/src/main/res/xml/advanced_settings.xml | 33 + .../app/src/main/res/xml/autocomplete.xml | 32 + .../src/main/res/xml/cookie_banner_settings.xml | 13 + .../app/src/main/res/xml/experiments_settings.xml | 11 + .../app/src/main/res/xml/general_settings.xml | 42 + .../src/main/res/xml/manual_add_search_engine.xml | 10 + .../app/src/main/res/xml/mozilla_settings.xml | 44 + .../src/main/res/xml/privacy_security_settings.xml | 159 + .../app/src/main/res/xml/provider_paths.xml | 9 + .../app/src/main/res/xml/remove_search_engines.xml | 9 + .../src/main/res/xml/search_engine_settings.xml | 23 + .../app/src/main/res/xml/search_settings.xml | 25 + .../app/src/main/res/xml/search_widget_info.xml | 15 + .../app/src/main/res/xml/secret_settings.xml | 17 + .../app/src/main/res/xml/settings.xml | 44 + .../app/src/main/res/xml/site_permissions.xml | 50 + .../app/src/nightly/ic_launcher-playstore.png | Bin 0 -> 71476 bytes .../java/org/mozilla/focus/utils/AdjustHelper.java | 14 + .../nightly/java/org/mozilla/focus/web/Config.kt | 10 + .../nightly/res/drawable-land/dark_background.xml | 28 + .../nightly/res/drawable-v24/ic_splash_screen.xml | 253 + .../src/nightly/res/drawable/dark_background.xml | 28 + .../res/drawable/ic_launcher_background.xml | 18 + .../res/drawable/ic_launcher_foreground.xml | 253 + .../src/nightly/res/drawable/ic_splash_screen.png | Bin 0 -> 5669 bytes .../src/nightly/res/drawable/onboarding_logo.xml | 248 + .../app/src/nightly/res/drawable/wordmark2.xml | 281 + .../nightly/res/mipmap-anydpi-v26/ic_launcher.xml | 8 + .../src/nightly/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4230 bytes .../nightly/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 6819 bytes .../src/nightly/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2521 bytes .../nightly/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3997 bytes .../src/nightly/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6134 bytes .../nightly/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 10083 bytes .../src/nightly/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 10218 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 16745 bytes .../src/nightly/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 15031 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 24870 bytes .../app/src/nightly/res/values-night/colors.xml | 7 + .../java/org/mozilla/focus/BrowserFragmentTest.kt | 108 + .../java/org/mozilla/focus/TestFocusApplication.kt | 95 + .../animation/TransitionDrawableGroupTest.java | 42 + .../BiometricAuthenticationFragmentTest.kt | 70 + .../integration/BrowserToolbarIntegrationTest.kt | 234 + .../integration/FindInPageIntegrationTest.kt | 46 + .../integration/FullScreenIntegrationTest.kt | 374 + .../integration/InputToolbarIntegrationTest.kt | 82 + .../org/mozilla/focus/cfr/CfrMiddlewareTest.kt | 127 + .../focus/contextmenu/ContextMenuCandidatesTest.kt | 81 + .../mozilla/focus/experiments/NimbusSetupTest.kt | 25 + .../org/mozilla/focus/ext/BrowserToolbarTest.kt | 80 + .../test/java/org/mozilla/focus/ext/StringTest.kt | 80 + .../src/test/java/org/mozilla/focus/ext/UriTest.kt | 82 + .../java/org/mozilla/focus/locale/LocalesTest.kt | 46 + .../focus/menu/BrowserMenuControllerTest.kt | 162 + .../focus/onboarding/OnboardingControllerTest.kt | 72 + .../focus/onboarding/OnboardingStorageTest.kt | 79 + .../SearchSuggestionsViewModelTest.kt | 59 + .../searchwidget/ExternalIntentNavigationTest.kt | 203 + .../focus/searchwidget/SearchWidgetProviderTest.kt | 65 + .../focus/settings/SearchEngineValidationTest.kt | 94 + .../org/mozilla/focus/shortcut/HomeScreenTest.kt | 20 + .../mozilla/focus/shortcut/IconGeneratorTest.kt | 59 + .../SitePermissionOptionsStorageTest.kt | 342 + .../SitePermissionOptionsStoreTest.kt | 89 + .../sitepermissions/SitePermissionsFragmentTest.kt | 57 + .../focus/telemetry/GleanMetricsServiceTest.kt | 42 + .../telemetry/ProfilerMarkerFactProcessorTest.kt | 96 + .../focus/telemetry/StartupActivityLogTest.kt | 99 + .../focus/telemetry/StartupPathProviderTest.kt | 213 + .../focus/telemetry/StartupStateProviderTest.kt | 417 ++ .../focus/telemetry/StartupTypeTelemetryTest.kt | 152 + .../focus/topsites/DefaultTopSitesStorageTest.kt | 136 + .../org/mozilla/focus/utils/IntentUtilsTest.kt | 21 + .../org/mozilla/focus/utils/SupportUtilsTest.kt | 64 + .../org.mockito.plugins.MockMaker | 2 + .../app/src/test/resources/robolectric.properties | 3 + mobile/android/focus-android/app/tags.yaml | 175 + .../androidTest/copy-robo-crash-artifacts.py | 273 + .../taskcluster/androidTest/flank-arm-beta.yml | 36 + .../androidTest/flank-arm-start-test-robo.yml | 27 + .../androidTest/flank-arm-start-test.yml | 36 + .../taskcluster/androidTest/flank-arm64-v8a.yml | 36 + .../taskcluster/androidTest/flank-x86.yml | 36 + .../androidTest/parse-ui-test-fromfile.py | 97 + .../taskcluster/androidTest/parse-ui-test.py | 89 + .../taskcluster/androidTest/robo-test.sh | 101 + .../automation/taskcluster/androidTest/ui-test.sh | 147 + mobile/android/focus-android/build.gradle | 181 + mobile/android/focus-android/codecov.yml | 22 + mobile/android/focus-android/docs/Adjust-Usage.md | 66 + .../focus-android/docs/Architecture-Decisions.md | 92 + .../focus-android/docs/Battery-Debugging.md | 37 + .../android/focus-android/docs/Content-blocking.md | 58 + .../docs/Crash-Reporting-with-Sentry.md | 147 + .../docs/Development-Custom-GeckoView.md | 90 + .../focus-android/docs/Feature-&-Issue-workflow.md | 54 + .../focus-android/docs/GeckoView-(In-Progress).md | 6 + mobile/android/focus-android/docs/Home.md | 40 + .../android/focus-android/docs/Homescreen-Tips.md | 35 + .../docs/Multisession-architecture.md | 19 + .../docs/Recommended-pre-push-hook.md | 30 + .../android/focus-android/docs/Release-Process.md | 58 + .../android/focus-android/docs/Release-tracks.md | 53 + .../android/focus-android/docs/Removing-strings.md | 29 + .../android/focus-android/docs/Sprint-Process.md | 47 + mobile/android/focus-android/docs/Telemetry.md | 345 + mobile/android/focus-android/docs/UI-Test.md | 54 + mobile/android/focus-android/docs/index.rst | 28 + .../docs/l10n-Screenshot-Generation.md | 29 + mobile/android/focus-android/gradle.properties | 33 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + mobile/android/focus-android/gradlew | 249 + mobile/android/focus-android/gradlew.bat | 92 + mobile/android/focus-android/l10n.toml | 118 + .../plugins/focusdependencies/build.gradle | 25 + .../plugins/focusdependencies/settings.gradle | 19 + .../src/main/java/FocusDependenciesPlugin.kt | 68 + .../android/focus-android/quality/checkstyle.xml | 45 + .../focus-android/quality/detekt-baseline.xml | 406 ++ mobile/android/focus-android/quality/detekt.yml | 789 +++ .../android/focus-android/quality/license.template | 3 + mobile/android/focus-android/quality/pmd-rules.xml | 42 + .../focus-android/quality/pre-push-recommended.sh | 25 + .../focus-android/quality/spotbugs-exclude.xml | 29 + mobile/android/focus-android/settings.gradle | 136 + .../focus-android/tools/data_renewal_generate.py | 189 + .../focus-android/tools/data_renewal_request.py | 55 + .../android/focus-android/tools/docker/Dockerfile | 78 + .../tools/docker/licenses/android-sdk-license | 2 + .../docker/licenses/android-sdk-preview-license | 2 + .../focus-android/tools/gradle/versionCode.gradle | 45 + .../focus-android/tools/update-glean-tags.py | 58 + 852 files changed, 165284 insertions(+) create mode 100644 mobile/android/focus-android/.buildconfig.yml create mode 100644 mobile/android/focus-android/.editorconfig create mode 100644 mobile/android/focus-android/CODEOWNERS create mode 100644 mobile/android/focus-android/CONTRIBUTING.md create mode 100644 mobile/android/focus-android/README.md create mode 100644 mobile/android/focus-android/Screengrabfile create mode 100644 mobile/android/focus-android/app/.experimenter.yaml create mode 100644 mobile/android/focus-android/app/.gitignore create mode 100644 mobile/android/focus-android/app/build.gradle create mode 100644 mobile/android/focus-android/app/lint-baseline.xml create mode 100644 mobile/android/focus-android/app/lint.xml create mode 100644 mobile/android/focus-android/app/metrics.yaml create mode 100644 mobile/android/focus-android/app/nimbus.fml.yaml create mode 100644 mobile/android/focus-android/app/pings.yaml create mode 100644 mobile/android/focus-android/app/proguard-rules.pro create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/audioPage.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/cross-site-cookies.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/download.jpg create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/etpPages/adsTrackers.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/etpPages/analyticsTrackers.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/etpPages/otherTrackers.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/etpPages/socialTrackers.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/genericPage.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/global_privacy_control.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/htmlControls.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/image_test.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/mutedVideoPage.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/rabbit.jpg create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/resources/audioSample.mp3 create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/resources/clip.mp4 create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/same-site-cookies.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/service-worker.js create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/storage_check.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/storage_start.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/tab1.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/tab2.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/tab3.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/test.html create mode 100644 mobile/android/focus-android/app/src/androidTest/assets/videoPage.html create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/AddToHomescreenTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ContextMenusTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/CustomTabTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/DownloadFileTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/EnhancedTrackingProtectionSettingsTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/EraseBrowsingDataTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ErrorPagesTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/FirstRunTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MediaPlaybackTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MozillaSupportPagesTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MultitaskingTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/OldFirstRunTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/OpenInExternalBrowserDialogueTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SafeBrowsingTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SearchTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsAdvancedTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsGeneralTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsPrivacyTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ShortcutsTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SitePermissionsTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SwitchContextTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ThreeDotMainMenuTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/URLAutocompleteTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/AddToHomeScreenRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/BrowserRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/CustomTabRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/DownloadRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/HomeScreenRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/NotificationRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SearchRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsAdvancedMenuRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsGeneralMenuRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsMozillaMenuRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsPrivacyMenuRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsSearchMenuRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsSitePermissionsRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SiteSecurityInfoSheetRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/TabsTrayRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/ThreeDotMainMenuRobot.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/Constants.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/DeleteFilesHelper.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/EspressoHelper.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/FeatureSettingsHelper.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/HostScreencapScreenshotStrategy.java create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MainActivityTestRule.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MockLocationUpdatesRule.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MockWebServerHelper.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/RetryTestRule.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/StringsHelper.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/TestAssetHelper.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/TestHelper.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/ext/WaitNotNull.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/idlingResources/RecyclerViewIdlingResource.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/idlingResources/SessionLoadedIdlingResource.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/privacy/GlobalPrivacyControlTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/privacy/LocalSessionStorageTest.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/AllowListScreenshots.java create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/BrowserScreenScreenshots.java create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/ErrorPagesScreenshots.java create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/FirstRunScreenshots.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/HomeScreenScreenshots.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/NotificationScreenshots.java create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/ScreenshotTest.java create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/SettingsScreenshots.kt create mode 100644 mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/testAnnotations/SmokeTest.kt create mode 100644 mobile/android/focus-android/app/src/beta/res/drawable-v24/ic_splash_screen.xml create mode 100644 mobile/android/focus-android/app/src/beta/res/drawable/ic_splash_screen.png create mode 100644 mobile/android/focus-android/app/src/beta/res/values-night/colors.xml create mode 100644 mobile/android/focus-android/app/src/debug/AndroidManifest.xml create mode 100644 mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/DebugFocusApplication.kt create mode 100644 mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/utils/AdjustHelper.java create mode 100644 mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/web/Config.kt create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/ic_launcher-playstore.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/java/org/mozilla/focus/utils/AdjustHelper.java create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/drawable-land/dark_background.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/drawable-v24/icon_foreground.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/drawable/dark_background.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/drawable/ic_launcher_background.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/drawable/onboarding_logo.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/drawable/wordmark2.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/values/app.xml create mode 100644 mobile/android/focus-android/app/src/focusBeta/res/xml-v25/shortcuts.xml create mode 100644 mobile/android/focus-android/app/src/focusDebug/res/xml-v25/shortcuts.xml create mode 100644 mobile/android/focus-android/app/src/focusNightly/res/values/app.xml create mode 100644 mobile/android/focus-android/app/src/focusNightly/res/xml-v25/shortcuts.xml create mode 100644 mobile/android/focus-android/app/src/focusRelease/AndroidManifest.xml create mode 100644 mobile/android/focus-android/app/src/focusRelease/java/org/mozilla/focus/utils/AdjustHelper.java create mode 100644 mobile/android/focus-android/app/src/focusRelease/java/org/mozilla/focus/web/Config.kt create mode 100644 mobile/android/focus-android/app/src/focusRelease/res/xml-v25/shortcuts.xml create mode 100644 mobile/android/focus-android/app/src/klar/res/drawable/background_gradient_dark.xml create mode 100644 mobile/android/focus-android/app/src/klar/res/drawable/wordmark2.xml create mode 100644 mobile/android/focus-android/app/src/klar/res/values/app.xml create mode 100644 mobile/android/focus-android/app/src/klarBeta/java/org/mozilla/focus/utils/AdjustHelper.java create mode 100644 mobile/android/focus-android/app/src/klarBeta/res/drawable/onboarding_logo.xml create mode 100644 mobile/android/focus-android/app/src/klarBeta/res/xml-v25/shortcuts.xml create mode 100644 mobile/android/focus-android/app/src/klarDebug/res/xml-v25/shortcuts.xml create mode 100644 mobile/android/focus-android/app/src/klarNightly/drawable-v24/icon_foreground.xml create mode 100644 mobile/android/focus-android/app/src/klarNightly/drawable/background_gradient_dark.xml create mode 100644 mobile/android/focus-android/app/src/klarNightly/drawable/icon_background.xml create mode 100644 mobile/android/focus-android/app/src/klarNightly/drawable/toolbar_url_background.xml create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/klarNightly/res/xml-v25/shortcuts.xml create mode 100644 mobile/android/focus-android/app/src/klarRelease/java/org/mozilla/focus/utils/AdjustHelper.java create mode 100644 mobile/android/focus-android/app/src/klarRelease/java/org/mozilla/focus/web/Config.kt create mode 100644 mobile/android/focus-android/app/src/klarRelease/res/xml-v25/shortcuts.xml create mode 100644 mobile/android/focus-android/app/src/main/AndroidManifest.xml create mode 100644 mobile/android/focus-android/app/src/main/assets/error_style.css create mode 100644 mobile/android/focus-android/app/src/main/assets/style.css create mode 100644 mobile/android/focus-android/app/src/main/ic_launcher-playstore.png create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/Components.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/FocusApplication.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/CrashListActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/CustomTabActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/EraseAndOpenShortcutActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/EraseShortcutActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/InstallFirefoxActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/IntentReceiverActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/TextActionActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/animation/TransitionDrawableGroup.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/appreview/AppReviewStep.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/appreview/AppReviewUtils.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteAddFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteCustomDomainsPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteDefaultDomainsPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteDomainFormatter.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteListFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteRemoveFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentCompose.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/LockObserver.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/BlockedTrackersMiddleware.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/LocalizedContent.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserMenuController.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegration.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/FindInPageIntegration.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/FullScreenIntegration.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/NavigationButtonsIntegration.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cfr/CfrMiddleware.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/components/EngineProvider.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/contextmenu/ContextMenuCandidates.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerOption.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerRejectAllPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerExceptionDetailsSwitch.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerDetailsPanel.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerItem.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerMiddleware.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerStatus.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerStore.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/DefaultCookieBannerReducerInteractor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/customtabs/CustomTabsService.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/downloads/DownloadService.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/AppContentInterceptor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/ClientWrapper.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/EngineSharedPreferencesListener.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/SanityCheckMiddleware.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/exceptions/ExceptionsListFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/exceptions/ExceptionsRemoveFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/experiments/NimbusSetup.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Activity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/AndroidViewModel.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserStore.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/ContentState.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Context.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Fragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/PreferenceFragmentCompat.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/SessionState.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/String.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Uri.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/firstrun/FirstrunCardView.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/firstrun/FirstrunPagerAdapter.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/AddToHomescreenDialogFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/BaseFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/BrowserFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/CrashReporterFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/FirstrunFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/UrlInputFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/about/AboutFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/about/SecretSettingsUnlocker.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/onboarding/OnboardingController.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/onboarding/OnboardingFirstFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/onboarding/OnboardingFirstScreenCompose.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/onboarding/OnboardingInteractor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/onboarding/OnboardingSecondFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/onboarding/OnboardingSecondScreenCompose.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/onboarding/OnboardingStep.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/onboarding/OnboardingStorage.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/input/InputToolbarIntegration.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/LocaleManager.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/Locales.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/DefaultLanguageScreenInteractor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/Language.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/LanguageFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/LanguageListItem.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/LanguageMiddleware.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/LanguageScreenStore.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/LanguageStorage.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/LocaleDescriptor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/locale/screen/LocaleFragmentCompose.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/media/MediaSessionService.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/menu/ToolbarMenu.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/menu/browser/CustomTabMenu.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/menu/browser/DefaultBrowserMenu.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/menu/home/HomeMenu.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/menu/home/HomeMenuItem.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/navigation/MainActivityNavigation.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/navigation/Navigator.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/navigation/StoreLink.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/open/AppAdapter.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/open/AppViewHolder.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/open/InstallBannerViewHolder.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/open/OpenWithFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/perf/Performance.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/search/ManualAddSearchEnginePreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/search/MultiselectSearchEngineListPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/search/RadioSearchEngineListPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/search/SearchEngineListPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/search/SearchEnginePreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/search/SearchFilterMiddleware.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/search/SearchMigration.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchsuggestions/SearchSuggestionsPreferences.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchsuggestions/SearchSuggestionsViewModel.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchsuggestions/ui/SearchOverlay.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchsuggestions/ui/SearchSuggestionsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchsuggestions/ui/SearchSuggestionsPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchwidget/ExternalIntentNavigation.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchwidget/PromoteSearchWidgetDialogCompose.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetProvider.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetUtils.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/searchwidget/VoiceSearchActivity.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/session/IntentProcessor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/session/PrivateNotificationFeature.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/session/SessionNotificationService.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/session/VisibilityLifeCycleCallback.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/session/ui/TabViewHolder.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/session/ui/TabsAdapter.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/session/ui/TabsPopup.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/AboutLibrariesFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/BaseComposeFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/BaseSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/BaseSettingsLikeFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/GeneralSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/HttpsOnlyModePreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/InstalledSearchEnginesSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/LearnMoreSwitchPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/ManualAddSearchEngineSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/MozillaSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/RadioButtonPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/RemoveSearchEnginesSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/SafeBrowsingSwitchPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/SearchSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/SettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/StatePreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/advanced/AdvancedSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/advanced/SecretSettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/advanced/SharedPreferenceUpdater.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/SitePermissionOption.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/SitePermissionsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/DefaultSitePermissionOptionsScreenInteractor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/HardwarePermissionCheckFeature.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/SitePermission.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/SitePermissionOptionListItem.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/SitePermissionOptionsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/SitePermissionOptionsFragmentCompose.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/SitePermissionOptionsScreenStore.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/SitePermissionOptionsStorage.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/permissions/permissionoptions/SitePermissionOptionsStorageMiddleware.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/ConnectionDetailsPanel.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/PreferenceSwitch.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/PreferenceToolTipCompose.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/PrivacySecuritySettingsFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/TrackingProtectionPanel.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/studies/StudiesAdapter.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/studies/StudiesFragment.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/studies/StudiesListItem.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/studies/StudiesRecyclerView.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/studies/StudiesViewHolder.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/settings/privacy/studies/StudiesViewModel.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/shortcut/HomeScreen.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/shortcut/IconGenerator.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/state/AppAction.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/state/AppReducer.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/state/AppState.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/state/AppStore.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/ActivationPing.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/BrowsersCache.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/FactsProcessor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/FenixProductDetector.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/GleanMetricsService.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/MetricsService.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/ProfilerMarkerFactProcessor.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/TelemetryMiddleware.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/startuptelemetry/AppStartReasonProvider.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/startuptelemetry/DefaultActivityLifecycleCallbacks.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/startuptelemetry/StartupActivityLog.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/startuptelemetry/StartupPathProvider.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/startuptelemetry/StartupStateProvider.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/telemetry/startuptelemetry/StartupTypeTelemetry.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/theme/Theme.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/topsites/DefaultTopSitesStorage.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/topsites/DefaultTopSitesView.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/topsites/RenameTopSiteDialog.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/topsites/TopSites.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/topsites/TopSitesOverlay.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/dialog/FocusDialog.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/menu/CustomDropdownMenu.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/menu/MenuItem.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/theme/FocusColors.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/theme/FocusDimensions.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/theme/FocusTheme.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/theme/FocusTypography.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/AppConstants.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/ClickableSubstringLink.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/Features.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/FocusSnackbar.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/FocusSnackbarDelegate.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/HtmlLoader.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/IntentUtils.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/OneShotOnPreDrawListener.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/SearchUtils.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/Settings.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/SupportUtils.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/utils/ViewUtils.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/AboutPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/CookiesPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/DefaultBrowserPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/LocaleListPreference.java create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/MozillaPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/ResizableKeyboardCoordinatorLayout.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/ResizableKeyboardLinearLayout.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/ResizableKeyboardViewDelegate.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/SwitchWithDescription.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/widget/TelemetrySwitchPreference.kt create mode 100644 mobile/android/focus-android/app/src/main/res/anim/erase_animation.xml create mode 100644 mobile/android/focus-android/app/src/main/res/anim/fab_reveal.xml create mode 100644 mobile/android/focus-android/app/src/main/res/anim/fade_in.xml create mode 100644 mobile/android/focus-android/app/src/main/res/anim/fade_out.xml create mode 100644 mobile/android/focus-android/app/src/main/res/color/preference_title_text.xml create mode 100644 mobile/android/focus-android/app/src/main/res/color/selected_search_engine_state.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-hdpi/focus_search_widget.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-hdpi/focus_search_widget_promote_dialog.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-hdpi/focus_snackbar_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-hdpi/onboarding_img1.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-hdpi/onboarding_img2.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-hdpi/onboarding_img3.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-hdpi/onboarding_img4.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-land-night/home_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-land/dark_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-land/home_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-night-hdpi/focus_search_widget.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-night-hdpi/focus_search_widget_promote_dialog.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-night/home_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-nodpi/ic_homescreen_shape.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-v24/ic_splash_screen.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-xhdpi/onboarding_img1.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-xhdpi/onboarding_img2.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-xhdpi/onboarding_img3.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-xhdpi/onboarding_img4.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-xxhdpi/onboarding_img1.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-xxhdpi/onboarding_img2.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-xxhdpi/onboarding_img3.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable-xxhdpi/onboarding_img4.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/background_gradient.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/background_install_banner.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/background_list_item_current_session.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/background_list_item_session.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/background_open_in_item.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/background_search_suggestion_section.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/background_snackbar.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/context_menu_navigation_view_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/dark_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/dialog_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/dialog_warning_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/find_in_page_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/firstrun_button_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/foreground_list_item_erase.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/highlight_dot.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/home_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_arrowhead_down.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_arrowhead_up.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_autoplay_enabled.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_back_button.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_camera_enabled.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_check.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_cookies_disable.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_developer.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_download.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_error_session_crashed.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_favorite.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_fingerprint.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_firefox.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_info.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_internet.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_language.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_link.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_menu.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_mozilla.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_notification.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_reorder.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_shortcut_erase.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_splash_screen.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/ic_tab_new.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/indicator_onboarding.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/indicator_onboarding_default.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/indicator_onboarding_selected.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/menu_item_dark_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/mozac_ic_broken_lock.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/onboarding_logo.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/onboarding_second_screen_icon.png create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/photon_progressbar.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/preference_foreground_disabled.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/preference_multiselect_search_engine_foreground.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/scrollbar_thumb.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/tab_number_border.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/toolbar_url_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/top_rounded_corners_bottom_sheet_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/urlbar_background.xml create mode 100644 mobile/android/focus-android/app/src/main/res/drawable/wordmark2.xml create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis.xml create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_black.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_blackitalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_bold.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_bolditalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_extrabold.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_extrabolditalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_extralighitalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_extralight.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_light.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_lightitalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_medium.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_mediumitalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_regular.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_regularitalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_semibold.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_semibolditalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_thin.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/font/metropolis_thinitalic.ttf create mode 100644 mobile/android/focus-android/app/src/main/res/layout/active_study_item.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/activity_customtab.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/activity_info.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/activity_main.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/connection_details.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/cookie_banner_reducer_details.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/cookies_preference.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/custom_tab_menu_item.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/dialog_add_to_homescreen2.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/dialog_full_screen_notification.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/dialog_tracking_protection_sheet.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/firstrun_page.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/focus_preference.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/focus_preference_category_no_title.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/focus_preference_compose_layout.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/focus_preference_left_checkbox.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/focus_preference_new_tab.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/focus_preference_no_icon.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/focus_snackbar.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_about.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_about_libraries.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_autocomplete_add_domain.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_autocomplete_customdomains.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_browser.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_crash_reporter.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_exceptions_domains.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_firstrun.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_info.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_open_with.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_search_suggestions.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_studies.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/fragment_urlinput.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/item_add_custom_domain.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/item_app.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/item_custom_domain.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/item_indicator_menu_button.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/item_install_banner.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/item_session.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/menu_item.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/menu_navigation.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/popup_tabs.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/preference_default_browser.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/preference_manual_add_search_engine.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/preference_radio_button.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/preference_screen_header_layout.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/preference_search_engine_chooser.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/preference_section_header_layout.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/preference_switch_learn_more.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/search_engine_checkbox_button.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/search_engine_radio_button.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/studies_section_item.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/switch_with_description.xml create mode 100644 mobile/android/focus-android/app/src/main/res/layout/toolbar.xml create mode 100644 mobile/android/focus-android/app/src/main/res/menu/menu_autocomplete_add.xml create mode 100644 mobile/android/focus-android/app/src/main/res/menu/menu_autocomplete_list.xml create mode 100644 mobile/android/focus-android/app/src/main/res/menu/menu_autocomplete_remove.xml create mode 100644 mobile/android/focus-android/app/src/main/res/menu/menu_exceptions_list.xml create mode 100644 mobile/android/focus-android/app/src/main/res/menu/menu_remove_search_engines.xml create mode 100644 mobile/android/focus-android/app/src/main/res/menu/menu_search_engine_manual_add.xml create mode 100644 mobile/android/focus-android/app/src/main/res/menu/menu_search_engines.xml create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/main/res/raw/about.html create mode 100644 mobile/android/focus-android/app/src/main/res/raw/gpl.html create mode 100644 mobile/android/focus-android/app/src/main/res/raw/initial_experiments.json create mode 100644 mobile/android/focus-android/app/src/main/res/raw/licenses.html create mode 100644 mobile/android/focus-android/app/src/main/res/raw/rights.html create mode 100644 mobile/android/focus-android/app/src/main/res/transition/firstrun_exit.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ace/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-af/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-am/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-an/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-anp/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ar/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ast/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ay/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-az/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-be/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-bg/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-bn/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-bo/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-bs/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ca/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-cak/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-co/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-cs/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-cy/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-da/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-de/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-dsb/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-el/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-en-rCA/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-en-rGB/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-eo/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-es-rAR/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-es-rCL/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-es-rES/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-es-rMX/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-et/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-eu/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-fa/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-fi/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-fr/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-fur/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-fy-rNL/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ga-rIE/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-gl/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-gu-rIN/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-hi-rIN/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-hr/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-hsb/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-hu/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-hus/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-hy-rAM/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ia/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-in/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-is/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-it/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-iw/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ixl/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ja/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-jv/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ka/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-kaa/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-kab/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-kk/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ko/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-kw/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-lo/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-lt/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-meh/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-mix/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-mr/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ms/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-my/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-nb-rNO/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ne-rNP/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-night/colors.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-nl/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-nn-rNO/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-nv/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-oc/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-pa-rIN/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-pai/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-pl/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ppl/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-pt-rBR/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-quc/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-quy/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ro/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ru/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-si/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-sk/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-skr/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-sl/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-sn/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-sq/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-sr/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-su/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-sv-rSE/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-sw480dp/dimens.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-sw600dp/dimens.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ta/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-te/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-tg/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-th/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-tr/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-trs/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-tsz/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-tt/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-uk/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-ur/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-vi/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-wo/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-yua/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-zam/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-zh-rCN/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-zh-rHK/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values-zh-rTW/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/app.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/attrs.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/colors.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/configuration.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/dimens.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/fonts.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/ids.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/preference_keys.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/static_strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/strings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/strings_references.xml create mode 100644 mobile/android/focus-android/app/src/main/res/values/styles.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/advanced_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/autocomplete.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/cookie_banner_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/experiments_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/general_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/manual_add_search_engine.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/mozilla_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/privacy_security_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/provider_paths.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/remove_search_engines.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/search_engine_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/search_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/search_widget_info.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/secret_settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/settings.xml create mode 100644 mobile/android/focus-android/app/src/main/res/xml/site_permissions.xml create mode 100644 mobile/android/focus-android/app/src/nightly/ic_launcher-playstore.png create mode 100644 mobile/android/focus-android/app/src/nightly/java/org/mozilla/focus/utils/AdjustHelper.java create mode 100644 mobile/android/focus-android/app/src/nightly/java/org/mozilla/focus/web/Config.kt create mode 100644 mobile/android/focus-android/app/src/nightly/res/drawable-land/dark_background.xml create mode 100644 mobile/android/focus-android/app/src/nightly/res/drawable-v24/ic_splash_screen.xml create mode 100644 mobile/android/focus-android/app/src/nightly/res/drawable/dark_background.xml create mode 100644 mobile/android/focus-android/app/src/nightly/res/drawable/ic_launcher_background.xml create mode 100644 mobile/android/focus-android/app/src/nightly/res/drawable/ic_launcher_foreground.xml create mode 100644 mobile/android/focus-android/app/src/nightly/res/drawable/ic_splash_screen.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/drawable/onboarding_logo.xml create mode 100644 mobile/android/focus-android/app/src/nightly/res/drawable/wordmark2.xml create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-hdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-mdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-xhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 mobile/android/focus-android/app/src/nightly/res/values-night/colors.xml create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/BrowserFragmentTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/TestFocusApplication.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/animation/TransitionDrawableGroupTest.java create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegrationTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FindInPageIntegrationTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FullScreenIntegrationTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/InputToolbarIntegrationTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/cfr/CfrMiddlewareTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/contextmenu/ContextMenuCandidatesTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/experiments/NimbusSetupTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/BrowserToolbarTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/StringTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/UriTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/locale/LocalesTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/menu/BrowserMenuControllerTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingControllerTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingStorageTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchsuggestions/SearchSuggestionsViewModelTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/ExternalIntentNavigationTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/SearchWidgetProviderTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/settings/SearchEngineValidationTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/HomeScreenTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/IconGeneratorTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStorageTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStoreTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionsFragmentTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/GleanMetricsServiceTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/ProfilerMarkerFactProcessorTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupActivityLogTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupPathProviderTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupStateProviderTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupTypeTelemetryTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/topsites/DefaultTopSitesStorageTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/IntentUtilsTest.kt create mode 100644 mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/SupportUtilsTest.kt create mode 100644 mobile/android/focus-android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 mobile/android/focus-android/app/src/test/resources/robolectric.properties create mode 100644 mobile/android/focus-android/app/tags.yaml create mode 100644 mobile/android/focus-android/automation/taskcluster/androidTest/copy-robo-crash-artifacts.py create mode 100644 mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-beta.yml create mode 100644 mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-start-test-robo.yml create mode 100644 mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm-start-test.yml create mode 100644 mobile/android/focus-android/automation/taskcluster/androidTest/flank-arm64-v8a.yml create mode 100644 mobile/android/focus-android/automation/taskcluster/androidTest/flank-x86.yml create mode 100644 mobile/android/focus-android/automation/taskcluster/androidTest/parse-ui-test-fromfile.py create mode 100644 mobile/android/focus-android/automation/taskcluster/androidTest/parse-ui-test.py create mode 100755 mobile/android/focus-android/automation/taskcluster/androidTest/robo-test.sh create mode 100755 mobile/android/focus-android/automation/taskcluster/androidTest/ui-test.sh create mode 100644 mobile/android/focus-android/build.gradle create mode 100644 mobile/android/focus-android/codecov.yml create mode 100644 mobile/android/focus-android/docs/Adjust-Usage.md create mode 100644 mobile/android/focus-android/docs/Architecture-Decisions.md create mode 100644 mobile/android/focus-android/docs/Battery-Debugging.md create mode 100644 mobile/android/focus-android/docs/Content-blocking.md create mode 100644 mobile/android/focus-android/docs/Crash-Reporting-with-Sentry.md create mode 100644 mobile/android/focus-android/docs/Development-Custom-GeckoView.md create mode 100644 mobile/android/focus-android/docs/Feature-&-Issue-workflow.md create mode 100644 mobile/android/focus-android/docs/GeckoView-(In-Progress).md create mode 100644 mobile/android/focus-android/docs/Home.md create mode 100644 mobile/android/focus-android/docs/Homescreen-Tips.md create mode 100644 mobile/android/focus-android/docs/Multisession-architecture.md create mode 100644 mobile/android/focus-android/docs/Recommended-pre-push-hook.md create mode 100644 mobile/android/focus-android/docs/Release-Process.md create mode 100644 mobile/android/focus-android/docs/Release-tracks.md create mode 100644 mobile/android/focus-android/docs/Removing-strings.md create mode 100644 mobile/android/focus-android/docs/Sprint-Process.md create mode 100644 mobile/android/focus-android/docs/Telemetry.md create mode 100644 mobile/android/focus-android/docs/UI-Test.md create mode 100644 mobile/android/focus-android/docs/index.rst create mode 100644 mobile/android/focus-android/docs/l10n-Screenshot-Generation.md create mode 100644 mobile/android/focus-android/gradle.properties create mode 100644 mobile/android/focus-android/gradle/wrapper/gradle-wrapper.jar create mode 100644 mobile/android/focus-android/gradle/wrapper/gradle-wrapper.properties create mode 100755 mobile/android/focus-android/gradlew create mode 100644 mobile/android/focus-android/gradlew.bat create mode 100644 mobile/android/focus-android/l10n.toml create mode 100644 mobile/android/focus-android/plugins/focusdependencies/build.gradle create mode 100644 mobile/android/focus-android/plugins/focusdependencies/settings.gradle create mode 100644 mobile/android/focus-android/plugins/focusdependencies/src/main/java/FocusDependenciesPlugin.kt create mode 100644 mobile/android/focus-android/quality/checkstyle.xml create mode 100644 mobile/android/focus-android/quality/detekt-baseline.xml create mode 100644 mobile/android/focus-android/quality/detekt.yml create mode 100644 mobile/android/focus-android/quality/license.template create mode 100644 mobile/android/focus-android/quality/pmd-rules.xml create mode 100755 mobile/android/focus-android/quality/pre-push-recommended.sh create mode 100644 mobile/android/focus-android/quality/spotbugs-exclude.xml create mode 100644 mobile/android/focus-android/settings.gradle create mode 100755 mobile/android/focus-android/tools/data_renewal_generate.py create mode 100755 mobile/android/focus-android/tools/data_renewal_request.py create mode 100644 mobile/android/focus-android/tools/docker/Dockerfile create mode 100644 mobile/android/focus-android/tools/docker/licenses/android-sdk-license create mode 100644 mobile/android/focus-android/tools/docker/licenses/android-sdk-preview-license create mode 100644 mobile/android/focus-android/tools/gradle/versionCode.gradle create mode 100755 mobile/android/focus-android/tools/update-glean-tags.py (limited to 'mobile/android/focus-android') diff --git a/mobile/android/focus-android/.buildconfig.yml b/mobile/android/focus-android/.buildconfig.yml new file mode 100644 index 0000000000..6b6e2c7fad --- /dev/null +++ b/mobile/android/focus-android/.buildconfig.yml @@ -0,0 +1,165 @@ +projects: + app: + upstream_dependencies: + - browser-domains + - browser-engine-gecko + - browser-errorpages + - browser-icons + - browser-menu + - browser-menu2 + - browser-session-storage + - browser-state + - browser-storage-sync + - browser-tabstray + - browser-thumbnails + - browser-toolbar + - compose-awesomebar + - compose-cfr + - concept-awesomebar + - concept-base + - concept-engine + - concept-fetch + - concept-menu + - concept-storage + - concept-sync + - concept-tabstray + - concept-toolbar + - feature-app-links + - feature-awesomebar + - feature-contextmenu + - feature-customtabs + - feature-downloads + - feature-findinpage + - feature-intent + - feature-media + - feature-prompts + - feature-search + - feature-session + - feature-sitepermissions + - feature-tabs + - feature-toolbar + - feature-top-sites + - feature-webcompat + - feature-webcompat-reporter + - lib-auth + - lib-crash + - lib-crash-sentry + - lib-fetch-okhttp + - lib-publicsuffixlist + - lib-state + - service-digitalassetlinks + - service-glean + - service-location + - service-nimbus + - support-base + - support-images + - support-ktx + - support-license + - support-locale + - support-remotesettings + - support-rusthttp + - support-rustlog + - support-test + - support-test-libstate + - support-utils + - support-webextensions + - tooling-lint + - ui-autocomplete + - ui-colors + - ui-icons + - ui-tabcounter + - ui-widgets +variants: +- apks: + - abi: arm64-v8a + fileName: app-focus-arm64-v8a-debug.apk + - abi: armeabi-v7a + fileName: app-focus-armeabi-v7a-debug.apk + - abi: x86 + fileName: app-focus-x86-debug.apk + - abi: x86_64 + fileName: app-focus-x86_64-debug.apk + build_type: debug + name: focusDebug +- apks: + - abi: arm64-v8a + fileName: app-klar-arm64-v8a-debug.apk + - abi: armeabi-v7a + fileName: app-klar-armeabi-v7a-debug.apk + - abi: x86 + fileName: app-klar-x86-debug.apk + - abi: x86_64 + fileName: app-klar-x86_64-debug.apk + build_type: debug + name: klarDebug +- apks: + - abi: arm64-v8a + fileName: app-focus-arm64-v8a-release-unsigned.apk + - abi: armeabi-v7a + fileName: app-focus-armeabi-v7a-release-unsigned.apk + - abi: x86 + fileName: app-focus-x86-release-unsigned.apk + - abi: x86_64 + fileName: app-focus-x86_64-release-unsigned.apk + build_type: release + name: focusRelease +- apks: + - abi: arm64-v8a + fileName: app-klar-arm64-v8a-release-unsigned.apk + - abi: armeabi-v7a + fileName: app-klar-armeabi-v7a-release-unsigned.apk + - abi: x86 + fileName: app-klar-x86-release-unsigned.apk + - abi: x86_64 + fileName: app-klar-x86_64-release-unsigned.apk + build_type: release + name: klarRelease +- apks: + - abi: arm64-v8a + fileName: app-focus-arm64-v8a-beta-unsigned.apk + - abi: armeabi-v7a + fileName: app-focus-armeabi-v7a-beta-unsigned.apk + - abi: x86 + fileName: app-focus-x86-beta-unsigned.apk + - abi: x86_64 + fileName: app-focus-x86_64-beta-unsigned.apk + build_type: beta + name: focusBeta +- apks: + - abi: arm64-v8a + fileName: app-klar-arm64-v8a-beta-unsigned.apk + - abi: armeabi-v7a + fileName: app-klar-armeabi-v7a-beta-unsigned.apk + - abi: x86 + fileName: app-klar-x86-beta-unsigned.apk + - abi: x86_64 + fileName: app-klar-x86_64-beta-unsigned.apk + build_type: beta + name: klarBeta +- apks: + - abi: arm64-v8a + fileName: app-focus-arm64-v8a-nightly-unsigned.apk + - abi: armeabi-v7a + fileName: app-focus-armeabi-v7a-nightly-unsigned.apk + - abi: x86 + fileName: app-focus-x86-nightly-unsigned.apk + - abi: x86_64 + fileName: app-focus-x86_64-nightly-unsigned.apk + build_type: nightly + name: focusNightly +- apks: + - abi: arm64-v8a + fileName: app-klar-arm64-v8a-nightly-unsigned.apk + - abi: armeabi-v7a + fileName: app-klar-armeabi-v7a-nightly-unsigned.apk + - abi: x86 + fileName: app-klar-x86-nightly-unsigned.apk + - abi: x86_64 + fileName: app-klar-x86_64-nightly-unsigned.apk + build_type: nightly + name: klarNightly +- apks: + - abi: noarch + fileName: app-debug-androidTest.apk + build_type: androidTest + name: androidTest diff --git a/mobile/android/focus-android/.editorconfig b/mobile/android/focus-android/.editorconfig new file mode 100644 index 0000000000..3232ddd4a3 --- /dev/null +++ b/mobile/android/focus-android/.editorconfig @@ -0,0 +1,5 @@ +[*.{kt,kts}] +ij_kotlin_allow_trailing_comma_on_call_site=true +ij_kotlin_allow_trailing_comma=true + +ktlint_standard_filename = disabled \ No newline at end of file diff --git a/mobile/android/focus-android/CODEOWNERS b/mobile/android/focus-android/CODEOWNERS new file mode 100644 index 0000000000..cedf4a9cf8 --- /dev/null +++ b/mobile/android/focus-android/CODEOWNERS @@ -0,0 +1,5 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +* @mozilla-mobile/focus-codeowners diff --git a/mobile/android/focus-android/CONTRIBUTING.md b/mobile/android/focus-android/CONTRIBUTING.md new file mode 100644 index 0000000000..597b7bc1d7 --- /dev/null +++ b/mobile/android/focus-android/CONTRIBUTING.md @@ -0,0 +1,4 @@ +# Contributing to Focus for Android + +Please see our guidelines in our shared-docs repo: +https://github.com/mozilla-mobile/shared-docs/blob/main/android/CONTRIBUTING.md diff --git a/mobile/android/focus-android/README.md b/mobile/android/focus-android/README.md new file mode 100644 index 0000000000..c3ca39d2fe --- /dev/null +++ b/mobile/android/focus-android/README.md @@ -0,0 +1,119 @@ +# Firefox Focus for Android + +_Browse like no one’s watching. The new Firefox Focus automatically blocks a wide range of online trackers — from the moment you launch it to the second you leave it. Easily erase your history, passwords and cookies, so you won’t get followed by things like unwanted ads._ + +Firefox Focus provides automatic ad blocking and tracking protection on an easy-to-use private browser. + +Get it on Google Play + +* [Google Play: Firefox Focus (Global)](https://play.google.com/store/apps/details?id=org.mozilla.focus) +* [Google Play: Firefox Klar (Germany, Austria & Switzerland)](https://play.google.com/store/apps/details?id=org.mozilla.klar) +* [Download APKs](https://github.com/mozilla-mobile/focus-android/releases) + + + +## Getting Involved + + +We encourage you to participate in this open source project. We love Pull Requests, Bug Reports, ideas, (security) code reviews or any other kind of positive contribution. + +Before you attempt to make a contribution please read the [Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/). + +* [Guide to Contributing](https://github.com/mozilla-mobile/shared-docs/blob/main/android/CONTRIBUTING.md) (**New contributors start here!**) + +* [View current Issues](https://github.com/mozilla-mobile/focus-android/issues), [view current Pull Requests](https://github.com/mozilla-mobile/focus-android/pulls), or [file a security issue][sec issue]. + +* Opt-in to our Mailing List [firefox-focus-public@](https://mail.mozilla.org/listinfo/firefox-focus-public) to keep up to date. + +* [View the Wiki](https://github.com/mozilla-mobile/focus-android/wiki). + +**Beginners!** - Watch out for [Issues with the "Good First Issue" label](https://github.com/mozilla-mobile/focus-android/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). These are easy bugs that have been left for first timers to have a go, get involved and make a positive contribution to the project! + +## Build Instructions + + +1. Clone or Download the repository: + + ```shell + git clone https://github.com/mozilla-mobile/focus-android + ``` + +2. Import the project into Android Studio **or** build on the command line: + + ```shell + ./gradlew clean app:assembleFocusDebug + ``` + +3. Make sure to select the correct build variant in Android Studio: +**focusArmDebug** for ARM +**focusX86Debug** for X86 +**focusAarch64Debug** for ARM64 + +## local.properties helpers +You can speed up or enhance local development by setting a few helper flags available in `local.properties` which will be made easily available as gradle properties. + +### Automatically sign release builds +To sign your release builds with your debug key automatically, add the following to `/local.properties`: + +```sh +autosignReleaseWithDebugKey +``` + +With this line, release build variants will automatically be signed with your debug key (like debug builds), allowing them to be built and installed directly through Android Studio or the command line. + +This is helpful when you're building release variants frequently, for example to test feature flags and or do performance analyses. + +### Building debuggable release variants + +Nightly, Beta and Release variants are getting published to Google Play and therefore are not debuggable. To locally create debuggable builds of those variants, add the following to `/local.properties`: + +```sh +debuggable +``` + +### Auto-publication workflow for application-services and glean +If you're making changes to these projects and want to test them in Focus, auto-publication workflow is the fastest, most reliable +way to do that. + +In `local.properties`, specify a relative path to your local `glean` and/or `application-services` projects. E.g.: +- `autoPublish.glean.dir=../glean` +- `autoPublish.application-services.dir=../application-services` + +Once these flags are set, your Focus builds will include any local modifications present in these projects. + +See a [demo of auto-publication workflow in action](https://www.youtube.com/watch?v=qZKlBzVvQGc). + +## Pre-push hooks +To reduce review turn-around time, we'd like all pushes to run tests locally. We'd +recommend you use our provided pre-push hook in `quality/pre-push-recommended.sh`. +Using this hook will guarantee your hook gets updated as the repository changes. +This hook tries to run as much as possible without taking too much time. + +To add it, run this command from the project root: +```sh +ln -s ../../quality/pre-push-recommended.sh .git/hooks/pre-push +``` + +To push without running the pre-push hook (e.g. doc updates): +```sh +git push --no-verify +``` + +## Test Channel on Google PlayStore +To get Focus Nightly on your device, follow these steps: + +1) Visit https://groups.google.com/g/firefox-focus-pre-release and join the Google Group +2) After you have joined the group opt-in to receive Nightly builds, again with the same Google account: https://play.google.com/apps/testing/org.mozilla.focus.nightly +3) Download Firefox Focus (Nightly) from Google Play: https://play.google.com/store/apps/details?id=org.mozilla.focus.nightly + +Make sure you use the same Google Account for both steps. + + +## 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/ + +[sec issue]: https://bugzilla.mozilla.org/enter_bug.cgi?assigned_to=nobody%40mozilla.org&bug_file_loc=http%3A%2F%2F&bug_ignored=0&bug_severity=normal&bug_status=NEW&cf_fx_iteration=---&cf_fx_points=---&component=Security%3A%20Android&contenttypemethod=autodetect&contenttypeselection=text%2Fplain&defined_groups=1&flag_type-4=X&flag_type-607=X&flag_type-791=X&flag_type-800=X&flag_type-803=X&form_name=enter_bug&groups=firefox-core-security&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Unspecified&priority=--&product=Focus&rep_platform=Unspecified&target_milestone=---&version=--- diff --git a/mobile/android/focus-android/Screengrabfile b/mobile/android/focus-android/Screengrabfile new file mode 100644 index 0000000000..fd85f68dba --- /dev/null +++ b/mobile/android/focus-android/Screengrabfile @@ -0,0 +1,22 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This is the template for our Screengrabfile used in automation. +# tools/taskcluster/generate_screengrab_config.py will read this +# file and generate the final configuration that we use inside +# a taskcluster task. + +app_package_name 'org.mozilla.focus.debug' +use_tests_in_packages ['org.mozilla.focus.screenshots'] + +app_apk_path('~/focus-android/focus-android/app/build/outputs/apk/focus/debug/app-focus-x86-debug.apk') +tests_apk_path('/focus-android/focus-android/app/build/outputs/apk/androidTest/focus/debug/app-focus-debug-androidTest.apk') + +locales(['en-US', 'fr-FR', 'it-IT', 'de-DE', 'ja', 'ru', 'zh-CN', 'zh-TW', 'ko']) + +# Clear all previous screenshots locally. Technically not needed in automation. +# But it's easier to debug this on a local device if there are no old screenshots +# hanging around. +clear_previous_screenshots true +reinstall_app true \ No newline at end of file diff --git a/mobile/android/focus-android/app/.experimenter.yaml b/mobile/android/focus-android/app/.experimenter.yaml new file mode 100644 index 0000000000..fa6a1d67f4 --- /dev/null +++ b/mobile/android/focus-android/app/.experimenter.yaml @@ -0,0 +1,23 @@ +--- +cookie-banner: + description: Nimbus feature name intended to control the cookie banner handling in the app. + hasExposure: true + exposureDescription: "" + variables: + is-cookie-handling-enabled: + type: boolean + description: "If 'true' , the app will show the settings part for cookie banner handling" +onboarding: + description: Nimbus feature name intended to control the onboarding plus all CFRs in the app. + hasExposure: true + exposureDescription: "" + variables: + is-cfr-enabled: + type: boolean + description: "If `true`, the app will show the cfrs" + is-enabled: + type: boolean + description: "If `true`, the app will show the new onboarding screen" + is-promote-search-widget-dialog-enabled: + type: boolean + description: "If `true`, the app will show the new dialog for promote search widget" diff --git a/mobile/android/focus-android/app/.gitignore b/mobile/android/focus-android/app/.gitignore new file mode 100644 index 0000000000..211d729623 --- /dev/null +++ b/mobile/android/focus-android/app/.gitignore @@ -0,0 +1,3 @@ +/build + +src/main/java/org/mozilla/focus/generated/ diff --git a/mobile/android/focus-android/app/build.gradle b/mobile/android/focus-android/app/build.gradle new file mode 100644 index 0000000000..3a82291ce9 --- /dev/null +++ b/mobile/android/focus-android/app/build.gradle @@ -0,0 +1,788 @@ +plugins { + id "com.jetbrains.python.envs" version "$python_envs_plugin" +} + +if (findProject(":geckoview") != null) { + buildDir "${topobjdir}/gradle/build/mobile/android/focus-android" +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' +apply plugin: 'jacoco' +apply plugin: 'com.google.android.gms.oss-licenses-plugin' + +def versionCodeGradle = "$project.rootDir/tools/gradle/versionCode.gradle" +if (findProject(":geckoview") != null) { + versionCodeGradle = "$project.rootDir/mobile/android/focus-android/tools/gradle/versionCode.gradle" +} +apply from: versionCodeGradle + +if (findProject(":geckoview") != null) { + apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle" +} + +import com.android.build.api.variant.FilterConfiguration +import groovy.json.JsonOutput +import org.gradle.internal.logging.text.StyledTextOutput.Style +import org.gradle.internal.logging.text.StyledTextOutputFactory +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +import static org.gradle.api.tasks.testing.TestResult.ResultType + +android { + if (project.hasProperty("testBuildType")) { + // Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=beta ..) + // in order to run UI tests against other build variants than debug in automation. + testBuildType project.property("testBuildType") + } + + defaultConfig { + applicationId "org.mozilla" + minSdkVersion config.minSdkVersion + compileSdk config.compileSdkVersion + targetSdkVersion config.targetSdkVersion + versionCode 11 // This versionCode is "frozen" for local builds. For "release" builds we + // override this with a generated versionCode at build time. + // The versionName is dynamically overridden for all the build variants at build time. + versionName Config.generateDebugVersionName() + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments clearPackageData: 'true' + // See override in release builds for why it's blank. + buildConfigField "String", "VCS_HASH", "\"\"" + + vectorDrawables.useSupportLibrary = true + } + + bundle { + language { + // Because we have runtime language selection we will keep all strings and languages + // in the base APKs. + enableSplit = false + } + } + + lint { + lintConfig file("lint.xml") + baseline file("lint-baseline.xml") + } + + // We have a three dimensional build configuration: + // BUILD TYPE (debug, release) X PRODUCT FLAVOR (focus, klar) + + buildTypes { + release { + // We allow disabling optimization by passing `-PdisableOptimization` to gradle. This is used + // in automation for UI testing non-debug builds. + shrinkResources !project.hasProperty("disableOptimization") + minifyEnabled !project.hasProperty("disableOptimization") + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + matchingFallbacks = ['release'] + buildConfigField "String", "VCS_HASH", "\"${Config.getVcsHash()}\"" + + if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) { + println ("All builds will be automatically signed with the debug key") + signingConfig signingConfigs.debug + } + + if (gradle.hasProperty("localProperties.debuggable")) { + println ("All builds will be debuggable") + debuggable true + } + } + debug { + applicationIdSuffix ".debug" + matchingFallbacks = ['debug'] + } + beta { + initWith release + applicationIdSuffix ".beta" + // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 + manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus_beta"] + } + nightly { + initWith release + applicationIdSuffix ".nightly" + // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 + manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus_nightly"] + } + } + testOptions { + execution 'ANDROIDX_TEST_ORCHESTRATOR' + animationsDisabled = true + unitTests { + includeAndroidResources = true + } + } + + buildFeatures { + compose true + viewBinding true + buildConfig true + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose_compiler + } + + if (findProject(":geckoview") != null) { + project.configureProductFlavors.delegate = it + project.configureProductFlavors() + } + + flavorDimensions.add("product") + + productFlavors { + // In most countries we are Firefox Focus - but in some we need to be Firefox Klar + focus { + dimension "product" + + applicationIdSuffix ".focus" + + // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 + manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_focus"] + } + klar { + dimension "product" + + applicationIdSuffix ".klar" + + // This is used when the user selects text in other third-party apps. See https://github.com/mozilla-mobile/focus-android/issues/6478 + manifestPlaceholders = [textSelectionSearchAction: "@string/text_selection_search_action_klar"] + } + } + + splits { + abi { + enable true + + reset() + + include "x86", "armeabi-v7a", "arm64-v8a", "x86_64" + } + } + + sourceSets { + test { + resources { + // Make the default asset folder available as test resource folder. Robolectric seems + // to fail to read assets for our setup. With this we can just read the files directly + // and do not need to rely on Robolectric. + srcDir "${projectDir}/src/main/assets/" + } + } + + if (findProject(":geckoview") != null) { + // Release + withGeckoBinariesFocusRelease.root = 'src/focusRelease' + withGeckoBinariesKlarRelease.root = 'src/klarRelease' + withoutGeckoBinariesFocusRelease.root = 'src/focusRelease' + withoutGeckoBinariesKlarRelease.root = 'src/klarRelease' + + // Debug + withGeckoBinariesFocusDebug.root = 'src/focusDebug' + withGeckoBinariesKlarDebug.root = 'src/klarDebug' + withoutGeckoBinariesFocusDebug.root = 'src/focusDebug' + withoutGeckoBinariesKlarDebug.root = 'src/klarDebug' + + // Nightly + withGeckoBinariesFocusNightly.root = 'src/focusNightly' + withGeckoBinariesKlarNightly.root = 'src/klarNightly' + withoutGeckoBinariesFocusNightly.root = 'src/focusNightly' + withoutGeckoBinariesKlarNightly.root = 'src/klarNightly' + } else { + // Release + focusRelease.root = 'src/focusRelease' + klarRelease.root = 'src/klarRelease' + + // Debug + focusDebug.root = 'src/focusDebug' + klarDebug.root = 'src/klarDebug' + + // Nightly + focusNightly.root = 'src/focusNightly' + klarNightly.root = 'src/klarNightly' + } + } + packagingOptions { + resources { + pickFirsts += ['META-INF/atomicfu.kotlin_module', 'META-INF/proguard/coroutines.pro'] + } + jniLibs { + useLegacyPackaging true + } + } + + namespace 'org.mozilla.focus' +} + +tasks.withType(KotlinCompile).configureEach { + kotlinOptions.allWarningsAsErrors = true + kotlinOptions.freeCompilerArgs += [ + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlin.RequiresOptIn", + "-Xjvm-default=all" + ] +} + +// ------------------------------------------------------------------------------------------------- +// Generate Kotlin code for the Focus Glean metrics. +// ------------------------------------------------------------------------------------------------- +apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" +apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin" + +nimbus { + // The path to the Nimbus feature manifest file + manifestFile = "nimbus.fml.yaml" + // Map from the variant name to the channel as experimenter and nimbus understand it. + // If nimbus's channels were accurately set up well for this project, then this + // shouldn't be needed. + channels = [ + focusDebug: "debug", + focusNightly: "nightly", + focusBeta: "beta", + focusRelease: "release", + klarDebug: "debug", + klarNightly: "nightly", + klarBeta: "beta", + klarRelease: "release", + withGeckoBinariesFocusDebug: "debug", + withGeckoBinariesFocusNightly: "nightly", + withGeckoBinariesFocusBeta: "beta", + withGeckoBinariesFocusRelease: "release", + withGeckoBinariesKlarDebug: "debug", + withGeckoBinariesKlarNightly: "nightly", + withGeckoBinariesKlarBeta: "beta", + withGeckoBinariesKlarRelease: "release", + withoutGeckoBinariesFocusDebug: "debug", + withoutGeckoBinariesFocusNightly: "nightly", + withoutGeckoBinariesFocusBeta: "beta", + withoutGeckoBinariesFocusRelease: "release", + withoutGeckoBinariesKlarDebug: "debug", + withoutGeckoBinariesKlarNightly: "nightly", + withoutGeckoBinariesKlarBeta: "beta", + withoutGeckoBinariesKlarRelease: "release", + ] + // This is generated by the FML and should be checked into git. + // It will be fetched by Experimenter (the Nimbus experiment website) + // and used to inform experiment configuration. + experimenterManifest = ".experimenter.yaml" +} + +dependencies { + implementation platform(ComponentsDependencies.androidx_compose_bom) + androidTestImplementation platform(ComponentsDependencies.androidx_compose_bom) + + implementation ComponentsDependencies.androidx_appcompat + implementation ComponentsDependencies.androidx_browser + implementation ComponentsDependencies.androidx_cardview + implementation ComponentsDependencies.androidx_compose_ui + implementation ComponentsDependencies.androidx_compose_ui_tooling + implementation ComponentsDependencies.androidx_compose_foundation + implementation ComponentsDependencies.androidx_compose_material + implementation ComponentsDependencies.androidx_compose_runtime_livedata + implementation ComponentsDependencies.androidx_constraintlayout + implementation FocusDependencies.androidx_constraint_layout_compose + implementation ComponentsDependencies.androidx_core_ktx + implementation ComponentsDependencies.androidx_fragment + implementation ComponentsDependencies.androidx_lifecycle_process + implementation ComponentsDependencies.androidx_lifecycle_viewmodel + implementation ComponentsDependencies.androidx_palette + implementation ComponentsDependencies.androidx_preferences + implementation ComponentsDependencies.androidx_recyclerview + implementation ComponentsDependencies.androidx_savedstate + implementation FocusDependencies.androidx_splashscreen + implementation FocusDependencies.androidx_transition + implementation ComponentsDependencies.androidx_work_runtime + implementation ComponentsDependencies.androidx_data_store_preferences + + implementation FocusDependencies.google_play + + implementation ComponentsDependencies.google_material + + implementation ComponentsDependencies.thirdparty_sentry + + implementation project(':browser-engine-gecko') + implementation project(':browser-domains') + implementation project(':browser-errorpages') + implementation project(':browser-icons') + implementation project(':browser-menu') + implementation project(':browser-state') + implementation project(':browser-toolbar') + + implementation project(':concept-awesomebar') + implementation project(':concept-engine') + implementation project(':concept-fetch') + implementation project(':concept-menu') + + implementation project(':compose-awesomebar') + + implementation project(':feature-awesomebar') + implementation project(':feature-app-links') + implementation project(':feature-customtabs') + implementation project(':feature-contextmenu') + implementation project(':feature-downloads') + implementation project(':feature-findinpage') + implementation project(':feature-intent') + implementation project(':feature-prompts') + implementation project(':feature-session') + implementation project(':feature-search') + implementation project(':feature-tabs') + implementation project(':feature-toolbar') + implementation project(':feature-top-sites') + implementation project(':feature-sitepermissions') + implementation project(':lib-crash') + implementation project(':lib-crash-sentry') + implementation project(':lib-state') + implementation project(':feature-media') + implementation project(':lib-auth') + implementation project(':lib-publicsuffixlist') + + implementation project(':service-glean'), { + exclude group: 'org.mozilla.telemetry', module: 'glean-native' + } + implementation project(':service-location') + implementation project(':service-nimbus') + + implementation project(':support-ktx') + implementation project(':support-utils') + implementation project(':support-rusthttp') + implementation project(':support-rustlog') + implementation project(':support-license') + + implementation project(':ui-autocomplete') + implementation project(':ui-colors') + implementation project(':ui-icons') + implementation project(':ui-tabcounter') + implementation project(':ui-widgets') + implementation project(':feature-webcompat') + implementation project(':feature-webcompat-reporter') + implementation project(':support-webextensions') + implementation project(':support-locale') + implementation project(':compose-cfr') + + implementation ComponentsDependencies.kotlin_coroutines + debugImplementation ComponentsDependencies.leakcanary + + focusImplementation FocusDependencies.adjust + focusImplementation FocusDependencies.install_referrer // Required by Adjust + + testImplementation "org.mozilla.telemetry:glean-native-forUnitTests:${project.ext.glean_version}" + + testImplementation FocusDependencies.testing_junit_api + testRuntimeOnly FocusDependencies.testing_junit_engine + testImplementation FocusDependencies.testing_junit_params + testImplementation ComponentsDependencies.testing_robolectric + testImplementation ComponentsDependencies.testing_mockito + testImplementation ComponentsDependencies.testing_coroutines + testImplementation ComponentsDependencies.androidx_work_testing + testImplementation ComponentsDependencies.androidx_arch_core_testing + testImplementation project(':support-test') + testImplementation project(':support-test-libstate') + androidTestImplementation ComponentsDependencies.androidx_espresso_core, { + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestImplementation FocusDependencies.espresso_idling_resource + androidTestImplementation FocusDependencies.espresso_web, { + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestImplementation FocusDependencies.espresso_intents + + + androidTestImplementation ComponentsDependencies.testing_mockwebserver + testImplementation ComponentsDependencies.testing_mockwebserver + testImplementation project(':lib-fetch-okhttp') + + androidTestImplementation FocusDependencies.fastlane + androidTestImplementation FocusDependencies.falcon // Required by fastlane + + androidTestImplementation FocusDependencies.espresso_contrib, { + exclude module: 'appcompat-v7' + exclude module: 'support-v4' + exclude module: 'support-annotations' + exclude module: 'recyclerview-v7' + exclude module: 'design' + exclude module: 'espresso-core' + } + testImplementation ComponentsDependencies.androidx_test_core + testImplementation ComponentsDependencies.androidx_test_runner + testImplementation ComponentsDependencies.androidx_test_rules + + androidTestImplementation ComponentsDependencies.androidx_test_core + androidTestImplementation ComponentsDependencies.androidx_test_junit + androidTestImplementation ComponentsDependencies.androidx_test_uiautomator + androidTestImplementation ComponentsDependencies.androidx_test_runner + androidTestUtil FocusDependencies.androidx_orchestrator + + lintChecks project(':tooling-lint') +} +// ------------------------------------------------------------------------------------------------- +// Dynamically set versionCode (See tools/build/versionCode.gradle +// ------------------------------------------------------------------------------------------------- + +android.applicationVariants.configureEach { variant -> + def buildType = variant.buildType.name + + println("----------------------------------------------") + println("Variant name: " + variant.name) + println("Application ID: " + [variant.applicationId, variant.buildType.applicationIdSuffix].findAll().join()) + println("Build type: " + variant.buildType.name) + println("Flavor: " + variant.flavorName) + + if (buildType == "release" || buildType == "nightly" || buildType == "beta") { + def baseVersionCode = generatedVersionCode + def versionName = buildType == "nightly" ? + "${Config.nightlyVersionName(project)}" : + "${Config.releaseVersionName(project)}" + println("versionName override: $versionName") + + // The Google Play Store does not allow multiple APKs for the same app that all have the + // same version code. Therefore we need to have different version codes for our ARM and x86 + // builds. See https://developer.android.com/studio/publish/versioning + + // Our generated version code now has a length of 9 (See tools/gradle/versionCode.gradle). + // Our x86 builds need a higher version code to avoid installing ARM builds on an x86 device + // with ARM compatibility mode. + + // AAB builds need a version code that is distinct from any APK builds. Since AAB and APK + // builds may run in parallel, AAB and APK version codes might be based on the same + // (minute granularity) time of day. To avoid conflicts, we ensure the minute portion + // of the version code is even for APKs and odd for AABs. + + variant.outputs.each { output -> + def abi = output.getFilter(FilterConfiguration.FilterType.ABI.name()) + def aab = project.hasProperty("aab") + // We use the same version code generator, that we inherited from Fennec, across all channels - even on + // channels that never shipped a Fennec build. + + // ensure baseVersionCode is an even number + if (baseVersionCode % 2) { + baseVersionCode = baseVersionCode + 1 + } + + def versionCodeOverride = baseVersionCode + + if (aab) { + // AAB version code is odd + versionCodeOverride = versionCodeOverride + 1 + println("versionCode for AAB = $versionCodeOverride") + } else { + if (abi == "x86_64") { + versionCodeOverride = versionCodeOverride + 6 + } else if (abi == "x86") { + versionCodeOverride = versionCodeOverride + 4 + } else if (abi == "arm64-v8a") { + versionCodeOverride = versionCodeOverride + 2 + } else if (abi == "armeabi-v7a") { + versionCodeOverride = versionCodeOverride + 0 + } else { + throw new RuntimeException("Unknown ABI: " + abi) + } + println("versionCode for $abi = $versionCodeOverride") + } + + if (versionName != null) { + output.versionNameOverride = versionName + } + output.versionCodeOverride = versionCodeOverride + + } + + } +} + +// ------------------------------------------------------------------------------------------------- +// MLS: Read token from local file if it exists (Only release builds) +// ------------------------------------------------------------------------------------------------- + +android.applicationVariants.configureEach { + print("MLS token: ") + try { + def token = new File("${rootDir}/.mls_token").text.trim() + buildConfigField 'String', 'MLS_TOKEN', '"' + token + '"' + println "(Added from .mls_token file)" + } catch (FileNotFoundException ignored) { + buildConfigField 'String', 'MLS_TOKEN', '""' + println("X_X") + } +} + +// ------------------------------------------------------------------------------------------------- +// Adjust: Read token from local file if it exists (Only release builds) +// ------------------------------------------------------------------------------------------------- + +android.applicationVariants.configureEach { variant -> + def variantName = variant.getName() + + print("Adjust token: ") + + if (variantName.contains("Release") && variantName.contains("focus")) { + try { + def token = new File("${rootDir}/.adjust_token").text.trim() + buildConfigField 'String', 'ADJUST_TOKEN', '"' + token + '"' + println "(Added from .adjust_token file)" + } catch (FileNotFoundException ignored) { + if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) { + buildConfigField 'String', 'ADJUST_TOKEN', '"fake"' + println("fake - only for local development") + } else { + buildConfigField 'String', 'ADJUST_TOKEN', 'null' + println("X_X") + } + } + } else { + buildConfigField 'String', 'ADJUST_TOKEN', 'null' + println("--") + } +} + +// ------------------------------------------------------------------------------------------------- +// Sentry: Read token from local file if it exists (Only release builds) +// ------------------------------------------------------------------------------------------------- + +android.applicationVariants.configureEach { + print("Sentry token: ") + try { + def token = new File("${rootDir}/.sentry_token").text.trim() + buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"' + println "(Added from .sentry_token file)" + } catch (FileNotFoundException ignored) { + buildConfigField 'String', 'SENTRY_TOKEN', '""' + println("X_X") + } +} + +// ------------------------------------------------------------------------------------------------- +// L10N: Generate list of locales +// Focus provides its own (Android independent) locale switcher. That switcher requires a list +// of locale codes. We generate that list here to avoid having to manually maintain a list of locales: +// ------------------------------------------------------------------------------------------------- + +def getEnabledLocales() { + def resDir = file('src/main/res') + + def potentialLanguageDirs = resDir.listFiles(new FilenameFilter() { + @Override + boolean accept(File dir, String name) { + return name.startsWith("values-") + } + }) + + def langs = potentialLanguageDirs.findAll { + // Only select locales where strings.xml exists + // Some locales might only contain e.g. sumo URLS in urls.xml, and should be skipped (see es vs es-ES/es-MX/etc) + return file(new File(it, "strings.xml")).exists() + } .collect { + // And reduce down to actual values-* names + return it.name + } .collect { + return it.substring("values-".length()) + } .collect { + if (it.length() > 3 && it.contains("-r")) { + // Android resource dirs add an "r" prefix to the region - we need to strip that for java usage + // Add 1 to have the index of the r, without the dash + def regionPrefixPosition = it.indexOf("-r") + 1 + + return it.substring(0, regionPrefixPosition) + it.substring(regionPrefixPosition + 1) + } else { + return it + } + }.collect { + return '"' + it + '"' + } + + // en-US is the default language (in "values") and therefore needs to be added separately + langs << "\"en-US\"" + + return langs.sort { it } +} + +// ------------------------------------------------------------------------------------------------- +// Nimbus: Read endpoint from local.properties of a local file if it exists +// ------------------------------------------------------------------------------------------------- + +print("Nimbus endpoint: ") +android.applicationVariants.configureEach { variant -> + def variantName = variant.getName() + + if (!variantName.contains("Debug")) { + try { + def url = new File("${rootDir}/.nimbus").text.trim() + buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"' + println "(Added from .nimbus file)" + } catch (FileNotFoundException ignored) { + buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' + println("X_X") + } + } else if (gradle.hasProperty("localProperties.nimbus.remote-settings.url")) { + def url = gradle.getProperty("localProperties.nimbus.remote-settings.url") + buildConfigField 'String', 'NIMBUS_ENDPOINT', '"' + url + '"' + println "(Added from local.properties file)" + } else { + buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' + println("--") + } +} + +def generatedLocaleListDir = 'src/main/java/org/mozilla/focus/generated' +def generatedLocaleListFilename = 'LocalesList.kt' + +tasks.register('generateLocaleList') { + doLast { + def dir = file(generatedLocaleListDir) + dir.mkdir() + def localeList = file(new File(dir, generatedLocaleListFilename)) + + localeList.delete() + localeList.createNewFile() + localeList << "package org.mozilla.focus.generated" << "\n" << "\n" + localeList << "import java.util.Collections" << "\n" + localeList << "\n" + localeList << "/**" + localeList << "\n" + localeList << " * Provides a list of bundled locales based on the language files in the res folder." + localeList << "\n" + localeList << " */" + localeList << "\n" + localeList << "object LocalesList {" << "\n" + localeList << " " << "val BUNDLED_LOCALES: List = Collections.unmodifiableList(" + localeList << "\n" + localeList << " " << "listOf(" + localeList << "\n" + localeList << " " + localeList << getEnabledLocales().join(",\n" + " ") + localeList << ",\n" + localeList << " )," << "\n" + localeList << " )" << "\n" + localeList << "}" << "\n" + } +} + +tasks.configureEach { task -> + if (name.contains("compile")) { + task.dependsOn generateLocaleList + } +} + +clean.doLast { + file(generatedLocaleListDir).deleteDir() +} + +if (project.hasProperty("coverage")) { + tasks.withType(Test).configureEach { + jacoco.includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] + } + + android.applicationVariants.configureEach { variant -> + tasks.register("jacoco${variant.name.capitalize()}TestReport", JacocoReport) { + + dependsOn(["test${variant.name.capitalize()}UnitTest"]) + reports { + html.required = true + xml.required = true + } + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', + '**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*'] + def kotlinTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${variant.name}", excludes: fileFilter) + def javaTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${variant.flavorName}/${variant.buildType.name}", + excludes: fileFilter) + def mainSrc = "$project.projectDir/src/main/java" + sourceDirectories.setFrom(files([mainSrc])) + classDirectories.setFrom(files([kotlinTree, javaTree])) + executionData.setFrom(fileTree(dir: project.layout.buildDirectory, includes: [ + "jacoco/test${variant.name.capitalize()}UnitTest.exec", 'outputs/code-coverage/connected/*coverage.ec' + ])) + } + } + + android { + buildTypes { + debug { + testCoverageEnabled true + applicationIdSuffix ".coverage" + } + } + } +} + +if (gradle.hasProperty('localProperties.autoPublish.glean.dir')) { + ext.gleanSrcDir = gradle."localProperties.autoPublish.glean.dir" + apply from: "../${gleanSrcDir}/build-scripts/substitute-local-glean.gradle" +} + +// ------------------------------------------------------------------------------------------------- +// Task for printing APK information for the requested variant +// Taskgraph Usage: "./gradlew printVariants +// ------------------------------------------------------------------------------------------------- +tasks.register('printVariants') { + doLast { + def variants = android.applicationVariants.collect { variant -> [ + apks: variant.outputs.collect { output -> [ + abi: output.getFilter(FilterConfiguration.FilterType.ABI.name()), + fileName: output.outputFile.name + ]}, + build_type: variant.buildType.name, + name: variant.name, + ]} + // AndroidTest is a special case not included above + variants.add([ + apks: [[ + abi: 'noarch', + fileName: 'app-debug-androidTest.apk', + ]], + build_type: 'androidTest', + name: 'androidTest', + ]) + println 'variants: ' + JsonOutput.toJson(variants) + } +} + +// Enable expiration by major version. +ext.gleanExpireByVersion = 1 + +afterEvaluate { + + // Format test output. Copied from Fenix, which was ported from AC #2401 + tasks.withType(Test).configureEach { + systemProperty "robolectric.logging", "stdout" + systemProperty "logging.test-mode", "true" + + testLogging.events = [] + + def out = services.get(StyledTextOutputFactory).create("tests") + + beforeSuite { descriptor -> + if (descriptor.getClassName() != null) { + out.style(Style.Header).println("\nSUITE: " + descriptor.getClassName()) + } + } + + beforeTest { descriptor -> + out.style(Style.Description).println(" TEST: " + descriptor.getName()) + } + + onOutput { descriptor, event -> + logger.lifecycle(" " + event.message.trim()) + } + + afterTest { descriptor, result -> + switch (result.getResultType()) { + case ResultType.SUCCESS: + out.style(Style.Success).println(" SUCCESS") + break + + case ResultType.FAILURE: + def testId = descriptor.getClassName() + "." + descriptor.getName() + out.style(Style.Failure).println(" TEST-UNEXPECTED-FAIL | " + testId + " | " + result.getException()) + break + + case ResultType.SKIPPED: + out.style(Style.Info).println(" SKIPPED") + break + } + logger.lifecycle("") + } + } +} diff --git a/mobile/android/focus-android/app/lint-baseline.xml b/mobile/android/focus-android/app/lint-baseline.xml new file mode 100644 index 0000000000..b230739f68 --- /dev/null +++ b/mobile/android/focus-android/app/lint-baseline.xml @@ -0,0 +1,7196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/lint.xml b/mobile/android/focus-android/app/lint.xml new file mode 100644 index 0000000000..f929411dd1 --- /dev/null +++ b/mobile/android/focus-android/app/lint.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/metrics.yaml b/mobile/android/focus-android/app/metrics.yaml new file mode 100644 index 0000000000..e0fecc694e --- /dev/null +++ b/mobile/android/focus-android/app/metrics.yaml @@ -0,0 +1,2458 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 + +no_lint: + - CATEGORY_GENERIC + - COMMON_PREFIX + +browser: + is_default: + type: boolean + lifetime: application + description: | + Is Focus the default browser? This is true only if the user + changes the default browser through the app settings. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/4545 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5065#issuecomment-894328647 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + default_search_engine: + type: string + lifetime: application + description: | + A string containing the default search engine name. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/4545 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5065#issuecomment-894328647 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + send_in_pings: + - baseline + - metrics + no_lint: + - BASELINE_PING + notification_emails: + - android-probes@mozilla.com + expires: never + locale_override: + type: string + lifetime: application + description: | + The locale that differs from the system locale if a user + specifically overrides it for the app. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/4545 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5065#issuecomment-894328647 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + - https://github.com/mozilla-mobile/firefox-android/pull/4040 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: never + total_uri_count: + type: counter + description: | + Records count of URIs visited by the user in the current session, + including page reloads. + It does not include background page requests and URIs from embedded pages + but may be incremented without user interaction by website scripts + that programmatically redirect to a new location. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5518 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5523 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - mcarare@mozilla.com + send_in_pings: + - metrics + - baseline + no_lint: + - BASELINE_PING + expires: never + install_source: + type: string + lifetime: application + description: Used to identify the source the app was installed from. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5684 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5694 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + back_button_pressed: + type: event + description: Back button has been presed on a browser tab. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5914 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5913 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + action_performed: + description: | + The action performed by pressing back button: + erase_to_home or erase_to_external_app. + type: string + report_site_issue_counter: + type: counter + description: | + A counter that indicates how many times a user has tapped + the report site issue from browser menu + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5897 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5898 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +perf.startup: + startup_type: + type: labeled_counter + description: | + Indicates how the browser was started. The label is divided into two + variables. `state` is how cached the browser is when started. `path` is + what code path we are expected to take. Together, they create a combined + label: `state_path`. For brevity, the specific states are documented in + the [Fenix perf + glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary). +

+ This implementation is intended to be simple, not comprehensive. We list + the implications below. + +

+ These ways of opening the app undesirably adds events to our primary + buckets (non-`unknown` cases): +
- App switcher cold/warm: `cold/warm_` + duplicates path from + previous launch +
- An Intent is sent internally that's uses `ACTION_MAIN` or + `ACTION_VIEW` could be: `*_main/view` (unknown if this ever happens) +
- A command-line launch uses `ACTION_MAIN` or `ACTION_VIEW` could be: + `*_main/view` + +

+ These ways of opening the app undesirably do not add their events to our + primary buckets: +
- Close and reopen the app very quickly: no event is recorded. + +

+ These ways of opening the app don't affect our primary buckets: +
- App switcher hot: `hot_unknown` +
- PWA (all states): `unknown_unknown` +
- Custom tab: `unknown_view` +
- Cold start where a service or other non-activity starts the process + (not manually tested) - this seems to happen if you have the homescreen + widget: `unknown_*` +
- Another activity is drawn before MainActivity or CustomTabActivity + (e.g. widget voice + search): `unknown_*` + +
+ In addition to the events above, the `unknown` state may be chosen when we + were unable to determine a cause due to implementation details or the API + was used incorrectly. We may be able to record the events listed above + into different buckets but we kept the implementation simple for now. +

+ N.B.: for implementation simplicity, we duplicate the logic in app that + determines `path` so it's not perfectly accurate. In one way, we record we + is intended to happen rather than what actually happened (e.g. the user + may click a link so we record VIEW but the app does a MAIN by going to the + homescreen because the link was invalid). + labels: + - cold_main + - cold_view + - cold_unknown + - warm_main + - warm_view + - warm_unknown + - hot_main + - hot_view + - hot_unknown + - unknown_main + - unknown_view + - unknown_unknown + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7079 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/ + data_sensitivity: + - interaction + notification_emails: + - perf-telemetry-alerts@mozilla.com + - mleclair@mozilla.com + expires: never + +activation: + activation_id: + type: uuid + lifetime: user + description: | + An alternate identifier, not correlated with the client_id, generated once + and only sent with the activation ping. + send_in_pings: + - activation + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/4545 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/issues/4901 + data_sensitivity: + - highly_sensitive + notification_emails: + - android-probes@mozilla.com + - jalmeida@mozilla.com + expires: never + +legacy_ids: + client_id: + type: uuid + description: | + Sets the legacy client ID as part of the deletion-request ping. + **No longer reported set since Focus 124, where legacy telemetry was removed**. + send_in_pings: + - deletion-request + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/4545 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1805256 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5512#issuecomment-1023668181 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1805256 + notification_emails: + - jalmeida@mozilla.com + - android-probes@mozilla.com + - tlong@mozilla.com + expires: never + +browser.search: + with_ads: + type: labeled_counter + description: | + Records counts of SERP pages with adverts displayed. + The key format is + `.in-content.[sap|sap-follow-on|organic].[code|none](.[channel])?`, + where: + + * `provider-name` is the name of the provider, + * `sap|sap-follow-on|organic` is the search access point, + * `code` is set when the url matches any of the provider's code prefixes, + * `channel` is set to the url "channel" query parameter. + send_in_pings: + - metrics + bugs: + - https://github.com/mozilla-mobile/fenix/issues/4967 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1804057 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1799049 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1809447 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/4968#issuecomment-879256443 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/focus-android/pull/8109#issuecomment-1337394286 + - https://github.com/mozilla-mobile/firefox-android/pull/523#issuecomment-1377494482 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + ad_clicks: + type: labeled_counter + description: | + Records clicks of adverts on SERP pages. + The key format is + `.in-content.[sap|sap-follow-on|organic].[code|none](.[channel])?`, + where: + + * `provider-name` is the name of the provider, + * `sap|sap-follow-on|organic` is the search access point, + * `code` is set when the url matches any of the provider's code prefixes, + * `channel` is set to the url "channel" query parameter. + send_in_pings: + - metrics + - baseline + no_lint: + - BASELINE_PING + bugs: + - https://github.com/mozilla-mobile/fenix/issues/4967 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1804057 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1809447 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/4968#issuecomment-879256443 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/focus-android/pull/8109#issuecomment-1337394286 + - https://github.com/mozilla-mobile/firefox-android/pull/523#issuecomment-1377494482 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + in_content: + type: labeled_counter + description: | + Records the type of interaction a user has on SERP pages. + send_in_pings: + - metrics + - baseline + no_lint: + - BASELINE_PING + bugs: + - https://github.com/mozilla-mobile/fenix/issues/4967 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1809447 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/4968#issuecomment-879256443 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/523#issuecomment-1377494482 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + + search_count: + type: labeled_counter + description: | + The labels for this counter are `.`. + + If the search engine is bundled with Focus `search-engine-name` will be + the name of the search engine. If it's a custom search engine (defined: + https://github.com/mozilla-mobile/fenix/issues/1607) the value will be + `custom`. + + `source` will be: `action`, `suggestion` + send_in_pings: + - metrics + - baseline + no_lint: + - BASELINE_PING + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/6229 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/6238 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - technical + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +mozilla_products: + has_fenix_installed: + type: boolean + lifetime: application + description: | + If Fenix is installed on the users's device. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5295 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5303 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - technical + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + is_fenix_default_browser: + type: boolean + lifetime: application + description: | + Fenix is the default browser on user's device + send_in_pings: + - metrics + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5295 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5303 + - https://github.com/mozilla-mobile/focus-android/pull/6315 + - https://github.com/mozilla-mobile/firefox-android/pull/632 + data_sensitivity: + - technical + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +autocomplete: + domain_added: + type: counter + description: | + A counter that indicates how many times a user has added + a website to the autocomplete list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5885 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5886 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + domain_removed: + type: counter + description: | + A counter that indicates how many times a user has removed + a website from the autocomplete list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5885 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5886 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + list_order_changed: + type: counter + description: | + A counter that indicates how many times a user has reordered + the autocomplete list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5885 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5886 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + top_sites_setting_changed: + type: event + description: | + Autocomplete setting for top sites has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5885 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5940 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting true for ON, false for OFF + type: boolean + favorite_sites_setting_changed: + type: event + description: | + Autocomplete setting for favorite sites has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5885 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5940 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting true for ON, false for OFF + type: boolean + +shortcuts: + shortcuts_on_home_number: + type: quantity + description: | + The number of shortcuts the user has on home screen, + 0, 1, 2, 3 or 4 (maximum) + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5056 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5189 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + unit: shortcut(s) + shortcut_opened_counter: + type: counter + description: | + A counter that indicates how many times a user has opened + a website from a shortcut in the home screen. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5056 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5189 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + shortcut_added_counter: + type: counter + description: | + A counter that indicates how many times a user has added + a website to shortcuts. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5056 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5189 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + shortcut_removed_counter: + type: labeled_counter + description: | + A counter that indicates how many times a user has removed + a website from shortcuts. + It also indicates the screen it was removed from, home or browser. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5056 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5189 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + labels: + - removed_from_browser_menu + - removed_from_home_screen +tracking_protection: + toolbar_shield_clicked: + type: counter + description: | + A counter that indicates how many times a user has opened + the tracking protection settings panel from the toolbar. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5057 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5163 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + + tracking_protection_changed: + type: event + description: | + The user has changed the setting for enhanced tracking protection. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5057 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5163 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting for ETP, true for ON, false for OFF + type: boolean + + has_ever_changed_etp: + type: boolean + description: | + The user has changed the setting for enhanced tracking protection + at least once. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5057 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5163 + - https://github.com/mozilla-mobile/focus-android/pull/5543 + data_sensitivity: + - interaction + lifetime: user + notification_emails: + - android-probes@mozilla.com + - mcarare@mozilla.com + expires: never + + tracker_setting_changed: + type: event + description: | + The user has changed the advertising tracker protection state. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5057 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5163 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + extra_keys: + source_of_change: + description: The source of interaction, "Panel" or "Settings" + type: string + tracker_changed: + description: | + The tracker changed, "Advertising", "Analytics", "Social", "Content" + type: string + is_enabled: + description: The new setting for tracker, true for ON, false for OFF + type: boolean + + has_social_blocked: + type: boolean + description: | + The user has changed the setting for enhanced tracking protection + at least once. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5057 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5163 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + + has_advertising_blocked: + type: boolean + description: | + The user has changed the setting for enhanced tracking protection + at least once. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5057 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5163 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + + has_analytics_blocked: + type: boolean + description: | + The user has changed the setting for enhanced tracking protection + at least once. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5057 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5163 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + + has_content_blocked: + type: boolean + description: | + The user has changed the setting for enhanced tracking protection + at least once. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5057 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5163 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never +pro_tips: + tip_displayed: + type: event + description: A pro tip has been displayed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5541 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5542 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + tip_id: + description: | + The tip code of tip being displayed. Can be one of fresh_look_tip, + shortcuts_tip, allow_list_tip, etp_tip,request_desktop_tip. + Note that fresh_look_tip is automatically displayed on home screen. + type: string + link_in_tip_clicked: + type: event + description: A link in a pro tip has been clicked. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5541 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5542 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + tip_id: + description: | + The tip code of tip being clicked. + Can be one of fresh_look_tip, allow_list_tip, request_desktop_tip. + type: string + +preferences: + user_theme: + type: string + description: > + A string that indicates the theme. + Can be one of LIGHT, DARK, or FOLLOW DEVICE. + Default is FOLLOW DEVICE. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5519 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5526 + - https://github.com/mozilla-mobile/focus-android/pull/7418#issuecomment-1195518264 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + - https://github.com/mozilla-mobile/firefox-android/pull/4040 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + +app_opened: + from_icons: + type: event + description: | + The user has opened the app using launcher icons + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5546 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5552 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + open_type: + description: | + Can be "Launch" if Focus was not already opened or "Resume" if it was. + type: string + from_launcher_site_shortcut: + type: event + description: | + The user has opened the app using launcher website shortcut + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5547 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5839 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + browse_intent: + type: event + description: | + App was opened from a browse intent. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5547 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5839 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + text_selection_intent: + type: event + description: | + App was opened from a text selection intent. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5547 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5839 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + share_intent: + type: event + description: | + App was opened from a share intent. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5547 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5839 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_search: + description: Is the shared intent a search? + type: boolean + + +add_to_home_screen: + dialog_displayed: + type: event + description: The dialog for adding home screen shorcut was displayed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5548 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5598 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + add_button_tapped: + type: event + description: The add(yes) option from add to home dialog was tapped. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5548 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5598 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + has_edited_title: + description: Did the user edit the default title provided by the app? + type: boolean + cancel_button_tapped: + type: event + description: The cancel(no) option from add to home dialog was tapped. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5548 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5598 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never +set_default_browser: + from_app_settings: + type: event + description: | + The user has changed default browser from the app. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5636 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5637 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_default: + description: Shows if Focus was already default. + type: boolean + from_os_settings: + type: event + description: | + The user has opened the OS settings to set default browser. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5636 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5637 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_default: + description: Shows if Focus was already default. + type: boolean + learn_more_opened: + type: event + description: | + The user has opened the learn more link. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5636 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5637 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_default: + description: Shows if Focus was already default. + type: boolean + +tab_count: + session_button_tapped: + type: event + description: The session button has been tapped to see session list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5583 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5644 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + opened_tabs: + description: Number of currently opened tabs + type: quantity + session_list_item_tapped: + type: event + description: The user has switched to a tab from the session list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5583 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5644 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + opened_tabs: + description: Number of currently opened tabs + type: quantity + session_list_closed: + type: event + description: The user has closed the session list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5583 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5644 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + opened_tabs: + description: Number of currently opened tabs + type: quantity + erase_button_tapped: + type: event + description: The erease button has been tapped to close opened sessions. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5583 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5644 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + opened_tabs: + description: Number of currently opened tabs + type: quantity + new_tab_opened: + type: event + description: A new tab has opened. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5583 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5644 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + opened_tabs: + description: Number of currently opened tabs + type: quantity + source: + description: | + Tab opened from "custom tab", "context menu" or from "Window.open()" + type: string + app_backgrounded: + type: custom_distribution + description: Number of opened tabs when the app has been send to background. + range_min: 0 + range_max: 50 + bucket_count: 51 + histogram_type: linear + unit: tabs + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5583 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5793 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +search_bar: + entered_url: + type: event + description: The user has entered a full url. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5546 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5660 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + performed_search: + type: event + description: The user has entered text and performed a search. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5546 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5660 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + engine_name: + description: The name of the engine used to perform the search. + type: string + +show_search_suggestions: + enabled_from_panel: + type: event + description: The "yes" option from the suggestion panel has been tapped. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5840 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5858 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + disabled_from_panel: + type: event + description: The "no"" option from the suggestion panel has been tapped. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5840 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5858 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + changed_from_settings: + type: event + description: The enabled state has been changed from the settings screen. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5840 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5858 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting value, true for ON, false for OFF + type: boolean + +downloads: + download_started: + type: event + description: A download has been started. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5650 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5663 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + download_paused: + type: event + description: A download has been paused. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5650 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5663 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + download_canceled: + type: event + description: A download has been cancelled. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5650 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5663 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + download_completed: + type: event + description: A download has been completed successfully. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5650 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5663 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + download_failed: + type: event + description: The download has failed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5650 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5663 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + file_extension: + description: The extension of the downloaded file. + type: string + open_button_tapped: + type: event + description: The open button from download confirmation was tapped. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5650 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5663 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + open_successful: + description: Did the user succeed in opening the downloaded file? + type: boolean + file_extension: + description: The extension of the downloaded file. + type: string + +search_engines: + open_settings: + type: event + description: The user has opened the search engines settings page. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5646 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5713 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + add_engine_tapped: + type: event + description: The user has tapped on the add another search engine button. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5646 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5713 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + save_engine_tapped: + type: event + description: The user has tried to save a custom engine. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5646 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5713 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + save_successful: + description: If the engine has been saved successfully. + type: boolean + set_default: + type: event + description: The user has set a search engine as default. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5646 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5713 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + engine_type: + description: | + The engine type set as default. Can be either "custom" or "bundled". + type: string + remove_engines: + type: event + description: The user has removed search engines. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5646 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5713 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + engine_count: + description: How many search engines has the user removed + type: quantity + open_remove_screen: + type: event + description: The user has clicked the remove option from menu. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5646 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5713 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + current_engines_count: + description: How many search engines did the user had at that point. + type: quantity + restore_default_engines: + type: event + description: The user has restored the default search engines + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5646 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5713 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + current_engines_count: + description: How many search engines did the user had at that point. + type: quantity + learn_more_tapped: + type: event + description: The learn more button was tapped. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5646 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5713 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +open_with: + list_displayed: + type: event + description: The list of apps has been opened. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5654 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5703 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + list_size: + description: The number of apps in the list + type: quantity + list_item_tapped: + type: event + description: The uer has opened the url with a app from the list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5654 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5703 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + package_name: + description: The app the user has chosen is a Mozilla product. + type: boolean + install_firefox: + type: event + description: The user has clicked install Firefox from store item. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5654 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5703 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +crash_reporter: + displayed: + type: event + description: The crash report has been displayed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5652 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5725 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + close_report: + type: event + description: The crash report has been submitted. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5652 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5725 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + submit_report: + description: Did the user choose to send the report? + type: boolean + +browser_menu: + navigation_toolbar_action: + type: event + description: The user has tapped on a navigation toolbar item. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5648 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5748 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + item: + description: | + A string containing the name of the item the user tapped: + back, forward, share, reload, stop + type: string + browser_menu_action: + type: event + description: The user has tapped on a browser menu item. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5648 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5748 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + item: + description: | + A string containing the name of the item the user tapped: + add_to_homescreen, desktop_view_off, desktop_view_on, + find_in_page, open_in_app, settings + type: string + +custom_tabs_toolbar: + navigation_toolbar_action: + type: event + description: The user tapped on a navigation toolbar item in a custom tab. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5649 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5748 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + item: + description: | + A string containing the name of the item the user tapped: + back, forward, reload, stop + type: string + browser_menu_action: + type: event + description: The user tapped on a browser menu item item in a custom tab. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5649 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5748 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + item: + description: | + A string containing the name of the item the user tapped: + desktop_view_off, desktop_view_on, find_in_page, open_in_app, + add_to_homescreen, open_in browser + type: string + close_tab_tapped: + type: event + description: The user has closed a custom tab. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5649 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5748 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + action_button_tapped: + type: event + description: The user has tapped the actionbutton a custom tab. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5649 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5748 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +tracking_protection_exceptions: + allow_list_opened: + type: event + description: The user has opened the exceptions list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5753 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5758 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + allow_list_cleared: + type: event + description: The user has removed all items from exceptions list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5753 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5758 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + list_size: + description: The number of exceptions in the list. + type: quantity + selected_items_removed: + type: event + description: The user has removed the selected items from exceptions list. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5753 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5758 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + list_size: + description: The number of selected items removed. + type: quantity + +notifications: + open_button_tapped: + type: event + description: The user has tapped the Open option button from notification. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5651 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5769 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + erase_open_button_tapped: + type: event + description: The user has tapped the Erase & Open button from notification. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5651 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5769 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + opened_tabs: + description: Number of currently opened tabs + type: quantity + notification_tapped: + type: event + description: The user has tapped the notification to close the app. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5651 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5769 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + permission_granted: + type: boolean + description: | + True if notifications are allowed from OS settings, otherwise false. + Prior to Android 13, notifications were allowed by default; + starting with Android 13,the user must explicitly grant the permission. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1803358 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/475 + - https://github.com/mozilla-mobile/firefox-android/pull/4040 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: never + +app_shortcuts: + just_erase_button_tapped: + type: event + description: The user has tapped the Erase option button from shortcuts. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5651 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5769 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + opened_tabs: + description: Number of currently opened tabs + type: quantity + erase_open_button_tapped: + type: event + description: The user has tapped the Erase & Open button from shortcuts. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5651 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5769 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + opened_tabs: + description: Number of currently opened tabs + type: quantity + +recent_apps: + app_removed_from_list: + type: event + description: The user removed the apps from recent apps screen. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5651 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5769 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +context_menu: + item_tapped: + type: event + description: The user has tapped an option from context menu. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5647 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5773 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + item_name: + description: | + The name of the item that was tapped. One of the following: + open_in_new_tab, open_in_private_tab, open_image_in_new_tab, + save_image, share_link, copy_link, copy_image_location, share_image + type: string + +onboarding: + first_screen_close_button: + type: event + description: The user has tapped on close button from the first screen. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7500 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7566#issuecomment-1235551604 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + get_started_button: + type: event + description: The user has tapped on get started button. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7500 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7566#issuecomment-1235551604 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + default_browser_button: + type: event + description: The user has tapped on set as default browser button. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7500 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7566#issuecomment-1235551604 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + skip_button: + type: event + description: The user has tapped on skip button. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7500 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7566#issuecomment-1235551604 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + second_screen_close_button: + type: event + description: The user has tapped on close button from the second screen. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7500 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7566#issuecomment-1235551604 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + page_displayed: + type: event + description: A page from onboarding has been displayed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5635 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5869 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + current_item: + description: The curent displayed item position. + type: quantity + skip_button_tapped: + type: event + description: The user has tapped to skip onboarding. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5635 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5869 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + current_item: + description: The curent displayed item position. + type: quantity + finish_button_tapped: + type: event + description: The user has tapped to finish onboarding. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5635 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5869 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + current_item: + description: The curent displayed item position. + type: quantity + next_button_tapped: + type: event + description: The user has tapped next onboarding. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5635 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5869 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + current_item: + description: The curent displayed item position. + type: quantity + +search_suggestions: + suggestion_tapped: + type: event + description: Search suggestion selected. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5662 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5864 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + engine_name: + description: The name of the engine used to perform the search. + type: string + search_tapped: + type: event + description: The typed text search was selected. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5662 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5864 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + engine_name: + description: The name of the engine used to perform the search. + type: string + autocomplete_arrow_tapped: + type: event + description: Search suggestion selected. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5662 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5864 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + +advanced_settings: + remote_debug_setting_changed: + type: event + description: | + Remote debugging setting has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5653 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5940 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting true for ON, false for OFF + type: boolean + open_links_setting_changed: + type: event + description: | + Open links setting has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5653 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5940 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting true for ON, false for OFF + type: boolean +privacy_settings: + telemetry_setting_changed: + type: event + description: | + Telemetry setting has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5653 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5940 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting true for ON, false for OFF + type: boolean + safe_browsing_setting_changed: + type: event + description: | + Safe browsing setting has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5653 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5940 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting true for ON, false for OFF + type: boolean + unlock_setting_changed: + type: event + description: | + Biometric unlock setting has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5653 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5940 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting true for ON, false for OFF + type: boolean + stealth_setting_changed: + type: event + description: | + Stealth setting has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/5653 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/5940 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: The new setting true for ON, false for OFF + type: boolean + block_cookies_changed: + type: event + description: | + Block cookies setting has changed. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/6097 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/6105 + - https://github.com/mozilla-mobile/focus-android/pull/7906 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - rtestard@mozilla.com + expires: never + extra_keys: + is_enabled: + description: | + A string containing the new block cookies option the user has chosen: + yes, third_party_only, third_party_tracker, cross_site, no + type: string + +metrics: + start_reason_process_error: + type: boolean + description: | + The `AppStartReasonProvider.ProcessLifecycleObserver.onCreate` was + unexpectedly called twice. We can use this metric to validate our + assumptions about how these APIs are called. This probe can be removed + once we validate these assumptions. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7079 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/ + data_sensitivity: + - technical + notification_emails: + - perf-telemetry-alerts@mozilla.com + - mleclair@mozilla.com + expires: never + metadata: + tags: + - Performance + start_reason_activity_error: + type: boolean + description: | + The `AppStartReasonProvider.ActivityLifecycleCallbacks.onActivityCreated` + was unexpectedly called twice. We can use this metric to validate our + assumptions about how these APIs are called. This probe can be removed + once we validate these assumptions. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7079 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/ + data_sensitivity: + - technical + notification_emails: + - perf-telemetry-alerts@mozilla.com + - mleclair@mozilla.com + expires: never + metadata: + tags: + - Performance + search_widget_installed: + type: boolean + lifetime: application + description: | + Whether or not the search widget is installed + send_in_pings: + - metrics + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/ + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7474 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + - https://github.com/mozilla-mobile/firefox-android/pull/4040 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Search + +search_widget: + new_tab_button: + type: event + description: | + A user pressed anywhere from the Focus logo until the start of the + microphone icon, opening a new tab search screen. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/ + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7474 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + - https://github.com/mozilla-mobile/firefox-android/pull/4040 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Search + voice_button: + type: event + description: | + A user pressed the microphone icon, opening a new voice search screen. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/ + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7474 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + - https://github.com/mozilla-mobile/firefox-android/pull/4040 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Search + promote_dialog_shown: + type: event + description: | + Promote search widget dialog is shown to the user. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7506 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7657#issuecomment-1252242947 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + - https://github.com/mozilla-mobile/firefox-android/pull/4040 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Search + add_to_home_screen_button: + type: event + description: | + The user has pressed on add search widget + to home screen button from promote dialog. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7506 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7657#issuecomment-1252242947 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Search + widget_was_added: + type: event + description: | + The user has added successfully the search widget from + promote search widget dialog. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7506 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/7657#issuecomment-1252242947 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Search +cookie_banner: + visited_setting: + type: event + description: A user visited the cookie banner handling screen + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7965 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/8008#issuecomment-1322266028 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + + setting_changed: + type: event + description: | + A user changed their setting. + extra_keys: + cookie_banner_setting: + description: | + The new setting for cookie banner handling: disabled,reject_all, + or reject_or_accept_all. + type: string + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7965 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/8008#issuecomment-1322266028 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + + cookie_banner_cfr_shown: + type: event + description: | + Cfr for cookie banner is shown to the user. + bugs: + - https://github.com/mozilla-mobile/focus-android/ + data_reviews: + - https://github.com/mozilla-mobile/focus-android/ + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + + exception_added: + type: event + description: | + A user added a cookie banner handling exception through + the toggle in the protections panel. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797578 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/8124#issuecomment-1344449866 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + + report_site_domain: + type: url + description: | + A user can report a site domain(Ex. for https://edition.cnn.com/ + site domain will be cnn.com) when the cookie banner reducer is not + working from the cookie banner details panel. + lifetime: ping + send_in_pings: + - cookie-banner-report-site + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1803589 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/389#pullrequestreview-1341440145 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - technical + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + + report_site_cancel_button: + type: event + description: | + The user has pressed the report site domain cancel button + from the cookie banner reducer details panel. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1803589 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/389#pullrequestreview-1341440145 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + + report_domain_site_button: + type: event + description: | + The user has pressed the report site domain button + from the cookie banner reducer details panel. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1803589 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/389#pullrequestreview-1341440145 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + + exception_removed: + type: event + description: | + A user removed a cookie banner handling + exception through the toggle in the protections panel. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797578 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/8124#issuecomment-1344449866 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security + + visited_panel: + type: event + description: A user visited the cookie banner exception panel + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797578 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/8124#issuecomment-1344449866 + - https://github.com/mozilla-mobile/firefox-android/pull/3320 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Privacy&Security diff --git a/mobile/android/focus-android/app/nimbus.fml.yaml b/mobile/android/focus-android/app/nimbus.fml.yaml new file mode 100644 index 0000000000..fb9ba783a7 --- /dev/null +++ b/mobile/android/focus-android/app/nimbus.fml.yaml @@ -0,0 +1,48 @@ +about: + description: Nimbus Feature Manifest for Focus Android + kotlin: + package: org.mozilla.focus + class: .nimbus.FocusNimbus +channels: + - debug + - nightly + - beta + - release +features: + onboarding: + description: Nimbus feature name intended to control the onboarding plus all CFRs in the app. + variables: + is-enabled: + description: If `true`, the app will show the new onboarding screen + type: Boolean + default: true + is-cfr-enabled: + description: If `true`, the app will show the cfrs + type: Boolean + default: false + is-promote-search-widget-dialog-enabled: + description: If `true`, the app will show the new dialog for promote search widget + type: Boolean + default: false + defaults: + - channel: debug + value: { + "is-enabled": true, + "is-cfr-enabled": true, + "is-promote-search-widget-dialog-enabled": true, + } + cookie-banner: + description: Nimbus feature name intended to control the cookie banner handling in the app. + variables: + is-cookie-handling-enabled: + description: If 'true' , the app will show the settings part for cookie banner handling + type: Boolean + default: false + defaults: + - channel: debug + value: { + "is-cookie-handling-enabled": true + } +types: + objects: { } + enums: { } diff --git a/mobile/android/focus-android/app/pings.yaml b/mobile/android/focus-android/app/pings.yaml new file mode 100644 index 0000000000..4816596f7c --- /dev/null +++ b/mobile/android/focus-android/app/pings.yaml @@ -0,0 +1,31 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +--- +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +activation: + description: | + This ping is intended to provide a measure of the activation of mobile + products. It's generated when Focus starts, right after Glean is + initialized. + include_client_id: false + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/4545 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/issues/4901 + notification_emails: + - jalmeida@mozilla.com + +cookie-banner-report-site: + description: | + This ping is needed when the cookie banner reducer doesn't work on + a website, and the user wants to report the site. + This ping doesn't include a client id. + include_client_id: false + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1803589 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/389#pullrequestreview-1341440145 + notification_emails: + - android-probes@mozilla.com diff --git a/mobile/android/focus-android/app/proguard-rules.pro b/mobile/android/focus-android/app/proguard-rules.pro new file mode 100644 index 0000000000..fac523684b --- /dev/null +++ b/mobile/android/focus-android/app/proguard-rules.pro @@ -0,0 +1,154 @@ + +# We do not want to obfuscate - It's just painful to debug without the right mapping file. +# If we update this, we'll have to update our Sentry config to upload ProGuard mappings. +-dontobfuscate + + +##### Default proguard settings: + +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/sebastian/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +#################################################################################################### +# Adjust +#################################################################################################### + +-keep public class com.adjust.sdk.** { *; } +-keep class com.google.android.gms.common.ConnectionResult { + int SUCCESS; +} +-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { + com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context); +} +-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info { + java.lang.String getId(); + boolean isLimitAdTrackingEnabled(); +} +-keep class dalvik.system.VMRuntime { + java.lang.String getRuntime(); +} +-keep class android.os.Build { + java.lang.String[] SUPPORTED_ABIS; + java.lang.String CPU_ABI; +} +-keep class android.content.res.Configuration { + android.os.LocaledList getLocales(); + java.util.Locale locale; +} +-keep class android.os.LocaledList { + java.util.Locale get(int); +} + + +#################################################################################################### +# Okhttp +#################################################################################################### + +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# A resource is loaded with a relative path so the package of this class must be preserved. +-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt dependency is available. +-dontwarn okhttp3.internal.platform.ConscryptPlatform + +#################################################################################################### +# Sentry +#################################################################################################### + +# Recommended config via https://docs.sentry.io/clients/java/modules/android/#manual-integration +# Since we don't obfuscate, we don't need to use their Gradle plugin to upload ProGuard mappings. +-keepattributes LineNumberTable,SourceFile +-dontwarn org.slf4j.** +-dontwarn javax.** + +# Our addition: this class is saved to disk via Serializable, which ProGuard doesn't like. +# If we exclude this, upload silently fails (Sentry swallows a NPE so we don't crash). +# I filed https://github.com/getsentry/sentry-java/issues/572 +# +# If Sentry ever mysteriously stops working after we upgrade it, this could be why. +-keep class io.sentry.event.Event { *; } + +#################################################################################################### +# Android architecture components +#################################################################################################### + +-dontwarn android.** +-dontwarn androidx.** +-dontwarn com.google.** +-dontwarn org.mozilla.geckoview.** +-dontwarn mozilla.components.** + +# https://developer.android.com/topic/libraries/architecture/release-notes.html +# According to the docs this won't be needed when 1.0 of the library is released. +-keep class * implements android.arch.lifecycle.GeneratedAdapter {(...);} + +# Temporary fix until we can use androidx +-dontwarn mozilla.components.service.fretboard.scheduler.workmanager.** + +# Fix for ViewModels +-keep class * extends androidx.lifecycle.ViewModel { + (); +} +-keep class * extends androidx.lifecycle.AndroidViewModel { + (android.app.Application); +} + +#################################################################################################### +# Mozilla Application Services +#################################################################################################### + +-keep class mozilla.appservices.** { *; } + +#################################################################################################### +# Kotlinx +#################################################################################################### + +-dontwarn kotlinx.atomicfu.** + +#################################################################################################### +# snakeyaml +#################################################################################################### + +-dontwarn java.beans.PropertyDescriptor +-dontwarn java.beans.Introspector +-dontwarn java.beans.BeanInfo +-dontwarn java.beans.IntrospectionException +-dontwarn java.beans.FeatureDescriptor + +#################################################################################################### +# REMOVE all Log messages except warnings and errors +#################################################################################################### +-assumenosideeffects class android.util.Log { + public static boolean isLoggable(java.lang.String, int); + public static int v(...); + public static int i(...); + public static int d(...); +} + +#################################################################################################### +# kotlinx.coroutines: use the fast service loader to init MainDispatcherLoader by including a rule +# to rewrite this property to return true: +# https://github.com/Kotlin/kotlinx.coroutines/blob/8c98180f177bbe4b26f1ed9685a9280fea648b9c/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt#L19 +# +# R8 is expected to optimize the default implementation to avoid a performance issue but a bug in R8 +# as bundled with AGP v7.0.0 causes this optimization to fail so we use the fast service loader instead. See: +# https://github.com/mozilla-mobile/focus-android/issues/5102#issuecomment-897854121 +# +# The fast service loader appears to be as performant as the R8 optimization so it's not worth the +# churn to later remove this workaround. If needed, the upstream fix is being handled in +# https://issuetracker.google.com/issues/196302685 +#################################################################################################### +-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader { + boolean FAST_SERVICE_LOADER_ENABLED return true; +} diff --git a/mobile/android/focus-android/app/src/androidTest/assets/audioPage.html b/mobile/android/focus-android/app/src/androidTest/assets/audioPage.html new file mode 100644 index 0000000000..f45ba6410f --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/audioPage.html @@ -0,0 +1,37 @@ + + + Audio_Test_Page + + +

Page content: audio player

+ +
+ +
+ +
+
+ + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/cross-site-cookies.html b/mobile/android/focus-android/app/src/androidTest/assets/cross-site-cookies.html new file mode 100644 index 0000000000..5cf99f3881 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/cross-site-cookies.html @@ -0,0 +1,12 @@ + + + + + +

known-tracker.englehardt-tracker.com

+

different site, cross-origin iframe, on blocklist

+ + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/download.jpg b/mobile/android/focus-android/app/src/androidTest/assets/download.jpg new file mode 100644 index 0000000000..bb55dd7063 Binary files /dev/null and b/mobile/android/focus-android/app/src/androidTest/assets/download.jpg differ diff --git a/mobile/android/focus-android/app/src/androidTest/assets/etpPages/adsTrackers.html b/mobile/android/focus-android/app/src/androidTest/assets/etpPages/adsTrackers.html new file mode 100644 index 0000000000..b8e0f7bc55 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/etpPages/adsTrackers.html @@ -0,0 +1,21 @@ + + + + + + + adsTrackers + + + + +

ads trackers:

+

if you can read this, then:

+

ads trackers not blocked

+ + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/etpPages/analyticsTrackers.html b/mobile/android/focus-android/app/src/androidTest/assets/etpPages/analyticsTrackers.html new file mode 100644 index 0000000000..e97a7e35c6 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/etpPages/analyticsTrackers.html @@ -0,0 +1,21 @@ + + + + + + + analyticsTrackers + + + + +

analytics trackers

+

if you can read this, then:

+

analytics trackers not blocked

+ + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/etpPages/otherTrackers.html b/mobile/android/focus-android/app/src/androidTest/assets/etpPages/otherTrackers.html new file mode 100644 index 0000000000..5e4bd63a78 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/etpPages/otherTrackers.html @@ -0,0 +1,22 @@ + + + + + + + otherTrackers + + + + +

Level 2 (Strict List) Tracker Blocking

+

other content trackers

+

if you can read this, then:

+

other content trackers not blocked

+ + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/etpPages/socialTrackers.html b/mobile/android/focus-android/app/src/androidTest/assets/etpPages/socialTrackers.html new file mode 100644 index 0000000000..5f1afd19aa --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/etpPages/socialTrackers.html @@ -0,0 +1,21 @@ + + + + + + + socialTrackers + + + + +

social trackers

+

if you can read this, then:

+

social trackers not blocked

+ + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/genericPage.html b/mobile/android/focus-android/app/src/androidTest/assets/genericPage.html new file mode 100644 index 0000000000..46f36bf6d1 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/genericPage.html @@ -0,0 +1,15 @@ + + + + + + GenericPage + + +

focus test page

+ +

groovy rabbits

+

This test page does nothing.

+ + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/global_privacy_control.html b/mobile/android/focus-android/app/src/androidTest/assets/global_privacy_control.html new file mode 100644 index 0000000000..e08df8c17f --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/global_privacy_control.html @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/htmlControls.html b/mobile/android/focus-android/app/src/androidTest/assets/htmlControls.html new file mode 100644 index 0000000000..3677417a28 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/htmlControls.html @@ -0,0 +1,66 @@ + + + + Html_Control_Form + + + + +

Misc Link Types

+
+ External link +
+ +
+ Email link + Telephone link +
+ +

Drop-down Form

+ + +
+ +
+

Copy me

+ +
+
+ +

Calendar Form

+
+ + +
+
+ + + + + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/image_test.html b/mobile/android/focus-android/app/src/androidTest/assets/image_test.html new file mode 100644 index 0000000000..ea8ea10f53 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/image_test.html @@ -0,0 +1,20 @@ + + + + + + gigantic experience + + +

focus test page

+ + +

groovy rabbits

+rabbit.jpg + + + download icon + + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/mutedVideoPage.html b/mobile/android/focus-android/app/src/androidTest/assets/mutedVideoPage.html new file mode 100644 index 0000000000..8c4fbfc686 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/mutedVideoPage.html @@ -0,0 +1,53 @@ + + + Muted_Video_Test_Page + + +

Page content: muted video player

+
+
+
+ + + +

+ +
+ + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/rabbit.jpg b/mobile/android/focus-android/app/src/androidTest/assets/rabbit.jpg new file mode 100644 index 0000000000..3225407b1c Binary files /dev/null and b/mobile/android/focus-android/app/src/androidTest/assets/rabbit.jpg differ diff --git a/mobile/android/focus-android/app/src/androidTest/assets/resources/audioSample.mp3 b/mobile/android/focus-android/app/src/androidTest/assets/resources/audioSample.mp3 new file mode 100644 index 0000000000..eb0420a48b Binary files /dev/null and b/mobile/android/focus-android/app/src/androidTest/assets/resources/audioSample.mp3 differ diff --git a/mobile/android/focus-android/app/src/androidTest/assets/resources/clip.mp4 b/mobile/android/focus-android/app/src/androidTest/assets/resources/clip.mp4 new file mode 100644 index 0000000000..20f739c7c8 Binary files /dev/null and b/mobile/android/focus-android/app/src/androidTest/assets/resources/clip.mp4 differ diff --git a/mobile/android/focus-android/app/src/androidTest/assets/same-site-cookies.html b/mobile/android/focus-android/app/src/androidTest/assets/same-site-cookies.html new file mode 100644 index 0000000000..dd4fa31be7 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/same-site-cookies.html @@ -0,0 +1,125 @@ + + + + + + + + + +
+

cookies

+ + +

localStorage

+

+
+
+
+
+
+

Storage Access API

+
+

Return value of requestStorageAccess():

not yet called

+
+

Return value of hasStorageAccess():

not yet called

+ + + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/service-worker.js b/mobile/android/focus-android/app/src/androidTest/assets/service-worker.js new file mode 100644 index 0000000000..8f77e519df --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/service-worker.js @@ -0,0 +1,2 @@ +// Just some token we are looking for on disk +const KANGAROO = true; diff --git a/mobile/android/focus-android/app/src/androidTest/assets/storage_check.html b/mobile/android/focus-android/app/src/androidTest/assets/storage_check.html new file mode 100644 index 0000000000..c52cae9b7b --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/storage_check.html @@ -0,0 +1,23 @@ + + + + + +

Storage check

+ + + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/storage_start.html b/mobile/android/focus-android/app/src/androidTest/assets/storage_start.html new file mode 100644 index 0000000000..e88f7f06d1 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/storage_start.html @@ -0,0 +1,28 @@ + + + + + +

Storage Start

+ +

+ + + + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/tab1.html b/mobile/android/focus-android/app/src/androidTest/assets/tab1.html new file mode 100644 index 0000000000..4a9c8ce88e --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/tab1.html @@ -0,0 +1,29 @@ + + + + + + tab1 + + +

Tab 1

+ + Tab 2 + + Tab 3 + + +

+ + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/tab2.html b/mobile/android/focus-android/app/src/androidTest/assets/tab2.html new file mode 100644 index 0000000000..be5f65e6a5 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/tab2.html @@ -0,0 +1,16 @@ + + + + + tab2 + + + +

Tab 2

+ + Tab 1 + + Tab 3 + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/tab3.html b/mobile/android/focus-android/app/src/androidTest/assets/tab3.html new file mode 100644 index 0000000000..fc8c08446c --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/tab3.html @@ -0,0 +1,20 @@ + + + + + tab3 + + + +

Tab 3

+ + Tab 1 + + Tab 2 + +

+ Mozilla Youtube link +

+ + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/test.html b/mobile/android/focus-android/app/src/androidTest/assets/test.html new file mode 100644 index 0000000000..7273622e6f --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/test.html @@ -0,0 +1,38 @@ + + + + + + gigantic experience + + +

focus test page

+ +

groovy rabbits

+

This test page installs a service worker and saves a cookie.

+ +

Cookie

+

Initial:

+

+ + +

+

Afterwards:

+ +

Service worker

+

+ + + + + diff --git a/mobile/android/focus-android/app/src/androidTest/assets/videoPage.html b/mobile/android/focus-android/app/src/androidTest/assets/videoPage.html new file mode 100644 index 0000000000..cd352268b3 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/assets/videoPage.html @@ -0,0 +1,53 @@ + + + Video_Test_Page + + +

Page content: video player

+
+
+
+ + + +

+ +
+ + + + diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/AddToHomescreenTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/AddToHomescreenTest.kt new file mode 100644 index 0000000000..3fae3efa46 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/AddToHomescreenTest.kt @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.RetryTestRule +import org.mozilla.focus.helpers.TestAssetHelper.getGenericTabAsset +import org.mozilla.focus.helpers.TestHelper.randomString +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest + +/** + * Tests to verify the functionality of Add to homescreen from the main menu + */ +@RunWith(AndroidJUnit4ClassRunner::class) +class AddToHomescreenTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Rule + @JvmField + val retryTestRule = RetryTestRule(3) + + @Before + fun setup() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun addPageToHomeScreenTest() { + val pageUrl = getGenericTabAsset(webServer, 1).url + val pageTitle = randomString(5) + + searchScreen { + }.loadPage(pageUrl) { + progressBar.waitUntilGone(waitingTime) + }.openMainMenu { + }.openAddToHSDialog { + addShortcutWithTitle(pageTitle) + handleAddAutomaticallyDialog() + }.searchAndOpenHomeScreenShortcut(pageTitle) { + verifyPageURL(pageUrl) + } + } + + @SmokeTest + @Test + fun noNameShortcutTest() { + val pageUrl = getGenericTabAsset(webServer, 1).url + + searchScreen { + }.loadPage(pageUrl) { + }.openMainMenu { + }.openAddToHSDialog { + // leave shortcut title empty and add it to HS + addShortcutNoTitle() + handleAddAutomaticallyDialog() + }.searchAndOpenHomeScreenShortcut(webServer.hostName) { + // only checking a part of the URL that is constant, + // in case it opens a different shortcut on a retry + verifyPageURL("tab1.html") + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ContextMenusTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ContextMenusTest.kt new file mode 100644 index 0000000000..8f74566efe --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ContextMenusTest.kt @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.DeleteFilesHelper.deleteFileUsingDisplayName +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityIntentsTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.RetryTestRule +import org.mozilla.focus.helpers.StringsHelper +import org.mozilla.focus.helpers.TestAssetHelper.getGenericTabAsset +import org.mozilla.focus.helpers.TestAssetHelper.getImageTestAsset +import org.mozilla.focus.helpers.TestHelper +import org.mozilla.focus.helpers.TestHelper.assertNativeAppOpens +import org.mozilla.focus.helpers.TestHelper.getTargetContext +import org.mozilla.focus.helpers.TestHelper.permAllowBtn +import org.mozilla.focus.testAnnotations.SmokeTest + +// These tests check the interaction with various context menu options +@RunWith(AndroidJUnit4ClassRunner::class) +class ContextMenusTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityIntentsTestRule(showFirstRun = false) + + @get: Rule + val retryTestRule = RetryTestRule(3) + + @Before + fun setup() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun linkedImageContextMenuItemsTest() { + val imagesTestPage = getImageTestAsset(webServer) + val imageAssetUrl = webServer.url("download.jpg").toString() + + searchScreen { + }.loadPage(imagesTestPage.url) { + longPressLink("download icon") + verifyImageContextMenu(true, imageAssetUrl) + } + } + + @SmokeTest + @Test + fun simpleImageContextMenuItemsTest() { + val imagesTestPage = getImageTestAsset(webServer) + val imageAssetUrl = webServer.url("rabbit.jpg").toString() + + searchScreen { + }.loadPage(imagesTestPage.url) { + longPressLink("rabbit.jpg") + verifyImageContextMenu(false, imageAssetUrl) + } + } + + @SmokeTest + @Test + fun linkContextMenuItemsTest() { + val tab1Page = getGenericTabAsset(webServer, 1) + val tab2Page = getGenericTabAsset(webServer, 2) + + searchScreen { + }.loadPage(tab1Page.url) { + verifyPageContent("Tab 1") + longPressLink("Tab 2") + verifyLinkContextMenu(tab2Page.url) + } + } + + @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1819872") + @SmokeTest + @Test + fun copyLinkAddressTest() { + val tab1Page = getGenericTabAsset(webServer, 1) + val tab2Page = getGenericTabAsset(webServer, 2) + + searchScreen { + }.loadPage(tab1Page.url) { + longPressLink("Tab 2") + verifyLinkContextMenu(tab2Page.url) + clickContextMenuCopyLink() + }.openSearchBar { + clearSearchBar() + longPressSearchBar() + }.pasteAndLoadLink { + progressBar.waitUntilGone(TestHelper.waitingTime) + verifyPageURL(tab2Page.url) + } + } + + @SmokeTest + @Test + fun shareLinkTest() { + val tab1Page = getGenericTabAsset(webServer, 1) + val tab2Page = getGenericTabAsset(webServer, 2) + + searchScreen { + }.loadPage(tab1Page.url) { + longPressLink("Tab 2") + verifyLinkContextMenu(tab2Page.url) + clickShareLink() + verifyShareAppsListOpened() + } + } + + @Test + fun copyImageLocationTest() { + val imagesTestPage = getImageTestAsset(webServer) + val imageAssetUrl = webServer.url("rabbit.jpg").toString() + + searchScreen { + }.loadPage(imagesTestPage.url) { + longPressLink("rabbit.jpg") + verifyImageContextMenu(false, imageAssetUrl) + clickCopyImageLocation() + }.openSearchBar { + clearSearchBar() + longPressSearchBar() + }.pasteAndLoadLink { + progressBar.waitUntilGone(TestHelper.waitingTime) + verifyPageURL(imageAssetUrl) + } + } + + @SmokeTest + @Test + fun saveImageTest() { + val imagesTestPage = getImageTestAsset(webServer) + val fileName = "rabbit.jpg" + + searchScreen { + }.loadPage(imagesTestPage.url) { + longPressLink(fileName) + }.clickSaveImage { + // If permission dialog appears on devices with API<30, grant it + if (permAllowBtn.exists()) { + permAllowBtn.click() + } + verifyDownloadConfirmationMessage(fileName) + openDownloadedFile() + assertNativeAppOpens(StringsHelper.GOOGLE_PHOTOS) + } + deleteFileUsingDisplayName( + getTargetContext.applicationContext, + fileName, + ) + } + + @Test + fun shareImageTest() { + val imagesTestPage = getImageTestAsset(webServer) + + searchScreen { + }.loadPage(imagesTestPage.url) { + longPressLink("rabbit.jpg") + clickShareImage() + verifyShareAppsListOpened() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/CustomTabTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/CustomTabTest.kt new file mode 100644 index 0000000000..35ff64cd37 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/CustomTabTest.kt @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:Suppress("DEPRECATION") + +package org.mozilla.focus.activity + +import androidx.lifecycle.Lifecycle +import androidx.test.core.app.launchActivity +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import androidx.test.rule.ActivityTestRule +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.browserScreen +import org.mozilla.focus.activity.robots.customTab +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper.getGenericAsset +import org.mozilla.focus.helpers.TestAssetHelper.getGenericTabAsset +import org.mozilla.focus.helpers.TestHelper.createCustomTabIntent +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest +import java.io.IOException + +@RunWith(AndroidJUnit4ClassRunner::class) +class CustomTabTest { + private lateinit var webServer: MockWebServer + private val MENU_ITEM_LABEL = "TestItem4223" + private val ACTION_BUTTON_DESCRIPTION = "TestButton" + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + val activityTestRule = ActivityTestRule( + IntentReceiverActivity::class.java, + true, + false, + ) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setShowStartBrowsingCfrEnabled(false) + featureSettingsHelper.setCookieBannerReductionEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + try { + webServer.shutdown() + } catch (e: IOException) { + throw AssertionError("Could not stop web server", e) + } + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun testCustomTabUI() { + val customTabPage = getGenericAsset(webServer) + val customTabActivity = + launchActivity( + createCustomTabIntent(customTabPage.url, MENU_ITEM_LABEL, ACTION_BUTTON_DESCRIPTION), + ) + + browserScreen { + progressBar.waitUntilGone(waitingTime) + verifyPageContent(customTabPage.content) + verifyPageURL(customTabPage.url) + } + + customTab { + verifyCustomTabActionButton(ACTION_BUTTON_DESCRIPTION) + verifyShareButtonIsDisplayed() + openCustomTabMenu() + verifyTheStandardMenuItems() + verifyCustomMenuItem(MENU_ITEM_LABEL) + // Close the menu and close the tab + mDevice.pressBack() + closeCustomTab() + assertEquals(Lifecycle.State.DESTROYED, customTabActivity.state) + } + } + + @SmokeTest + @Test + fun openCustomTabInFocusTest() { + val customTabPage = getGenericTabAsset(webServer, 1) + + launchActivity(createCustomTabIntent(customTabPage.url)) + customTab { + progressBar.waitUntilGone(waitingTime) + verifyPageURL(customTabPage.url) + openCustomTabMenu() + }.clickOpenInFocusButton { + verifyPageURL(customTabPage.url) + } + } + + @SmokeTest + @Test + fun customTabNavigationButtonsTest() { + val firstPage = getGenericTabAsset(webServer, 1) + val secondPage = getGenericTabAsset(webServer, 2) + + launchActivity(createCustomTabIntent(firstPage.url)) + customTab { + verifyPageContent(firstPage.content) + clickLinkMatchingText("Tab 2") + verifyPageURL(secondPage.url) + }.openCustomTabMenu { + }.pressBack { + progressBar.waitUntilGone(waitingTime) + verifyPageURL(firstPage.url) + }.openMainMenu { + }.pressForward { + verifyPageURL(secondPage.url) + }.openMainMenu { + }.clickReloadButton { + verifyPageContent(secondPage.content) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/DownloadFileTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/DownloadFileTest.kt new file mode 100644 index 0000000000..0d3d52abac --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/DownloadFileTest.kt @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.downloadRobot +import org.mozilla.focus.activity.robots.notificationTray +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.DeleteFilesHelper.deleteFileUsingDisplayName +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityIntentsTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.RetryTestRule +import org.mozilla.focus.helpers.StringsHelper.GOOGLE_PHOTOS +import org.mozilla.focus.helpers.TestAssetHelper.getImageTestAsset +import org.mozilla.focus.helpers.TestHelper.assertNativeAppOpens +import org.mozilla.focus.helpers.TestHelper.getTargetContext +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.permAllowBtn +import org.mozilla.focus.helpers.TestHelper.verifyDownloadedFileOnStorage +import org.mozilla.focus.helpers.TestHelper.verifySnackBarText +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest +import java.io.IOException + +@RunWith(AndroidJUnit4ClassRunner::class) +class DownloadFileTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + private val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html" + private var downloadFileName: String = "" + + @get:Rule + var mActivityTestRule = MainActivityIntentsTestRule(showFirstRun = false) + + @Rule + @JvmField + val retryTestRule = RetryTestRule(3) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + try { + webServer.shutdown() + } catch (e: IOException) { + throw AssertionError("Could not stop web server", e) + } + deleteFileUsingDisplayName(getTargetContext.applicationContext, downloadFileName) + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun downloadNotificationTest() { + val downloadPageUrl = getImageTestAsset(webServer).url + downloadFileName = "download.jpg" + + notificationTray { + mDevice.openNotification() + clearNotifications() + } + + // Load website with service worker + searchScreen { + }.loadPage(downloadPageUrl) { } + + downloadRobot { + clickDownloadIconAsset() + // If permission dialog appears, grant it + if (permAllowBtn.waitForExists(waitingTime)) { + permAllowBtn.click() + } + verifyDownloadDialog(downloadFileName) + clickDownloadButton() + verifySnackBarText("finished") + mDevice.openNotification() + notificationTray { + verifyDownloadNotification("Download completed", downloadFileName) + } + } + } + + @SmokeTest + @Test + fun cancelDownloadTest() { + val downloadPageUrl = getImageTestAsset(webServer).url + + searchScreen { + }.loadPage(downloadPageUrl) { } + + downloadRobot { + clickDownloadIconAsset() + // If permission dialog appears, grant it + if (permAllowBtn.waitForExists(waitingTime)) { + permAllowBtn.click() + } + clickCancelDownloadButton() + verifyDownloadDialogGone() + } + } + + @SmokeTest + @Test + fun downloadAndOpenJpgFileTest() { + val downloadPageUrl = getImageTestAsset(webServer).url + downloadFileName = "download.jpg" + + // Load website with service worker + searchScreen { + }.loadPage(downloadPageUrl) { } + + downloadRobot { + clickDownloadIconAsset() + // If permission dialog appears on devices with API<30, grant it + if (permAllowBtn.waitForExists(waitingTime)) { + permAllowBtn.click() + } + verifyDownloadDialog(downloadFileName) + clickDownloadButton() + verifyDownloadConfirmationMessage(downloadFileName) + openDownloadedFile() + assertNativeAppOpens(GOOGLE_PHOTOS) + } + } + + @SmokeTest + @Test + fun openPdfFileTest() { + downloadFileName = "washington.pdf" + val pdfFileURL = "https://storage.googleapis.com/mobile_test_assets/public/washington.pdf" + val pdfFileContent = "Washington Crossing the Delaware" + searchScreen { + }.loadPage(downloadTestPage) { + progressBar.waitUntilGone(waitingTime) + clickLinkMatchingText(downloadFileName) + verifyPageURL(pdfFileURL) + verifyPageContent(pdfFileContent) + } + } + + @SmokeTest + @Test + fun downloadAndOpenWebmFileTest() { + downloadFileName = "videoSample.webm" + + searchScreen { + }.loadPage(downloadTestPage) { + progressBar.waitUntilGone(waitingTime) + clickLinkMatchingText(downloadFileName) + } + // If permission dialog appears on devices with API<30, grant it + if (permAllowBtn.waitForExists(waitingTime)) { + permAllowBtn.click() + } + downloadRobot { + verifyDownloadDialog(downloadFileName) + clickDownloadButton() + verifyDownloadConfirmationMessage(downloadFileName) + openDownloadedFile() + assertNativeAppOpens(GOOGLE_PHOTOS) + } + } + + @SmokeTest + @Test + fun verifyDownloadedFileOnStorageTest() { + downloadFileName = "textfile.txt" + + searchScreen { + }.loadPage(downloadTestPage) { + progressBar.waitUntilGone(waitingTime) + clickLinkMatchingText(downloadFileName) + } + // If permission dialog appears on devices with API<30, grant it + if (permAllowBtn.waitForExists(waitingTime)) { + permAllowBtn.click() + } + downloadRobot { + verifyDownloadDialog(downloadFileName) + clickDownloadButton() + verifyDownloadConfirmationMessage(downloadFileName) + verifyDownloadedFileOnStorage(downloadFileName) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/EnhancedTrackingProtectionSettingsTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/EnhancedTrackingProtectionSettingsTest.kt new file mode 100644 index 0000000000..c8fc44db06 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/EnhancedTrackingProtectionSettingsTest.kt @@ -0,0 +1,351 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.espresso.Espresso.pressBack +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.browserScreen +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper.getEnhancedTrackingProtectionAsset +import org.mozilla.focus.helpers.TestAssetHelper.getGenericAsset +import org.mozilla.focus.helpers.TestHelper.exitToBrowser +import org.mozilla.focus.helpers.TestHelper.exitToTop +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest +import java.io.IOException + +@RunWith(AndroidJUnit4ClassRunner::class) +class EnhancedTrackingProtectionSettingsTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + } + + @After + fun tearDown() { + try { + webServer.shutdown() + } catch (e: IOException) { + throw AssertionError("Could not stop web server", e) + } + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun trackingProtectionTogglesListTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + verifyBlockAdTrackersEnabled(true) + verifyBlockAnalyticTrackersEnabled(true) + verifyBlockSocialTrackersEnabled(true) + verifyBlockOtherTrackersEnabled(false) + } + } + + // Some workarounds are temp needed, because of https://bugzilla.mozilla.org/show_bug.cgi?id=1794130: + // going to the Settings screen, + // loading another page, + // or refreshing the page multiple times until ETP starts working. + @SmokeTest + @Test + fun blockAdTrackersTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "adsTrackers") + + searchScreen { + }.loadPage(genericPage.url) { + // loading a generic page to allow GV to fully load on first run + verifyPageContent(genericPage.content) + }.openMainMenu { + }.openSettings { + exitToBrowser() + pressBack() + } + searchScreen { + }.loadPage(trackingPage.url) { + verifyTrackingProtectionAlert("ads trackers blocked") + } + } + + @SmokeTest + @Test + fun allowAdTrackersTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "adsTrackers") + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + clickAdTrackersBlockSwitch() + verifyBlockAdTrackersEnabled(false) + exitToTop() + } + searchScreen { + }.loadPage(genericPage.url) { + // loading a generic page to allow GV to fully load on first run + verifyPageContent(genericPage.content) + pressBack() + } + searchScreen { + }.loadPage(trackingPage.url) { + verifyPageContent("ads trackers not blocked") + } + } + + // Some workarounds are temp needed, because of https://bugzilla.mozilla.org/show_bug.cgi?id=1794130: + // going to the Settings screen, + // loading another page, + // or refreshing the page multiple times until ETP starts working. + @SmokeTest + @Test + fun blockAnalyticsTrackersTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "analyticsTrackers") + + searchScreen { + }.loadPage(genericPage.url) { + // loading a generic page to allow GV to fully load on first run + verifyPageContent(genericPage.content) + }.openMainMenu { + }.openSettings { + exitToBrowser() + pressBack() + } + searchScreen { + }.loadPage(trackingPage.url) { + verifyTrackingProtectionAlert("analytics trackers blocked") + } + } + + @SmokeTest + @Test + fun allowAnalyticsTrackersTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "analyticsTrackers") + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + clickAnalyticsTrackersBlockSwitch() + verifyBlockAnalyticTrackersEnabled(false) + exitToTop() + } + searchScreen { + }.loadPage(genericPage.url) { + // loading a generic page to allow GV to fully load on first run + verifyPageContent(genericPage.content) + pressBack() + } + searchScreen { + }.loadPage(trackingPage.url) { + verifyPageContent("analytics trackers not blocked") + } + } + + // Some workarounds are temp needed, because of https://bugzilla.mozilla.org/show_bug.cgi?id=1794130: + // going to the Settings screen, + // loading another page, + // or refreshing the page multiple times until ETP starts working. + @SmokeTest + @Test + fun blockSocialTrackersTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "socialTrackers") + + searchScreen { + }.loadPage(genericPage.url) { + // loading a generic page to allow GV to fully load on first run + verifyPageContent(genericPage.content) + }.openMainMenu { + }.openSettings { + exitToBrowser() + pressBack() + } + searchScreen { + }.loadPage(trackingPage.url) { + verifyTrackingProtectionAlert("social trackers blocked") + } + } + + @SmokeTest + @Test + fun allowSocialTrackersTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "socialTrackers") + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + clickSocialTrackersBlockSwitch() + verifyBlockSocialTrackersEnabled(false) + exitToTop() + } + searchScreen { + }.loadPage(genericPage.url) { + // loading a generic page to allow GV to fully load on first run + verifyPageContent(genericPage.content) + pressBack() + } + searchScreen { + }.loadPage(trackingPage.url) { + verifyPageContent("social trackers not blocked") + } + } + + @SmokeTest + @Test + fun allowOtherContentTrackersTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "otherTrackers") + + searchScreen { + }.loadPage(genericPage.url) { + // loading a generic page to allow GV to fully load on first run + verifyPageContent(genericPage.content) + pressBack() + } + searchScreen { + }.loadPage(trackingPage.url) { + verifyPageContent("other content trackers not blocked") + } + } + + // Some workarounds are temp needed, because of https://bugzilla.mozilla.org/show_bug.cgi?id=1794130: + // going to the Settings screen, + // loading another page, + // or refreshing the page multiple times until ETP starts working. + @SmokeTest + @Test + fun blockOtherContentTrackersTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "otherTrackers") + + searchScreen { + }.loadPage(genericPage.url) { + // loading a generic page to allow GV to fully load on first run + verifyPageContent(genericPage.content) + pressBack() + } + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + clickOtherContentTrackersBlockSwitch() + verifyBlockOtherTrackersEnabled(true) + exitToTop() + } + searchScreen { + }.loadPage(trackingPage.url) { + verifyTrackingProtectionAlert("other content trackers blocked") + } + } + + @SmokeTest + @Test + fun addURLToTPExceptionsListTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "otherTrackers") + + searchScreen { + }.loadPage(genericPage.url) { + verifyPageContent(genericPage.content) + }.openSearchBar { + }.loadPage(trackingPage.url) { + verifyPageContent(trackingPage.content) + }.openSiteSecurityInfoSheet { + }.clickTrackingProtectionSwitch { + progressBar.waitUntilGone(waitingTime) + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + openExceptionsList() + verifyExceptionURL(webServer.hostName) + } + } + + @SmokeTest + @Test + fun removeOneExceptionURLTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "otherTrackers") + + searchScreen { + }.loadPage(genericPage.url) { + verifyPageContent(genericPage.content) + }.openSearchBar { + }.loadPage(trackingPage.url) { + verifyPageContent(trackingPage.content) + }.openSiteSecurityInfoSheet { + }.clickTrackingProtectionSwitch { + progressBar.waitUntilGone(waitingTime) + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + openExceptionsList() + removeException() + verifyExceptionsListDisabled() + exitToBrowser() + } + browserScreen { + }.openSiteSecurityInfoSheet { + verifyTrackingProtectionIsEnabled(true) + } + } + + @SmokeTest + @Test + fun removeAllExceptionURLTest() { + val genericPage = getGenericAsset(webServer) + val trackingPage = getEnhancedTrackingProtectionAsset(webServer, "otherTrackers") + + searchScreen { + }.loadPage(genericPage.url) { + verifyPageContent(genericPage.content) + }.openSearchBar { + }.loadPage(trackingPage.url) { + verifyPageContent(trackingPage.content) + }.openSiteSecurityInfoSheet { + }.clickTrackingProtectionSwitch { + progressBar.waitUntilGone(waitingTime) + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + openExceptionsList() + removeAllExceptions() + verifyExceptionsListDisabled() + exitToBrowser() + } + browserScreen { + }.openSiteSecurityInfoSheet { + verifyTrackingProtectionIsEnabled(true) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/EraseBrowsingDataTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/EraseBrowsingDataTest.kt new file mode 100644 index 0000000000..c2c011a7f1 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/EraseBrowsingDataTest.kt @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.content.Intent +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.R +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.notificationTray +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.RetryTestRule +import org.mozilla.focus.helpers.TestAssetHelper.getGenericTabAsset +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.pressHomeKey +import org.mozilla.focus.helpers.TestHelper.restartApp +import org.mozilla.focus.helpers.TestHelper.verifySnackBarText +import org.mozilla.focus.testAnnotations.SmokeTest + +// These tests verify interaction with the browsing notification and erasing browsing data +@RunWith(AndroidJUnit4ClassRunner::class) +class EraseBrowsingDataTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Rule + @JvmField + val retryTestRule = RetryTestRule(3) + + @Before + fun setUp() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun trashButtonTest() { + val testPage = getGenericTabAsset(webServer, 1) + + searchScreen { + }.loadPage(testPage.url) { + verifyPageContent(testPage.content) + // Press erase button, and check for message and return to the main page + }.clearBrowsingData { + verifySnackBarText(getStringResource(R.string.feedback_erase2)) + verifyEmptySearchBar() + } + } + + @SmokeTest + @Test + fun notificationEraseAndOpenButtonTest() { + val testPage = getGenericTabAsset(webServer, 1) + + notificationTray { + mDevice.openNotification() + clearNotifications() + } + + searchScreen { + }.loadPage(testPage.url) { } + // Send app to background + pressHomeKey() + // Pull down system bar and select Erase and Open + mDevice.openNotification() + notificationTray { + verifySystemNotificationExists(getStringResource(R.string.notification_erase_text)) + expandEraseBrowsingNotification() + }.clickEraseAndOpenNotificationButton { + verifySnackBarText(getStringResource(R.string.feedback_erase2)) + verifyEmptySearchBar() + } + } + + @SmokeTest + @Test + fun deleteHistoryOnRestartTest() { + val testPage = getGenericTabAsset(webServer, 1) + + searchScreen { + }.loadPage(testPage.url) {} + restartApp(mActivityTestRule) + homeScreen { + verifyEmptySearchBar() + } + } + + @SmokeTest + @Test + fun systemBarHomeViewTest() { + val testPage = getGenericTabAsset(webServer, 1) + val LAUNCH_TIMEOUT = 5000 + val launcherPackage = mDevice.launcherPackageName + + notificationTray { + mDevice.openNotification() + clearNotifications() + } + + // Leave Focus open, delete browsing history and check the app is still running + searchScreen { + }.loadPage(testPage.url) { } + mDevice.openNotification() + notificationTray { + verifySystemNotificationExists(getStringResource(R.string.notification_erase_text)) + expandEraseBrowsingNotification() + }.clickNotificationMessage { + verifyEmptySearchBar() + } + + // Switch out of Focus, delete browsing history and check the app is killed + searchScreen { + }.loadPage(testPage.url) { } + pressHomeKey() + mDevice.openNotification() + notificationTray { + verifySystemNotificationExists(getStringResource(R.string.notification_erase_text)) + expandEraseBrowsingNotification() + }.clickNotificationMessage { + // Wait for launcher + Assert.assertNotNull(launcherPackage) + mDevice.wait( + Until.hasObject(By.pkg(launcherPackage).depth(0)), + LAUNCH_TIMEOUT.toLong(), + ) + + // Re-launch the app, verify it's not showing the previous browsing session + mActivityTestRule.launchActivity(Intent(Intent.ACTION_MAIN)) + verifyEmptySearchBar() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ErrorPagesTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ErrorPagesTest.kt new file mode 100644 index 0000000000..eb511ff63d --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ErrorPagesTest.kt @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.R +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.setNetworkEnabled + +// This tests verify invalid URL and no network connection error pages +@RunWith(AndroidJUnit4ClassRunner::class) +class ErrorPagesTest { + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + val mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + } + + @After + fun tearDown() { + featureSettingsHelper.resetAllFeatureFlags() + } + + @Test + fun badURLCheckTest() { + val badURl = "bad.url" + + searchScreen { + }.loadPage(badURl) { + verifyPageContent(getStringResource(R.string.mozac_browser_errorpages_unknown_host_title)) + verifyPageContent("Try Again") + } + } + + @Test + fun noNetworkConnectionErrorPageTest() { + val pageUrl = "mozilla.org" + + setNetworkEnabled(false) + searchScreen { + }.loadPage(pageUrl) { + verifyPageContent(getStringResource(R.string.mozac_browser_errorpages_unknown_host_title)) + verifyPageContent("Try Again") + setNetworkEnabled(true) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/FirstRunTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/FirstRunTest.kt new file mode 100644 index 0000000000..e110d1035c --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/FirstRunTest.kt @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestHelper.restartApp +import org.mozilla.focus.testAnnotations.SmokeTest + +// Tests the First run onboarding screens +@RunWith(AndroidJUnit4ClassRunner::class) +class FirstRunTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + val mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = true) + + @Before + fun startWebServer() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + } + + @After + fun stopWebServer() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun onboardingScreensTest() { + homeScreen { + verifyFirstOnboardingScreenItems() + restartApp(mActivityTestRule) + verifyFirstOnboardingScreenItems() + clickGetStartedButton() + verifySecondOnboardingScreenItems() + restartApp(mActivityTestRule) + verifySecondOnboardingScreenItems() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MediaPlaybackTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MediaPlaybackTest.kt new file mode 100644 index 0000000000..c8129b9490 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MediaPlaybackTest.kt @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.activity.robots.browserScreen +import org.mozilla.focus.activity.robots.notificationTray +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper.getMediaTestAsset +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.testAnnotations.SmokeTest + +class MediaPlaybackTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get:Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun testVideoPlayback() { + val videoPageUrl = getMediaTestAsset(webServer, "videoPage").url + + searchScreen { + }.loadPage(videoPageUrl) { + clickPlayButton() + waitForPlaybackToStart() + // need this alert hack to check the video is playing, + // currently the test cannot verify the text in the page + clickPauseButton() + verifyPlaybackStopped() + } + } + + @SmokeTest + @Test + fun testAudioPlayback() { + val audioPageUrl = getMediaTestAsset(webServer, "audioPage").url + + searchScreen { + }.loadPage(audioPageUrl) { + clickPlayButton() + waitForPlaybackToStart() + // need this alert hack to check the video is playing, + // currently the test cannot verify the text in the page + clickPauseButton() + verifyPlaybackStopped() + } + } + + @SmokeTest + @Test + fun testMediaContentNotification() { + val audioPageUrl = getMediaTestAsset(webServer, "audioPage").url + val notificationMessage = "A site is playing media" + + searchScreen { + }.loadPage(audioPageUrl) { + clickPlayButton() + waitForPlaybackToStart() + } + mDevice.openNotification() + notificationTray { + verifyMediaNotificationExists("A site is playing media") + clickMediaNotificationControlButton("Pause") + verifyMediaNotificationButtonState("Play") + } + mDevice.pressBack() + browserScreen { + verifyPlaybackStopped() + }.clearBrowsingData {} + mDevice.openNotification() + notificationTray { + verifyNotificationGone(notificationMessage) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MozillaSupportPagesTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MozillaSupportPagesTest.kt new file mode 100644 index 0000000000..2098180ef4 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MozillaSupportPagesTest.kt @@ -0,0 +1,128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.R +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.TestHelper.getTargetContext +import org.mozilla.focus.testAnnotations.SmokeTest + +// This test visits each About page and checks whether some essential elements are being displayed +@RunWith(AndroidJUnit4ClassRunner::class) +class MozillaSupportPagesTest { + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + val mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + } + + @After + fun tearDown() { + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun openMenuHelpPageTest() { + homeScreen { + }.openMainMenu { + }.clickHelpPageLink { + verifyPageURL("what-firefox-focus-android") + } + } + + @SmokeTest + @Test + fun openAboutPageTest() { + // Go to settings "About" page + homeScreen { + }.openMainMenu { + }.openSettings { + }.openMozillaSettingsMenu { + }.openAboutPage { + verifyVersionNumbers() + }.openAboutPageLearnMoreLink { + verifyPageURL("www.mozilla.org/en-US/about/manifesto/") + } + } + + @SmokeTest + @Test + fun openMozillaSettingsHelpLinkTest() { + // Go to settings "About" page + homeScreen { + }.openMainMenu { + }.openSettings { + }.openMozillaSettingsMenu { + }.openHelpLink { + verifyPageURL("what-firefox-focus-android") + } + } + + @SmokeTest + @Test + fun openYourRightsPageTest() { + val yourRightsString = getTargetContext.getString( + R.string.your_rights_content1, + getTargetContext.getString(R.string.app_name), + "Mozilla Public License", + ) + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openMozillaSettingsMenu { + }.openYourRightsPage { + verifyPageContent(yourRightsString) + } + } + + @SmokeTest + @Test + fun openLibrariesThatWeUse() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openMozillaSettingsMenu { + }.openLibrariesUsedPage { + verifyLibrariesUsedTitle() + } + } + + @SmokeTest + @Test + fun openAboutLicenses() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openMozillaSettingsMenu { + }.openLicenseInformation { + verifyPageURL("about:license") + } + } + + @SmokeTest + @Test + fun openPrivacyNoticeTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openMozillaSettingsMenu { + }.openPrivacyNotice { + verifyPageURL("privacy/firefox-focus") + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MultitaskingTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MultitaskingTest.kt new file mode 100644 index 0000000000..061f44d9a3 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/MultitaskingTest.kt @@ -0,0 +1,147 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import androidx.test.platform.app.InstrumentationRegistry +import mozilla.components.browser.state.selector.privateTabs +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.R +import org.mozilla.focus.activity.robots.browserScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.ext.components +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.RetryTestRule +import org.mozilla.focus.helpers.TestAssetHelper.getGenericAsset +import org.mozilla.focus.helpers.TestAssetHelper.getGenericTabAsset +import org.mozilla.focus.helpers.TestHelper.clickSnackBarActionButton +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.openAppFromExternalLink +import org.mozilla.focus.helpers.TestHelper.verifySnackBarText +import org.mozilla.focus.testAnnotations.SmokeTest + +/** + * Open multiple sessions and verify that the trash icon changes to a tabs counter + */ +@RunWith(AndroidJUnit4ClassRunner::class) +class MultitaskingTest { + private lateinit var webServer: MockWebServer + private val store = InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + .components + .store + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Rule + @JvmField + val retryTestRule = RetryTestRule(3) + + @Before + @Throws(Exception::class) + fun startWebServer() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + @Throws(Exception::class) + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun testVisitingMultipleSites() { + val tab1 = getGenericTabAsset(webServer, 1) + val tab2 = getGenericTabAsset(webServer, 2) + val tab3 = getGenericTabAsset(webServer, 3) + val eraseBrowsingSnackBarText = getStringResource(R.string.feedback_erase2) + val customTabPage = getGenericAsset(webServer) + + // Load website: Erase button visible, Tabs button not + searchScreen { + }.loadPage(tab1.url) { + longPressLink("Tab 2") + verifyLinkContextMenu(tab2.url) + openLinkInNewTab() + verifyNumberOfTabsOpened(2) + longPressLink("Tab 3") + openLinkInNewTab() + verifySnackBarText("New private tab opened") + clickSnackBarActionButton("SWITCH") + verifyNumberOfTabsOpened(3) + } + + openAppFromExternalLink(customTabPage.url) + browserScreen { + verifyNumberOfTabsOpened(4) + }.openTabsTray { + verifyTabsOrder(tab1.title, tab3.title, tab2.title, customTabPage.title) + }.selectTab(tab1.title) { + verifyPageContent("Tab 1") + }.clearBrowsingData { + verifySnackBarText(eraseBrowsingSnackBarText) + assertTrue(store.state.privateTabs.isEmpty()) + } + } + + @SmokeTest + @Test + fun closeTabButtonTest() { + val tab1 = getGenericTabAsset(webServer, 1) + val tab2 = getGenericTabAsset(webServer, 2) + val tab3 = getGenericTabAsset(webServer, 3) + + searchScreen { + }.loadPage(tab1.url) { + verifyPageContent("Tab 1") + longPressLink("Tab 2") + openLinkInNewTab() + longPressLink("Tab 3") + openLinkInNewTab() + verifyNumberOfTabsOpened(3) + }.openTabsTray { + verifyTabsOrder(tab1.title, tab3.title, tab2.title) + }.closeTab(tab1.title) { + }.openTabsTray { + verifyTabsOrder(tab3.title, tab2.title) + }.closeTab(tab3.title) { + verifyTabsCounterNotShown() + } + } + + @SmokeTest + @Test + fun verifyTabsTrayListTest() { + val tab1 = getGenericTabAsset(webServer, 1) + val tab2 = getGenericTabAsset(webServer, 2) + + searchScreen { + }.loadPage(tab1.url) { + longPressLink("Tab 2") + openLinkInNewTab() + }.openTabsTray { + }.selectTab(tab2.title) { + }.openTabsTray { + verifyCloseTabButton(tab1.title) + verifyCloseTabButton(tab2.title) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/OldFirstRunTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/OldFirstRunTest.kt new file mode 100644 index 0000000000..8bb0ec78db --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/OldFirstRunTest.kt @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper + +// Tests the First run onboarding screens +@RunWith(AndroidJUnit4ClassRunner::class) +class OldFirstRunTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + val mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = true, showNewOnboarding = false) + + @Before + fun setUp() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @Test + fun firstRunOnboardingTest() { + homeScreen { + verifyOnboardingFirstSlide() + clickOnboardingNextButton() + verifyOnboardingSecondSlide() + clickOnboardingNextButton() + verifyOnboardingThirdSlide() + clickOnboardingNextButton() + verifyOnboardingLastSlide() + clickOnboardingFinishButton() + verifyEmptySearchBar() + } + } + + @Test + fun skipFirstRunOnboardingTest() { + homeScreen { + verifyOnboardingFirstSlide() + clickOnboardingNextButton() + verifyOnboardingSecondSlide() + skipFirstRun() + verifyEmptySearchBar() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/OpenInExternalBrowserDialogueTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/OpenInExternalBrowserDialogueTest.kt new file mode 100644 index 0000000000..59df011f65 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/OpenInExternalBrowserDialogueTest.kt @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityIntentsTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.StringsHelper.GOOGLE_CHROME +import org.mozilla.focus.helpers.TestAssetHelper.getGenericAsset +import org.mozilla.focus.helpers.TestHelper.assertNativeAppOpens +import org.mozilla.focus.testAnnotations.SmokeTest + +// This test verifies the "Open in..." option from the main menu +@RunWith(AndroidJUnit4ClassRunner::class) +class OpenInExternalBrowserDialogueTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityIntentsTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + mActivityTestRule.activity.finishAndRemoveTask() + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun openPageInExternalAppTest() { + val pageUrl = getGenericAsset(webServer).url + + searchScreen { + }.loadPage(pageUrl) { + }.openMainMenu { + clickOpenInOption() + verifyOpenInDialog() + clickOpenInChrome() + assertNativeAppOpens(GOOGLE_CHROME) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SafeBrowsingTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SafeBrowsingTest.kt new file mode 100644 index 0000000000..4843da7d24 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SafeBrowsingTest.kt @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.R +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestHelper.exitToTop +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.testAnnotations.SmokeTest + +// These tests verify the Safe Browsing feature by visiting unsafe URLs and checking they are blocked +class SafeBrowsingTest { + private lateinit var webServer: MockWebServer + private val malwareWarning = getStringResource(R.string.mozac_browser_errorpages_safe_browsing_malware_uri_title) + private val phishingWarning = getStringResource(R.string.mozac_browser_errorpages_safe_phishing_uri_title) + private val unwantedSoftwareWarning = + getStringResource(R.string.mozac_browser_errorpages_safe_browsing_unwanted_uri_title) + private val harmfulSiteWarning = getStringResource(R.string.mozac_browser_errorpages_safe_harmful_uri_title) + private val tryAgainButton = getStringResource(R.string.mozac_browser_errorpages_page_refresh) + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + val mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun blockMalwarePageTest() { + val malwareURl = "http://itisatrap.org/firefox/its-an-attack.html" + + searchScreen { + }.loadPage(malwareURl) { + verifyPageContent(malwareWarning) + verifyPageContent(tryAgainButton) + } + } + + @SmokeTest + @Test + fun blockPhishingPageTest() { + val phishingURl = "http://itisatrap.org/firefox/its-a-trap.html" + + searchScreen { + }.loadPage(phishingURl) { + verifyPageContent(phishingWarning) + verifyPageContent(tryAgainButton) + } + } + + @SmokeTest + @Test + fun blockUnwantedSoftwarePageTest() { + val unwantedURl = "http://itisatrap.org/firefox/unwanted.html" + + searchScreen { + }.loadPage(unwantedURl) { + verifyPageContent(unwantedSoftwareWarning) + verifyPageContent(tryAgainButton) + } + } + + @SmokeTest + @Test + fun blockHarmfulPageTest() { + val harmfulURl = "https://www.itisatrap.org/firefox/harmful.html" + + searchScreen { + }.loadPage(harmfulURl) { + verifyPageContent(harmfulSiteWarning) + verifyPageContent(tryAgainButton) + } + } + + @SmokeTest + @Test + fun unblockSafeBrowsingTest() { + val malwareURl = "http://itisatrap.org/firefox/its-an-attack.html" + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + switchSafeBrowsingToggle() + exitToTop() + } + searchScreen { + }.loadPage(malwareURl) { + verifyPageContent("It’s an Attack!") + } + } + + @SmokeTest + @Test + fun verifyPageSecurityIconAndInfo() { + val safePageUrl = "https://mozilla-mobile.github.io/testapp/" + val insecurePageUrl = "http://itisatrap.org/firefox/its-a-trap.html" + + searchScreen { + }.loadPage(safePageUrl) { + verifyPageContent("Lets test!") + verifySiteTrackingProtectionIconShown() + }.openSiteSecurityInfoSheet { + verifySiteConnectionInfoIsSecure(true) + }.closeSecurityInfoSheet { + }.clearBrowsingData {} + searchScreen { + }.loadPage(insecurePageUrl) { + verifyPageURL(insecurePageUrl) + verifySiteSecurityIndicatorShown() + }.openSiteSecurityInfoSheet { + verifySiteConnectionInfoIsSecure(false) + }.closeSecurityInfoSheet { } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SearchTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SearchTest.kt new file mode 100644 index 0000000000..82b7384965 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SearchTest.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 org.mozilla.focus.activity + +import androidx.test.espresso.Espresso.pressBack +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.activity.robots.browserScreen +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.TestHelper.exitToTop +import org.mozilla.focus.helpers.TestHelper.pressEnterKey +import org.mozilla.focus.helpers.TestHelper.verifyKeyboardVisibility +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest + +// This test checks the search engine can be changed and that search suggestions appear +class SearchTest { + private lateinit var searchString: String + private val enginesList = listOf("DuckDuckGo", "Google", "Amazon.com", "Wikipedia") + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + } + + @After + fun tearDown() { + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun changeSearchEngineTest() { + for (searchEngine in enginesList) { + // Open [settings menu] and select Search engine + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + openSearchEngineSubMenu() + selectSearchEngine(searchEngine) + exitToTop() + } + + searchScreen { + typeInSearchBar("mozilla ") + pressEnterKey() + } + + browserScreen { + verifyPageURL(searchEngine) + pressBack() + } + } + } + + @SmokeTest + @Test + fun enableSearchSuggestionOnFirstRunTest() { + searchString = "mozilla " + + searchScreen { + // type and check search suggestions are displayed + typeInSearchBar(searchString) + allowEnableSearchSuggestions() + verifySearchSuggestionsAreShown() + clearSearchBar() + } + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + verifySearchSuggestionsSwitchState(true) + } + } + + @SmokeTest + @Test + fun disableSearchSuggestionOnFirstRunTest() { + searchString = "mozilla " + + searchScreen { + typeInSearchBar(searchString) + denyEnableSearchSuggestions() + verifySearchSuggestionsAreNotShown() + clearSearchBar() + } + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + verifySearchSuggestionsSwitchState(false) + } + } + + @Test + fun testBlankSearchDoesNothing() { + searchScreen { + // Search on blank spaces should not do anything + typeInSearchBar(" ") + pressEnterKey() + searchScreen { + verifySearchEditBarContainsText(" ") + } + } + } + + @Test + fun testSearchBarShowsSearchTermOnEdit() { + searchString = "mozilla focus" + + searchScreen { + typeInSearchBar(searchString) + pressEnterKey() + } + browserScreen { + progressBar.waitUntilGone(waitingTime) + verifyPageURL("google") + }.openSearchBar { + // Tap URL bar, check it displays search term (instead of URL) + verifySearchEditBarContainsText(searchString) + } + } + + @SmokeTest + @Test + fun disableSearchSuggestionsTest() { + searchString = "mozilla " + + searchScreen { + // Search on blank spaces should not do anything + verifySearchBarIsDisplayed() + typeInSearchBar(searchString) + allowEnableSearchSuggestions() + clearSearchBar() + } + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + clickSearchSuggestionsSwitch() + exitToTop() + } + + searchScreen { + typeInSearchBar(searchString) + verifySearchSuggestionsAreNotShown() + } + } + + @SmokeTest + @Test + fun clearSearchButtonTest() { + searchString = "mozilla " + + homeScreen { + }.openSearchBar { + typeInSearchBar(searchString) + verifyKeyboardVisibility(true) + verifySearchEditBarContainsText(searchString) + clearSearchBar() + verifyKeyboardVisibility(true) + verifySearchEditBarIsEmpty() + } + + searchString = "firefox" + + searchScreen { + typeInSearchBar(searchString) + verifySearchEditBarContainsText(searchString) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsAdvancedTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsAdvancedTest.kt new file mode 100644 index 0000000000..aa31ed5441 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsAdvancedTest.kt @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper.getGenericTabAsset +import org.mozilla.focus.helpers.TestHelper.waitingTimeShort +import org.mozilla.focus.testAnnotations.SmokeTest + +// These tests check the advanced settings options +@RunWith(AndroidJUnit4ClassRunner::class) +class SettingsAdvancedTest { + private lateinit var webServer: MockWebServer + + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setup() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun openLinksInAppsTest() { + val tab3Url = getGenericTabAsset(webServer, 3).url + val youtubeLink = "https://www.youtube.com/c/MozillaChannel/videos" + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openAdvancedSettingsMenu { + verifyOpenLinksInAppsSwitchState(false) + clickOpenLinksInAppsSwitch() + verifyOpenLinksInAppsSwitchState(true) + }.goBackToSettings { + }.goBackToHomeScreen { + }.loadPage(tab3Url) { + progressBar.waitUntilGone(waitingTimeShort) + clickLinkMatchingText("Mozilla Youtube link") + verifyOpenLinksInAppsPrompt(true, youtubeLink) + clickOpenLinksInAppsCancelButton() + }.clearBrowsingData { + }.openMainMenu { + }.openSettings { + }.openAdvancedSettingsMenu { + verifyOpenLinksInAppsSwitchState(true) + clickOpenLinksInAppsSwitch() + verifyOpenLinksInAppsSwitchState(false) + }.goBackToSettings { + }.goBackToHomeScreen { + }.loadPage(tab3Url) { + progressBar.waitUntilGone(waitingTimeShort) + clickLinkMatchingText("Mozilla Youtube link") + verifyOpenLinksInAppsPrompt(false, youtubeLink) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsGeneralTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsGeneralTest.kt new file mode 100644 index 0000000000..e5b49920d7 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsGeneralTest.kt @@ -0,0 +1,178 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.content.res.Configuration +import android.os.Build +import androidx.test.platform.app.InstrumentationRegistry +import mozilla.components.support.locale.LocaleManager +import mozilla.components.support.locale.LocaleUseCases +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.mozilla.focus.activity.robots.browserScreen +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.ext.components +import org.mozilla.focus.helpers.MainActivityIntentsTestRule +import org.mozilla.focus.helpers.StringsHelper.AF_GENERAL_HEADING +import org.mozilla.focus.helpers.StringsHelper.AF_HELP +import org.mozilla.focus.helpers.StringsHelper.AF_LANGUAGE_MENU +import org.mozilla.focus.helpers.StringsHelper.AF_LANGUAGE_SYSTEM_DEFAULT +import org.mozilla.focus.helpers.StringsHelper.AF_SETTINGS +import org.mozilla.focus.helpers.StringsHelper.EN_AFRIKAANS_LOCALE +import org.mozilla.focus.helpers.StringsHelper.EN_LANGUAGE_MENU_HEADING +import org.mozilla.focus.helpers.StringsHelper.FR_GENERAL_HEADING +import org.mozilla.focus.helpers.StringsHelper.FR_LANGUAGE_MENU +import org.mozilla.focus.helpers.StringsHelper.FR_LANGUAGE_SYSTEM_DEFAULT +import org.mozilla.focus.helpers.StringsHelper.FR_SETTINGS +import org.mozilla.focus.helpers.TestHelper.exitToTop +import org.mozilla.focus.helpers.TestHelper.verifyTranslatedTextExists +import org.mozilla.focus.locale.Locales +import org.mozilla.focus.testAnnotations.SmokeTest +import org.mozilla.gecko.util.ThreadUtils.runOnUiThread + +// Tests for the General settings sub-menu: changing theme, locale and default browser +class SettingsGeneralTest { + @get: Rule + var mActivityTestRule = MainActivityIntentsTestRule(showFirstRun = false) + + @get: Rule + var watcher: TestRule = object : TestWatcher() { + override fun starting(description: Description) { + println("Starting test: " + description.methodName) + if (description.methodName == "frenchLocaleTest") { + changeLocale("fr") + } + } + } + + @After + fun tearDown() { + changeLocale("en") + } + + @SmokeTest + @Test + fun changeThemeTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openGeneralSettingsMenu { + verifyThemesList() + selectDarkTheme() + verifyThemeApplied(isDarkTheme = true, getThemeState = getUiTheme()) + selectLightTheme() + verifyThemeApplied(isLightTheme = true, getThemeState = getUiTheme()) + selectDeviceTheme() + verifyThemeApplied(isLightTheme = true, getThemeState = getUiTheme()) + } + } + + @SmokeTest + @Test + fun englishSystemLocaleTest() { + /* Go to Settings and change language to French*/ + homeScreen { + }.openMainMenu { + }.openSettings { + }.openGeneralSettingsMenu { + openLanguageSelectionMenu() + verifySystemLocaleSelected() + selectLanguage(EN_AFRIKAANS_LOCALE) + verifyTranslatedTextExists(AF_LANGUAGE_MENU) + exitToTop() + } + /* Exit to main and see the UI is in French as well */ + homeScreen { + }.openMainMenu { + verifyTranslatedTextExists(AF_SETTINGS) + verifyTranslatedTextExists(AF_HELP) + /* change back to system locale, verify the locale is changed */ + }.openSettings(AF_SETTINGS) { + }.openGeneralSettingsMenu(AF_GENERAL_HEADING) { + openLanguageSelectionMenu(AF_LANGUAGE_MENU) + selectLanguage(AF_LANGUAGE_SYSTEM_DEFAULT) + verifyTranslatedTextExists(EN_LANGUAGE_MENU_HEADING) + exitToTop() + } + homeScreen { + }.openMainMenu { + verifySettingsButtonExists() + verifyHelpPageLinkExists() + } + } + + @Test + fun frenchLocaleTest() { + /* Go to Settings */ + homeScreen { + }.openMainMenu { + }.openSettings(FR_SETTINGS) { + }.openGeneralSettingsMenu(FR_GENERAL_HEADING) { + openLanguageSelectionMenu(FR_LANGUAGE_MENU) + verifySystemLocaleSelected(FR_LANGUAGE_SYSTEM_DEFAULT) + /* change locale to English, verify the locale is changed */ + selectLanguage(EN_AFRIKAANS_LOCALE) + verifyTranslatedTextExists(AF_LANGUAGE_MENU) + exitToTop() + } + homeScreen { + }.openMainMenu { + verifyTranslatedTextExists(AF_SETTINGS) + verifyTranslatedTextExists(AF_HELP) + } + } + + @SmokeTest + @Test + fun changeDefaultBrowserTest() { + val supportPageUrl = "https://support.mozilla.org/en-US/kb/set-firefox-focus-default-browser-android" + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openGeneralSettingsMenu { + clickSetDefaultBrowser() + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + browserScreen { + verifyPageURL(supportPageUrl) + } + } else { + verifyAndroidDefaultAppsMenuAppears() + // for API 24 to 28 we'll skip these steps because the switch doesn't update after + // returning from Default apps settings, not reproducing manually + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + selectFocusDefaultBrowser() + verifySwitchIsToggled(true) + } + } + } + } + + fun changeLocale(languageTag: String) { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val locale = Locales.parseLocaleCode(languageTag) + LocaleManager.setNewLocale( + mActivityTestRule.activity, + LocaleUseCases(context.components.store), + locale, + ) + runOnUiThread { mActivityTestRule.activity.recreate() } + } + + private fun getUiTheme(): Boolean { + val mode = + mActivityTestRule.activity.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) + + return when (mode) { + Configuration.UI_MODE_NIGHT_YES -> true // dark theme is set + Configuration.UI_MODE_NIGHT_NO -> false // dark theme is not set, using light theme + else -> false // default option is light theme + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsPrivacyTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsPrivacyTest.kt new file mode 100644 index 0000000000..f313eef047 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsPrivacyTest.kt @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.RetryTestRule +import org.mozilla.focus.helpers.TestAssetHelper.getStorageTestAsset +import org.mozilla.focus.helpers.TestHelper.exitToTop +import org.mozilla.focus.helpers.TestHelper.progressBar +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest + +// These tests the Privacy and Security settings menus and options +@RunWith(AndroidJUnit4ClassRunner::class) +class SettingsPrivacyTest { + private val featureSettingsHelper = FeatureSettingsHelper() + private lateinit var webServer: MockWebServer + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Rule + @JvmField + val retryTestRule = RetryTestRule(3) + + @Before + fun setup() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + featureSettingsHelper.resetAllFeatureFlags() + webServer.shutdown() + } + + @SmokeTest + @Test + fun verifyCookiesAndSiteDataItemsTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + verifyCookiesAndSiteDataSection() + clickBlockCookies() + verifyBlockCookiesPrompt() + clickCancelBlockCookiesPrompt() + } + } + + @SmokeTest + @Test + fun verifyAllCookiesBlockedTest() { + val sameSiteCookiesUrl = getStorageTestAsset(webServer, "same-site-cookies.html").url + val thirdPartyCookiesUrl = getStorageTestAsset(webServer, "cross-site-cookies.html").url + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + clickBlockCookies() + clickYesPleaseOption() + exitToTop() + } + searchScreen { + }.loadPage(sameSiteCookiesUrl) { + progressBar.waitUntilGone(waitingTime) + verifyCookiesEnabled("BLOCKED") + }.clearBrowsingData { + }.openSearchBar { + }.loadPage(thirdPartyCookiesUrl) { + progressBar.waitUntilGone(waitingTime) + verifyCookiesEnabled("BLOCKED") + } + } + + @SmokeTest + @Test + fun verify3rdPartyCookiesBlockedTest() { + val sameSiteCookiesUrl = getStorageTestAsset(webServer, "same-site-cookies.html").url + val thirdPartyCookiesURL = getStorageTestAsset(webServer, "cross-site-cookies.html").url + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + clickBlockCookies() + clickBlockThirdPartyCookiesOnly() + }.goBackToSettings { + }.goBackToHomeScreen { + }.loadPage(thirdPartyCookiesURL) { + progressBar.waitUntilGone(waitingTime) + verifyCookiesEnabled("BLOCKED") + }.clearBrowsingData { + }.openSearchBar { + }.loadPage(sameSiteCookiesUrl) { + progressBar.waitUntilGone(waitingTime) + verifyCookiesEnabled("UNRESTRICTED") + } + } + + @Test + fun verifyCrossSiteCookiesBlockedTest() { + val sameSiteCookiesUrl = getStorageTestAsset(webServer, "same-site-cookies.html").url + val crossSiteCookiesURL = getStorageTestAsset(webServer, "cross-site-cookies.html").url + + searchScreen { + }.loadPage(crossSiteCookiesURL) { + progressBar.waitUntilGone(waitingTime) + verifyCookiesEnabled("PARTITIONED") + }.clearBrowsingData { + }.openSearchBar { + }.loadPage(sameSiteCookiesUrl) { + progressBar.waitUntilGone(waitingTime) + verifyCookiesEnabled("UNRESTRICTED") + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsTest.kt new file mode 100644 index 0000000000..83e813c486 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SettingsTest.kt @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.testAnnotations.SmokeTest + +// This test checks all the headings in the Settings menu are there +@RunWith(AndroidJUnit4ClassRunner::class) +class SettingsTest { + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @SmokeTest + @Test + fun accessSettingsMenuTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + verifySettingsMenuItems() + } + } + + @SmokeTest + @Test + fun verifyGeneralSettingsMenuTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openGeneralSettingsMenu { + verifyGeneralSettingsItems() + } + } + + @SmokeTest + @Test + fun verifyPrivacySettingsMenuTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + verifyPrivacySettingsItems() + } + } + + @SmokeTest + @Test + fun verifySearchSettingsMenuTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + verifySearchSettingsItems() + } + } + + @SmokeTest + @Test + fun verifyAdvancedSettingsMenuTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openAdvancedSettingsMenu { + verifyAdvancedSettingsItems() + } + } + + @SmokeTest + @Test + fun verifyMozillaMenuTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openMozillaSettingsMenu { + verifyMozillaMenuItems() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ShortcutsTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ShortcutsTest.kt new file mode 100644 index 0000000000..3c62d8efeb --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ShortcutsTest.kt @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:Suppress("DEPRECATION") + +package org.mozilla.focus.activity + +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.activity.robots.browserScreen +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper +import org.mozilla.focus.helpers.TestAssetHelper.getGenericAsset +import org.mozilla.focus.testAnnotations.SmokeTest +import java.io.IOException + +class ShortcutsTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + try { + webServer.shutdown() + } catch (e: IOException) { + throw AssertionError("Could not stop web server", e) + } + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun renameShortcutTest() { + val webPage = object { + val url = getGenericAsset(webServer).url + val title = getGenericAsset(webServer).title + val content = getGenericAsset(webServer).content + val newTitle = "TestShortcut" + } + + searchScreen { + }.loadPage(webPage.url) { + verifyPageContent(webPage.content) + }.openMainMenu { + clickAddToShortcuts() + } + browserScreen { + }.clearBrowsingData { + verifyPageShortcutExists(webPage.title) + longTapPageShortcut(webPage.title) + clickRenameShortcut() + renameShortcutAndSave(webPage.newTitle) + verifyPageShortcutExists(webPage.newTitle) + } + } + + @SmokeTest + @Test + fun shortcutsDoNotOpenInNewTabTest() { + val tab1 = TestAssetHelper.getGenericTabAsset(webServer, 1) + val tab2 = TestAssetHelper.getGenericTabAsset(webServer, 2) + + searchScreen { + }.loadPage(tab1.url) { + }.openMainMenu { + clickAddToShortcuts() + } + browserScreen { + }.clearBrowsingData { + verifyPageShortcutExists(tab1.title) + } + + searchScreen { + }.loadPage(tab2.url) { + }.openSearchBar { + } + + homeScreen { + }.clickPageShortcut(tab1.title) { + verifyTabsCounterNotShown() + } + } + + @SmokeTest + @Test + fun searchBarShowsPageShortcutsTest() { + val webPage = getGenericAsset(webServer) + + searchScreen { + }.loadPage(webPage.url) { + verifyPageContent(webPage.content) + }.openMainMenu { + clickAddToShortcuts() + } + browserScreen { + }.clearBrowsingData { + verifyPageShortcutExists(webPage.title) + }.clickPageShortcut(webPage.title) { + }.openSearchBar { + verifySearchSuggestionsContain(webPage.title) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SitePermissionsTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SitePermissionsTest.kt new file mode 100644 index 0000000000..acbbeedbe4 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SitePermissionsTest.kt @@ -0,0 +1,272 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.Manifest +import android.content.Context +import android.hardware.camera2.CameraManager +import androidx.test.rule.GrantPermissionRule +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockLocationUpdatesRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper.getGenericAsset +import org.mozilla.focus.helpers.TestAssetHelper.getMediaTestAsset +import org.mozilla.focus.helpers.TestHelper.exitToTop +import org.mozilla.focus.helpers.TestHelper.getTargetContext +import org.mozilla.focus.helpers.TestHelper.grantAppPermission +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest + +class SitePermissionsTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + /* Test page created and handled by the Mozilla mobile test-eng team */ + private val permissionsPage = "https://mozilla-mobile.github.io/testapp/permissions" + private val testPageSubstring = "https://mozilla-mobile.github.io:443" + private val cameraManager = getTargetContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager + + @get: Rule + val mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @get:Rule + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.ACCESS_COARSE_LOCATION, + ) + + @get: Rule + val mockLocationUpdatesRule = MockLocationUpdatesRule() + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @Test + fun sitePermissionsSettingsItemsTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + }.clickSitePermissionsSettings { + verifySitePermissionsItems() + } + } + + @SmokeTest + @Test + fun autoplayPermissionsSettingsItemsTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + }.clickSitePermissionsSettings { + openAutoPlaySettings() + verifyAutoplaySection() + } + } + + // Tests the default autoplay setting: Block audio only on a video with autoplay attribute and not muted + @SmokeTest + @Test + fun blockAudioAutoplayPermissionTest() { + val videoPage = getMediaTestAsset(webServer, "videoPage") + + searchScreen { + }.loadPage(videoPage.url) { + progressBar.waitUntilGone(waitingTime) + // an un-muted video won't be able to autoplay with this setting, so we have to press play + clickPlayButton() + waitForPlaybackToStart() + } + } + + // Tests the default autoplay setting: Block audio only on a video with autoplay and muted attributes + @SmokeTest + @Test + fun blockAudioAutoplayPermissionOnMutedVideoTest() { + val mutedVideoPage = getMediaTestAsset(webServer, "mutedVideoPage") + + searchScreen { + }.loadPage(mutedVideoPage.url) { + // a muted video will autoplay with this setting + waitForPlaybackToStart() + } + } + + // Tests the autoplay setting: Allow audio and video on a video with autoplay attribute and not muted + @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1830312") + @SmokeTest + @Test + fun allowAudioVideoAutoplayPermissionTest() { + val videoPage = getMediaTestAsset(webServer, "videoPage") + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + }.clickSitePermissionsSettings { + openAutoPlaySettings() + selectAllowAudioVideoAutoplay() + exitToTop() + } + searchScreen { + }.loadPage(videoPage.url) { + waitForPlaybackToStart() + } + } + + // Tests the autoplay setting: Allow audio and video on a video with autoplay and muted attributes + @SmokeTest + @Test + fun allowAudioVideoAutoplayPermissionOnMutedVideoTest() { + val genericPage = getGenericAsset(webServer) + val mutedVideoPage = getMediaTestAsset(webServer, "mutedVideoPage") + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + }.clickSitePermissionsSettings { + openAutoPlaySettings() + selectAllowAudioVideoAutoplay() + exitToTop() + } + searchScreen { + }.loadPage(genericPage.url) { + }.clearBrowsingData {} + searchScreen { + }.loadPage(mutedVideoPage.url) { + waitForPlaybackToStart() + } + } + + // Tests the autoplay setting: Block audio and video + @SmokeTest + @Test + fun blockAudioVideoAutoplayPermissionTest() { + val videoPage = getMediaTestAsset(webServer, "videoPage") + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + }.clickSitePermissionsSettings { + openAutoPlaySettings() + selectBlockAudioVideoAutoplay() + exitToTop() + } + searchScreen { + }.loadPage(videoPage.url) { + clickPlayButton() + waitForPlaybackToStart() + } + } + + @SmokeTest + @Test + fun cameraPermissionsSettingsItemsTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + }.clickSitePermissionsSettings { + openCameraPermissionsSettings() + verifyPermissionsStateSettings() + verifyAskToAllowChecked() + verifyBlockedByAndroidState() + } + } + + @SmokeTest + @Test + fun locationPermissionsSettingsItemsTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openPrivacySettingsMenu { + }.clickSitePermissionsSettings { + openLocationPermissionsSettings() + verifyPermissionsStateSettings() + verifyAskToAllowChecked() + verifyBlockedByAndroidState() + } + } + + @Test + fun testLocationSharingNotAllowed() { + searchScreen { + }.loadPage(permissionsPage) { + clickGetLocationButton() + verifyLocationPermissionPrompt(testPageSubstring) + denySitePermissionRequest() + verifyPageContent("User denied geolocation prompt") + } + } + + @SmokeTest + @Test + fun testLocationSharingAllowed() { + mockLocationUpdatesRule.setMockLocation() + + searchScreen { + }.loadPage(permissionsPage) { + clickGetLocationButton() + verifyLocationPermissionPrompt(testPageSubstring) + allowSitePermissionRequest() + verifyPageContent("${mockLocationUpdatesRule.latitude}") + verifyPageContent("${mockLocationUpdatesRule.longitude}") + } + } + + @SmokeTest + @Test + fun allowCameraPermissionsTest() { + Assume.assumeTrue(cameraManager.cameraIdList.isNotEmpty()) + searchScreen { + }.loadPage(permissionsPage) { + clickGetCameraButton() + grantAppPermission() + verifyCameraPermissionPrompt(testPageSubstring) + allowSitePermissionRequest() + verifyPageContent("Camera allowed") + } + } + + @SmokeTest + @Test + fun denyCameraPermissionsTest() { + Assume.assumeTrue(cameraManager.cameraIdList.isNotEmpty()) + searchScreen { + }.loadPage(permissionsPage) { + clickGetCameraButton() + grantAppPermission() + verifyCameraPermissionPrompt(testPageSubstring) + denySitePermissionRequest() + verifyPageContent("Camera not allowed") + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SwitchContextTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SwitchContextTest.kt new file mode 100644 index 0000000000..8e8c9d71bb --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/SwitchContextTest.kt @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.Until +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.R +import org.mozilla.focus.activity.robots.notificationTray +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.pressHomeKey +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest +import java.io.IOException + +// This test switches out of Focus and opens it from the private browsing notification +@RunWith(AndroidJUnit4ClassRunner::class) +class SwitchContextTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + + notificationTray { + mDevice.openNotification() + clearNotifications() + } + } + + @After + fun tearDown() { + try { + webServer.shutdown() + } catch (e: IOException) { + throw AssertionError("Could not stop web server", e) + } + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun notificationOpenButtonTest() { + val testPage = TestAssetHelper.getGenericAsset(webServer) + + searchScreen { + }.loadPage(testPage.url) { + verifyPageContent(testPage.content) + } + // Send app to background + pressHomeKey() + // Pull down system bar and select Open + mDevice.openNotification() + notificationTray { + verifySystemNotificationExists(getStringResource(R.string.notification_erase_text)) + expandEraseBrowsingNotification() + }.clickNotificationOpenButton { + verifyBrowserView() + } + } + + @SmokeTest + @Test + fun switchFromSettingsToFocusTest() { + // Initialize UiDevice instance + val LAUNCH_TIMEOUT = 5000 + val SETTINGS_APP = "com.android.settings" + val settingsApp = mDevice.findObject( + UiSelector() + .packageName(SETTINGS_APP) + .enabled(true), + ) + val launcherPackage = mDevice.launcherPackageName + val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext + val intent = context.packageManager + .getLaunchIntentForPackage(SETTINGS_APP) + val testPage = TestAssetHelper.getGenericAsset(webServer) + + // Open a webpage + searchScreen { + }.loadPage(testPage.url) { } + + // Switch out of Focus, open settings app + pressHomeKey() + + // Wait for launcher + Assert.assertNotNull(launcherPackage) + mDevice.wait( + Until.hasObject(By.pkg(launcherPackage).depth(0)), + LAUNCH_TIMEOUT.toLong(), + ) + + // Launch the app + context.startActivity(intent) + settingsApp.waitForExists(waitingTime) + assertTrue(settingsApp.exists()) + + // switch to Focus + mDevice.openNotification() + notificationTray { + verifySystemNotificationExists(getStringResource(R.string.notification_erase_text)) + expandEraseBrowsingNotification() + }.clickNotificationOpenButton { + verifyBrowserView() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ThreeDotMainMenuTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ThreeDotMainMenuTest.kt new file mode 100644 index 0000000000..d622fb5c9f --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/ThreeDotMainMenuTest.kt @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.RetryTestRule +import org.mozilla.focus.helpers.TestAssetHelper +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest + +/** + * Verifies main menu items on the homescreen and on a browser page. + */ +class ThreeDotMainMenuTest { + private lateinit var webServer: MockWebServer + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + val mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Rule + @JvmField + val retryTestRule = RetryTestRule(3) + + @Before + fun startWebServer() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun homeScreenMenuItemsTest() { + homeScreen { + }.openMainMenu { + verifyHelpPageLinkExists() + verifySettingsButtonExists() + } + } + + @SmokeTest + @Test + fun browserMenuItemsTest() { + val pageUrl = TestAssetHelper.getGenericTabAsset(webServer, 1).url + + searchScreen { + }.loadPage(pageUrl) { + verifyPageContent("Tab 1") + }.openMainMenu { + verifyShareButtonExists() + verifyAddToHomeButtonExists() + verifyFindInPageExists() + verifyOpenInButtonExists() + verifyRequestDesktopSiteExists() + verifySettingsButtonExists() + verifyReportSiteIssueButtonExists() + } + } + + @SmokeTest + @Test + fun shareTabTest() { + val pageUrl = TestAssetHelper.getGenericTabAsset(webServer, 1).url + + searchScreen { + }.loadPage(pageUrl) { + progressBar.waitUntilGone(waitingTime) + }.openMainMenu { + }.openShareScreen { + verifyShareAppsListOpened() + mDevice.pressBack() + } + } + + @SmokeTest + @Test + fun findInPageTest() { + val pageUrl = TestAssetHelper.getGenericTabAsset(webServer, 1).url + + searchScreen { + }.loadPage(pageUrl) { + progressBar.waitUntilGone(waitingTime) + }.openMainMenu { + }.openFindInPage { + enterFindInPageQuery("tab") + verifyFindNextInPageResult("1/3") + verifyFindNextInPageResult("2/3") + verifyFindNextInPageResult("3/3") + verifyFindPrevInPageResult("1/3") + verifyFindPrevInPageResult("3/3") + verifyFindPrevInPageResult("2/3") + closeFindInPage() + } + } + + @SmokeTest + @Test + fun switchDesktopModeTest() { + val pageUrl = TestAssetHelper.getGenericTabAsset(webServer, 1).url + + searchScreen { + }.loadPage(pageUrl) { + progressBar.waitUntilGone(waitingTime) + verifyPageContent("mobile-site") + }.openMainMenu { + }.switchDesktopSiteMode { + progressBar.waitUntilGone(waitingTime) + verifyPageContent("desktop-site") + }.openMainMenu { + verifyRequestDesktopSiteIsEnabled(true) + }.switchDesktopSiteMode { + verifyPageContent("mobile-site") + }.openMainMenu { + verifyRequestDesktopSiteIsEnabled(false) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/URLAutocompleteTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/URLAutocompleteTest.kt new file mode 100644 index 0000000000..4ccec3b0de --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/URLAutocompleteTest.kt @@ -0,0 +1,147 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.TestHelper.exitToTop +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.testAnnotations.SmokeTest + +// Tests url autocompletion and adding custom autocomplete urls +@RunWith(AndroidJUnit4ClassRunner::class) +class URLAutocompleteTest { + private val searchTerm = "mozilla" + private val autocompleteSuggestion = "mozilla.org" + private val pageUrl = "https://www.mozilla.org/" + private val customURL = "680news.com" + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + // Test the url autocomplete feature with default settings + @SmokeTest + @Test + fun defaultAutoCompleteURLTest() { + searchScreen { + // type a partial url, and check it autocompletes + typeInSearchBar(searchTerm) + verifySearchEditBarContainsText(autocompleteSuggestion) + clearSearchBar() + // type a full url, and check it does not autocomplete + typeInSearchBar(pageUrl) + verifySearchEditBarContainsText(pageUrl) + } + } + + @SmokeTest + @Test + fun disableTopSitesAutocompleteTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + openUrlAutocompleteSubMenu() + toggleTopSitesAutocomplete() + exitToTop() + } + + searchScreen { + typeInSearchBar(searchTerm) + verifySearchEditBarContainsText(searchTerm) + } + } + + // Add custom Url, verify it works, then remove it and check it is no longer autocompleted + @Test + fun customUrlAutoCompletionTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + // Add custom autocomplete url + openUrlAutocompleteSubMenu() + openManageSitesSubMenu() + openAddCustomUrlDialog() + enterCustomUrl(customURL) + saveCustomUrl() + verifySavedCustomURL(customURL) + exitToTop() + } + // verify the custom url auto-completes + searchScreen { + typeInSearchBar(customURL.substring(0, 1)) + verifySearchEditBarContainsText(customURL) + clearSearchBar() + } + + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + // remove custom Url + openUrlAutocompleteSubMenu() + openManageSitesSubMenu() + removeCustomUrl() + exitToTop() + } + // verify it is no longer auto-completed + searchScreen { + typeInSearchBar(customURL.substring(0, 3)) + verifySearchEditBarContainsText(customURL.substring(0, 3)) + clearSearchBar() + } + } + + // Add custom autocompletion site, then disable autocomplete + @Test + fun disableAutocompleteForCustomSiteTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + openUrlAutocompleteSubMenu() + openManageSitesSubMenu() + openAddCustomUrlDialog() + enterCustomUrl(customURL) + saveCustomUrl() + verifySavedCustomURL(customURL) + mDevice.pressBack() + toggleCustomAutocomplete() + exitToTop() + } + + searchScreen { + typeInSearchBar(customURL.substring(0, 3)) + verifySearchEditBarContainsText(customURL.substring(0, 3)) + clearSearchBar() + } + } + + // Verifies the custom Url can't be added twice + @Test + fun duplicateCustomUrlNotAllowedTest() { + homeScreen { + }.openMainMenu { + }.openSettings { + }.openSearchSettingsMenu { + openUrlAutocompleteSubMenu() + openManageSitesSubMenu() + openAddCustomUrlDialog() + enterCustomUrl(customURL) + saveCustomUrl() + verifySavedCustomURL(customURL) + openAddCustomUrlDialog() + enterCustomUrl(customURL) + saveCustomUrl() + verifyCustomUrlDialogNotClosed() + exitToTop() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt new file mode 100644 index 0000000000..84a3e68755 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityIntentsTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.RetryTestRule +import org.mozilla.focus.helpers.StringsHelper.GMAIL_APP +import org.mozilla.focus.helpers.StringsHelper.PHONE_APP +import org.mozilla.focus.helpers.TestAssetHelper +import org.mozilla.focus.helpers.TestHelper.assertNativeAppOpens +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.testAnnotations.SmokeTest + +// These tests check that various web controls work properly +@RunWith(AndroidJUnit4ClassRunner::class) +class WebControlsTest { + private lateinit var webServer: MockWebServer + + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityIntentsTestRule(showFirstRun = false) + + @Rule + @JvmField + val retryTestRule = RetryTestRule(3) + + @Before + fun setup() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + } + + @After + fun tearDown() { + webServer.shutdown() + featureSettingsHelper.resetAllFeatureFlags() + } + + @SmokeTest + @Test + fun verifyTextInputTest() { + val htmlControlsPage = TestAssetHelper.getHTMLControlsPageAsset(webServer).url + + searchScreen { + }.loadPage(htmlControlsPage) { + progressBar.waitUntilGone(waitingTime) + clickAndWriteTextInInputBox("wiki") + clickSubmitTextInputButton() + verifyPageContent("You entered: wiki") + } + } + + @SmokeTest + @Test + fun verifyDropdownMenuTest() { + val htmlControlsPage = TestAssetHelper.getHTMLControlsPageAsset(webServer).url + + searchScreen { + }.loadPage(htmlControlsPage) { + progressBar.waitUntilGone(waitingTime) + clickDropDownForm() + selectDropDownOption("The National") + clickSubmitDropDownButton() + verifySelectedDropDownOption("The National") + } + } + + @SmokeTest + @Test + fun verifyExternalLinksTest() { + val htmlControlsPage = TestAssetHelper.getHTMLControlsPageAsset(webServer).url + + searchScreen { + }.loadPage(htmlControlsPage) { + progressBar.waitUntilGone(waitingTime) + clickLinkMatchingText("External link") + progressBar.waitUntilGone(waitingTime) + verifyPageURL("DuckDuckGo") + } + } + + @SmokeTest + @Test + fun emailLinkTest() { + val htmlControlsPage = TestAssetHelper.getHTMLControlsPageAsset(webServer).url + + searchScreen { + }.loadPage(htmlControlsPage) { + clickLinkMatchingText("Email link") + clickOpenLinksInAppsOpenButton() + assertNativeAppOpens(GMAIL_APP) + } + } + + @SmokeTest + @Test + fun telephoneLinkTest() { + val htmlControlsPage = TestAssetHelper.getHTMLControlsPageAsset(webServer).url + + searchScreen { + }.loadPage(htmlControlsPage) { + clickLinkMatchingText("Telephone link") + clickOpenLinksInAppsOpenButton() + assertNativeAppOpens(PHONE_APP) + } + } + + @SmokeTest + @Test + fun verifyDismissTextSelectionToolbarTest() { + val tab1Url = TestAssetHelper.getGenericTabAsset(webServer, 1).url + val htmlControlsPage = TestAssetHelper.getHTMLControlsPageAsset(webServer).url + + searchScreen { + }.loadPage(tab1Url) { + progressBar.waitUntilGone(waitingTime) + }.openSearchBar { + }.loadPage(htmlControlsPage) { + progressBar.waitUntilGone(waitingTime) + longClickText("Copy") + }.goToPreviousPage { + verifyCopyOptionDoesNotExist() + } + } + + @Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1841006") + @SmokeTest + @Test + fun verifySelectTextTest() { + val htmlControlsPage = TestAssetHelper.getHTMLControlsPageAsset(webServer).url + + searchScreen { + }.loadPage(htmlControlsPage) { + progressBar.waitUntilGone(waitingTime) + longClickAndCopyText("Copy") + clickAndPasteTextInInputBox() + clickSubmitTextInputButton() + verifyPageContent("You entered: Copy") + } + } + + @SmokeTest + @Test + fun verifyCalendarFormTest() { + val htmlControlsPage = TestAssetHelper.getHTMLControlsPageAsset(webServer).url + + searchScreen { + }.loadPage(htmlControlsPage) { + progressBar.waitUntilGone(waitingTime) + clickCalendarForm() + selectDate() + clickButtonWithText("OK") + clickSubmitDateButton() + verifySelectedDate() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/AddToHomeScreenRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/AddToHomeScreenRobot.kt new file mode 100644 index 0000000000..d1e9ec7c8f --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/AddToHomeScreenRobot.kt @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime + +class AddToHomeScreenRobot { + + fun handleAddAutomaticallyDialog() { + if (addAutomaticallyBtn.waitForExists(waitingTime)) { + addAutomaticallyBtn.click() + addAutomaticallyBtn.waitUntilGone(waitingTime) + } + } + + fun addShortcutWithTitle(title: String) { + shortcutTitle.waitForExists(waitingTime) + shortcutTitle.clearTextField() + shortcutTitle.setText(title) + addToHSOKBtn.click() + } + + fun addShortcutNoTitle() { + shortcutTitle.waitForExists(waitingTime) + shortcutTitle.clearTextField() + addToHSOKBtn.click() + } + + class Transition { + // Searches a page shortcut on the device homescreen + fun searchAndOpenHomeScreenShortcut(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + mDevice.waitForIdle(waitingTime) + mDevice.pressHome() + + fun deviceHomeScreen() = UiScrollable(UiSelector().scrollable(true)) + deviceHomeScreen().setAsHorizontalList() + + fun shortcut() = + deviceHomeScreen() + .getChildByText(UiSelector().text(title), title, true) + shortcut().waitForExists(waitingTime) + shortcut().clickAndWaitForNewWindow() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + } +} + +private val addToHSOKBtn = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/addtohomescreen_dialog_add") + .enabled(true), +) + +private val addAutomaticallyBtn = mDevice.findObject( + UiSelector() + .className("android.widget.Button") + .textContains("Add automatically"), +) + +private val shortcutTitle = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/edit_title"), +) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/BrowserRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/BrowserRobot.kt new file mode 100644 index 0000000000..a817a5642a --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/BrowserRobot.kt @@ -0,0 +1,694 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.IdlingResource +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiObject +import androidx.test.uiautomator.UiObjectNotFoundException +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.Until +import org.hamcrest.Matchers.not +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.Constants.RETRY_COUNT +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.pageLoadingTime +import org.mozilla.focus.helpers.TestHelper.progressBar +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.helpers.TestHelper.waitingTimeShort +import org.mozilla.focus.idlingResources.SessionLoadedIdlingResource +import java.time.LocalDate + +class BrowserRobot { + + private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource + + val progressBar = + mDevice.findObject( + UiSelector().resourceId("$packageName:id/progress"), + ) + + fun verifyBrowserView() = + assertTrue( + mDevice.findObject(UiSelector().resourceId("$packageName:id/engineView")) + .waitForExists(waitingTime), + ) + + fun verifyPageContent(expectedText: String) { + sessionLoadedIdlingResource = SessionLoadedIdlingResource() + + runWithIdleRes(sessionLoadedIdlingResource) { + for (i in 1..RETRY_COUNT) { + try { + assertTrue( + webPageItemContainingText(expectedText).waitForExists(pageLoadingTime), + ) + break + } catch (e: AssertionError) { + if (i == RETRY_COUNT) { + throw e + } else { + refreshPageIfStillLoading(expectedText) + } + } + } + } + } + + fun verifyTrackingProtectionAlert(expectedText: String) { + // Because of https://bugzilla.mozilla.org/show_bug.cgi?id=1794130, + // ETP has delays so we need to refresh the page multiple times until ETP starts working. + for (i in 1..RETRY_COUNT) { + try { + assertTrue( + webPageItemContainingText(expectedText) + .waitForExists(pageLoadingTime), + ) + // close the JavaScript alert + mDevice.pressBack() + break + } catch (e: AssertionError) { + if (i == RETRY_COUNT) { + throw e + } else { + refreshPageIfStillLoading(expectedText) + } + } + } + } + + fun refreshPageIfStillLoading(pageContent: String) { + browserScreen { + }.openMainMenu { + when (mDevice.findObject(UiSelector().description("Reload website")).exists()) { + true -> ThreeDotMainMenuRobot.Transition().clickReloadButton {} + false -> { + ThreeDotMainMenuRobot.Transition().clickStopLoadingButton { + if (!mDevice.findObject(UiSelector().textContains(pageContent)) + .waitForExists(waitingTime) + ) { + browserScreen { + }.openMainMenu { + }.clickReloadButton {} + } + } + } + } + } + } + + fun verifyPageURL(expectedText: String) { + browserURLbar.waitForExists(waitingTime) + sessionLoadedIdlingResource = SessionLoadedIdlingResource() + + runWithIdleRes(sessionLoadedIdlingResource) { + assertTrue( + mDevice.findObject(UiSelector().textContains(expectedText)) + .waitForExists(waitingTime), + ) + } + } + + fun clickGetLocationButton() = clickPageObject(webPageItemContainingText("Get Location")) + + fun clickGetCameraButton() = clickPageObject(webPageItemContainingText("Open camera")) + + fun verifyCameraPermissionPrompt(url: String) { + assertTrue( + mDevice.findObject(UiSelector().text("Allow $url to use your camera?")) + .waitForExists(waitingTime), + ) + } + + fun verifyLocationPermissionPrompt(url: String) { + assertTrue( + mDevice.findObject(UiSelector().text("Allow $url to use your location?")) + .waitForExists(waitingTime), + ) + } + + fun allowSitePermissionRequest() { + if (permissionAllowBtn.waitForExists(waitingTime)) { + permissionAllowBtn.click() + } + } + + fun denySitePermissionRequest() { + if (permissionDenyBtn.waitForExists(waitingTime)) { + permissionDenyBtn.click() + } + } + + fun longPressLink(linkText: String) = longClickPageObject(webPageItemWithText(linkText)) + + fun openLinkInNewTab() { + mDevice.findObject( + UiSelector().textContains("Open link in private tab"), + ).waitForExists(waitingTime) + openLinkInPrivateTab.perform(click()) + } + + fun verifyNumberOfTabsOpened(tabsCount: Int) { + assertTrue( + mDevice.findObject( + UiSelector().description("$tabsCount open tabs. Tap to switch tabs."), + ).waitForExists(waitingTime), + ) + } + + fun verifyTabsCounterNotShown() { + assertFalse( + mDevice.findObject(UiSelector().resourceId("$packageName:id/counter_root")) + .waitForExists(waitingTimeShort), + ) + } + + fun verifyShareAppsListOpened() = + assertTrue(shareAppsList.waitForExists(waitingTime)) + + fun clickPlayButton() = clickPageObject(webPageItemWithText("Play")) + + fun clickPauseButton() = clickPageObject(webPageItemWithText("Pause")) + + fun waitForPlaybackToStart() { + for (i in 1..RETRY_COUNT) { + try { + assertTrue(webPageItemWithText("Media file is playing").waitForExists(pageLoadingTime)) + break + } catch (e: AssertionError) { + if (i == RETRY_COUNT) { + throw e + } else { + clickPlayButton() + } + } + } + + // dismiss the js alert + mDevice.findObject(UiSelector().textContains("ok")).click() + } + + fun verifyPlaybackStopped() { + assertTrue(webPageItemWithText("Media file is paused").waitForExists(waitingTime)) + // dismiss the js alert + mDevice.findObject(UiSelector().textContains("ok")).click() + } + + fun verifySiteTrackingProtectionIconShown() = assertTrue(securityIcon.waitForExists(waitingTime)) + + fun verifySiteSecurityIndicatorShown() = assertTrue(site_security_indicator.waitForExists(waitingTime)) + + fun verifyLinkContextMenu(linkAddress: String) { + assertTrue( + mDevice.findObject( + UiSelector().resourceId("$packageName:id/parentPanel"), + ).waitForExists(waitingTime), + ) + onView(withId(R.id.titleView)).check(matches(withText(linkAddress))) + openLinkInPrivateTab.check(matches(isDisplayed())) + copyLink.check(matches(isDisplayed())) + shareLink.check(matches(isDisplayed())) + } + + fun verifyImageContextMenu(hasLink: Boolean, linkAddress: String) { + assertTrue( + mDevice.findObject( + UiSelector().resourceId("$packageName:id/parentPanel"), + ).waitForExists(waitingTime), + ) + onView(withId(R.id.titleView)).check(matches(withText(linkAddress))) + if (hasLink) { + openLinkInPrivateTab.check(matches(isDisplayed())) + downloadLink.check(matches(isDisplayed())) + } + copyLink.check(matches(isDisplayed())) + shareLink.check(matches(isDisplayed())) + shareImage.check(matches(isDisplayed())) + openImageInNewTab.check(matches(isDisplayed())) + saveImage.check(matches(isDisplayed())) + copyImageLocation.check(matches(isDisplayed())) + } + + fun clickContextMenuCopyLink(): ViewInteraction = copyLink.perform(click()) + + fun clickShareImage() = + shareImage.inRoot(RootMatchers.isDialog()).check(matches(isDisplayed())).perform(click()) + + fun clickShareLink(): ViewInteraction = shareLink.perform(click()) + + fun clickCopyImageLocation(): ViewInteraction = copyImageLocation.perform(click()) + + fun clickLinkMatchingText(expectedText: String) = clickPageObject(webPageItemContainingText(expectedText)) + + fun verifyOpenLinksInAppsPrompt(openLinksInAppsEnabled: Boolean, link: String) = assertOpenLinksInAppsPrompt(openLinksInAppsEnabled, link) + + fun clickOpenLinksInAppsCancelButton() { + for (i in 1..RETRY_COUNT) { + try { + openLinksInAppsCancelButton.click() + assertTrue(openLinksInAppsMessage.waitUntilGone(waitingTime)) + + break + } catch (e: AssertionError) { + if (i == RETRY_COUNT) { + throw e + } + } + } + } + + fun clickOpenLinksInAppsOpenButton() = openLinksInAppsOpenButton.click() + + fun clickDropDownForm() = clickPageObject(webPageItemWithResourceId("dropDown")) + + fun clickCalendarForm() = clickPageObject(webPageItemWithResourceId("calendar")) + + fun selectDate() { + mDevice.findObject(UiSelector().resourceId("android:id/month_view")).waitForExists(waitingTime) + + mDevice.findObject( + UiSelector() + .textContains("$currentDay") + .descriptionContains("$currentDay $currentMonth $currentYear"), + ).click() + } + + fun clickButtonWithText(button: String) = mDevice.findObject(UiSelector().textContains(button)).click() + + fun clickSubmitDateButton() = clickPageObject(webPageItemWithResourceId("submitDate")) + + fun verifySelectedDate() { + mDevice.findObject( + UiSelector() + .textContains("Submit date") + .resourceId("submitDate"), + ).waitForExists(waitingTime) + + assertTrue( + mDevice.findObject( + UiSelector() + .text("Selected date is: $currentDate"), + ).waitForExists(waitingTime), + ) + } + + fun clickAndWriteTextInInputBox(text: String) { + clickPageObject(webPageItemWithResourceId("textInput")) + setPageObjectText(webPageItemWithResourceId("textInput"), text) + } + + fun longPressTextInputBox() = longClickPageObject(webPageItemWithResourceId("textInput")) + + fun longClickText(expectedText: String) = longClickPageObject(webPageItemContainingText(expectedText)) + + fun longClickAndCopyText(expectedText: String) { + var currentTries = 0 + while (currentTries++ < 3) { + try { + longClickPageObject(webPageItemContainingText(expectedText)) + + webPageItemContainingText("Copy").waitForExists(waitingTime) + mDevice.findObject(By.textContains("Copy")).click() + + break + } catch (e: NullPointerException) { + browserScreen { + }.openMainMenu { + }.clickReloadButton {} + } + } + } + + fun verifyCopyOptionDoesNotExist() = + assertFalse(mDevice.findObject(UiSelector().textContains("Copy")).waitForExists(waitingTime)) + + fun clickAndPasteTextInInputBox() { + var currentTries = 0 + while (currentTries++ < 3) { + try { + mDevice.findObject(UiSelector().textContains("Paste")).waitForExists(waitingTime) + mDevice.findObject(By.textContains("Paste")).click() + break + } catch (e: NullPointerException) { + longPressTextInputBox() + } + } + } + + fun clickSubmitTextInputButton() = clickPageObject(webPageItemWithResourceId("submitInput")) + + fun selectDropDownOption(optionName: String) { + mDevice.findObject( + UiSelector().resourceId("$packageName:id/customPanel"), + ).waitForExists(waitingTime) + mDevice.findObject(UiSelector().textContains(optionName)).click() + } + + fun clickSubmitDropDownButton() = clickPageObject(webPageItemWithResourceId("submitOption")) + + fun verifySelectedDropDownOption(optionName: String) { + mDevice.findObject( + UiSelector() + .textContains("Submit drop down option") + .resourceId("submitOption"), + ).waitForExists(waitingTime) + + assertTrue( + mDevice.findObject( + UiSelector() + .text("Selected option is: $optionName"), + ).waitForExists(waitingTime), + ) + } + + fun enterFindInPageQuery(expectedText: String) { + mDevice.wait(Until.findObject(By.res("$packageName:id/find_in_page_query_text")), waitingTime) + findInPageQuery.perform(ViewActions.clearText()) + mDevice.wait(Until.gone(By.res("$packageName:id/find_in_page_result_text")), waitingTime) + findInPageQuery.perform(ViewActions.typeText(expectedText)) + mDevice.wait(Until.findObject(By.res("$packageName:id/find_in_page_result_text")), waitingTime) + } + + fun verifyFindNextInPageResult(ratioCounter: String) { + mDevice.wait(Until.findObject(By.text(ratioCounter)), waitingTime) + val resultsCounter = mDevice.findObject(By.text(ratioCounter)) + findInPageResult.check(matches(withText((ratioCounter)))) + findInPageNextButton.perform(click()) + resultsCounter.wait(Until.textNotEquals(ratioCounter), waitingTime) + } + + fun verifyFindPrevInPageResult(ratioCounter: String) { + mDevice.wait(Until.findObject(By.text(ratioCounter)), waitingTime) + val resultsCounter = mDevice.findObject(By.text(ratioCounter)) + findInPageResult.check(matches(withText((ratioCounter)))) + findInPagePrevButton.perform(click()) + resultsCounter.wait(Until.textNotEquals(ratioCounter), waitingTime) + } + + fun closeFindInPage() { + findInPageCloseButton.perform(click()) + findInPageQuery.check(matches(not(isDisplayed()))) + } + + fun verifyCookiesEnabled(areCookiesEnabled: String) { + for (i in 1..RETRY_COUNT) { + try { + assertTrue( + mDevice.findObject( + UiSelector() + .resourceId("cookie_message") + .childSelector( + UiSelector().textContains(areCookiesEnabled), + ), + ).waitForExists(waitingTime), + ) + break + } catch (e: AssertionError) { + if (i == RETRY_COUNT) { + throw e + } else { + refreshPageIfStillLoading(areCookiesEnabled) + } + } + } + } + + fun clickSetCookiesButton() = clickPageObject(webPageItemWithResourceId("setCookies")) + + fun clickPageObject(webPageItem: UiObject) { + for (i in 1..RETRY_COUNT) { + try { + webPageItem.also { + it.waitForExists(waitingTime) + it.click() + } + + break + } catch (e: UiObjectNotFoundException) { + if (i == RETRY_COUNT) { + throw e + } else { + browserScreen { + }.openMainMenu { + }.clickReloadButton { + progressBar.waitUntilGone(waitingTime) + } + } + } + } + } + + fun longClickPageObject(webPageItem: UiObject) { + for (i in 1..RETRY_COUNT) { + try { + webPageItem.also { + it.waitForExists(waitingTime) + it.longClick() + } + + break + } catch (e: UiObjectNotFoundException) { + if (i == RETRY_COUNT) { + throw e + } else { + browserScreen { + }.openMainMenu { + }.clickReloadButton { + progressBar.waitUntilGone(waitingTime) + } + } + } + } + } + + private fun setPageObjectText(webPageItem: UiObject, text: String) { + for (i in 1..RETRY_COUNT) { + try { + webPageItem.also { + it.waitForExists(waitingTime) + it.setText(text) + } + + break + } catch (e: UiObjectNotFoundException) { + if (i == RETRY_COUNT) { + throw e + } else { + browserScreen { + }.openMainMenu { + }.clickReloadButton { + progressBar.waitUntilGone(waitingTime) + } + } + } + } + } + + class Transition { + fun openSearchBar(interact: SearchRobot.() -> Unit): SearchRobot.Transition { + browserURLbar.waitForExists(waitingTime) + browserURLbar.click() + + SearchRobot().interact() + return SearchRobot.Transition() + } + + fun clearBrowsingData(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + eraseBrowsingButton + .check(matches(isDisplayed())) + .perform(click()) + + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } + + fun openMainMenu(interact: ThreeDotMainMenuRobot.() -> Unit): ThreeDotMainMenuRobot.Transition { + browserURLbar.waitForExists(waitingTime) + mainMenu + .check(matches(isDisplayed())) + .perform(click()) + + ThreeDotMainMenuRobot().interact() + return ThreeDotMainMenuRobot.Transition() + } + + fun openSiteSettingsMenu(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + securityIcon.click() + + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } + + fun openSiteSecurityInfoSheet(interact: SiteSecurityInfoSheetRobot.() -> Unit): SiteSecurityInfoSheetRobot.Transition { + if (securityIcon.exists()) { + securityIcon.click() + } else { + site_security_indicator.click() + } + + SiteSecurityInfoSheetRobot().interact() + return SiteSecurityInfoSheetRobot.Transition() + } + + fun openTabsTray(interact: TabsTrayRobot.() -> Unit): TabsTrayRobot.Transition { + tabsCounter.perform(click()) + + TabsTrayRobot().interact() + return TabsTrayRobot.Transition() + } + + fun goToPreviousPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + mDevice.pressBack() + progressBar.waitUntilGone(waitingTime) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun clickSaveImage(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition { + saveImage.inRoot(RootMatchers.isDialog()).check(matches(isDisplayed())).perform(click()) + + DownloadRobot().interact() + return DownloadRobot.Transition() + } + } +} + +fun browserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + BrowserRobot().interact() + return BrowserRobot.Transition() +} + +inline fun runWithIdleRes(ir: IdlingResource?, pendingCheck: () -> Unit) { + try { + IdlingRegistry.getInstance().register(ir) + pendingCheck() + } finally { + IdlingRegistry.getInstance().unregister(ir) + } +} + +private fun assertOpenLinksInAppsPrompt(openLinksInAppsEnabled: Boolean, link: String) { + if (openLinksInAppsEnabled) { + mDevice.findObject(UiSelector().resourceId("$packageName:id/parentPanel")).waitForExists(waitingTime) + assertTrue(openLinksInAppsMessage.waitForExists(waitingTimeShort)) + assertTrue(openLinksInAppsLink(link).exists()) + assertTrue(openLinksInAppsCancelButton.waitForExists(waitingTimeShort)) + assertTrue(openLinksInAppsOpenButton.waitForExists(waitingTimeShort)) + } else { + assertFalse( + mDevice.findObject( + UiSelector().resourceId("$packageName:id/parentPanel"), + ).waitForExists(waitingTimeShort), + ) + } +} + +private fun openLinksInAppsLink(link: String) = mDevice.findObject(UiSelector().textContains(link)) + +private val browserURLbar = mDevice.findObject( + UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_url_view"), +) + +private val eraseBrowsingButton = onView(withContentDescription("Erase browsing history")) + +private val tabsCounter = onView(withId(R.id.counter_root)) + +private val mainMenu = onView(withId(R.id.mozac_browser_toolbar_menu)) + +private val shareAppsList = + mDevice.findObject(UiSelector().resourceId("android:id/resolver_list")) + +private val securityIcon = + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/mozac_browser_toolbar_tracking_protection_indicator"), + ) + +private val site_security_indicator = + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/mozac_browser_toolbar_security_indicator"), + ) + +// Link long-tap context menu items +private val openLinkInPrivateTab = onView(withText("Open link in private tab")) + +private val copyLink = onView(withText("Copy link")) + +private val shareLink = onView(withText("Share link")) + +// Image long-tap context menu items +private val openImageInNewTab = onView(withText("Open image in new tab")) + +private val downloadLink = onView(withText("Download link")) + +private val saveImage = onView(withText("Save image")) + +private val copyImageLocation = onView(withText("Copy image location")) + +private val shareImage = onView(withText("Share image")) + +// Find in page toolbar +private val findInPageQuery = onView(withId(R.id.find_in_page_query_text)) + +private val findInPageResult = onView(withId(R.id.find_in_page_result_text)) + +private val findInPageNextButton = onView(withId(R.id.find_in_page_next_btn)) + +private val findInPagePrevButton = onView(withId(R.id.find_in_page_prev_btn)) + +private val findInPageCloseButton = onView(withId(R.id.find_in_page_close_btn)) + +private val openLinksInAppsMessage = mDevice.findObject(UiSelector().resourceId("$packageName:id/alertTitle")) + +private val openLinksInAppsCancelButton = mDevice.findObject(UiSelector().textContains("CANCEL")) + +private val openLinksInAppsOpenButton = + mDevice.findObject( + UiSelector() + .index(1) + .textContains("OPEN") + .className("android.widget.Button") + .packageName(packageName), + ) + +private val currentDate = LocalDate.now() +private val currentDay = currentDate.dayOfMonth +private val currentMonth = currentDate.month +private val currentYear = currentDate.year + +private val permissionAllowBtn = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/allow_button"), +) + +private val permissionDenyBtn = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/deny_button"), +) + +private fun webPageItemContainingText(itemText: String) = + mDevice.findObject(UiSelector().textContains(itemText)) + +private fun webPageItemWithText(itemText: String) = + mDevice.findObject(UiSelector().text(itemText)) + +private fun webPageItemWithResourceId(resourceId: String) = + mDevice.findObject(UiSelector().resourceId(resourceId)) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/CustomTabRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/CustomTabRobot.kt new file mode 100644 index 0000000000..8f7c0ea142 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/CustomTabRobot.kt @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withSubstring +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.UiObject +import androidx.test.uiautomator.UiSelector +import junit.framework.TestCase.assertTrue +import org.junit.Assert +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.appName +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.idlingResources.SessionLoadedIdlingResource + +class CustomTabRobot { + + private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource + + val progressBar: UiObject = + mDevice.findObject( + UiSelector().resourceId("$packageName:id/progress"), + ) + + fun verifyCustomTabActionButton(buttonDescription: String) { + actionButton(buttonDescription).check(matches(isDisplayed())) + } + + fun verifyCustomMenuItem(buttonDescription: String) { + customMenuItem(buttonDescription).check(matches(isDisplayed())) + } + + fun openCustomTabMenu(): ViewInteraction = menuButton.perform(click()) + + fun verifyShareButtonIsDisplayed(): ViewInteraction = shareButton.check(matches(isDisplayed())) + + fun verifyTheStandardMenuItems() { + onView(withText("Add to Home screen")).check(matches(isDisplayed())) + onView(withText("Find in Page")).check(matches(isDisplayed())) + onView(withText("Open in…")).check(matches(isDisplayed())) + openInFocusButton.check(matches(isDisplayed())) + onView(withSubstring("Desktop site")).check(matches(isDisplayed())) + // Removed until https://github.com/mozilla-mobile/android-components/issues/10791 is fixed + // onView(withText("Report site issue")).check(matches(isDisplayed())) + onView(withText("Powered by $appName")).check(matches(isDisplayed())) + } + + fun closeCustomTab() { + closeCustomTabButton + .check(matches(isDisplayed())) + .perform(click()) + } + + fun verifyPageURL(expectedText: String) { + sessionLoadedIdlingResource = SessionLoadedIdlingResource() + + runWithIdleRes(sessionLoadedIdlingResource) { + mDevice.findObject(UiSelector().textContains(expectedText)).waitForExists(waitingTime) + assertTrue( + "Actual url: ${customTabUrl.text}", + customTabUrl.text.contains(expectedText), + ) + } + } + + fun verifyPageContent(expectedText: String) { + val sessionLoadedIdlingResource = SessionLoadedIdlingResource() + + mDevice.findObject(UiSelector().resourceId("$packageName:id/engineView")) + .waitForExists(waitingTime) + + runWithIdleRes(sessionLoadedIdlingResource) { + Assert.assertTrue( + mDevice.findObject(UiSelector().textContains(expectedText)) + .waitForExists(waitingTime), + ) + } + } + + fun clickLinkMatchingText(expectedText: String) { + mDevice.findObject(UiSelector().textContains(expectedText)).waitForExists(waitingTime) + mDevice.findObject(UiSelector().textContains(expectedText)).also { it.click() } + } + + class Transition { + fun clickOpenInFocusButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + openInFocusButton + .check(matches(isDisplayed())) + .perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun openCustomTabMenu(interact: ThreeDotMainMenuRobot.() -> Unit): ThreeDotMainMenuRobot.Transition { + menuButton.perform(click()) + + ThreeDotMainMenuRobot().interact() + return ThreeDotMainMenuRobot.Transition() + } + } +} + +fun customTab(interact: CustomTabRobot.() -> Unit): CustomTabRobot.Transition { + CustomTabRobot().interact() + return CustomTabRobot.Transition() +} + +private fun actionButton(description: String) = onView(withContentDescription(description)) + +private val menuButton = onView(withId(R.id.mozac_browser_toolbar_menu)) + +private val shareButton = onView(withContentDescription("Share link")) + +private fun customMenuItem(description: String) = onView(withText(description)) + +private val closeCustomTabButton = onView(withContentDescription(R.string.mozac_feature_customtabs_exit_button)) + +private val openInFocusButton = onView(withText("Open in $appName")) + +private val customTabUrl = + mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_url_view")) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/DownloadRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/DownloadRobot.kt new file mode 100644 index 0000000000..171782154c --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/DownloadRobot.kt @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity.robots + +import androidx.test.uiautomator.UiObject +import androidx.test.uiautomator.UiSelector +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.helpers.TestHelper.waitingTimeShort +import org.mozilla.focus.idlingResources.SessionLoadedIdlingResource + +class DownloadRobot { + fun verifyDownloadDialog(fileName: String) { + assertTrue(downloadDialogTitle.waitForExists(waitingTime)) + assertTrue(downloadCancelBtn.exists()) + assertTrue(downloadBtn.exists()) + assertTrue(downloadFileName.text.contains(fileName)) + } + + fun verifyDownloadDialogGone() = assertTrue(downloadDialogTitle.waitUntilGone(waitingTime)) + + fun clickDownloadIconAsset() { + val sessionLoadedIdlingResource = SessionLoadedIdlingResource() + runWithIdleRes(sessionLoadedIdlingResource) { + downloadIconAsset.waitForExists(waitingTime) + downloadIconAsset.click() + } + } + + fun clickDownloadButton() { + downloadBtn.waitForExists(waitingTime) + downloadBtn.click() + } + + fun clickCancelDownloadButton() { + downloadCancelBtn.waitForExists(waitingTime) + downloadCancelBtn.click() + } + + fun verifyDownloadConfirmationMessage(fileName: String) { + val snackBar = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/snackbar_text"), + ) + snackBar.waitForExists(waitingTimeShort) + assertTrue( + snackBar.text, + snackBar.text.equals("$fileName finished"), + ) + } + + fun openDownloadedFile() { + val snackBarButton = mDevice.findObject(UiSelector().resourceId("$packageName:id/snackbar_action")) + snackBarButton.waitForExists(waitingTime) + snackBarButton.clickAndWaitForNewWindow(waitingTime) + } + + class Transition +} + +fun downloadRobot(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition { + DownloadRobot().interact() + return DownloadRobot.Transition() +} + +val downloadIconAsset: UiObject = mDevice.findObject( + UiSelector() + .resourceId("download"), +) + +private val downloadDialogTitle = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/title"), +) + +private val downloadFileName = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/filename"), +) + +private val downloadCancelBtn = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/close_button"), +) + +private val downloadBtn = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/download_button"), +) + +private val downloadNotificationText = getStringResource(R.string.mozac_feature_downloads_completed_notification_text2) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/HomeScreenRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/HomeScreenRobot.kt new file mode 100644 index 0000000000..4309cb2e63 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/HomeScreenRobot.kt @@ -0,0 +1,235 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiSelector +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.appName +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.helpers.TestHelper.waitingTimeShort + +class HomeScreenRobot { + + fun verifyEmptySearchBar() { + editURLBar.waitForExists(waitingTime) + assertTrue(editURLBar.text.equals(getStringResource(R.string.urlbar_hint))) + } + + fun skipFirstRun() = onView(withId(R.id.skip)).perform(click()) + + fun closeOnboarding() = onboardingCloseButton.clickAndWaitForNewWindow(waitingTime) + + fun verifyOnboardingFirstSlide() = assertTrue(firstSlideTitle.waitForExists(waitingTime)) + + fun verifyOnboardingSecondSlide() = assertTrue(secondSlideTitle.waitForExists(waitingTime)) + + fun verifyOnboardingThirdSlide() = assertTrue(thirdSlideTitle.waitForExists(waitingTime)) + + fun verifyOnboardingLastSlide() = assertTrue(lastSlide.waitForExists(waitingTime)) + + fun clickOnboardingNextButton() = nextButton.clickAndWaitForNewWindow(waitingTimeShort) + + fun clickOnboardingFinishButton() = finishButton.clickAndWaitForNewWindow(waitingTimeShort) + + fun verifyPageShortcutExists(title: String) { + assertTrue( + topSitesList + .getChild(UiSelector().textContains(title)) + .waitForExists(waitingTime), + ) + } + + fun longTapPageShortcut(title: String) { + mDevice.findObject(By.text(title)).click(4000) + } + + fun clickRenameShortcut() { + mDevice.findObject(UiSelector().text("Rename")) + .also { + it.waitForExists(waitingTimeShort) + it.click() + } + } + + fun renameShortcutAndSave(newTitle: String) { + val titleTextField = mDevice.findObject(UiSelector().className("android.widget.EditText")) + val okButton = mDevice.findObject(UiSelector().textContains("ok")) + + titleTextField.clearTextField() + titleTextField.setText(newTitle) + okButton.click() + // the dialog is not always dismissed on first click of the Ok button (not manually reproducible) + if (!mDevice.findObject(UiSelector().text("Rename")).waitUntilGone(waitingTimeShort)) { + okButton.click() + } + } + + fun verifyFirstOnboardingScreenItems() { + assertTrue(onboardingCloseButton.waitForExists(waitingTime)) + assertTrue(onboardingLogo.waitForExists(waitingTime)) + assertTrue(onboardingFirstScreenTitle.waitForExists(waitingTime)) + assertTrue(onboardingFirstScreenSubtitle.waitForExists(waitingTime)) + assertTrue(onboardingGetStartedButton.waitForExists(waitingTime)) + } + + fun verifySecondOnboardingScreenItems() { + assertTrue(onboardingCloseButton.waitForExists(waitingTime)) + assertTrue(onboardingLogo.waitForExists(waitingTime)) + assertTrue(onboardingSecondScreenTitle.waitForExists(waitingTime)) + assertTrue(onboardingSecondScreenFirstSubtitle.waitForExists(waitingTime)) + assertTrue(onboardingSecondScreenSecondSubtitle.waitForExists(waitingTime)) + assertTrue(onboardingSetAsDefaultBrowserButton.waitForExists(waitingTime)) + assertTrue(onboardingSkipButton.waitForExists(waitingTime)) + } + + fun clickGetStartedButton() { + onboardingGetStartedButton + .also { it.waitForExists(waitingTime) } + .also { it.clickAndWaitForNewWindow(waitingTime) } + } + + class Transition { + fun openMainMenu(interact: ThreeDotMainMenuRobot.() -> Unit): ThreeDotMainMenuRobot.Transition { + editURLBar.waitForExists(waitingTime) + mainMenu + .check(matches(isDisplayed())) + .perform(click()) + + ThreeDotMainMenuRobot().interact() + return ThreeDotMainMenuRobot.Transition() + } + + fun clickPageShortcut(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTimeShort) + mDevice.findObject(UiSelector().text(title)).click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun openSearchBar(interact: SearchRobot.() -> Unit): SearchRobot.Transition { + editURLBar.waitForExists(waitingTime) + editURLBar.click() + + SearchRobot().interact() + return SearchRobot.Transition() + } + } +} + +fun homeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() +} + +private val editURLBar = + mDevice.findObject( + UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"), + ) + +private val mainMenu = onView(withId(R.id.menuView)) + +/********* First Run Locators */ +private val firstSlideTitle = + mDevice.findObject(UiSelector().textContains(getStringResource(R.string.firstrun_defaultbrowser_title))) + +private val secondSlideTitle = + mDevice.findObject(UiSelector().textContains(getStringResource(R.string.firstrun_search_title))) + +private val thirdSlideTitle = + mDevice.findObject(UiSelector().textContains(getStringResource(R.string.firstrun_shortcut_title))) + +private val lastSlide = + mDevice.findObject(UiSelector().textContains(getStringResource(R.string.firstrun_privacy_title))) + +private val nextButton = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/next") + .enabled(true), +) + +private val finishButton = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/finish") + .enabled(true), +) + +private val topSitesList = mDevice.findObject(UiSelector().resourceId("$packageName:id/topSites")) + +/** New onboarding elements **/ + +private val onboardingCloseButton = + mDevice.findObject( + UiSelector() + .descriptionContains(getStringResource(R.string.onboarding_close_button_content_description)), + ) + +private val onboardingLogo = + mDevice.findObject( + UiSelector() + .className("android.widget.ImageView") + .descriptionContains(appName), + ) + +private val onboardingFirstScreenTitle = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.onboarding_first_screen_title)), + ) + +private val onboardingSecondScreenTitle = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.onboarding_short_app_name) + " isn’t like other browsers"), + ) + +private val onboardingFirstScreenSubtitle = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.onboarding_first_screen_subtitle)), + ) + +private val onboardingSecondScreenFirstSubtitle = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.onboarding_second_screen_subtitle_one)), + ) + +private val onboardingSecondScreenSecondSubtitle = + mDevice.findObject( + UiSelector() + .textContains( + "Make " + getStringResource(R.string.onboarding_short_app_name) + + " your default to protect your data with every link you open.", + ), + ) + +private val onboardingGetStartedButton = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.onboarding_first_screen_button_text)), + ) + +private val onboardingSetAsDefaultBrowserButton = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.onboarding_second_screen_default_browser_button_text)), + ) + +private val onboardingSkipButton = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.onboarding_second_screen_skip_button_text)), + ) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/NotificationRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/NotificationRobot.kt new file mode 100644 index 0000000000..ce53788fb4 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/NotificationRobot.kt @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import android.os.Build +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.Until +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.appName +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.waitingTime + +class NotificationRobot { + + private val NOTIFICATION_SHADE = "com.android.systemui:id/notification_stack_scroller" + private val QS_PANEL = "com.android.systemui:id/quick_qs_panel" + + fun clearNotifications() { + if (clearButton.exists()) { + clearButton.click() + } else { + notificationTray.flingToEnd(3) + if (clearButton.exists()) { + clearButton.click() + } else if (notificationTray.exists()) { + mDevice.pressBack() + } + } + } + + fun expandEraseBrowsingNotification() { + notificationHeader.click() + } + + fun verifySystemNotificationExists(notificationMessage: String) { + val notification = mDevice.findObject(UiSelector().text(notificationMessage)) + while (!notification.waitForExists(waitingTime)) { + UiScrollable( + UiSelector().resourceId(NOTIFICATION_SHADE), + ).flingToEnd(1) + } + + assertTrue(notification.exists()) + } + + fun verifyMediaNotificationExists(notificationMessage: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val notificationInTray = mDevice.wait( + Until.hasObject( + By.res(QS_PANEL).hasDescendant( + By.text(notificationMessage), + ), + ), + waitingTime, + ) + + assertTrue(notificationInTray) + } else { + verifySystemNotificationExists(notificationMessage) + } + } + + fun verifyNotificationGone(notificationMessage: String) { + assertTrue( + mDevice.findObject(UiSelector().text(notificationMessage)) + .waitUntilGone(waitingTime), + ) + } + + fun clickMediaNotificationControlButton(action: String) { + mediaNotificationControlButton(action).click() + } + + fun verifyMediaNotificationButtonState(action: String) { + mediaNotificationControlButton(action).waitForExists(waitingTime) + } + + fun verifyDownloadNotification(notificationMessage: String, fileName: String) { + val notification = UiSelector().text(notificationMessage) + var notificationFound = mDevice.findObject(notification).waitForExists(waitingTime) + val downloadFilename = mDevice.findObject(UiSelector().text(fileName)) + + while (!notificationFound) { + notificationTray.swipeUp(2) + notificationFound = mDevice.findObject(notification).waitForExists(waitingTime) + } + + assertTrue(notificationFound) + assertTrue(downloadFilename.exists()) + } + + class Transition { + fun clickEraseAndOpenNotificationButton(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + notificationEraseAndOpenButton.waitForExists(waitingTime) + notificationEraseAndOpenButton.click() + + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } + + fun clickNotificationOpenButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + notificationOpenButton.waitForExists(waitingTime) + notificationOpenButton.clickAndWaitForNewWindow() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun clickNotificationMessage(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + eraseBrowsingNotification.waitForExists(waitingTime) + eraseBrowsingNotification.click() + + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } + } +} + +fun notificationTray(interact: NotificationRobot.() -> Unit): NotificationRobot.Transition { + NotificationRobot().interact() + return NotificationRobot.Transition() +} + +private val eraseBrowsingNotification = + mDevice.findObject( + UiSelector().text(getStringResource(R.string.notification_erase_text)), + ) + +private val notificationEraseAndOpenButton = + mDevice.findObject( + UiSelector().description(getStringResource(R.string.notification_action_erase_and_open)), + ) + +private val notificationOpenButton = mDevice.findObject( + UiSelector().description(getStringResource(R.string.notification_action_open)), +) + +private val notificationTray = UiScrollable( + UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller"), +) + .setAsVerticalList() + +private val notificationHeader = mDevice.findObject( + UiSelector() + .resourceId("android:id/app_name_text") + .textContains(appName), +) + +private val clearButton = + mDevice.findObject(UiSelector().resourceId("com.android.systemui:id/btn_clear_all")) + +private fun mediaNotificationControlButton(action: String) = + mDevice.findObject(UiSelector().description(action)) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SearchRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SearchRobot.kt new file mode 100644 index 0000000000..490f4cb9ab --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SearchRobot.kt @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiSelector +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.Constants.LONG_CLICK_DURATION +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.pressEnterKey +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.idlingResources.SessionLoadedIdlingResource + +class SearchRobot { + + fun verifySearchBarIsDisplayed() = assertTrue(searchBar.exists()) + + fun typeInSearchBar(searchString: String) { + assertTrue(searchBar.waitForExists(waitingTime)) + searchBar.clearTextField() + searchBar.setText(searchString) + } + + // Would you like to turn on search suggestions? Yes No + // fresh install only + fun allowEnableSearchSuggestions() { + if (searchSuggestionsTitle.waitForExists(waitingTime)) { + searchSuggestionsButtonYes.waitForExists(waitingTime) + searchSuggestionsButtonYes.click() + } + } + + // Would you like to turn on search suggestions? Yes No + // fresh install only + fun denyEnableSearchSuggestions() { + if (searchSuggestionsTitle.waitForExists(waitingTime)) { + searchSuggestionsButtonNo.waitForExists(waitingTime) + searchSuggestionsButtonNo.click() + } + } + + fun verifySearchSuggestionsAreShown() { + suggestionsList.waitForExists(waitingTime) + assertTrue(suggestionsList.childCount >= 1) + } + + fun verifySearchSuggestionsAreNotShown() { + assertFalse(suggestionsList.exists()) + } + + fun verifySearchEditBarContainsText(text: String) { + mDevice.findObject(UiSelector().textContains(text)).waitForExists(waitingTime) + assertTrue(searchBar.text.equals(text)) + } + + fun verifySearchEditBarIsEmpty() { + searchBar.waitForExists(waitingTime) + assertTrue(searchBar.text.equals(getStringResource(R.string.urlbar_hint))) + } + + fun clickToolbar() { + toolbar.waitForExists(waitingTime) + toolbar.click() + } + + fun longPressSearchBar() { + searchBar.waitForExists(waitingTime) + mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")).click(LONG_CLICK_DURATION) + } + + fun clearSearchBar() = clearSearchButton.click() + + fun verifySearchSuggestionsContain(title: String) { + assertTrue( + suggestionsList.getChild(UiSelector().textContains(title)).waitForExists(waitingTime), + ) + } + + class Transition { + + private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource + + fun loadPage(url: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + val geckoEngineView = mDevice.findObject(UiSelector().resourceId("$packageName:id/engineView")) + val trackingProtectionDialog = mDevice.findObject(UiSelector().resourceId("$packageName:id/message")) + + sessionLoadedIdlingResource = SessionLoadedIdlingResource() + + searchScreen { typeInSearchBar(url) } + pressEnterKey() + + runWithIdleRes(sessionLoadedIdlingResource) { + assertTrue( + BrowserRobot().progressBar.waitUntilGone(waitingTime), + ) + assertTrue( + geckoEngineView.waitForExists(waitingTime) || + trackingProtectionDialog.waitForExists(waitingTime), + ) + } + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun pasteAndLoadLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + var currentTries = 0 + while (currentTries++ < 3) { + try { + mDevice.findObject(UiSelector().textContains("Paste")).waitForExists(waitingTime) + val pasteText = mDevice.findObject(By.textContains("Paste")) + pasteText.click() + mDevice.pressEnter() + break + } catch (e: NullPointerException) { + SearchRobot().longPressSearchBar() + } + } + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + } +} + +fun searchScreen(interact: SearchRobot.() -> Unit): SearchRobot.Transition { + SearchRobot().interact() + return SearchRobot.Transition() +} + +private val searchBar = + mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")) + +private val toolbar = + mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_url_view")) + +private val searchSuggestionsTitle = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/enable_search_suggestions_title") + .enabled(true), +) + +private val searchSuggestionsButtonYes = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/enable_search_suggestions_button") + .enabled(true), +) + +private val searchSuggestionsButtonNo = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/disable_search_suggestions_button") + .enabled(true), +) + +private val suggestionsList = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/search_suggestions_view"), +) + +private val clearSearchButton = mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_clear_view")) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsAdvancedMenuRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsAdvancedMenuRobot.kt new file mode 100644 index 0000000000..7c44b0a015 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsAdvancedMenuRobot.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 org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import org.hamcrest.CoreMatchers.allOf +import org.mozilla.focus.R +import org.mozilla.focus.helpers.EspressoHelper.hasCousin +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime + +class SettingsAdvancedMenuRobot { + fun verifyAdvancedSettingsItems() { + advancedSettingsList.waitForExists(waitingTime) + developerToolsHeading().check(matches(isDisplayed())) + remoteDebuggingSwitch().check(matches(isDisplayed())) + assertRemoteDebuggingSwitchState() + openLinksInAppsButton().check(matches(isDisplayed())) + assertOpenLinksInAppsSwitchState() + } + + fun verifyOpenLinksInAppsSwitchState(enabled: Boolean) = assertOpenLinksInAppsSwitchState(enabled) + fun clickOpenLinksInAppsSwitch() = openLinksInAppsButton().perform(click()) + + class Transition { + fun goBackToSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { + mDevice.pressBack() + SettingsRobot().interact() + return SettingsRobot.Transition() + } + } +} + +private fun openLinksInAppsButton() = onView(withText(R.string.preferences_open_links_in_apps)) + +private fun assertOpenLinksInAppsSwitchState(enabled: Boolean = false) { + if (enabled) { + openLinksInAppsButton() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + openLinksInAppsButton() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private val advancedSettingsList = + UiScrollable(UiSelector().resourceId("$packageName:id/recycler_view")) + +private fun developerToolsHeading() = onView(withText(R.string.preference_advanced_summary)) + +private fun remoteDebuggingSwitch() = onView(withText("Remote debugging via USB/Wi-Fi")) + +private fun assertRemoteDebuggingSwitchState(enabled: Boolean = false) { + if (enabled) { + remoteDebuggingSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + remoteDebuggingSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsGeneralMenuRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsGeneralMenuRobot.kt new file mode 100644 index 0000000000..4601e7d6bb --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsGeneralMenuRobot.kt @@ -0,0 +1,197 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import android.os.Build +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import junit.framework.TestCase.assertTrue +import org.hamcrest.Matchers.allOf +import org.junit.Assert.assertFalse +import org.mozilla.focus.R +import org.mozilla.focus.helpers.EspressoHelper.hasCousin +import org.mozilla.focus.helpers.TestHelper.appName +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.waitingTime + +class SettingsGeneralMenuRobot { + companion object { + const val ACTION_REQUEST_ROLE = "android.app.role.action.REQUEST_ROLE" + } + + fun verifyGeneralSettingsItems(defaultBrowserSwitchState: Boolean = false) { + verifyThemesList() + languageMenuButton().check(matches(isDisplayed())) + defaultBrowserSwitch().check(matches(isDisplayed())) + assertDefaultBrowserSwitchState(defaultBrowserSwitchState) + } + + fun clickSetDefaultBrowser() { + defaultBrowserSwitch() + .check(matches(isDisplayed())) + .perform(click()) + } + + fun verifyAndroidDefaultAppsMenuAppears() { + // method used to assert the default apps menu on API 24 and above + when (Build.VERSION.SDK_INT) { + in Build.VERSION_CODES.N..Build.VERSION_CODES.P -> + assertTrue( + mDevice.findObject(UiSelector().resourceId("com.android.settings:id/list")) + .waitForExists(waitingTime), + ) + in Build.VERSION_CODES.Q..Build.VERSION_CODES.R -> + intended(IntentMatchers.hasAction(ACTION_REQUEST_ROLE)) + } + } + + fun selectFocusDefaultBrowser() { + // method used to set default browser on API 30 and above + mDevice.findObject(UiSelector().text(appName)).click() + mDevice.findObject(UiSelector().textContains("Set as default")).click() + } + + fun verifySwitchIsToggled(checked: Boolean) { + onView(withId(R.id.switch_widget)).check( + matches( + if (checked) { + isChecked() + } else { + isNotChecked() + }, + ), + ) + } + + fun openLanguageSelectionMenu(localizedText: String = "Language"): ViewInteraction = + languageMenuButton(localizedText).perform(click()) + + fun verifySystemLocaleSelected(localizedText: String = "System default") { + assertTrue( + languageMenu.getChild( + UiSelector() + .text(localizedText) + .index(1), + ).getFromParent( + UiSelector().index(0), + ).isChecked, + ) + } + + fun selectLanguage(language: String) { + // Due to scrolling issues on Compose LazyList, avoid setting a language that is under the fold. + // see https://github.com/mozilla-mobile/focus-android/issues/7282 + languageMenu + .getChildByText(UiSelector().text(language), language, true) + .click() + } + + fun verifyThemesList() { + darkThemeToggle.check(matches(isDisplayed())) + lightThemeToggle.check(matches(isDisplayed())) + deviceThemeToggle + .check(matches(isDisplayed())) + .check(matches(isChecked())) + } + + fun verifyThemeApplied(isDarkTheme: Boolean = false, isLightTheme: Boolean = false, getThemeState: Boolean) { + when { + // getUiTheme() returns true if dark is applied + isDarkTheme -> assertTrue("Dark theme not applied", getThemeState) + // getUiTheme() returns false if light is applied + isLightTheme -> assertFalse("Light theme not applied", getThemeState) + } + } + + fun selectDarkTheme() = darkThemeToggle.perform(click()) + + fun selectLightTheme() = lightThemeToggle.perform(click()) + + fun selectDeviceTheme() = deviceThemeToggle.perform(click()) + + class Transition { + // add here transitions to other robot classes + } +} + +private fun defaultBrowserSwitch() = onView(withText("Make $appName default browser")) + +private fun assertDefaultBrowserSwitchState(enabled: Boolean) { + if (enabled) { + defaultBrowserSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switch_widget), + isChecked(), + ), + ), + ), + ) + } else { + defaultBrowserSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switch_widget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private val openWithDialogTitle = mDevice.findObject( + UiSelector() + .text("Open with"), +) + +private val openWithList = mDevice.findObject( + UiSelector() + .resourceId("android:id/resolver_list"), +) + +private fun languageMenuButton(localizedText: String = "Language") = onView(withText(localizedText)) + +private val languageMenu = UiScrollable(UiSelector().scrollable(true)) + +private val darkThemeToggle = + onView( + allOf( + withId(R.id.radio_button), + hasSibling(withText("Dark")), + ), + ) + +private val lightThemeToggle = + onView( + allOf( + withId(R.id.radio_button), + hasSibling(withText("Light")), + ), + ) + +private val deviceThemeToggle = + onView( + allOf( + withId(R.id.radio_button), + hasSibling(withText("Follow device theme")), + ), + ) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsMozillaMenuRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsMozillaMenuRobot.kt new file mode 100644 index 0000000000..c5ce1748eb --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsMozillaMenuRobot.kt @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import junit.framework.TestCase.assertTrue +import mozilla.components.support.utils.ext.getPackageInfoCompat +import org.hamcrest.Matchers.allOf +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.appName +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.idlingResources.SessionLoadedIdlingResource + +class SettingsMozillaMenuRobot { + + private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource + + fun verifyMozillaMenuItems() { + mozillaSettingsList.waitForExists(waitingTime) + aboutFocusPageLink.check(matches(isDisplayed())) + helpPageLink.check(matches(isDisplayed())) + yourRightsLink.check(matches(isDisplayed())) + privacyNoticeLink.check(matches(isDisplayed())) + licenseInfo.check(matches(isDisplayed())) + librariesUsedLink.check(matches(isDisplayed())) + } + + fun verifyVersionNumbers() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val packageInfo = context.packageManager.getPackageInfoCompat(context.packageName, 0) + val versionName = packageInfo.versionName + val gvBuildId = org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID + val gvVersion = org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION + + sessionLoadedIdlingResource = SessionLoadedIdlingResource() + + runWithIdleRes(sessionLoadedIdlingResource) { + assertTrue( + "Expected app version number not found", + mDevice.findObject(UiSelector().textContains(versionName)) + .waitForExists(waitingTime), + ) + + assertTrue( + "Expected GV version not found", + mDevice.findObject(UiSelector().textContains(gvVersion)) + .waitForExists(waitingTime), + ) + + assertTrue( + "Expected GV build ID not found", + mDevice.findObject(UiSelector().textContains(gvBuildId)) + .waitForExists(waitingTime), + ) + } + } + + fun verifyLibrariesUsedTitle() { + librariesUsedTitle + .check(matches(isDisplayed())) + } + + class Transition { + fun openAboutPage(interact: SettingsMozillaMenuRobot.() -> Unit): Transition { + aboutFocusPageLink + .check(matches(isDisplayed())) + .perform(click()) + + SettingsMozillaMenuRobot().interact() + return Transition() + } + + fun openAboutPageLearnMoreLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + mDevice.findObject(UiSelector().text("Learn more")).click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun openYourRightsPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + yourRightsLink + .check(matches(isDisplayed())) + .perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun openLicenseInformation(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + licenseInfo + .check(matches(isDisplayed())) + .perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun openLibrariesUsedPage(interact: SettingsMozillaMenuRobot.() -> Unit): BrowserRobot.Transition { + librariesUsedLink + .check(matches(isDisplayed())) + .perform(click()) + + SettingsMozillaMenuRobot().interact() + return BrowserRobot.Transition() + } + + fun openPrivacyNotice(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + privacyNoticeLink + .check(matches(isDisplayed())) + .perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun openHelpLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + helpPageLink + .check(matches(isDisplayed())) + .perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + } +} + +private val mozillaSettingsList = + UiScrollable(UiSelector().resourceId("$packageName:id/recycler_view")) + +private val aboutFocusPageLink = onView(withText("About $appName")) + +private val helpPageLink = + onView( + allOf( + withText("Help"), + withParent( + hasSibling(withId(R.id.icon_frame)), + ), + ), + ) + +private val yourRightsLink = + onView( + allOf( + withText("Your Rights"), + withParent( + hasSibling(withId(R.id.icon_frame)), + ), + ), + ) + +private val privacyNoticeLink = + onView( + allOf( + withText("Privacy Notice"), + withParent( + hasSibling(withId(R.id.icon_frame)), + ), + ), + ) + +private val licenseInfo = + onView( + allOf( + withText("Licensing information"), + withParent( + hasSibling(withId(R.id.icon_frame)), + ), + ), + ) + +private val librariesUsedLink = onView(withText("Libraries that we use")) +private val librariesUsedTitle = onView(withText("$appName | OSS Libraries")) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsPrivacyMenuRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsPrivacyMenuRobot.kt new file mode 100644 index 0000000000..7c549c4d36 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsPrivacyMenuRobot.kt @@ -0,0 +1,741 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:Suppress("TooManyFunctions") + +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.Matchers +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.EspressoHelper.hasCousin +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.getTargetContext +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.helpers.TestHelper.waitingTimeShort + +class SettingsPrivacyMenuRobot { + + fun verifyPrivacySettingsItems() { + privacySettingsList.waitForExists(waitingTime) + adTrackersBlockSwitch().check(matches(isDisplayed())) + assertAdTrackersBlockSwitchState() + analyticTrackersBlockSwitch().check(matches(isDisplayed())) + assertAnalyticTrackersBlockSwitchState() + socialTrackersBlockSwitch().check(matches(isDisplayed())) + assertSocialTrackersBlockSwitchState() + otherContentTrackersBlockSwitch().check(matches(isDisplayed())) + assertOtherContentTrackersBlockSwitchState() + blockWebFontsSwitch().check(matches(isDisplayed())) + assertBlockWebFontsSwitchState() + blockJavaScriptSwitch().check(matches(isDisplayed())) + assertBlockJavaScriptSwitchState() + assertTrue(cookiesAndSiteDataSection().exists()) + assertTrue(blockCookiesMenuButton().exists()) + assertTrue(blockCookiesDefaultOption().exists()) + assertTrue(sitePermissions().exists()) + verifyExceptionsListDisabled() + useFingerprintSwitch().check(matches(isDisplayed())) + assertUseFingerprintSwitchState() + stealthModeSwitch().check(matches(isDisplayed())) + assertStealthModeSwitchState() + safeBrowsingSwitch().check(matches(isDisplayed())) + assertSafeBrowsingSwitchState() + httpsOnlyModeSwitch().check(matches(isDisplayed())) + assertHttpsOnlyModeSwitchState() + sendDataSwitch().check(matches(isDisplayed())) + if (packageName != "org.mozilla.focus.debug") { + assertSendDataSwitchState(true) + } else { + assertSendDataSwitchState() + } + studiesOption().check(matches(isDisplayed())) + studiesDefaultOption().check(matches(isDisplayed())) + } + + fun verifyCookiesAndSiteDataSection() { + privacySettingsList.waitForExists(waitingTime) + assertTrue(cookiesAndSiteDataSection().exists()) + assertTrue(blockCookiesMenuButton().exists()) + assertTrue(blockCookiesDefaultOption().exists()) + assertTrue(sitePermissions().exists()) + } + + fun verifyBlockCookiesPrompt() { + assertTrue(blockCookiesPromptHeading.waitForExists(waitingTimeShort)) + assertTrue(blockCookiesYesPleaseOption.waitForExists(waitingTimeShort)) + assertTrue(block3rdPartyCookiesOnlyOption.waitForExists(waitingTimeShort)) + assertTrue(block3rdPartyTrackerCookiesOnlyOption.waitForExists(waitingTimeShort)) + assertTrue(blockCrossSiteCookiesOption.waitForExists(waitingTimeShort)) + assertTrue(noThanksOption.waitForExists(waitingTimeShort)) + assertTrue(cancelBlockCookiesPrompt.waitForExists(waitingTimeShort)) + } + + fun verifyBlockAdTrackersEnabled(enabled: Boolean) { + if (enabled) { + adTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + adTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } + } + + fun verifyBlockAnalyticTrackersEnabled(enabled: Boolean) { + if (enabled) { + analyticTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + analyticTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } + } + + fun verifyBlockSocialTrackersEnabled(enabled: Boolean) { + if (enabled) { + socialTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + socialTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } + } + + fun verifyBlockOtherTrackersEnabled(shouldBeEnabled: Boolean) { + if (shouldBeEnabled) { + otherContentTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + otherContentTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } + } + + fun clickAdTrackersBlockSwitch() = adTrackersBlockSwitch().perform(click()) + + fun clickAnalyticsTrackersBlockSwitch() = analyticTrackersBlockSwitch().perform(click()) + + fun clickSocialTrackersBlockSwitch() = socialTrackersBlockSwitch().perform(click()) + + fun clickOtherContentTrackersBlockSwitch() = otherContentTrackersBlockSwitch().perform(click()) + + fun clickBlockCookies() = blockCookiesMenuButton().click() + + fun clickCancelBlockCookiesPrompt() { + cancelBlockCookiesPrompt.click() + mDevice.waitForIdle(waitingTimeShort) + } + + fun clickYesPleaseOption() = blockCookiesYesPleaseOption.click() + fun clickBlockThirdPartyCookiesOnly() = block3rdPartyCookiesOnlyOption.click() + + fun switchSafeBrowsingToggle(): ViewInteraction = safeBrowsingSwitch().perform(click()) + + fun verifyExceptionsListDisabled() { + exceptionsList() + .check(matches(Matchers.not(isEnabled()))) + } + + fun openExceptionsList() { + exceptionsList() + .check(matches(isEnabled())) + .perform(click()) + } + + fun verifyExceptionURL(url: String) { + onView(withId(R.id.domainView)).check(matches(withText(containsString(url)))) + } + + fun removeException() { + openActionBarOverflowOrOptionsMenu(getTargetContext) + onView(withText("Remove")) + .perform(click()) + onView(withId(R.id.checkbox)) + .perform(click()) + onView(withId(R.id.remove)) + .perform(click()) + } + + fun removeAllExceptions() { + onView(withId(R.id.removeAllExceptions)) + .perform(click()) + } + + class Transition { + fun goBackToSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { + mDevice.pressBack() + + SettingsRobot().interact() + return SettingsRobot.Transition() + } + + fun clickSitePermissionsSettings(interact: SettingsSitePermissionsRobot.() -> Unit): SettingsSitePermissionsRobot.Transition { + sitePermissions().waitForExists(waitingTime) + sitePermissions().click() + + SettingsSitePermissionsRobot().interact() + return SettingsSitePermissionsRobot.Transition() + } + } +} + +private val privacySettingsList = + UiScrollable(UiSelector().resourceId("$packageName:id/recycler_view")) + +private fun adTrackersBlockSwitch(): ViewInteraction { + privacySettingsList + .scrollTextIntoView("Block ad trackers") + return onView(withText("Block ad trackers")) +} + +private fun assertAdTrackersBlockSwitchState(enabled: Boolean = true) { + if (enabled) { + adTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + adTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun analyticTrackersBlockSwitch(): ViewInteraction { + privacySettingsList + .scrollTextIntoView("Block analytic trackers") + return onView(withText("Block analytic trackers")) +} + +private fun assertAnalyticTrackersBlockSwitchState(enabled: Boolean = true) { + if (enabled) { + analyticTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + analyticTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun socialTrackersBlockSwitch(): ViewInteraction { + privacySettingsList + .scrollTextIntoView("Block social trackers") + return onView(withText("Block social trackers")) +} + +private fun assertSocialTrackersBlockSwitchState(enabled: Boolean = true) { + if (enabled) { + socialTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + socialTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun otherContentTrackersBlockSwitch(): ViewInteraction { + privacySettingsList + .scrollTextIntoView("Block other content trackers") + return onView(withText("Block other content trackers")) +} + +private fun assertOtherContentTrackersBlockSwitchState(enabled: Boolean = false) { + if (enabled) { + otherContentTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + otherContentTrackersBlockSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun blockWebFontsSwitch(): ViewInteraction { + privacySettingsList + .scrollTextIntoView("Block web fonts") + return onView(withText("Block web fonts")) +} + +private fun assertBlockWebFontsSwitchState(enabled: Boolean = false) { + if (enabled) { + blockWebFontsSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + blockWebFontsSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun blockJavaScriptSwitch(): ViewInteraction { + privacySettingsList + .scrollTextIntoView("Block JavaScript") + return onView(withText("Block JavaScript")) +} + +private fun assertBlockJavaScriptSwitchState(enabled: Boolean = false) { + if (enabled) { + blockJavaScriptSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + blockJavaScriptSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun cookiesAndSiteDataSection() = + privacySettingsList + .getChildByText( + UiSelector().text("Cookies and Site Data"), + "Cookies and Site Data", + true, + ) + +private fun blockCookiesMenuButton() = + privacySettingsList + .getChildByText( + UiSelector().text("Block cookies"), + "Block cookies", + true, + ) + +private fun blockCookiesDefaultOption() = + privacySettingsList + .getChildByText( + UiSelector().text("Block cross-site cookies"), + "Block cross-site cookies", + true, + ) + +private fun sitePermissions() = + privacySettingsList + .getChildByText(UiSelector().text("Site permissions"), "Site permissions", true) + +private fun useFingerprintSwitch(): ViewInteraction { + val useFingerprintSwitchSummary = getStringResource(R.string.preference_security_biometric_summary2) + privacySettingsList.scrollTextIntoView(useFingerprintSwitchSummary) + return onView(withText(useFingerprintSwitchSummary)) +} + +private fun assertUseFingerprintSwitchState(enabled: Boolean = false) { + if (enabled) { + useFingerprintSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + useFingerprintSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun stealthModeSwitch(): ViewInteraction { + val stealthModeSwitchSummary = getStringResource(R.string.preference_privacy_stealth_summary) + privacySettingsList.scrollTextIntoView(stealthModeSwitchSummary) + return onView(withText(stealthModeSwitchSummary)) +} + +private fun assertStealthModeSwitchState(enabled: Boolean = false) { + if (enabled) { + stealthModeSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + stealthModeSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun safeBrowsingSwitch(): ViewInteraction { + val safeBrowsingSwitchText = + mDevice.findObject( + UiSelector().text( + getStringResource(R.string.preference_safe_browsing_summary), + ), + ) + privacySettingsList.scrollToEnd(3) + privacySettingsList.scrollIntoView(safeBrowsingSwitchText) + return onView(withText(getStringResource(R.string.preference_safe_browsing_summary))) +} + +private fun assertSafeBrowsingSwitchState(enabled: Boolean = true) { + if (enabled) { + safeBrowsingSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + safeBrowsingSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun httpsOnlyModeSwitch(): ViewInteraction { + val httpsOnlyModeSwitchText = getStringResource(R.string.preference_https_only_title) + privacySettingsList.scrollTextIntoView(httpsOnlyModeSwitchText) + return onView(withText(httpsOnlyModeSwitchText)) +} + +private fun assertHttpsOnlyModeSwitchState(enabled: Boolean = true) { + if (enabled) { + httpsOnlyModeSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + httpsOnlyModeSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun sendDataSwitch(): ViewInteraction { + val sendDataSwitchSummary = getStringResource(R.string.preference_mozilla_telemetry_summary2) + privacySettingsList.scrollTextIntoView(sendDataSwitchSummary) + return onView(withText(sendDataSwitchSummary)) +} + +private fun assertSendDataSwitchState(enabled: Boolean = false) { + if (enabled) { + sendDataSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isChecked(), + ), + ), + ), + ) + } else { + sendDataSwitch() + .check( + matches( + hasCousin( + allOf( + withId(R.id.switchWidget), + isNotChecked(), + ), + ), + ), + ) + } +} + +private fun studiesOption(): ViewInteraction { + val studies = getStringResource(R.string.preference_studies) + privacySettingsList.scrollTextIntoView(studies) + return onView(withText(R.string.preference_studies)) +} + +private fun studiesDefaultOption(): ViewInteraction { + privacySettingsList.scrollToEnd(3) + return onView(withText(R.string.preference_state_on)) +} + +private fun exceptionsList(): ViewInteraction { + val exceptionsTitle = getStringResource(R.string.preference_exceptions) + privacySettingsList.scrollTextIntoView(exceptionsTitle) + return onView(withText(exceptionsTitle)) +} + +private val blockCookiesPromptHeading = + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/alertTitle") + .textContains(getStringResource(R.string.preference_block_cookies_title)), + ) + +private val blockCookiesYesPleaseOption = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.preference_privacy_should_block_cookies_yes_option2)), + ) + +private val block3rdPartyCookiesOnlyOption = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.preference_privacy_should_block_cookies_third_party_only_option)), + ) + +private val block3rdPartyTrackerCookiesOnlyOption = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.preference_privacy_should_block_cookies_third_party_tracker_cookies_option)), + ) + +private val blockCrossSiteCookiesOption = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.preference_privacy_should_block_cookies_cross_site_option)), + ) + +private val noThanksOption = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.preference_privacy_should_block_cookies_no_option2)), + ) + +private val cancelBlockCookiesPrompt = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.action_cancel)), + ) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsRobot.kt new file mode 100644 index 0000000000..b29032d656 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsRobot.kt @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity.robots + +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime + +class SettingsRobot { + + fun verifySettingsMenuItems() { + settingsMenuList.waitForExists(waitingTime) + assertTrue(generalSettingsMenu().exists()) + assertTrue(searchSettingsMenu.exists()) + assertTrue(privacySettingsMenu.exists()) + assertTrue(advancedSettingsMenu.exists()) + assertTrue(mozillaSettingsMenu.exists()) + } + + class Transition { + fun openSearchSettingsMenu(interact: SearchSettingsRobot.() -> Unit): SearchSettingsRobot.Transition { + searchSettingsMenu.waitForExists(waitingTime) + searchSettingsMenu.click() + + SearchSettingsRobot().interact() + return SearchSettingsRobot.Transition() + } + + fun openGeneralSettingsMenu( + localizedText: String = getStringResource(R.string.preference_category_general), + interact: SettingsGeneralMenuRobot.() -> Unit, + ): SettingsGeneralMenuRobot.Transition { + generalSettingsMenu(localizedText).waitForExists(waitingTime) + generalSettingsMenu(localizedText).click() + + SettingsGeneralMenuRobot().interact() + return SettingsGeneralMenuRobot.Transition() + } + + fun openPrivacySettingsMenu( + interact: SettingsPrivacyMenuRobot.() -> Unit, + ): SettingsPrivacyMenuRobot.Transition { + privacySettingsMenu.waitForExists(waitingTime) + privacySettingsMenu.click() + + SettingsPrivacyMenuRobot().interact() + return SettingsPrivacyMenuRobot.Transition() + } + + fun openAdvancedSettingsMenu( + interact: SettingsAdvancedMenuRobot.() -> Unit, + ): SettingsAdvancedMenuRobot.Transition { + advancedSettingsMenu.waitForExists(waitingTime) + advancedSettingsMenu.click() + + SettingsAdvancedMenuRobot().interact() + return SettingsAdvancedMenuRobot.Transition() + } + + fun openMozillaSettingsMenu( + interact: SettingsMozillaMenuRobot.() -> Unit, + ): SettingsMozillaMenuRobot.Transition { + mozillaSettingsMenu.waitForExists(waitingTime) + mozillaSettingsMenu.click() + + SettingsMozillaMenuRobot().interact() + return SettingsMozillaMenuRobot.Transition() + } + + fun goBackToHomeScreen( + interact: SearchRobot.() -> Unit, + ): SearchRobot.Transition { + mDevice.pressBack() + + SearchRobot().interact() + return SearchRobot.Transition() + } + } +} + +private val settingsMenuList = + UiScrollable(UiSelector().resourceId("$packageName:id/recycler_view")) + +private fun generalSettingsMenu(localizedText: String = getStringResource(R.string.preference_category_general)) = + settingsMenuList.getChild( + UiSelector() + .text(localizedText), + ) + +private val searchSettingsMenu = settingsMenuList.getChild( + UiSelector() + .text(getStringResource(R.string.preference_category_search)), +) + +private val privacySettingsMenu = settingsMenuList.getChild( + UiSelector() + .text(getStringResource(R.string.preference_privacy_and_security_header)), +) + +private val advancedSettingsMenu = settingsMenuList.getChild( + UiSelector() + .text(getStringResource(R.string.preference_category_advanced)), +) + +private val mozillaSettingsMenu = settingsMenuList.getChild( + UiSelector() + .text(getStringResource(R.string.preference_mozilla_summary)), +) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsSearchMenuRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsSearchMenuRobot.kt new file mode 100644 index 0000000000..f96d01fbd0 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsSearchMenuRobot.kt @@ -0,0 +1,169 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.UiObject +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.getTargetContext +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime + +class SearchSettingsRobot { + + fun verifySearchSettingsItems() { + assertTrue(searchEngineSubMenu.exists()) + assertTrue(searchEngineDefaultOption.exists()) + assertTrue(searchSuggestionsHeading.exists()) + assertTrue(searchSuggestionDescription.exists()) + verifySearchSuggestionsSwitchState() + assertTrue(searchSuggestionLearnMoreLink.exists()) + assertTrue(urlAutocompleteSubMenu.exists()) + assertTrue(urlAutocompleteDefaultOption.exists()) + } + + fun openSearchEngineSubMenu() { + searchEngineSubMenu.waitForExists(waitingTime) + searchEngineSubMenu.click() + } + + fun selectSearchEngine(engineName: String) { + searchEngineList.waitForExists(waitingTime) + searchEngineList + .getChild(UiSelector().text(engineName)) + .click() + } + + fun clickSearchSuggestionsSwitch() { + searchSuggestionsSwitch.waitForExists(waitingTime) + searchSuggestionsSwitch.click() + } + + fun verifySearchSuggestionsSwitchState(enabled: Boolean = false) { + if (enabled) { + assertTrue(searchSuggestionsSwitch.isChecked) + } else { + assertFalse(searchSuggestionsSwitch.isChecked) + } + } + + fun openUrlAutocompleteSubMenu() { + urlAutocompleteSubMenu.waitForExists(waitingTime) + urlAutocompleteSubMenu.click() + } + + fun openManageSitesSubMenu() { + manageSitesSubMenu.check(matches(isDisplayed())) + manageSitesSubMenu.perform(click()) + } + + fun openAddCustomUrlDialog() { + addCustomUrlButton.check(matches(isDisplayed())) + addCustomUrlButton.perform(click()) + } + + fun enterCustomUrl(url: String) { + customUrlText.check(matches(isDisplayed())) + customUrlText.perform(typeText(url)) + } + + fun saveCustomUrl() = saveButton.perform(click()) + + fun verifySavedCustomURL(url: String) { + customUrlText.check(matches(withText(url))) + } + + fun removeCustomUrl() { + Espresso.openActionBarOverflowOrOptionsMenu(getTargetContext) + onView(withText(R.string.preference_autocomplete_menu_remove)).perform(click()) + customUrlText.perform(click()) + onView(withId(R.id.remove)).perform(click()) + } + + fun verifyCustomUrlDialogNotClosed() { + saveButton.check(matches(isDisplayed())) + } + + fun toggleCustomAutocomplete() { + onView(withText(R.string.preference_switch_autocomplete_user_list)).perform(click()) + } + + fun toggleTopSitesAutocomplete() { + onView(withText(R.string.preference_switch_autocomplete_topsites)).perform(click()) + } + + class Transition +} + +private val searchEngineSubMenu = + UiScrollable(UiSelector().resourceId("$packageName:id/recycler_view")) + .getChild(UiSelector().text(getStringResource(R.string.preference_search_engine_label))) + +private val searchEngineDefaultOption = + mDevice.findObject(UiSelector().textContains("Google")) + +private val searchEngineList = UiScrollable( + UiSelector() + .resourceId("$packageName:id/search_engine_group").enabled(true), +) + +private val searchSuggestionsHeading: UiObject = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.preference_show_search_suggestions)), + ) + +private val searchSuggestionDescription: UiObject = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.preference_show_search_suggestions_summary)), + ) + +private val searchSuggestionLearnMoreLink: UiObject = + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/link"), + ) + +private val searchSuggestionsSwitch: UiObject = + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/switchWidget"), + ) + +private val urlAutocompleteSubMenu = + UiScrollable(UiSelector().resourceId("$packageName:id/recycler_view")) + .getChildByText( + UiSelector().text(getStringResource(R.string.preference_subitem_autocomplete)), + getStringResource(R.string.preference_subitem_autocomplete), + true, + ) + +private val urlAutocompleteDefaultOption = + mDevice.findObject( + UiSelector() + .textContains(getStringResource(R.string.preference_state_on)), + ) + +private val manageSitesSubMenu = onView(withText(R.string.preference_autocomplete_subitem_manage_sites)) + +private val addCustomUrlButton = onView(withText(R.string.preference_autocomplete_action_add)) + +private val customUrlText = onView(withId(R.id.domainView)) + +private val saveButton = onView(withId(R.id.save)) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsSitePermissionsRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsSitePermissionsRobot.kt new file mode 100644 index 0000000000..e1bc361186 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SettingsSitePermissionsRobot.kt @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import androidx.test.uiautomator.UiSelector +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.waitingTime + +class SettingsSitePermissionsRobot { + + fun verifySitePermissionsItems() { + assertTrue(autoplaySettings.waitForExists(waitingTime)) + assertTrue(autoplayDefaultValue.exists()) + assertTrue(cameraPermissionsSettings.exists()) + assertTrue(cameraDefaultValue.exists()) + assertTrue(locationPermissionsSettings.exists()) + assertTrue(locationDefaultValue.exists()) + assertTrue(microphonePermissionsSettings.exists()) + assertTrue(microphoneDefaultValue.exists()) + assertTrue(notificationPermissionsSettings.exists()) + assertTrue(notificationDefaultValue.exists()) + assertTrue(DRMContentPermissionsSettings.exists()) + assertTrue(DRMContentDefaultValue.exists()) + } + + fun verifyAutoplaySection() { + assertTrue(autoplayAllowAudioAndVideoOption.exists()) + assertTrue(autoplayBlockAudioOnlyOption.exists()) + assertTrue(recommendedDescription.exists()) + assertBlockAudioOnlyIsChecked() + assertTrue(blockAudioAndVideoOption.exists()) + } + + fun openAutoPlaySettings() { + autoplaySettings.waitForExists(waitingTime) + autoplaySettings.click() + } + + fun openCameraPermissionsSettings() { + cameraPermissionsSettings.waitForExists(waitingTime) + cameraPermissionsSettings.click() + } + + fun openLocationPermissionsSettings() { + locationPermissionsSettings.waitForExists(waitingTime) + locationPermissionsSettings.click() + } + + fun verifyAskToAllowChecked() { + askToAllowRadioButton.waitForExists(waitingTime) + assertTrue(askToAllowRadioButton.isChecked) + } + + fun verifyPermissionsStateSettings() { + assertTrue(askToAllowRadioButton.waitForExists(waitingTime)) + assertTrue(blockedRadioButton.waitForExists(waitingTime)) + } + + fun verifyBlockedByAndroidState() { + assertTrue(blockedByAndroidInfo.waitForExists(waitingTime)) + assertTrue(goToSettingsButton.waitForExists(waitingTime)) + } + + fun selectBlockAudioVideoAutoplay() { + blockAudioAndVideoOption.waitForExists(waitingTime) + blockAudioAndVideoOption.click() + } + + fun selectAllowAudioVideoAutoplay() { + autoplayAllowAudioAndVideoOption.waitForExists(waitingTime) + autoplayAllowAudioAndVideoOption.click() + } + + class Transition +} + +private val autoplaySettings = + mDevice.findObject( + UiSelector().text(getStringResource(R.string.preference_autoplay)), + ) + +private val autoplayDefaultValue = + mDevice.findObject( + UiSelector().text(getStringResource(R.string.preference_block_autoplay_audio_only)), + ) + +private val autoplayAllowAudioAndVideoOption = + mDevice.findObject(UiSelector().text(getStringResource(R.string.preference_allow_audio_video_autoplay))) + +private val autoplayBlockAudioOnlyOption = + mDevice.findObject(UiSelector().text(getStringResource(R.string.preference_block_autoplay_audio_only))) + +private val recommendedDescription = + mDevice.findObject(UiSelector().text(getStringResource(R.string.preference_block_autoplay_audio_only_summary))) + +private val blockAudioAndVideoOption = + mDevice.findObject(UiSelector().text(getStringResource(R.string.preference_block_autoplay_audio_video))) + +private fun assertBlockAudioOnlyIsChecked() { + // the childSelector doesn't work anymore, so we are unable to find it by text + val radioButton = + mDevice.findObject( + UiSelector() + .checkable(true) + .index(1), + ) + assertTrue(radioButton.isChecked) +} + +private val locationPermissionsSettings = + mDevice.findObject(UiSelector().text("Location")) + +private val locationDefaultValue = + mDevice.findObject(UiSelector().text("Location")) + .getFromParent(UiSelector().text("Blocked by Android")) + +private val cameraPermissionsSettings = + mDevice.findObject(UiSelector().text("Camera")) + +private val cameraDefaultValue = + mDevice.findObject(UiSelector().text("Camera")) + .getFromParent(UiSelector().text("Blocked by Android")) + +private val microphonePermissionsSettings = + mDevice.findObject(UiSelector().text("Microphone")) + +private val microphoneDefaultValue = + mDevice.findObject(UiSelector().text("Microphone")) + .getFromParent(UiSelector().text("Blocked by Android")) + +private val notificationPermissionsSettings = + mDevice.findObject(UiSelector().text("Notification")) + +private val notificationDefaultValue = + mDevice.findObject(UiSelector().text("Notification")) + .getFromParent(UiSelector().text("Ask to allow")) + +private val DRMContentPermissionsSettings = + mDevice.findObject(UiSelector().text("DRM-controlled content")) + +private val DRMContentDefaultValue = + mDevice.findObject(UiSelector().text("DRM-controlled content")) + .getFromParent(UiSelector().text("Ask to allow")) + +private val askToAllowRadioButton = + // the childSelector doesn't work anymore, so we are unable to find it by text + mDevice.findObject( + UiSelector() + .checkable(true) + .index(0), + ) + +private val blockedRadioButton = + mDevice.findObject(UiSelector().text("Blocked")) + .getFromParent(UiSelector().className("android.widget.RadioButton")) + +private val blockedByAndroidInfo = + mDevice.findObject(UiSelector().text("Blocked by Android")) + +private val goToSettingsButton = + mDevice.findObject(UiSelector().text("Go to Settings")) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SiteSecurityInfoSheetRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SiteSecurityInfoSheetRobot.kt new file mode 100644 index 0000000000..243e0f8d8d --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/SiteSecurityInfoSheetRobot.kt @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.UiSelector +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.not +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime + +class SiteSecurityInfoSheetRobot { + + fun verifySiteConnectionInfoIsSecure(isSecure: Boolean) { + assertTrue(site_security_info.waitForExists(waitingTime)) + assertTrue(site_identity_title.exists()) + assertTrue(site_identity_Icon.exists()) + if (isSecure) { + assertTrue(site_security_info.text.equals("Connection is secure")) + } else { + assertTrue(site_security_info.text.equals("Connection is not secure")) + } + } + + fun verifyTrackingProtectionIsEnabled(enabled: Boolean) { + if (enabled) { + trackingProtectionSwitch.check(matches(isChecked())) + } else { + trackingProtectionSwitch.check(matches(not(isChecked()))) + } + } + + class Transition { + fun closeSecurityInfoSheet(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + mDevice.pressBack() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun clickTrackingProtectionSwitch(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + trackingProtectionSwitch.perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + } +} + +private val site_security_info = mDevice.findObject(UiSelector().resourceId("$packageName:id/security_info")) + +private val site_identity_title = + mDevice.findObject(UiSelector().resourceId("$packageName:id/site_title")) + +private val site_identity_Icon = + mDevice.findObject(UiSelector().resourceId("$packageName:id/site_favicon")) + +private val trackingProtectionSwitch = + onView( + allOf( + withId(R.id.switch_widget), + hasSibling(withText("Enhanced Tracking Protection")), + ), + ).inRoot(isDialog()) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/TabsTrayRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/TabsTrayRobot.kt new file mode 100644 index 0000000000..d99d704b56 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/TabsTrayRobot.kt @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.UiSelector +import org.hamcrest.Matchers.allOf +import org.hamcrest.Matchers.containsString +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.helpers.TestHelper.waitingTimeShort + +class TabsTrayRobot { + fun verifyTabsOrder(vararg tabTitle: String) { + for (tab in tabTitle.indices) { + assertTrue( + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/session_item") + .index(tab) + .childSelector(UiSelector().textContains(tabTitle[tab])), + ).waitForExists(waitingTime), + ) + } + } + + fun verifyCloseTabButton(tabTitle: String) = closeTabButton(tabTitle).check(matches(isDisplayed())) + + class Transition { + fun selectTab(tabTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + onView(withText(containsString(tabTitle))).perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun closeTab(tabTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + closeTabButton(tabTitle).perform(click()) + // waiting for the tab to be completely gone before trying other actions on the toolbar + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/mozac_browser_toolbar_url_view") + .textContains(tabTitle), + ).waitUntilGone(waitingTimeShort) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + } +} + +private fun closeTabButton(tabTitle: String) = + onView( + allOf( + withId(R.id.close_button), + hasSibling(withText(containsString(tabTitle))), + ), + ) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/ThreeDotMainMenuRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/ThreeDotMainMenuRobot.kt new file mode 100644 index 0000000000..6bb650dc34 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/ThreeDotMainMenuRobot.kt @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.Until +import junit.framework.TestCase.assertTrue +import org.hamcrest.Matchers.allOf +import org.junit.Assert.assertFalse +import org.mozilla.focus.R +import org.mozilla.focus.helpers.TestHelper.getStringResource +import org.mozilla.focus.helpers.TestHelper.mDevice +import org.mozilla.focus.helpers.TestHelper.packageName +import org.mozilla.focus.helpers.TestHelper.progressBar +import org.mozilla.focus.helpers.TestHelper.waitingTime +import org.mozilla.focus.helpers.TestHelper.waitingTimeShort + +class ThreeDotMainMenuRobot { + + fun verifyShareButtonExists() = assertTrue(shareBtn.exists()) + + fun verifyAddToHomeButtonExists() = assertTrue(addToHomeButton.exists()) + + fun verifyFindInPageExists() = findInPageButton.check(matches(isDisplayed())) + + fun verifyOpenInButtonExists() = assertTrue(openInBtn.exists()) + + fun verifyRequestDesktopSiteExists() = assertTrue(requestDesktopSiteButton.exists()) + + fun verifyRequestDesktopSiteIsEnabled(expectedState: Boolean) { + if (expectedState) { + assertTrue(requestDesktopSiteButton.isChecked) + } else { + assertFalse(requestDesktopSiteButton.isChecked) + } + } + + fun verifySettingsButtonExists() = settingsMenuButton().check(matches(isDisplayed())) + + fun verifyReportSiteIssueButtonExists() { + // Report Site Issue lazily appears, so we need to wait + val reportSiteIssueButton = mDevice.wait( + Until.hasObject( + By.res("$packageName:id/mozac_browser_menu_menuView").hasDescendant( + By.text("Report Site Issue…"), + ), + ), + waitingTime, + ) + + assertTrue(reportSiteIssueButton) + } + + fun verifyHelpPageLinkExists() = helpPageMenuLink.check(matches(isDisplayed())) + + fun clickOpenInOption() { + openInBtn.waitForExists(waitingTime) + openInBtn.click() + } + + fun verifyOpenInDialog() { + assertTrue(openInDialogTitle.waitForExists(waitingTime)) + assertTrue(openWithList.waitForExists(waitingTime)) + } + + fun clickOpenInChrome() { + val chromeBrowser = mDevice.findObject(UiSelector().text("Chrome")) + if (chromeBrowser.exists()) { + chromeBrowser.click() + } + } + + fun clickAddToShortcuts() { + addShortcutButton.waitForExists(waitingTimeShort) + addShortcutButton.click() + } + + class Transition { + fun openSettings( + localizedText: String = getStringResource(R.string.menu_settings), + interact: SettingsRobot.() -> Unit, + ): SettingsRobot.Transition { + mDevice.findObject(UiSelector().text(localizedText)).waitForExists(waitingTime) + settingsMenuButton(localizedText) + .check(matches(isDisplayed())) + .perform(click()) + + SettingsRobot().interact() + return SettingsRobot.Transition() + } + + fun openShareScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + shareBtn.waitForExists(waitingTime) + shareBtn.click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun openAddToHSDialog(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition { + addToHomeButton.waitForExists(waitingTime) + addToHomeButton.click() + + AddToHomeScreenRobot().interact() + return AddToHomeScreenRobot.Transition() + } + + fun clickHelpPageLink(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + helpPageMenuLink + .check(matches(isDisplayed())) + .perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun clickReloadButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + reloadButton.click() + progressBar.waitUntilGone(waitingTime) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun clickStopLoadingButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + stopLoadingButton.click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun openFindInPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + findInPageButton.perform(click()) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun switchDesktopSiteMode(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + requestDesktopSiteButton.waitForExists(waitingTime) + requestDesktopSiteButton.click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun pressBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + backButton.click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun pressForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + forwardButton.click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + } +} + +private fun settingsMenuButton(localizedText: String = "Settings") = + onView( + allOf(withText(localizedText), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)), + ) + +private val shareBtn = mDevice.findObject( + UiSelector() + .description("Share…"), +) + +private val addShortcutButton = + mDevice.findObject( + UiSelector() + .text("Add to Shortcuts"), + ) + +private val reloadButton = mDevice.findObject( + UiSelector() + .description("Reload website"), +) + +private val stopLoadingButton = mDevice.findObject( + UiSelector() + .description("Stop loading website"), +) + +private val addToHomeButton = mDevice.findObject( + UiSelector() + .text("Add to Home screen"), +) + +private val findInPageButton = onView(withText("Find in Page")) + +private val helpPageMenuLink = onView(withText("Help")) + +private val openInBtn = mDevice.findObject( + UiSelector() + .text("Open in…"), +) + +private val openInDialogTitle = mDevice.findObject( + UiSelector() + .text("Open in…"), +) + +private val openWithList = mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/apps"), +) + +private val requestDesktopSiteButton = + mDevice.findObject( + UiSelector() + .resourceId("$packageName:id/switch_widget"), + ) + +private val backButton = + mDevice.findObject( + UiSelector() + .description("Navigate back"), + ) + +private val forwardButton = + mDevice.findObject( + UiSelector() + .description("Navigate forward"), + ) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/Constants.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/Constants.kt new file mode 100644 index 0000000000..81d273a294 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/Constants.kt @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.helpers + +object Constants { + const val RETRY_COUNT = 3 + const val LONG_CLICK_DURATION: Long = 5000 +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/DeleteFilesHelper.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/DeleteFilesHelper.kt new file mode 100644 index 0000000000..85c2a76842 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/DeleteFilesHelper.kt @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.helpers + +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.provider.MediaStore +import android.util.Log +import androidx.core.net.toUri + +object DeleteFilesHelper { + fun deleteFileUsingDisplayName(context: Context, displayName: String): Boolean { + val uri = getUriFromDisplayName(context, displayName) + if (uri != null) { + val resolver = context.contentResolver + val selectionArgs = arrayOf(displayName) + resolver.delete( + uri, + MediaStore.Files.FileColumns.DISPLAY_NAME + "=?", + selectionArgs, + ) + Log.d("TestLog", "Download file deleted") + return true + } + Log.d("TestLog", "Download file could not be found") + return false + } + + private fun getUriFromDisplayName(context: Context, displayName: String): Uri? { + val projection = arrayOf(MediaStore.Files.FileColumns._ID) + val extUri: Uri = MediaStore.Files.getContentUri("external") + val cursor: Cursor = context.contentResolver.query( + extUri, + projection, + MediaStore.Files.FileColumns.DISPLAY_NAME + " LIKE ?", + arrayOf(displayName), + null, + )!! + cursor.moveToFirst() + return if (cursor.count > 0) { + val columnIndex: Int = cursor.getColumnIndex(projection[0]) + val fileId: Long = cursor.getLong(columnIndex) + cursor.close() + "$extUri/$fileId".toUri() + } else { + null + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/EspressoHelper.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/EspressoHelper.kt new file mode 100644 index 0000000000..78a4b64cdb --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/EspressoHelper.kt @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.helpers + +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.withChild +import androidx.test.espresso.matcher.ViewMatchers.withParent +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.Matchers +import org.hamcrest.TypeSafeMatcher +import org.mozilla.focus.R + +/** + * Some convenient methods for testing Focus with espresso. + */ +object EspressoHelper { + fun hasCousin(matcher: Matcher): Matcher { + return withParent( + hasSibling( + withChild( + matcher, + ), + ), + ) + } + + @JvmStatic + fun openSettings() { + openMenu() + Espresso.onView(ViewMatchers.withId(R.id.settings)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + .perform(ViewActions.click()) + assertToolbarMatchesText(R.string.menu_settings) + } + + @JvmStatic + fun openMenu() { + Espresso.onView(ViewMatchers.withId(R.id.menuView)) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + .perform(ViewActions.click()) + } + + @JvmStatic + fun assertToolbarMatchesText(@StringRes titleResource: Int) { + Espresso.onView( + Matchers.allOf( + ViewMatchers.withClassName(Matchers.endsWith("TextView")), + ViewMatchers.withParent(ViewMatchers.withId(R.id.toolbar)), + ), + ) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + .check(ViewAssertions.matches(ViewMatchers.withText(titleResource))) + } + + @JvmStatic + fun childAtPosition( + parentMatcher: Matcher, + position: Int, + ): Matcher { + return object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("Child at position $position in parent ") + parentMatcher.describeTo(description) + } + + public override fun matchesSafely(view: View): Boolean { + val parent = view.parent + return ( + parent is ViewGroup && parentMatcher.matches(parent) && + view == parent.getChildAt(position) + ) + } + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/FeatureSettingsHelper.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/FeatureSettingsHelper.kt new file mode 100644 index 0000000000..b0bf3f3db0 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/FeatureSettingsHelper.kt @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.helpers + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import org.mozilla.focus.cookiebanner.CookieBannerOption +import org.mozilla.focus.ext.settings + +class FeatureSettingsHelper { + val context: Context = InstrumentationRegistry.getInstrumentation().targetContext + private val settings = context.settings + + // saving default values of feature flags + private var shouldShowCfrForTrackingProtection: Boolean = + settings.shouldShowCfrForTrackingProtection + + fun setCfrForTrackingProtectionEnabled(enabled: Boolean) { + settings.shouldShowCfrForTrackingProtection = enabled + } + + fun setShowStartBrowsingCfrEnabled(enabled: Boolean) { + settings.shouldShowStartBrowsingCfr = enabled + } + + fun setCookieBannerReductionEnabled(enabled: Boolean) { + settings.isCookieBannerEnable = enabled + if (enabled) { + settings.saveCurrentCookieBannerOptionInSharePref(CookieBannerOption.CookieBannerRejectAll()) + } else { + settings.saveCurrentCookieBannerOptionInSharePref(CookieBannerOption.CookieBannerDisabled()) + } + } + + fun setSearchWidgetDialogEnabled(enabled: Boolean) { + if (enabled) { + settings.addClearBrowsingSessions(4) + } else { + settings.addClearBrowsingSessions(10) + } + } + + // Important: + // Use this after each test if you have modified these feature settings + // to make sure the app goes back to the default state + fun resetAllFeatureFlags() { + settings.shouldShowCfrForTrackingProtection = shouldShowCfrForTrackingProtection + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/HostScreencapScreenshotStrategy.java b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/HostScreencapScreenshotStrategy.java new file mode 100644 index 0000000000..1a8428218e --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/HostScreencapScreenshotStrategy.java @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.helpers; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import androidx.test.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import tools.fastlane.screengrab.ScreenshotCallback; +import tools.fastlane.screengrab.ScreenshotStrategy; + +/** + * This ScreenshotStrategy implementation pings the host system (the computer the emulator is + * running on) to take a screenshot using "screencap" via adb. After that the screenshot is read + * from the internal app storage and passed back to fastlane/screengrab using the provided callback. + */ +public class HostScreencapScreenshotStrategy implements ScreenshotStrategy { + private static final int CONNECT_TIMEOUT = 1000; + private static final int READ_TIMEOUT = 5000; + private static final String HOST_LOOPBACK = "10.0.2.2"; + private static final int PORT = 9771; + + private UiDevice device; + + public HostScreencapScreenshotStrategy(UiDevice device) { + this.device = device; + } + + @Override + public void takeScreenshot(String screenshotName, ScreenshotCallback screenshotCallback) { + device.waitForIdle(); + + takeScreenshotViaHost(screenshotName); + + Bitmap bitmap = readScreenshotFromStorage(); + + if (bitmap == null) { + bitmap = createDummyScreenShot(); + } + + screenshotCallback.screenshotCaptured(screenshotName, bitmap); + } + + private void takeScreenshotViaHost(String name) { + try { + final HttpURLConnection connection = (HttpURLConnection) new URL("http://" + HOST_LOOPBACK + ":" + PORT + "/" + name).openConnection(); + connection.setConnectTimeout(CONNECT_TIMEOUT); + connection.setReadTimeout(READ_TIMEOUT); + + try (final InputStream stream = connection.getInputStream()) { + final String response = readInput(stream); + stream.close(); + connection.disconnect(); + + if (!"screenshot, exit=0".equals(response)) { + throw new RuntimeException("Taking screenshot failed, response: " + response); + } + } + } catch (ConnectException | SocketTimeoutException e) { + // We ignore those two exceptions because they occur if the http server on the host + // system is not running and we want to be able to execute this test even if we are + // not taking screenshots with fastlane. By running this test whenever we run the + // other UI tests we make sure that this test doesn't break without us noticing. + } catch (IOException e) { + throw new RuntimeException("Taking screenshot failed", e); + } + } + + private Bitmap readScreenshotFromStorage() { + final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + final String screenshotPath = new File(context.getFilesDir(), "temp_screen.png").getAbsolutePath(); + + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + + return BitmapFactory.decodeFile(screenshotPath); + } + + private Bitmap createDummyScreenShot() { + final Bitmap bitmap = Bitmap.createBitmap(480, 800, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + canvas.drawColor(Color.MAGENTA); + return bitmap; + } + + private static String readInput(InputStream stream) throws IOException { + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + final StringBuilder builder = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + builder.append(line); + } + + return builder.toString(); + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MainActivityTestRule.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MainActivityTestRule.kt new file mode 100644 index 0000000000..b0d670421e --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MainActivityTestRule.kt @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@file:Suppress("DEPRECATION") + +package org.mozilla.focus.helpers + +import android.view.ViewConfiguration.getLongPressTimeout +import androidx.annotation.CallSuper +import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.ActivityTestRule +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import kotlinx.coroutines.runBlocking +import mozilla.components.support.utils.ThreadUtils +import org.mozilla.focus.activity.MainActivity +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.settings +import org.mozilla.focus.helpers.TestHelper.getTargetContext +import org.mozilla.focus.helpers.TestHelper.pressBackKey +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.state.AppStore +import org.mozilla.focus.state.Screen + +// Basic Test rule with pref to skip the onboarding screen +open class MainActivityFirstrunTestRule( + launchActivity: Boolean = true, + private val showFirstRun: Boolean, + private val showNewOnboarding: Boolean = true, + private val showStartBrowsingCfrVisibility: Boolean = false, +) : ActivityTestRule(MainActivity::class.java, launchActivity) { + private val longTapUserPreference = getLongPressTimeout() + private val featureSettingsHelper = FeatureSettingsHelper() + + @CallSuper + override fun beforeActivityLaunched() { + super.beforeActivityLaunched() + updateFirstRun(showFirstRun) + featureSettingsHelper.setShowStartBrowsingCfrEnabled(showStartBrowsingCfrVisibility) + featureSettingsHelper.setCookieBannerReductionEnabled(false) + setNewOnboarding(showNewOnboarding) + setLongTapTimeout(3000) + } + + override fun afterActivityFinished() { + super.afterActivityFinished() + + ThreadUtils.postToMainThread { + InstrumentationRegistry + .getInstrumentation() + .targetContext + .applicationContext + .components + .tabsUseCases + .removeAllTabs() + } + + featureSettingsHelper.resetAllFeatureFlags() + closeNotificationShade() + setLongTapTimeout(longTapUserPreference) + } +} + +// Test rule that allows usage of Espresso Intents +open class MainActivityIntentsTestRule( + launchActivity: Boolean = true, + private val showFirstRun: Boolean, + private val showStartBrowsingCfrVisibility: Boolean = false, + private val cookieBannerReductionEnabled: Boolean = false, +) : + IntentsTestRule(MainActivity::class.java, launchActivity) { + private val longTapUserPreference = getLongPressTimeout() + private val featureSettingsHelper = FeatureSettingsHelper() + + @CallSuper + override fun beforeActivityLaunched() { + super.beforeActivityLaunched() + + updateFirstRun(showFirstRun) + featureSettingsHelper.setShowStartBrowsingCfrEnabled(showStartBrowsingCfrVisibility) + featureSettingsHelper.setCookieBannerReductionEnabled(cookieBannerReductionEnabled) + setLongTapTimeout(3000) + } + + override fun afterActivityFinished() { + super.afterActivityFinished() + ThreadUtils.postToMainThread { + InstrumentationRegistry + .getInstrumentation() + .targetContext + .applicationContext + .components + .tabsUseCases + .removeAllTabs() + } + + closeNotificationShade() + setLongTapTimeout(longTapUserPreference) + } +} + +// Some tests will leave the notification shade open if they fail, needs to be closed before the next tests +private fun closeNotificationShade() { + val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + if (mDevice.findObject( + UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller"), + ).exists() + ) { + pressBackKey() + } +} + +private fun updateFirstRun(showFirstRun: Boolean) { + val appContext = InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + + val appStore = appContext.components.appStore + if (appStore.state.screen is Screen.FirstRun && !showFirstRun) { + hideFirstRun(appStore) + } else if (appStore.state.screen !is Screen.FirstRun && showFirstRun) { + showFirstRun(appStore) + } + appContext.settings.isFirstRun = showFirstRun +} + +private fun showFirstRun(appStore: AppStore) { + val job = appStore.dispatch( + AppAction.ShowFirstRun, + ) + runBlocking { job.join() } +} + +private fun hideFirstRun(appStore: AppStore) { + val job = appStore.dispatch( + AppAction.FinishFirstRun(tabId = null), + ) + runBlocking { job.join() } +} + +private fun setNewOnboarding(enabled: Boolean) { + getTargetContext.settings.isNewOnboardingEnable = enabled +} + +// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click +private fun setLongTapTimeout(delay: Int) { + val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + mDevice.executeShellCommand("settings put secure long_press_timeout $delay") +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MockLocationUpdatesRule.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MockLocationUpdatesRule.kt new file mode 100644 index 0000000000..5ed7ae4d51 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MockLocationUpdatesRule.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 org.mozilla.focus.helpers + +import android.content.Context +import android.location.Location +import android.location.LocationManager +import android.os.Build +import android.os.SystemClock +import android.util.Log +import androidx.test.core.app.ApplicationProvider +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import org.junit.rules.ExternalResource +import org.mozilla.focus.helpers.TestHelper.executeShellCommandBlocking +import java.util.Date +import kotlin.random.Random + +private const val mockProviderName = LocationManager.GPS_PROVIDER + +/** + * Rule that sets up a mock location provider that can inject location samples + * straight to the device that the test is running on. + * + * Credit to the mapbox team + * https://github.com/mapbox/mapbox-navigation-android/blob/87fab7ea1152b29533ee121eaf6c05bc202adf02/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/MockLocationUpdatesRule.kt + * + */ +class MockLocationUpdatesRule : ExternalResource() { + private val instrumentation = getInstrumentation() + private val appContext = (ApplicationProvider.getApplicationContext() as Context) + val latitude = Random.nextDouble(-90.0, 90.0) + val longitude = Random.nextDouble(-180.0, 180.0) + + private val locationManager: LocationManager by lazy { + (appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager) + } + + override fun before() { + /* ADB command to enable the mock location setting on the device. + * Will not be turned back off due to limitations on knowing its initial state. + */ + instrumentation.uiAutomation.executeShellCommandBlocking( + "appops set " + + appContext.packageName + + " android:mock_location allow", + ) + + // To mock locations we need a location provider, so we generate and set it here. + try { + locationManager.addTestProvider( + mockProviderName, + false, + false, + false, + false, + true, + true, + true, + 3, + 2, + ) + } catch (ex: Exception) { + // unstable + Log.w("MockLocationUpdatesRule", "addTestProvider failed") + } + locationManager.setTestProviderEnabled(mockProviderName, true) + } + + // Cleaning up the location provider after the test. + override fun after() { + locationManager.setTestProviderEnabled(mockProviderName, false) + locationManager.removeTestProvider(mockProviderName) + } + + /** + * Generate a valid mock location data and set with the help of a test provider. + * + * @param modifyLocation optional callback for modifying the constructed location before setting it. + */ + fun setMockLocation(modifyLocation: (Location.() -> Unit)? = null) { + check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + "MockLocationUpdatesRule is supported only on Android devices " + + "running version >= Build.VERSION_CODES.M" + } + + val location = Location(mockProviderName) + location.time = Date().time + location.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() + location.accuracy = 5f + location.altitude = 0.0 + location.bearing = 0f + location.speed = 5f + location.latitude = latitude + location.longitude = longitude + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + location.verticalAccuracyMeters = 5f + location.bearingAccuracyDegrees = 5f + location.speedAccuracyMetersPerSecond = 5f + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + location.elapsedRealtimeUncertaintyNanos = 0.0 + } + + modifyLocation?.let { + location.apply(it) + } + + locationManager.setTestProviderLocation(mockProviderName, location) + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MockWebServerHelper.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MockWebServerHelper.kt new file mode 100644 index 0000000000..0679e6282c --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/MockWebServerHelper.kt @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.helpers + +import android.os.Handler +import android.os.Looper +import androidx.core.net.toUri +import androidx.test.platform.app.InstrumentationRegistry +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import okio.Buffer +import okio.source +import java.io.IOException +import java.io.InputStream + +object MockWebServerHelper { + /** + * A [MockWebServer] [Dispatcher] that will return Android assets in the body of requests. + * + * If the dispatcher is unable to read a requested asset, it will fail the test by throwing an + * Exception on the main thread. + * + */ + const val HTTP_OK = 200 + const val HTTP_NOT_FOUND = 404 + + class AndroidAssetDispatcher : Dispatcher() { + private val mainThreadHandler = Handler(Looper.getMainLooper()) + + override fun dispatch(request: RecordedRequest): MockResponse { + val assetManager = InstrumentationRegistry.getInstrumentation().context.assets + try { + val pathWithoutQueryParams = request.path!!.drop(1).toUri().path + assetManager.open(pathWithoutQueryParams!!).use { inputStream -> + return fileToResponse(pathWithoutQueryParams, inputStream) + } + } catch (e: IOException) { // e.g. file not found. + // We're on a background thread so we need to forward the exception to the main thread. + mainThreadHandler.postAtFrontOfQueue { throw e } + return MockResponse().setResponseCode(HTTP_NOT_FOUND) + } + } + } + + @Throws(IOException::class) + private fun fileToResponse(path: String, file: InputStream): MockResponse { + return MockResponse() + .setResponseCode(HTTP_OK) + .setBody(fileToBytes(file)!!) + .addHeader("content-type: " + contentType(path)) + } + + @Throws(IOException::class) + private fun fileToBytes(file: InputStream): Buffer? { + val result = Buffer() + result.writeAll(file.source()) + return result + } + + private fun contentType(path: String): String? { + return when { + path.endsWith(".png") -> "image/png" + path.endsWith(".jpg") -> "image/jpeg" + path.endsWith(".jpeg") -> "image/jpeg" + path.endsWith(".gif") -> "image/gif" + path.endsWith(".svg") -> "image/svg+xml" + path.endsWith(".html") -> "text/html; charset=utf-8" + path.endsWith(".txt") -> "text/plain; charset=utf-8" + else -> "application/octet-stream" + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/RetryTestRule.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/RetryTestRule.kt new file mode 100644 index 0000000000..e854df2496 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/RetryTestRule.kt @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.helpers + +import androidx.test.espresso.NoMatchingViewException +import androidx.test.uiautomator.UiObjectNotFoundException +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.lang.AssertionError + +class RetryTestRule(private val retryCount: Int = 5) : TestRule { + + override fun apply(base: Statement, description: Description): Statement { + return statement { + for (i in 1..retryCount) { + try { + base.evaluate() + break + } catch (t: AssertionError) { + if (i == retryCount) { + throw t + } + } catch (t: UiObjectNotFoundException) { + if (i == retryCount) { + throw t + } + } catch (t: NoMatchingViewException) { + if (i == retryCount) { + throw t + } + } + } + } + } + + private inline fun statement(crossinline eval: () -> Unit): Statement { + return object : Statement() { + override fun evaluate() = eval() + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/StringsHelper.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/StringsHelper.kt new file mode 100644 index 0000000000..affe13e4ee --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/StringsHelper.kt @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.helpers + +object StringsHelper { + const val FR_ENGLISH_LOCALE = "English (United States)" + const val FR_LANGUAGE_MENU = "Langue" + const val FR_SETTINGS = "Paramètres" + const val FR_GENERAL_HEADING = "Général" + const val FR_LANGUAGE_SYSTEM_DEFAULT = "Valeur par défaut du système" + + const val EN_LANGUAGE_MENU_HEADING = "Language" + const val EN_AFRIKAANS_LOCALE = "Afrikaans" + + const val AF_LANGUAGE_MENU = "Taal" + const val AF_SETTINGS = "Instellings" + const val AF_HELP = "Hulp" + const val AF_GENERAL_HEADING = "Algemeen" + const val AF_LANGUAGE_SYSTEM_DEFAULT = "System default" + + // App package names + const val GMAIL_APP = "com.google.android.gm" + const val PHONE_APP = "com.android.dialer" + const val GOOGLE_PHOTOS = "com.google.android.apps.photos" + const val GOOGLE_CHROME = "com.google.android.apps.chrome" +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/TestAssetHelper.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/TestAssetHelper.kt new file mode 100644 index 0000000000..27247458ca --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/TestAssetHelper.kt @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.helpers + +import okhttp3.mockwebserver.MockWebServer + +/** + * Helper for hosting web pages locally for testing purposes. + */ +object TestAssetHelper { + data class TestAsset(val url: String, val content: String, val title: String) + + /** + * Hosts simple websites, found at androidTest/assets/tab[1|2|3].html + * Returns a list of TestAsset, which can be used to navigate to each and + * assert that the correct information is being displayed. + * + * Content for these pages all follow the same pattern. See [tab1.html] for + * content implementation details. + */ + fun getGenericTabAsset(server: MockWebServer, pageNum: Int): TestAsset { + val url = server.url("tab$pageNum.html").toString() + val content = "Tab $pageNum" + val title = "tab$pageNum" + + return TestAsset(url, content, title) + } + + fun getGenericAsset(server: MockWebServer): TestAsset { + val url = server.url("genericPage.html").toString() + val content = "focus test page" + val title = "GenericPage" + + return TestAsset(url, content, title) + } + + fun getHTMLControlsPageAsset(server: MockWebServer): TestAsset { + val url = server.url("htmlControls.html").toString() + val content = "" + val title = "Html_Control_Form" + + return TestAsset(url, content, title) + } + + fun getEnhancedTrackingProtectionAsset(server: MockWebServer, pageTitle: String): TestAsset { + val url = server.url("etpPages/$pageTitle.html").toString() + val content = "" + + return TestAsset(url, content, pageTitle) + } + + fun getImageTestAsset(server: MockWebServer): TestAsset { + val url = server.url("image_test.html").toString() + + return TestAsset(url, "", "") + } + + fun getStorageTestAsset(server: MockWebServer, pageTitle: String): TestAsset { + val url = server.url(pageTitle).toString() + + return TestAsset(url, "", "") + } + + fun getMediaTestAsset(server: MockWebServer, pageTitle: String): TestAsset { + val url = server.url("$pageTitle.html").toString() + + return TestAsset(url, "", pageTitle) + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/TestHelper.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/TestHelper.kt new file mode 100644 index 0000000000..522c07de6c --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/TestHelper.kt @@ -0,0 +1,379 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.helpers + +import android.app.PendingIntent +import android.app.UiAutomation +import android.content.ActivityNotFoundException +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.storage.StorageManager +import android.os.storage.StorageVolume +import android.util.Log +import android.view.KeyEvent +import android.view.inputmethod.InputMethodManager +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON +import androidx.core.net.toUri +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.web.sugar.Web +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import junit.framework.AssertionFailedError +import mozilla.components.support.utils.ext.getApplicationInfoCompat +import okio.Buffer +import org.hamcrest.Matchers +import org.hamcrest.Matchers.allOf +import org.junit.Assert +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.mozilla.focus.R +import org.mozilla.focus.activity.IntentReceiverActivity +import org.mozilla.focus.utils.IntentUtils +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.util.concurrent.TimeUnit + +@Suppress("TooManyFunctions") +object TestHelper { + @JvmField + var mDevice = UiDevice.getInstance(getInstrumentation()) + val waitingTime = TimeUnit.SECONDS.toMillis(15) + val pageLoadingTime = TimeUnit.SECONDS.toMillis(25) + val waitingTimeShort: Long = TimeUnit.SECONDS.toMillis(3) + + private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + fun randomString(stringLength: Int) = + (1..stringLength) + .map { kotlin.random.Random.nextInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") + + @JvmStatic + val getTargetContext: Context = getInstrumentation().targetContext + + @JvmStatic + val packageName: String = getTargetContext.packageName + + @JvmStatic + val appName: String = getTargetContext.getString(R.string.app_name) + + fun getStringResource(id: Int) = getTargetContext.resources.getString(id, appName) + + fun verifySnackBarText(text: String) { + val snackbarText = mDevice.findObject(UiSelector().textContains(text)) + assertTrue(snackbarText.waitForExists(waitingTime)) + } + + fun clickSnackBarActionButton(action: String) { + val snackbarActionButton = + onView( + allOf( + withId(R.id.snackbar_action), + withText(action), + ), + ) + snackbarActionButton.perform(click()) + } + + fun waitUntilSnackBarGone() { + mDevice.findObject(UiSelector().resourceId("$appName:id/snackbar_layout")) + .waitUntilGone(waitingTime) + } + + fun isPackageInstalled(packageName: String): Boolean { + return try { + val packageManager = getInstrumentation().context.packageManager + packageManager.getApplicationInfoCompat(packageName, 0).enabled + } catch (exception: PackageManager.NameNotFoundException) { + Log.d("TestLog", exception.message.toString()) + false + } + } + + fun restartApp(activity: MainActivityFirstrunTestRule) { + with(activity) { + finishActivity() + mDevice.waitForIdle() + launchActivity(null) + } + } + + // exit to the main view + fun exitToTop() { + val homeScreen = + mDevice.findObject(UiSelector().resourceId("$packageName:id/landingLayout")) + var homeScreenVisible = false + while (!homeScreenVisible) { + mDevice.pressBack() + homeScreenVisible = homeScreen.waitForExists(2000) + } + } + + // exit to the browser view + fun exitToBrowser() { + val browserScreen = + mDevice.findObject(UiSelector().resourceId("$packageName:id/main_content")) + var browserScreenVisible = false + while (!browserScreenVisible) { + mDevice.pressBack() + browserScreenVisible = browserScreen.waitForExists(2000) + } + } + + fun setNetworkEnabled(enabled: Boolean) { + when (enabled) { + true -> { + mDevice.executeShellCommand("svc data enable") + mDevice.executeShellCommand("svc wifi enable") + } + + false -> { + mDevice.executeShellCommand("svc data disable") + mDevice.executeShellCommand("svc wifi disable") + } + } + mDevice.waitForIdle(waitingTime) + } + + // verifies localized strings in different UIs + fun verifyTranslatedTextExists(text: String) = + assertTrue(mDevice.findObject(UiSelector().text(text)).waitForExists(waitingTime)) + + fun openAppFromExternalLink(url: String) { + val intent = Intent().apply { + action = Intent.ACTION_VIEW + data = url.toUri() + `package` = packageName + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + try { + getTargetContext.startActivity(intent) + } catch (ex: ActivityNotFoundException) { + intent.setPackage(null) + getTargetContext.startActivity(intent) + } + } + + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) + fun verifyDownloadedFileOnStorage(fileName: String) { + val storageManager = + getInstrumentation().targetContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager + val storageVolumes = storageManager.storageVolumes + val storageVolume: StorageVolume = storageVolumes[0] + val file = File("${storageVolume.directory!!.path}/Download/$fileName") + assertTrue(file.exists()) + } + + // Method for granting app permission to access location/camera/mic + fun grantAppPermission() { + if (SDK_INT >= 23) { + mDevice.findObject( + UiSelector().textContains( + when (SDK_INT) { + Build.VERSION_CODES.R -> + "While using the app" + else -> "Allow" + }, + ), + ).click() + } + } + + fun UiAutomation.executeShellCommandBlocking(command: String) { + val output = executeShellCommand(command) + FileInputStream(output.fileDescriptor).use { it.readBytes() } + } + + @JvmStatic + fun pressEnterKey() { + mDevice.pressKeyCode(KeyEvent.KEYCODE_ENTER) + } + + @JvmStatic + fun pressBackKey() { + mDevice.pressBack() + } + + @JvmStatic + fun pressHomeKey() { + mDevice.pressHome() + } + + fun createCustomTabIntent( + pageUrl: String, + customMenuItemLabel: String = "", + customActionButtonDescription: String = "", + ): Intent { + val appContext = getInstrumentation() + .targetContext + .applicationContext + val pendingIntent = PendingIntent.getActivity(appContext, 0, Intent(), IntentUtils.defaultIntentPendingFlags()) + + val customTabColorSchemeBuilder = CustomTabColorSchemeParams.Builder() + customTabColorSchemeBuilder.setToolbarColor(Color.MAGENTA) + + val customTabsIntent = CustomTabsIntent.Builder() + .addMenuItem(customMenuItemLabel, pendingIntent) + .setShareState(SHARE_STATE_ON) + .setActionButton(createTestBitmap(), customActionButtonDescription, pendingIntent, true) + .setDefaultColorSchemeParams(customTabColorSchemeBuilder.build()) + .build() + customTabsIntent.intent.data = pageUrl.toUri() + customTabsIntent.intent.component = ComponentName(appContext, IntentReceiverActivity::class.java) + return customTabsIntent.intent + } + + fun assertNativeAppOpens(appPackageName: String) { + try { + if (isPackageInstalled(packageName)) { + intended(toPackage(appPackageName)) + } + } catch (e: AssertionFailedError) { + e.printStackTrace() + } + } + + private fun createTestBitmap(): Bitmap { + val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawColor(Color.GREEN) + return bitmap + } + + /** + * Wrapper for tests to run only when certain conditions are met. + * For example: this method will avoid accidentally running a test on GV versions where the feature is disabled. + */ + fun runWithCondition(condition: Boolean, testBlock: () -> Unit) { + if (condition) { + testBlock() + } + } + + /********* Old code locators - used only in Screenshots tests */ + // wait for web area to be visible + @JvmStatic + fun waitForWebContent() { + Assert.assertTrue(geckoView.waitForExists(waitingTime)) + } + + @JvmField + var menuButton = Espresso.onView( + Matchers.allOf( + ViewMatchers.withId(R.id.menuView), + ViewMatchers.isDisplayed(), + ), + ) + + @JvmField + var permAllowBtn = mDevice.findObject( + UiSelector() + .textContains("Allow") + .clickable(true), + ) + + @JvmField + var webView = mDevice.findObject( + UiSelector() + .className("android.webkit.WebView") + .enabled(true), + ) + var geckoView = mDevice.findObject( + UiSelector() + .resourceId(packageName + ":id/engineView") + .enabled(true), + ) + + @JvmField + var progressBar = mDevice.findObject( + UiSelector() + .resourceId(packageName + ":id/progress") + .enabled(true), + ) + + @JvmField + var AddtoHSmenuItem = mDevice.findObject( + UiSelector() + .resourceId(packageName + ":id/add_to_homescreen") + .enabled(true), + ) + + @JvmField + var AddtoHSCancelBtn = mDevice.findObject( + UiSelector() + .resourceId(packageName + ":id/addtohomescreen_dialog_cancel") + .enabled(true), + ) + + @JvmField + var securityInfoIcon = mDevice.findObject( + UiSelector() + .resourceId(packageName + ":id/security_info") + .enabled(true), + ) + + @JvmField + var identityState = mDevice.findObject( + UiSelector() + .resourceId(packageName + ":id/site_identity_state") + .enabled(true), + ) + + @JvmField + var shareAppList = mDevice.findObject( + UiSelector() + .resourceId("android:id/resolver_list") + .enabled(true), + ) + + @JvmStatic + @Throws(IOException::class) + fun readTestAsset(filename: String?): Buffer { + getInstrumentation().getContext().assets.open(filename!!) + .use { stream -> return readStreamFile(stream) } + } + + @Throws(IOException::class) + fun readStreamFile(file: InputStream?): Buffer { + val buffer = Buffer() + buffer.write(file!!.readBytes()) + return buffer + } + + @JvmStatic + fun waitForWebSiteTitleLoad() { + Web.onWebView(ViewMatchers.withText("focus test page")) + } + + @JvmStatic + fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean) { + val imm = getTargetContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + + if (isExpectedToBeVisible) { + assertTrue(imm.isAcceptingText) + } else { + assertFalse(imm.isAcceptingText) + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/ext/WaitNotNull.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/ext/WaitNotNull.kt new file mode 100644 index 0000000000..33ca1a5d4a --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/helpers/ext/WaitNotNull.kt @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.helpers.ext + +import androidx.test.uiautomator.SearchCondition +import androidx.test.uiautomator.UiDevice +import org.junit.Assert.assertNotNull +import org.mozilla.focus.helpers.TestHelper + +/** + * Blocks the test for [waitTime] miliseconds before continuing. + * + * Will cause the test to fail is the condition is not met before the timeout. + */ +fun UiDevice.waitNotNull( + searchCondition: SearchCondition<*>, + waitTime: Long = TestHelper.waitingTime, +) = assertNotNull(wait(searchCondition, waitTime)) diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/idlingResources/RecyclerViewIdlingResource.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/idlingResources/RecyclerViewIdlingResource.kt new file mode 100644 index 0000000000..08edc53908 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/idlingResources/RecyclerViewIdlingResource.kt @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.idlingResources + +import androidx.test.espresso.IdlingResource + +class RecyclerViewIdlingResource constructor( + private val recycler: androidx.recyclerview.widget.RecyclerView, + private val minItemCount: Int = 0, +) : IdlingResource { + + private var callback: IdlingResource.ResourceCallback? = null + + override fun isIdleNow(): Boolean { + if (recycler.adapter != null && recycler.adapter!!.itemCount >= minItemCount) { + if (callback != null) { + callback!!.onTransitionToIdle() + } + return true + } + return false + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) { + this.callback = callback + } + + override fun getName(): String { + return RecyclerViewIdlingResource::class.java.name + ":" + recycler.id + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/idlingResources/SessionLoadedIdlingResource.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/idlingResources/SessionLoadedIdlingResource.kt new file mode 100644 index 0000000000..63cb622f0d --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/idlingResources/SessionLoadedIdlingResource.kt @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.idlingResources + +import androidx.test.espresso.IdlingResource +import androidx.test.platform.app.InstrumentationRegistry +import mozilla.components.browser.state.selector.selectedTab +import org.mozilla.focus.FocusApplication + +/** + * An IdlingResource implementation that waits until the current session is not loading anymore. + * Only after loading has completed further actions will be performed. + */ +class SessionLoadedIdlingResource : IdlingResource { + private var resourceCallback: IdlingResource.ResourceCallback? = null + + override fun getName(): String { + return SessionLoadedIdlingResource::class.java.simpleName + } + + override fun isIdleNow(): Boolean { + val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as FocusApplication + val tab = context.components.store.state.selectedTab + + return if (tab?.content?.loading == true) { + false + } else { + if (tab?.content?.progress == 100) { + invokeCallback() + true + } else { + false + } + } + } + + private fun invokeCallback() { + if (resourceCallback != null) { + resourceCallback!!.onTransitionToIdle() + } + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) { + this.resourceCallback = callback + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/privacy/GlobalPrivacyControlTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/privacy/GlobalPrivacyControlTest.kt new file mode 100644 index 0000000000..0675a59f70 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/privacy/GlobalPrivacyControlTest.kt @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.privacy + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper.getStorageTestAsset +import java.io.IOException + +/** + * Test that Global Privacy Control is always enabled in Focus. + */ +@RunWith(AndroidJUnit4ClassRunner::class) +class GlobalPrivacyControlTest { + private lateinit var webServer: MockWebServer + + private val featureSettingsHelper = FeatureSettingsHelper() + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + } + + @After + fun tearDown() { + try { + webServer.shutdown() + } catch (e: IOException) { + throw AssertionError("Could not stop web server", e) + } + } + + @Test + fun gpcTest() { + val storageStartUrl = getStorageTestAsset(webServer, "global_privacy_control.html").url + + searchScreen { + }.loadPage(storageStartUrl) { + verifyPageContent("GPC is enabled.") + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/privacy/LocalSessionStorageTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/privacy/LocalSessionStorageTest.kt new file mode 100644 index 0000000000..839f90f924 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/privacy/LocalSessionStorageTest.kt @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.privacy + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.FeatureSettingsHelper +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.MockWebServerHelper +import org.mozilla.focus.helpers.TestAssetHelper.getStorageTestAsset +import org.mozilla.focus.testAnnotations.SmokeTest +import java.io.IOException + +/** + * Make sure that session storage values are kept and written but removed at the end of a session. + */ +@RunWith(AndroidJUnit4ClassRunner::class) +class LocalSessionStorageTest { + private lateinit var webServer: MockWebServer + + private val featureSettingsHelper = FeatureSettingsHelper() + + companion object { + const val SESSION_STORAGE_HIT = "Session storage has value" + const val LOCAL_STORAGE_MISS = "Local storage empty" + } + + @get: Rule + var mActivityTestRule = MainActivityFirstrunTestRule(showFirstRun = false) + + @Before + fun setUp() { + webServer = MockWebServer().apply { + dispatcher = MockWebServerHelper.AndroidAssetDispatcher() + start() + } + featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) + } + + @After + fun tearDown() { + try { + webServer.shutdown() + } catch (e: IOException) { + throw AssertionError("Could not stop web server", e) + } + } + + @SmokeTest + @Test + fun testLocalAndSessionStorageIsWrittenAndRemoved() { + val storageStartUrl = getStorageTestAsset(webServer, "storage_start.html").url + val storageCheckUrl = getStorageTestAsset(webServer, "storage_check.html").url + + searchScreen { + }.loadPage(storageStartUrl) { + // Assert website is loaded and values are written. + verifyPageContent("Values written to storage") + }.openSearchBar { + // Now load the next website and assert that the values are still in the storage + }.loadPage(storageCheckUrl) { + verifyPageContent(SESSION_STORAGE_HIT) + verifyPageContent(LOCAL_STORAGE_MISS) + }.clearBrowsingData {} + searchScreen { + }.loadPage(storageCheckUrl) { + verifyPageContent("Session storage empty") + verifyPageContent("Local storage empty") + } + } + + @SmokeTest + @Test + fun eraseCookiesTest() { + val storageStartUrl = getStorageTestAsset(webServer, "storage_start.html").url + + searchScreen { + }.loadPage(storageStartUrl) { + verifyPageContent("No cookies set") + clickSetCookiesButton() + verifyPageContent("user=android") + }.clearBrowsingData {} + searchScreen { + }.loadPage(storageStartUrl) { + verifyPageContent("No cookies set") + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/AllowListScreenshots.java b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/AllowListScreenshots.java new file mode 100644 index 0000000000..8b1cdba03b --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/AllowListScreenshots.java @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.screenshots; + +import android.os.SystemClock; + +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.UiObjectNotFoundException; +import androidx.test.uiautomator.UiScrollable; +import androidx.test.uiautomator.UiSelector; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.focus.R; +import org.mozilla.focus.helpers.TestHelper; + +import java.io.IOException; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import tools.fastlane.screengrab.Screengrab; +import tools.fastlane.screengrab.locale.LocaleTestRule; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.pressImeActionButton; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.hasFocus; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +@Ignore("See: https://github.com/mozilla-mobile/mobile-test-eng/issues/305") +@RunWith(AndroidJUnit4.class) +public class AllowListScreenshots extends ScreenshotTest { + + @ClassRule + public static final LocaleTestRule localeTestRule = new LocaleTestRule(); + + private MockWebServer webServer; + + @Before + public void setUpWebServer() throws IOException { + webServer = new MockWebServer(); + + // Test page + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("image_test.html"))); + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("rabbit.jpg"))); + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("download.jpg"))); + // Download + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("image_test.html"))); + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("rabbit.jpg"))); + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("download.jpg"))); + } + + @After + public void tearDownWebServer() { + try { + webServer.close(); + webServer.shutdown(); + } catch (IOException e) { + throw new AssertionError("Could not stop web server", e); + } + } + + @Test + public void takeScreenshotsOfMenuandAllowlist() throws UiObjectNotFoundException { + SystemClock.sleep(5000); + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .check(matches(hasFocus())) + .perform(click(), replaceText(webServer.url("/").toString())); + + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .check(matches(hasFocus())) + .perform(pressImeActionButton()); + + device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/webview") + .enabled(true)) + .waitForExists(waitingTime); + + TestHelper.menuButton.perform(click()); + Screengrab.screenshot("BrowserViewMenu"); + onView(withId(R.id.enhanced_tracking)).perform(click()); + + // Open setting + onView(withId(R.id.menuView)) + .check(matches(isDisplayed())) + .perform(click()); + onView(withId(R.id.settings)) + .check(matches(isDisplayed())) + .perform(click()); + onView(withText(R.string.preference_privacy_and_security_header)).perform(click()); + + UiScrollable settingsView = new UiScrollable(new UiSelector().scrollable(true)); + if (settingsView.exists()) { // On tablet, this will not be found + settingsView.scrollToEnd(5); + onView(withText(R.string.preference_exceptions)).perform(click()); + } + + onView(withId(R.id.removeAllExceptions)) + .check(matches(isDisplayed())); + Screengrab.screenshot("ExceptionsDialog"); + onView(withId(R.id.removeAllExceptions)) + .perform(click()); + + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/BrowserScreenScreenshots.java b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/BrowserScreenScreenshots.java new file mode 100644 index 0000000000..1fa82b2a83 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/BrowserScreenScreenshots.java @@ -0,0 +1,301 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.screenshots; + +import android.os.SystemClock; + +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiObject; +import androidx.test.uiautomator.UiObjectNotFoundException; +import androidx.test.uiautomator.UiSelector; +import androidx.test.uiautomator.Until; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.focus.R; +import org.mozilla.focus.helpers.TestHelper; + +import java.io.IOException; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import tools.fastlane.screengrab.Screengrab; +import tools.fastlane.screengrab.locale.LocaleTestRule; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.pressImeActionButton; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.hasFocus; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withResourceName; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static junit.framework.Assert.assertTrue; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.mozilla.focus.helpers.EspressoHelper.openSettings; + +@Ignore("See: https://github.com/mozilla-mobile/mobile-test-eng/issues/305") +@RunWith(AndroidJUnit4.class) +public class BrowserScreenScreenshots extends ScreenshotTest { + + + @ClassRule + public static final LocaleTestRule localeTestRule = new LocaleTestRule(); + + private MockWebServer webServer; + + @Before + public void setUpWebServer() throws IOException { + webServer = new MockWebServer(); + + // Test page + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("image_test.html"))); + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("rabbit.jpg"))); + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("download.jpg"))); + // Download + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("image_test.html"))); + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("rabbit.jpg"))); + webServer.enqueue(new MockResponse() + .setBody(TestHelper.readTestAsset("download.jpg"))); + } + + @After + public void tearDownWebServer() { + try { + webServer.close(); + webServer.shutdown(); + } catch (IOException e) { + throw new AssertionError("Could not stop web server", e); + } + } + + @Test + public void takeScreenshotsOfBrowsingScreen() throws Exception { + SystemClock.sleep(5000); + takeScreenshotsOfBrowsingView(); + takeScreenshotsOfOpenWithAndShare(); + takeAddToHomeScreenScreenshot(); + takeScreenshotofInsecureCon(); + takeScreenshotOfFindDialog(); + takeScreenshotOfTabsTrayAndErase(); + takeScreenshotofSecureCon(); + } + + private void takeScreenshotsOfBrowsingView() { + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())); + + // click yes, then go into search dialog and change to twitter, or create twitter engine + // If it does not exist (In order to get search unavailable dialog) + openSettings(); + onView(withText(R.string.preference_category_search)) + .perform(click()); + onView(allOf(withText(R.string.preference_search_engine_label), + withResourceName("title"))) + .perform(click()); + onView(withText(R.string.preference_search_installed_search_engines)) + .check(matches(isDisplayed())); + + try { + onView(withText("Twitter")) + .check(matches(isDisplayed())) + .perform(click()); + } catch (NoMatchingViewException doesnotexist) { + final String addEngineLabel = getString(R.string.preference_search_add2); + onView(withText(addEngineLabel)) + .check(matches(isEnabled())) + .perform(click()); + onView(withId(R.id.edit_engine_name)) + .check(matches(isEnabled())); + onView(withId(R.id.edit_engine_name)) + .perform(replaceText("twitter")); + onView(withId(R.id.edit_search_string)) + .perform(replaceText("https://twitter.com/search?q=%s")); + onView(withId(R.id.menu_save_search_engine)) + .check(matches(isEnabled())) + .perform(click()); + } + + device.pressBack(); + onView(allOf(withText(R.string.preference_search_engine_label), + withResourceName("title"))) + .check(matches(isDisplayed())); + device.pressBack(); + onView(withText(R.string.preference_category_search)) + .check(matches(isDisplayed())); + device.pressBack(); + + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .check(matches(hasFocus())) + .perform(click(), replaceText(webServer.url("/").toString())); + try { + onView(withId(R.id.enable_search_suggestions_button)) + .check(matches(isDisplayed())); + Screengrab.screenshot("Enable_Suggestion_dialog"); + onView(withId(R.id.enable_search_suggestions_button)) + .perform(click()); + Screengrab.screenshot("Suggestion_unavailable_dialog"); + onView(withId(R.id.dismiss_no_suggestions_message)) + .perform(click()); + } catch (AssertionError dne) { } + + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .check(matches(hasFocus())) + .perform(pressImeActionButton()); + + device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/engineView") + .enabled(true)) + .waitForExists(waitingTime); + + onView(withId(R.id.mozac_browser_toolbar_url_view)) + .check(matches(isDisplayed())) + .check(matches(withText(containsString(webServer.getHostName())))); + } + + private void takeScreenshotsOfOpenWithAndShare() throws Exception { + /* Open_With View */ + TestHelper.menuButton.perform(click()); + + UiObject openWithBtn = device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/open_select_browser") + .enabled(true)); + assertTrue(openWithBtn.waitForExists(waitingTime)); + openWithBtn.click(); + UiObject shareList = device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/apps") + .enabled(true)); + assertTrue(shareList.waitForExists(waitingTime)); + Screengrab.screenshot("OpenWith_Dialog"); + + /* Share View */ + UiObject shareBtn = device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/share") + .enabled(true)); + device.pressBack(); + TestHelper.menuButton.perform(click()); + assertTrue(shareBtn.waitForExists(waitingTime)); + shareBtn.click(); + TestHelper.shareAppList.waitForExists(waitingTime); + Screengrab.screenshot("Share_Dialog"); + + device.pressBack(); + } + + private void takeAddToHomeScreenScreenshot() throws UiObjectNotFoundException { + TestHelper.menuButton.perform(click()); + + TestHelper.AddtoHSmenuItem.waitForExists(waitingTime); + TestHelper.AddtoHSmenuItem.click(); + + TestHelper.AddtoHSCancelBtn.waitForExists(waitingTime); + Screengrab.screenshot("AddtoHSDialog"); + TestHelper.AddtoHSCancelBtn.click(); + } + + private void takeScreenshotOfTabsTrayAndErase() throws Exception { + final UiObject mozillaImage = device.findObject(new UiSelector() + .resourceId("download") + .enabled(true)); + + UiObject imageMenuTitle = device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/topPanel") + .enabled(true)); + UiObject openNewTabTitle = device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/design_menu_item_text") + .text(getString(R.string.mozac_feature_contextmenu_open_link_in_private_tab)) + .enabled(true)); + UiObject multiTabBtn = device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/tabs") + .enabled(true)); + UiObject eraseHistoryBtn = device.findObject(new UiSelector() + .text(getString(R.string.tabs_tray_action_erase)) + .enabled(true)); + + assertTrue(mozillaImage.waitForExists(waitingTime)); + mozillaImage.dragTo(mozillaImage, 7); + assertTrue(imageMenuTitle.waitForExists(waitingTime)); + assertTrue(imageMenuTitle.exists()); + Screengrab.screenshot("Image_Context_Menu"); + + //Open a new tab + openNewTabTitle.click(); + TestHelper.mDevice.wait(Until.findObject( + By.res(TestHelper.getAppName(), "snackbar_text")), 5000); + Screengrab.screenshot("New_Tab_Popup"); + TestHelper.mDevice.wait(Until.gone( + By.res(TestHelper.getAppName(), "snackbar_text")), 5000); + + assertTrue(multiTabBtn.waitForExists(waitingTime)); + multiTabBtn.click(); + assertTrue(eraseHistoryBtn.waitForExists(waitingTime)); + Screengrab.screenshot("Multi_Tab_Menu"); + + eraseHistoryBtn.click(); + + device.wait(Until.findObject( + By.res(TestHelper.getAppName(), "snackbar_text")), waitingTime); + + Screengrab.screenshot("YourBrowsingHistoryHasBeenErased"); + } + + private void takeScreenshotOfFindDialog() throws Exception { + UiObject findinpageMenuItem = device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/find_in_page") + .enabled(true)); + UiObject findinpageCloseBtn = device.findObject(new UiSelector() + .resourceId(TestHelper.getAppName() + ":id/close_find_in_page") + .enabled(true)); + + TestHelper.menuButton.perform(click()); + findinpageMenuItem.waitForExists(waitingTime); + findinpageMenuItem.click(); + + findinpageCloseBtn.waitForExists(waitingTime); + Screengrab.screenshot("Find_In_Page_Dialog"); + findinpageCloseBtn.click(); + } + + private void takeScreenshotofInsecureCon() throws Exception { + + TestHelper.securityInfoIcon.click(); + TestHelper.identityState.waitForExists(waitingTime); + Screengrab.screenshot("insecure_connection"); + device.pressBack(); + } + + // This test requires external internet connection + private void takeScreenshotofSecureCon() throws Exception { + + // take the security info of google.com for https connection + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .check(matches(hasFocus())) + .perform(click(), replaceText("www.google.com"), pressImeActionButton()); + TestHelper.waitForWebContent(); + TestHelper.progressBar.waitUntilGone(waitingTime); + TestHelper.securityInfoIcon.click(); + TestHelper.identityState.waitForExists(waitingTime); + Screengrab.screenshot("secure_connection"); + device.pressBack(); + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/ErrorPagesScreenshots.java b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/ErrorPagesScreenshots.java new file mode 100644 index 0000000000..aa95e0923e --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/ErrorPagesScreenshots.java @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.screenshots; + +import android.os.Build; + +import androidx.test.espresso.web.webdriver.Locator; +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.UiObject; +import androidx.test.uiautomator.UiSelector; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.focus.R; +import org.mozilla.focus.helpers.TestHelper; + +import tools.fastlane.screengrab.Screengrab; +import tools.fastlane.screengrab.locale.LocaleTestRule; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.pressImeActionButton; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.hasFocus; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.web.sugar.Web.onWebView; +import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement; +import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick; +import static androidx.test.espresso.web.webdriver.DriverAtoms.webScrollIntoView; +import static junit.framework.Assert.assertTrue; + +@Ignore("See: https://github.com/mozilla-mobile/mobile-test-eng/issues/305") +@RunWith(AndroidJUnit4.class) +public class ErrorPagesScreenshots extends ScreenshotTest { + + @ClassRule + public static final LocaleTestRule localeTestRule = new LocaleTestRule(); + + private enum ErrorTypes { + ERROR_UNKNOWN (-1), + ERROR_HOST_LOOKUP (-2), + ERROR_CONNECT (-6), + ERROR_TIMEOUT (-8), + ERROR_REDIRECT_LOOP (-9), + ERROR_UNSUPPORTED_SCHEME (-10), + ERROR_FAILED_SSL_HANDSHAKE (-11), + ERROR_BAD_URL (-12), + ERROR_TOO_MANY_REQUESTS (-15); + private int value; + + ErrorTypes(int value) { + this.value = value; + } + } + + @Test + public void takeScreenshotsOfErrorPages() { + for (ErrorTypes error: ErrorTypes.values()) { + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .check(matches(hasFocus())) + .perform(click(), replaceText("error:" + error.value), pressImeActionButton()); + + assertTrue(TestHelper.webView.waitForExists(waitingTime)); + assertTrue(TestHelper.progressBar.waitUntilGone(waitingTime)); + + // Android O has an issue with using Locator.ID + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + UiObject tryAgainBtn = device.findObject(new UiSelector() + .resourceId("errorTryAgain") + .clickable(true)); + assertTrue(tryAgainBtn.waitForExists(waitingTime)); + } else { + onWebView() + .withElement(findElement(Locator.ID, "errorTitle")) + .perform(webClick()); + + onWebView() + .withElement(findElement(Locator.ID, "errorTryAgain")) + .perform(webScrollIntoView()); + } + + Screengrab.screenshot(error.name()); + + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .perform(click()); + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/FirstRunScreenshots.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/FirstRunScreenshots.kt new file mode 100644 index 0000000000..cdd8fd0465 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/FirstRunScreenshots.kt @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@file:Suppress("DEPRECATION") + +package org.mozilla.focus.screenshots + +import android.os.SystemClock +import androidx.test.rule.ActivityTestRule +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.activity.MainActivity +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.locale.LocaleTestRule + +class FirstRunScreenshots : ScreenshotTest() { + @Rule + @JvmField + var mActivityTestRule: ActivityTestRule = + object : MainActivityFirstrunTestRule(true, true) { + } + + @Rule + @JvmField + val localeTestRule = LocaleTestRule() + + @Ignore + @Test + fun takeScreenshotsOfFirstrun() { + homeScreen { + verifyOnboardingFirstSlide() + device.waitForIdle() + SystemClock.sleep(5000) + Screengrab.screenshot("Onboarding_1_View") + + clickOnboardingNextButton() + + verifyOnboardingSecondSlide() + Screengrab.screenshot("Onboarding_2_View") + + clickOnboardingNextButton() + + verifyOnboardingThirdSlide() + Screengrab.screenshot("Onboarding_3_View") + + clickOnboardingNextButton() + + verifyOnboardingLastSlide() + Screengrab.screenshot("Onboarding_last_View") + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/HomeScreenScreenshots.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/HomeScreenScreenshots.kt new file mode 100644 index 0000000000..9f724f8b0c --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/HomeScreenScreenshots.kt @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@file:Suppress("DEPRECATION") + +package org.mozilla.focus.screenshots + +import android.os.SystemClock +import androidx.appcompat.app.AppCompatDelegate +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.rule.ActivityTestRule +import androidx.test.uiautomator.UiObject +import androidx.test.uiautomator.UiSelector +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.R +import org.mozilla.focus.activity.MainActivity +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.TestHelper +import org.mozilla.focus.helpers.TestHelper.waitForWebContent +import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.locale.LocaleTestRule +import java.util.Locale + +class HomeScreenScreenshots : ScreenshotTest() { + @Rule @JvmField + var mActivityTestRule: ActivityTestRule = + object : MainActivityFirstrunTestRule(true, false) { + } + + @Rule @JvmField + val localeTestRule = LocaleTestRule() + + @Before + fun setUp() { + mActivityTestRule.runOnUiThread { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) + } + } + + @Test + fun takeScreenshotsOfHomeScreen() { + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .check(matches(ViewMatchers.hasFocus())) + SystemClock.sleep(5000) + editURLBar.click() + typeInSearchBar("") + + Screengrab.screenshot("Home_View") + } + + @Test + fun takeScreenshotShortCutsHomeScreen() { + homeScreen { + addSiteToShortCuts("mozilla.com") + addSiteToShortCuts("pocket.com") + addSiteToShortCuts("relay.com") + addSiteToShortCuts("monitor.firefox.com") + TestHelper.mDevice.waitForIdle() + Espresso.closeSoftKeyboard() + SystemClock.sleep(5000) + Screengrab.screenshot("ShortCuts") + } + } + + @Test + fun openWebsiteFocus() { + var currentLocale: String = Locale.getDefault().getLanguage() + + editURLBar.click() + SystemClock.sleep(1000) + + typeInSearchBar("www.mozilla.org/" + currentLocale + "/firefox/browsers/mobile/focus/") + TestHelper.mDevice.waitForIdle() + TestHelper.mDevice.pressEnter() + waitForWebContent() + TestHelper.waitForWebSiteTitleLoad() + + SystemClock.sleep(30000) + waitForWebContent() + Screengrab.screenshot("FocusWebsite") + } + + private fun addSiteToShortCuts(website: String) { + editURLBar.click() + SystemClock.sleep(1000) + typeInSearchBar(website) + TestHelper.mDevice.pressEnter() + TestHelper.mDevice.waitForIdle() + menuButton.click() + TestHelper.mDevice.waitForIdle() + addToShortCuts() + TestHelper.mDevice.waitForIdle() + eraseButton.click() + TestHelper.mDevice.waitForIdle() + } + private val editURLBar: UiObject = + TestHelper.mDevice.findObject( + UiSelector().resourceId("${TestHelper.packageName}:id/mozac_browser_toolbar_edit_url_view"), + ) + + private fun typeInSearchBar(searchString: String) { + searchBar.clearTextField() + searchBar.setText(searchString) + } + + private val searchBar = + TestHelper.mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/mozac_browser_toolbar_edit_url_view")) + + private val menuButton = + TestHelper.mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/mozac_browser_toolbar_menu")) + + private fun addToShortCuts(): ViewInteraction? = onView(ViewMatchers.withText(R.string.menu_add_to_shortcuts)).perform( + ViewActions.click(), + ) + + private val eraseButton = + TestHelper.mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/erase")) +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/NotificationScreenshots.java b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/NotificationScreenshots.java new file mode 100644 index 0000000000..3a7e63738b --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/NotificationScreenshots.java @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.screenshots; + +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.UiObject; +import androidx.test.uiautomator.UiSelector; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.focus.R; +import org.mozilla.focus.helpers.TestHelper; + +import java.io.IOException; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import tools.fastlane.screengrab.Screengrab; +import tools.fastlane.screengrab.locale.LocaleTestRule; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.pressImeActionButton; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.hasFocus; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static junit.framework.Assert.assertTrue; +import static org.hamcrest.Matchers.containsString; + +/** + * This test has been super flaky in the past so we moved it to its own test. This way we wat least + * only lose screenshots of this test in case it fails. + */ +@Ignore("See: https://github.com/mozilla-mobile/mobile-test-eng/issues/305") +@RunWith(AndroidJUnit4.class) +public class NotificationScreenshots extends ScreenshotTest { + + @ClassRule + public static final LocaleTestRule localeTestRule = new LocaleTestRule(); + + private MockWebServer webServer; + + @Before + public void setUpWebServer() throws IOException { + webServer = new MockWebServer(); + + // Test page + webServer.enqueue(new MockResponse().setBody(TestHelper.readTestAsset("genericPage.html"))); + } + + @After + public void tearDownWebServer() { + try { + webServer.close(); + webServer.shutdown(); + } catch (IOException e) { + throw new AssertionError("Could not stop web server", e); + } + } + + @Test + public void takeScreenshotOfNotification() throws Exception { + onView(withId(R.id.mozac_browser_toolbar_edit_url_view)) + .check(matches(isDisplayed())) + .check(matches(hasFocus())) + .perform(click(), replaceText(webServer.url("/").toString()), pressImeActionButton()); + + onView(withId(R.id.mozac_browser_toolbar_url_view)) + .check(matches(isDisplayed())) + .check(matches(withText(containsString(webServer.getHostName())))); + + final UiObject openAction = device.findObject(new UiSelector() + .descriptionContains(getString(R.string.notification_action_open)) + .resourceId("android:id/action0") + .enabled(true)); + + device.openNotification(); + + try { + if (!openAction.waitForExists(waitingTime)) { + // The notification is not expanded. Let's expand it now. + device.findObject(new UiSelector() + .text(getString(R.string.app_name))) + .swipeDown(20); + + assertTrue(openAction.waitForExists(waitingTime)); + } + + Screengrab.screenshot("DeleteHistory_NotificationBar"); + } finally { + // Close notification tray again + device.pressBack(); + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/ScreenshotTest.java b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/ScreenshotTest.java new file mode 100644 index 0000000000..427d2f0661 --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/ScreenshotTest.java @@ -0,0 +1,94 @@ +package org.mozilla.focus.screenshots; + +import android.app.Instrumentation; +import android.content.Context; +import android.text.format.DateUtils; + +import androidx.annotation.StringRes; +import androidx.test.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; +import androidx.test.uiautomator.UiDevice; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.mozilla.focus.activity.MainActivity; +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule; +import org.mozilla.focus.idlingResources.SessionLoadedIdlingResource; + +import tools.fastlane.screengrab.Screengrab; +import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy; + +/** + * Base class for tests that take screenshots. + */ +@Ignore("This test was written specifically for WebView and needs to be adapted for GeckoView, see: https://github.com/mozilla-mobile/mobile-test-eng/issues/305") +public abstract class ScreenshotTest { + final long waitingTime = DateUtils.SECOND_IN_MILLIS * 10; + + private Context targetContext; + private SessionLoadedIdlingResource loadingIdlingResource; + + UiDevice device; + + @Rule + public ActivityTestRule mActivityTestRule = new MainActivityFirstrunTestRule(true, false, true,false) { + @Override + protected void beforeActivityLaunched() { + super.beforeActivityLaunched(); + } + }; + + @Rule + public TestRule screenshotOnFailureRule = new TestWatcher() { + @Override + protected void failed(Throwable e, Description description) { + // On error take a screenshot so that we can debug it easily + Screengrab.screenshot("FAILURE-" + getScreenshotName(description)); + } + + private String getScreenshotName(Description description) { + return description.getClassName().replace(".", "-") + + "_" + + description.getMethodName().replace(".", "-"); + } + }; + + @Before + public void setUpScreenshots() { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + targetContext = instrumentation.getTargetContext(); + device = UiDevice.getInstance(instrumentation); + + // Use this to switch between default strategy and HostScreencap strategy + Screengrab.setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy()); + //Screengrab.setDefaultScreenshotStrategy(new HostScreencapScreenshotStrategy(device)); + + device.waitForIdle(); + } + + /* Disable idlingResources. This causes error when accessing Settings Dialog */ + /* + @Before + public void setUpIdlingResources() { + loadingIdlingResource = new SessionLoadedIdlingResource(); + IdlingRegistry.getInstance().register(loadingIdlingResource); + } + + @After + public void tearDownIdlingResources() { + device.waitForIdle(); + IdlingRegistry.getInstance().unregister(loadingIdlingResource); + } + */ + String getString(@StringRes int resourceId) { + return targetContext.getString(resourceId).trim(); + } + + String getString(@StringRes int resourceId, Object... formatArgs) { + return targetContext.getString(resourceId, formatArgs).trim(); + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/SettingsScreenshots.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/SettingsScreenshots.kt new file mode 100644 index 0000000000..cf051b0f4c --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/screenshots/SettingsScreenshots.kt @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@file:Suppress("DEPRECATION") + +package org.mozilla.focus.screenshots + +import android.os.SystemClock +import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES +import androidx.test.rule.ActivityTestRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.focus.activity.MainActivity +import org.mozilla.focus.activity.robots.homeScreen +import org.mozilla.focus.activity.robots.searchScreen +import org.mozilla.focus.helpers.MainActivityFirstrunTestRule +import org.mozilla.focus.helpers.TestHelper +import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.locale.LocaleTestRule + +class SettingsScreenshots : ScreenshotTest() { + @Rule + @JvmField + var mActivityTestRule: ActivityTestRule = + object : MainActivityFirstrunTestRule(true, false) { + } + + @Before + fun setUp() { + mActivityTestRule.runOnUiThread { + AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES) + } + } + + @Rule + @JvmField + val localeTestRule = LocaleTestRule() + + @Test + fun takeScreenshotOfSiteSettings() { + val pageUrl = "https://www.mozilla.org" + + searchScreen { + }.loadPage(pageUrl) { + verifySiteTrackingProtectionIconShown() + SystemClock.sleep(5000) + TestHelper.waitForWebSiteTitleLoad() + TestHelper.waitForWebContent() + SystemClock.sleep(3000) + }.openSiteSettingsMenu { + SystemClock.sleep(5000) + Screengrab.screenshot("SiteSettingsSubMenu_View") + } + } + + @Test + fun takeScreenshotOfSearchSettings() { + homeScreen { + }.openMainMenu { + }.openSettings { + verifySettingsMenuItems() + }.openSearchSettingsMenu { + verifySearchSettingsItems() + openSearchEngineSubMenu() + SystemClock.sleep(5000) + Screengrab.screenshot("SearchEngineSubMenu_View") + } + } +} diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/testAnnotations/SmokeTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/testAnnotations/SmokeTest.kt new file mode 100644 index 0000000000..7894012bea --- /dev/null +++ b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/testAnnotations/SmokeTest.kt @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.testAnnotations + +/** + * A custom annotation to mark the smoke tests corresponding to the ones in TestRail: + * https://testrail.stage.mozaws.net/index.php?/suites/view/1028 + */ +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class SmokeTest diff --git a/mobile/android/focus-android/app/src/beta/res/drawable-v24/ic_splash_screen.xml b/mobile/android/focus-android/app/src/beta/res/drawable-v24/ic_splash_screen.xml new file mode 100644 index 0000000000..dac3eb8620 --- /dev/null +++ b/mobile/android/focus-android/app/src/beta/res/drawable-v24/ic_splash_screen.xml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/beta/res/drawable/ic_splash_screen.png b/mobile/android/focus-android/app/src/beta/res/drawable/ic_splash_screen.png new file mode 100644 index 0000000000..d19e90b7d9 Binary files /dev/null and b/mobile/android/focus-android/app/src/beta/res/drawable/ic_splash_screen.png differ diff --git a/mobile/android/focus-android/app/src/beta/res/values-night/colors.xml b/mobile/android/focus-android/app/src/beta/res/values-night/colors.xml new file mode 100644 index 0000000000..38c1ed8f3e --- /dev/null +++ b/mobile/android/focus-android/app/src/beta/res/values-night/colors.xml @@ -0,0 +1,7 @@ + + + + #f0f0f4 + diff --git a/mobile/android/focus-android/app/src/debug/AndroidManifest.xml b/mobile/android/focus-android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..a170dd5bbb --- /dev/null +++ b/mobile/android/focus-android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/DebugFocusApplication.kt b/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/DebugFocusApplication.kt new file mode 100644 index 0000000000..c8b42487df --- /dev/null +++ b/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/DebugFocusApplication.kt @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus + +import androidx.preference.PreferenceManager +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import leakcanary.AppWatcher +import leakcanary.LeakCanary +import org.mozilla.focus.ext.application + +class DebugFocusApplication : FocusApplication() { + + @OptIn(DelicateCoroutinesApi::class) + override fun setupLeakCanary() { + if (!AppWatcher.isInstalled) { + AppWatcher.manualInstall( + application = application, + watchersToInstall = AppWatcher.appDefaultWatchers(application), + ) + } + GlobalScope.launch(Dispatchers.IO) { + val isEnabled = PreferenceManager.getDefaultSharedPreferences(applicationContext) + .getBoolean(getString(R.string.pref_key_leakcanary), true) + updateLeakCanaryState(isEnabled) + } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun updateLeakCanaryState(isEnabled: Boolean) { + GlobalScope.launch(Dispatchers.IO) { + LeakCanary.showLeakDisplayActivityLauncherIcon(isEnabled) + LeakCanary.config = LeakCanary.config.copy(dumpHeap = isEnabled) + } + } +} diff --git a/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/utils/AdjustHelper.java b/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/utils/AdjustHelper.java new file mode 100644 index 0000000000..b0aa2e1835 --- /dev/null +++ b/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/utils/AdjustHelper.java @@ -0,0 +1,14 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.utils; + +import android.content.Context; + +public class AdjustHelper { + public static void setupAdjustIfNeeded(Context context) { + // DEBUG: No Adjust - This class has different implementations for all build types. + } +} diff --git a/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/web/Config.kt b/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/web/Config.kt new file mode 100644 index 0000000000..33ddf545d7 --- /dev/null +++ b/mobile/android/focus-android/app/src/debug/java/org/mozilla/focus/web/Config.kt @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.web + +object Config { + const val EXPERIMENT_DESCRIPTOR_GECKOVIEW_ENGINE = "use-gecko" + const val EXPERIMENT_DESCRIPTOR_HOME_SCREEN_TIPS = "use-homescreen-tips" +} diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/focus-android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..a8b7928d2e --- /dev/null +++ b/mobile/android/focus-android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/mobile/android/focus-android/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..3a67ba31af Binary files /dev/null and b/mobile/android/focus-android/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile/android/focus-android/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..eda227ffe8 Binary files /dev/null and b/mobile/android/focus-android/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..ef9c4579f1 Binary files /dev/null and b/mobile/android/focus-android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile/android/focus-android/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..20f689eb47 Binary files /dev/null and b/mobile/android/focus-android/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..caca7f639e Binary files /dev/null and b/mobile/android/focus-android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile/android/focus-android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..0962261d8d Binary files /dev/null and b/mobile/android/focus-android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..9e430cd544 Binary files /dev/null and b/mobile/android/focus-android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile/android/focus-android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..4a562102cf Binary files /dev/null and b/mobile/android/focus-android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/ic_launcher-playstore.png b/mobile/android/focus-android/app/src/focusBeta/ic_launcher-playstore.png new file mode 100644 index 0000000000..0e543571d0 Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/ic_launcher-playstore.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/java/org/mozilla/focus/utils/AdjustHelper.java b/mobile/android/focus-android/app/src/focusBeta/java/org/mozilla/focus/utils/AdjustHelper.java new file mode 100644 index 0000000000..b0aa2e1835 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/java/org/mozilla/focus/utils/AdjustHelper.java @@ -0,0 +1,14 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.utils; + +import android.content.Context; + +public class AdjustHelper { + public static void setupAdjustIfNeeded(Context context) { + // DEBUG: No Adjust - This class has different implementations for all build types. + } +} diff --git a/mobile/android/focus-android/app/src/focusBeta/res/drawable-land/dark_background.xml b/mobile/android/focus-android/app/src/focusBeta/res/drawable-land/dark_background.xml new file mode 100644 index 0000000000..b225a9d47a --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/drawable-land/dark_background.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusBeta/res/drawable-v24/ic_launcher_foreground.xml b/mobile/android/focus-android/app/src/focusBeta/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..ab0b4c11c5 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusBeta/res/drawable-v24/icon_foreground.xml b/mobile/android/focus-android/app/src/focusBeta/res/drawable-v24/icon_foreground.xml new file mode 100644 index 0000000000..4be0323b4c --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/drawable-v24/icon_foreground.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusBeta/res/drawable/dark_background.xml b/mobile/android/focus-android/app/src/focusBeta/res/drawable/dark_background.xml new file mode 100644 index 0000000000..0ee46fa4e7 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/drawable/dark_background.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusBeta/res/drawable/ic_launcher_background.xml b/mobile/android/focus-android/app/src/focusBeta/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..61f5b8183f --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/drawable/ic_launcher_background.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusBeta/res/drawable/onboarding_logo.xml b/mobile/android/focus-android/app/src/focusBeta/res/drawable/onboarding_logo.xml new file mode 100644 index 0000000000..0c7fbd412e --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/drawable/onboarding_logo.xml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusBeta/res/drawable/wordmark2.xml b/mobile/android/focus-android/app/src/focusBeta/res/drawable/wordmark2.xml new file mode 100644 index 0000000000..16f39c2eea --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/drawable/wordmark2.xml @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..c7743a9582 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..c7743a9582 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-hdpi/ic_launcher.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..899dfb375a Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-hdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..8f36b391ce Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-mdpi/ic_launcher.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..f56d854a80 Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-mdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..60225a687f Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..f68b32b974 Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..9a02a5aad2 Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..8cb67765b9 Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..221f27c4d2 Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..86d19f8622 Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..481a4155c4 Binary files /dev/null and b/mobile/android/focus-android/app/src/focusBeta/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/focusBeta/res/values/app.xml b/mobile/android/focus-android/app/src/focusBeta/res/values/app.xml new file mode 100644 index 0000000000..f57980c9ad --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/values/app.xml @@ -0,0 +1,7 @@ + + + + Firefox Focus Beta + diff --git a/mobile/android/focus-android/app/src/focusBeta/res/xml-v25/shortcuts.xml b/mobile/android/focus-android/app/src/focusBeta/res/xml-v25/shortcuts.xml new file mode 100644 index 0000000000..daaece8883 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusBeta/res/xml-v25/shortcuts.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusDebug/res/xml-v25/shortcuts.xml b/mobile/android/focus-android/app/src/focusDebug/res/xml-v25/shortcuts.xml new file mode 100644 index 0000000000..d9c6f9e185 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusDebug/res/xml-v25/shortcuts.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusNightly/res/values/app.xml b/mobile/android/focus-android/app/src/focusNightly/res/values/app.xml new file mode 100644 index 0000000000..ed5943efac --- /dev/null +++ b/mobile/android/focus-android/app/src/focusNightly/res/values/app.xml @@ -0,0 +1,8 @@ + + + + + Firefox Focus Nightly + diff --git a/mobile/android/focus-android/app/src/focusNightly/res/xml-v25/shortcuts.xml b/mobile/android/focus-android/app/src/focusNightly/res/xml-v25/shortcuts.xml new file mode 100644 index 0000000000..c5524dfc01 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusNightly/res/xml-v25/shortcuts.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusRelease/AndroidManifest.xml b/mobile/android/focus-android/app/src/focusRelease/AndroidManifest.xml new file mode 100644 index 0000000000..39155c35b5 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusRelease/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/focusRelease/java/org/mozilla/focus/utils/AdjustHelper.java b/mobile/android/focus-android/app/src/focusRelease/java/org/mozilla/focus/utils/AdjustHelper.java new file mode 100644 index 0000000000..8181faf397 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusRelease/java/org/mozilla/focus/utils/AdjustHelper.java @@ -0,0 +1,72 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.utils; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import android.text.TextUtils; + +import com.adjust.sdk.Adjust; +import com.adjust.sdk.AdjustConfig; +import com.adjust.sdk.LogLevel; + +import org.mozilla.focus.BuildConfig; +import org.mozilla.focus.FocusApplication; +import org.mozilla.focus.telemetry.GleanMetricsService; + +public class AdjustHelper { + public static void setupAdjustIfNeeded(FocusApplication application) { + // RELEASE: Enable Adjust - This class has different implementations for all build types. + + //noinspection ConstantConditions + if (TextUtils.isEmpty(BuildConfig.ADJUST_TOKEN)) { + throw new IllegalStateException("No adjust token defined for release build"); + } + + if (!GleanMetricsService.isTelemetryEnabled(application)) { + return; + } + + final AdjustConfig config = new AdjustConfig(application, + BuildConfig.ADJUST_TOKEN, + AdjustConfig.ENVIRONMENT_PRODUCTION, + true); + + config.setLogLevel(LogLevel.SUPRESS); + + Adjust.onCreate(config); + + application.registerActivityLifecycleCallbacks(new AdjustLifecycleCallbacks()); + } + + private static final class AdjustLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { + @Override + public void onActivityResumed(Activity activity) { + Adjust.onResume(); + } + + @Override + public void onActivityPaused(Activity activity) { + Adjust.onPause(); + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} + + @Override + public void onActivityStarted(Activity activity) {} + + @Override + public void onActivityStopped(Activity activity) {} + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + + @Override + public void onActivityDestroyed(Activity activity) {} + } +} diff --git a/mobile/android/focus-android/app/src/focusRelease/java/org/mozilla/focus/web/Config.kt b/mobile/android/focus-android/app/src/focusRelease/java/org/mozilla/focus/web/Config.kt new file mode 100644 index 0000000000..33ddf545d7 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusRelease/java/org/mozilla/focus/web/Config.kt @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.web + +object Config { + const val EXPERIMENT_DESCRIPTOR_GECKOVIEW_ENGINE = "use-gecko" + const val EXPERIMENT_DESCRIPTOR_HOME_SCREEN_TIPS = "use-homescreen-tips" +} diff --git a/mobile/android/focus-android/app/src/focusRelease/res/xml-v25/shortcuts.xml b/mobile/android/focus-android/app/src/focusRelease/res/xml-v25/shortcuts.xml new file mode 100644 index 0000000000..c1f98fe8d6 --- /dev/null +++ b/mobile/android/focus-android/app/src/focusRelease/res/xml-v25/shortcuts.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/klar/res/drawable/background_gradient_dark.xml b/mobile/android/focus-android/app/src/klar/res/drawable/background_gradient_dark.xml new file mode 100644 index 0000000000..79a9e23abf --- /dev/null +++ b/mobile/android/focus-android/app/src/klar/res/drawable/background_gradient_dark.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/mobile/android/focus-android/app/src/klar/res/drawable/wordmark2.xml b/mobile/android/focus-android/app/src/klar/res/drawable/wordmark2.xml new file mode 100644 index 0000000000..89c3112480 --- /dev/null +++ b/mobile/android/focus-android/app/src/klar/res/drawable/wordmark2.xml @@ -0,0 +1,357 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/klar/res/values/app.xml b/mobile/android/focus-android/app/src/klar/res/values/app.xml new file mode 100644 index 0000000000..7811e21146 --- /dev/null +++ b/mobile/android/focus-android/app/src/klar/res/values/app.xml @@ -0,0 +1,11 @@ + + + + + Firefox Klar + + + Klar + diff --git a/mobile/android/focus-android/app/src/klarBeta/java/org/mozilla/focus/utils/AdjustHelper.java b/mobile/android/focus-android/app/src/klarBeta/java/org/mozilla/focus/utils/AdjustHelper.java new file mode 100644 index 0000000000..b0aa2e1835 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarBeta/java/org/mozilla/focus/utils/AdjustHelper.java @@ -0,0 +1,14 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.utils; + +import android.content.Context; + +public class AdjustHelper { + public static void setupAdjustIfNeeded(Context context) { + // DEBUG: No Adjust - This class has different implementations for all build types. + } +} diff --git a/mobile/android/focus-android/app/src/klarBeta/res/drawable/onboarding_logo.xml b/mobile/android/focus-android/app/src/klarBeta/res/drawable/onboarding_logo.xml new file mode 100644 index 0000000000..0c7fbd412e --- /dev/null +++ b/mobile/android/focus-android/app/src/klarBeta/res/drawable/onboarding_logo.xml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/klarBeta/res/xml-v25/shortcuts.xml b/mobile/android/focus-android/app/src/klarBeta/res/xml-v25/shortcuts.xml new file mode 100644 index 0000000000..1024f92407 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarBeta/res/xml-v25/shortcuts.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/klarDebug/res/xml-v25/shortcuts.xml b/mobile/android/focus-android/app/src/klarDebug/res/xml-v25/shortcuts.xml new file mode 100644 index 0000000000..b18e391208 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarDebug/res/xml-v25/shortcuts.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/klarNightly/drawable-v24/icon_foreground.xml b/mobile/android/focus-android/app/src/klarNightly/drawable-v24/icon_foreground.xml new file mode 100644 index 0000000000..4be0323b4c --- /dev/null +++ b/mobile/android/focus-android/app/src/klarNightly/drawable-v24/icon_foreground.xml @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/klarNightly/drawable/background_gradient_dark.xml b/mobile/android/focus-android/app/src/klarNightly/drawable/background_gradient_dark.xml new file mode 100644 index 0000000000..79a9e23abf --- /dev/null +++ b/mobile/android/focus-android/app/src/klarNightly/drawable/background_gradient_dark.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/mobile/android/focus-android/app/src/klarNightly/drawable/icon_background.xml b/mobile/android/focus-android/app/src/klarNightly/drawable/icon_background.xml new file mode 100644 index 0000000000..d2c843b554 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarNightly/drawable/icon_background.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/mobile/android/focus-android/app/src/klarNightly/drawable/toolbar_url_background.xml b/mobile/android/focus-android/app/src/klarNightly/drawable/toolbar_url_background.xml new file mode 100644 index 0000000000..ee5d803fd4 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarNightly/drawable/toolbar_url_background.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-anydpi-v26/ic_launcher.xml b/mobile/android/focus-android/app/src/klarNightly/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..39b175e682 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarNightly/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/android/focus-android/app/src/klarNightly/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..1b3296f41d --- /dev/null +++ b/mobile/android/focus-android/app/src/klarNightly/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-hdpi/ic_launcher.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..57266ce663 Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-hdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..200d210606 Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-hdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-mdpi/ic_launcher.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..6035524f91 Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-mdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..92290c2ddd Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-mdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-xhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..e62f9490c2 Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-xhdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..c8a9087966 Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-xxhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..efbf73d970 Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-xxhdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..a241232589 Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-xxxhdpi/ic_launcher.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..40f1e0d060 Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/android/focus-android/app/src/klarNightly/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..00c87c538d Binary files /dev/null and b/mobile/android/focus-android/app/src/klarNightly/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/mobile/android/focus-android/app/src/klarNightly/res/xml-v25/shortcuts.xml b/mobile/android/focus-android/app/src/klarNightly/res/xml-v25/shortcuts.xml new file mode 100644 index 0000000000..c35341d8c1 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarNightly/res/xml-v25/shortcuts.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/klarRelease/java/org/mozilla/focus/utils/AdjustHelper.java b/mobile/android/focus-android/app/src/klarRelease/java/org/mozilla/focus/utils/AdjustHelper.java new file mode 100644 index 0000000000..b0aa2e1835 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarRelease/java/org/mozilla/focus/utils/AdjustHelper.java @@ -0,0 +1,14 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.utils; + +import android.content.Context; + +public class AdjustHelper { + public static void setupAdjustIfNeeded(Context context) { + // DEBUG: No Adjust - This class has different implementations for all build types. + } +} diff --git a/mobile/android/focus-android/app/src/klarRelease/java/org/mozilla/focus/web/Config.kt b/mobile/android/focus-android/app/src/klarRelease/java/org/mozilla/focus/web/Config.kt new file mode 100644 index 0000000000..33ddf545d7 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarRelease/java/org/mozilla/focus/web/Config.kt @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.web + +object Config { + const val EXPERIMENT_DESCRIPTOR_GECKOVIEW_ENGINE = "use-gecko" + const val EXPERIMENT_DESCRIPTOR_HOME_SCREEN_TIPS = "use-homescreen-tips" +} diff --git a/mobile/android/focus-android/app/src/klarRelease/res/xml-v25/shortcuts.xml b/mobile/android/focus-android/app/src/klarRelease/res/xml-v25/shortcuts.xml new file mode 100644 index 0000000000..c35341d8c1 --- /dev/null +++ b/mobile/android/focus-android/app/src/klarRelease/res/xml-v25/shortcuts.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/main/AndroidManifest.xml b/mobile/android/focus-android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..72926930ae --- /dev/null +++ b/mobile/android/focus-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/focus-android/app/src/main/assets/error_style.css b/mobile/android/focus-android/app/src/main/assets/error_style.css new file mode 100644 index 0000000000..82823afb5e --- /dev/null +++ b/mobile/android/focus-android/app/src/main/assets/error_style.css @@ -0,0 +1,172 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Below styling is mirroring the Android Components styling from +https://github.com/mozilla-mobile/firefox-android/blob/main/android-components/components/browser/errorpages/src/main/assets/error_style.css */ +html, +body { + margin: 0; + padding: 0; + height: 100%; + --moz-vertical-spacing: 10px; + --moz-background-height: 32px; +} + +body { + background-size: 64px var(--moz-background-height); + /* background-size: 64px 32px; */ + background-repeat: repeat-x; + + background-color: #363B40; + color: #FFFFFF; + padding: 0 20px; + + font-weight: 300; + font-size: 13px; + -moz-text-size-adjust: none; + font-family: sans-serif; +} + +ul { + /* Shove the list indicator so that its left aligned, but use outside so that text + * doesn't don't wrap the text around it */ + padding: 0 1em; + margin: 0; + list-style: round outside none; +} + +#errorShortDesc, +li:not(:last-of-type) { + /* Margins between the li and buttons below it won't be collapsed. Remove the bottom margin here. */ + margin: var(--moz-vertical-spacing) 0; +} + +h1 { + margin: 0; + /* Since this has an underline, use padding for vertical spacing rather than margin */ + padding: var(--moz-vertical-spacing) 0; + font-weight: 300; + border-bottom: 1px solid #e0e2e5; +} + +h2 { + font-size: small; + padding: 0; + margin: var(--moz-vertical-spacing) 0; +} + +p { + margin: var(--moz-vertical-spacing) 0; +} + +button { + /* Force buttons to display: block here to try and enfoce collapsing margins */ + display: block; + width: 100%; + border: none; + padding: 1rem; + font-family: sans-serif; + background-color: #00A4DC; + color: #FFFFFF; + font-weight: 300; + border-radius: 2px; + background-image: none; + margin: var(--moz-vertical-spacing) 0 0; +} + +.buttonSecondary{ + /* Force buttons to display: block here to try and enforce collapsing margins */ + display: block; + width: 100%; + border: none; + padding: 1rem; + font-family: sans-serif; + background-color: rgba(249, 249, 250, 0.1); + color: #FFFFFF; + font-weight: 300; + border-radius: 2px; + background-image: none; + margin: var(--moz-vertical-spacing) 0 0; +} + +#errorPageContainer { + /* If the page is greater than 550px center the content. + * This number should be kept in sync with the media query for tablets below */ + max-width: 550px; + margin: 0 auto; + transform: translateY(var(--moz-background-height)); + padding-bottom: var(--moz-vertical-spacing); + + min-height: calc(100% - var(--moz-background-height) - var(--moz-vertical-spacing)); + display: flex; + flex-direction: column; +} + +/* On large screen devices (hopefully a 7+ inch tablet, we already center content (see #errorPageContainer above). + Apply tablet specific styles here */ +@media (min-width: 550px) { + button { + min-width: 160px; + width: auto; + } + + /* If the tablet is tall as well, add some padding to make content feel a bit more centered */ + @media (min-height: 550px) { + #errorPageContainer { + padding-top: 64px; + min-height: calc(100% - 64px); + } + } +} + +.advancedPanelButtonContainer { + background-color: rgba(128, 128, 147, 0.1); + display: flex; + justify-content: center; + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 0.5em; +} + +#advancedPanelBackButtonContainer { + padding-bottom: 0; +} + +#advancedPanelContainer { + width: 100%; + left: 0; +} + +.advanced-panel { + display: none; + background-color: #202023; + border: 1px solid rgba(249, 249, 250, 0.2); + margin: 48px auto; + min-width: 13em; + max-width: 52em; +} + +.button-container { + display: flex; + flex-flow: row; +} + +#badCertTechnicalInfo { + margin: 0em 1em 1em; + overflow: auto; + white-space: pre-line; +} + +#advancedButton { + display: none; +} + +#badCertAdvancedPanel { + display: none; +} + +/* Below styling is Focus specific */ +a { + color: white; +} diff --git a/mobile/android/focus-android/app/src/main/assets/style.css b/mobile/android/focus-android/app/src/main/assets/style.css new file mode 100644 index 0000000000..7dd085df52 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/assets/style.css @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +body, html { + background: #221F1F; + color: #FFFFFF; + font-family: sans-serif; + line-height: 24px; + font-size: 14px; +} + +body{ + padding-left: 24px; + padding-right: 24px; + margin-left: 0px; + margin-right: 0px; +} + +a { + color: #0A9AF4; +} + +/* Make only about page links ("learn more") white */ +.about a { + color: #FFFFFF; +} + +p.subtitle { + text-align: center; + opacity: .7; + margin: 0; +} + +img#wordmark { + /* We need to set the dp size here, because by default webview assumes the image is not + density specific (but since it's an android resource, we get a density specific version). */ + width: 180px; + display: block; + margin-left: auto; + margin-right: auto; + padding-top: 24px; +} diff --git a/mobile/android/focus-android/app/src/main/ic_launcher-playstore.png b/mobile/android/focus-android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000..7de2611aff Binary files /dev/null and b/mobile/android/focus-android/app/src/main/ic_launcher-playstore.png differ diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/Components.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/Components.kt new file mode 100644 index 0000000000..478a5a75fa --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/Components.kt @@ -0,0 +1,321 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.core.app.NotificationManagerCompat +import mozilla.components.browser.engine.gecko.cookiebanners.GeckoCookieBannersStorage +import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.state.engine.EngineMiddleware +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.DefaultSettings +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.fetch.Client +import mozilla.components.feature.app.links.AppLinksInterceptor +import mozilla.components.feature.app.links.AppLinksUseCases +import mozilla.components.feature.contextmenu.ContextMenuUseCases +import mozilla.components.feature.customtabs.store.CustomTabsServiceStore +import mozilla.components.feature.downloads.DownloadMiddleware +import mozilla.components.feature.downloads.DownloadsUseCases +import mozilla.components.feature.media.MediaSessionFeature +import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware +import mozilla.components.feature.prompts.PromptMiddleware +import mozilla.components.feature.prompts.file.FileUploadsDirCleaner +import mozilla.components.feature.prompts.file.FileUploadsDirCleanerMiddleware +import mozilla.components.feature.search.SearchUseCases +import mozilla.components.feature.search.middleware.AdsTelemetryMiddleware +import mozilla.components.feature.search.middleware.SearchMiddleware +import mozilla.components.feature.search.region.RegionMiddleware +import mozilla.components.feature.search.telemetry.ads.AdsTelemetry +import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry +import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.session.SettingsUseCases +import mozilla.components.feature.session.TrackingProtectionUseCases +import mozilla.components.feature.tabs.CustomTabsUseCases +import mozilla.components.feature.tabs.TabsUseCases +import mozilla.components.feature.top.sites.PinnedSiteStorage +import mozilla.components.feature.top.sites.TopSitesUseCases +import mozilla.components.feature.webcompat.WebCompatFeature +import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.crash.sentry.SentryService +import mozilla.components.lib.crash.service.CrashReporterService +import mozilla.components.lib.crash.service.GleanCrashReporterService +import mozilla.components.lib.crash.service.MozillaSocorroService +import mozilla.components.lib.publicsuffixlist.PublicSuffixList +import mozilla.components.service.location.LocationService +import mozilla.components.service.location.MozillaLocationService +import mozilla.components.service.nimbus.NimbusApi +import mozilla.components.support.base.android.NotificationsDelegate +import mozilla.components.support.locale.LocaleManager +import org.mozilla.focus.activity.MainActivity +import org.mozilla.focus.browser.BlockedTrackersMiddleware +import org.mozilla.focus.cfr.CfrMiddleware +import org.mozilla.focus.components.EngineProvider +import org.mozilla.focus.downloads.DownloadService +import org.mozilla.focus.engine.AppContentInterceptor +import org.mozilla.focus.engine.ClientWrapper +import org.mozilla.focus.engine.SanityCheckMiddleware +import org.mozilla.focus.experiments.createNimbus +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.settings +import org.mozilla.focus.media.MediaSessionService +import org.mozilla.focus.search.SearchFilterMiddleware +import org.mozilla.focus.search.SearchMigration +import org.mozilla.focus.state.AppState +import org.mozilla.focus.state.AppStore +import org.mozilla.focus.state.Screen +import org.mozilla.focus.telemetry.GleanMetricsService +import org.mozilla.focus.telemetry.TelemetryMiddleware +import org.mozilla.focus.telemetry.startuptelemetry.AppStartReasonProvider +import org.mozilla.focus.telemetry.startuptelemetry.StartupActivityLog +import org.mozilla.focus.telemetry.startuptelemetry.StartupStateProvider +import org.mozilla.focus.topsites.DefaultTopSitesStorage +import org.mozilla.focus.utils.Settings +import java.util.Locale + +/** + * Helper object for lazily initializing components. + */ +class Components( + context: Context, + private val engineOverride: Engine? = null, + private val clientOverride: Client? = null, +) { + val appStore: AppStore by lazy { + AppStore( + AppState( + screen = if (context.settings.isFirstRun) Screen.FirstRun else Screen.Home, + topSites = emptyList(), + ), + ) + } + + private val notificationManagerCompat = NotificationManagerCompat.from(context) + + val notificationsDelegate: NotificationsDelegate by lazy { + NotificationsDelegate( + notificationManagerCompat, + ) + } + + val appStartReasonProvider by lazy { AppStartReasonProvider() } + + val startupActivityLog by lazy { StartupActivityLog() } + + val startupStateProvider by lazy { StartupStateProvider(startupActivityLog, appStartReasonProvider) } + + val settings by lazy { Settings(context) } + + val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy { + FileUploadsDirCleaner { context.cacheDir } + } + + val engineDefaultSettings by lazy { + DefaultSettings( + requestInterceptor = AppContentInterceptor(context), + trackingProtectionPolicy = settings.createTrackingProtectionPolicy(), + javascriptEnabled = !settings.shouldBlockJavaScript(), + remoteDebuggingEnabled = settings.shouldEnableRemoteDebugging(), + webFontsEnabled = !settings.shouldBlockWebFonts(), + httpsOnlyMode = settings.getHttpsOnlyMode(), + preferredColorScheme = settings.getPreferredColorScheme(), + cookieBannerHandlingModePrivateBrowsing = settings.getCurrentCookieBannerOptionFromSharePref().mode, + ) + } + + val engine: Engine by lazy { + engineOverride ?: EngineProvider.createEngine(context, engineDefaultSettings).apply { + this@Components.settings.setupSafeBrowsing(this) + WebCompatFeature.install(this) + WebCompatReporterFeature.install(this, "focus-geckoview") + } + } + + val client: ClientWrapper by lazy { + ClientWrapper(clientOverride ?: EngineProvider.createClient(context)) + } + + val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) } + + val settingsUseCases by lazy { SettingsUseCases(engine, store) } + + @Suppress("DEPRECATION") + private val locationService: LocationService by lazy { + if (BuildConfig.MLS_TOKEN.isEmpty()) { + LocationService.default() + } else { + MozillaLocationService(context, client.unwrap(), BuildConfig.MLS_TOKEN) + } + } + + val store by lazy { + BrowserStore( + middleware = listOf( + TelemetryMiddleware(), + DownloadMiddleware(context, DownloadService::class.java), + SanityCheckMiddleware(), + // We are currently using the default location service. We should consider using + // an actual implementation: + // https://github.com/mozilla-mobile/focus-android/issues/4781 + RegionMiddleware(context, locationService), + SearchMiddleware(context, migration = SearchMigration(context)), + SearchFilterMiddleware(), + PromptMiddleware(), + AdsTelemetryMiddleware(adsTelemetry), + BlockedTrackersMiddleware(context), + RecordingDevicesMiddleware(context, notificationsDelegate), + CfrMiddleware(context), + FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner), + ) + EngineMiddleware.create( + engine, + // We are disabling automatic suspending of engine sessions under memory pressure. + // Instead we solely rely on GeckoView and the Android system to reclaim memory + // when needed. For details, see: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1752594 + // https://github.com/mozilla-mobile/fenix/issues/12731 + // https://github.com/mozilla-mobile/android-components/issues/11300 + // https://github.com/mozilla-mobile/android-components/issues/11653 + trimMemoryAutomatically = false, + ), + ).apply { + MediaSessionFeature(context, MediaSessionService::class.java, this).start() + } + } + + /** + * The [CustomTabsServiceStore] holds global custom tabs related data. + */ + val customTabsStore by lazy { CustomTabsServiceStore() } + + val sessionUseCases: SessionUseCases by lazy { SessionUseCases(store) } + + val tabsUseCases: TabsUseCases by lazy { TabsUseCases(store) } + + val cookieBannerStorage: GeckoCookieBannersStorage by lazy { EngineProvider.createCookieBannerStorage(context) } + + val publicSuffixList by lazy { PublicSuffixList(context) } + + val searchUseCases: SearchUseCases by lazy { + SearchUseCases(store, tabsUseCases, sessionUseCases) + } + + val contextMenuUseCases: ContextMenuUseCases by lazy { ContextMenuUseCases(store) } + + val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) } + + val appLinksUseCases: AppLinksUseCases by lazy { AppLinksUseCases(context.applicationContext) } + + val customTabsUseCases: CustomTabsUseCases by lazy { CustomTabsUseCases(store, sessionUseCases.loadUrl) } + + val crashReporter: CrashReporter by lazy { createCrashReporter(context, notificationsDelegate) } + + val metrics: GleanMetricsService by lazy { GleanMetricsService(context) } + + val experiments: NimbusApi by lazy { + createNimbus(context, BuildConfig.NIMBUS_ENDPOINT) + } + + val adsTelemetry: AdsTelemetry by lazy { AdsTelemetry() } + + val searchTelemetry: InContentTelemetry by lazy { InContentTelemetry() } + + val icons by lazy { BrowserIcons(context, client) } + + val topSitesStorage by lazy { DefaultTopSitesStorage(PinnedSiteStorage(context)) } + + val topSitesUseCases: TopSitesUseCases by lazy { TopSitesUseCases(topSitesStorage) } + + val appLinksInterceptor by lazy { + AppLinksInterceptor( + context, + interceptLinkClicks = true, + launchInApp = { + context.settings.openLinksInExternalApp + }, + ) + } +} + +private fun createCrashReporter(context: Context, notificationsDelegate: NotificationsDelegate): CrashReporter { + val services = mutableListOf() + + if (BuildConfig.SENTRY_TOKEN.isNotEmpty()) { + val sentryService = SentryService( + context, + BuildConfig.SENTRY_TOKEN, + tags = mapOf( + "build_flavor" to BuildConfig.FLAVOR, + "build_type" to BuildConfig.BUILD_TYPE, + "locale_lang_tag" to getLocaleTag(context), + ), + environment = BuildConfig.BUILD_TYPE, + sendEventForNativeCrashes = false, // Do not send native crashes to Sentry + ) + + services.add(sentryService) + } + + val socorroService = MozillaSocorroService( + context, + appName = "Focus", + version = org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION, + buildId = org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID, + vendor = org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR, + releaseChannel = org.mozilla.geckoview.BuildConfig.MOZ_UPDATE_CHANNEL, + ) + services.add(socorroService) + + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + val crashReportingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE + } else { + 0 // No flags. Default behavior. + } + + val pendingIntent = PendingIntent.getActivity( + context, + 0, + intent, + crashReportingIntentFlags, + ) + + return CrashReporter( + context = context, + services = services, + telemetryServices = listOf(GleanCrashReporterService(context)), + promptConfiguration = CrashReporter.PromptConfiguration( + appName = context.resources.getString(R.string.app_name), + ), + shouldPrompt = CrashReporter.Prompt.ALWAYS, + enabled = true, + nonFatalCrashIntent = pendingIntent, + notificationsDelegate = notificationsDelegate, + ) +} + +private fun getLocaleTag(context: Context): String { + val currentLocale = LocaleManager.getCurrentLocale(context) + return if (currentLocale != null) { + currentLocale.toLanguageTag() + } else { + Locale.getDefault().toLanguageTag() + } +} + +/** + * Returns the [Components] object from within a [Composable]. + */ +val components: Components + @Composable + get() = LocalContext.current.components diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/FocusApplication.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/FocusApplication.kt new file mode 100644 index 0000000000..0aa99630df --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/FocusApplication.kt @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus + +import android.content.Context +import android.os.Build +import android.os.StrictMode +import android.util.Log.INFO +import androidx.appcompat.app.AppCompatDelegate +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.preference.PreferenceManager +import androidx.work.Configuration.Builder +import androidx.work.Configuration.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import mozilla.components.support.base.facts.register +import mozilla.components.support.base.log.Log +import mozilla.components.support.base.log.sink.AndroidLogSink +import mozilla.components.support.ktx.android.content.isMainProcess +import mozilla.components.support.locale.LocaleAwareApplication +import mozilla.components.support.rusthttp.RustHttpConfig +import mozilla.components.support.rustlog.RustLog +import mozilla.components.support.webextensions.WebExtensionSupport +import org.mozilla.focus.biometrics.LockObserver +import org.mozilla.focus.experiments.finishNimbusInitialization +import org.mozilla.focus.ext.settings +import org.mozilla.focus.navigation.StoreLink +import org.mozilla.focus.nimbus.FocusNimbus +import org.mozilla.focus.session.VisibilityLifeCycleCallback +import org.mozilla.focus.telemetry.FactsProcessor +import org.mozilla.focus.telemetry.ProfilerMarkerFactProcessor +import org.mozilla.focus.utils.AdjustHelper +import org.mozilla.focus.utils.AppConstants +import kotlin.coroutines.CoroutineContext + +@Suppress("TooManyFunctions") +open class FocusApplication : LocaleAwareApplication(), Provider, CoroutineScope { + private var job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + open val components: Components by lazy { Components(this) } + + var visibilityLifeCycleCallback: VisibilityLifeCycleCallback? = null + private set + + private val storeLink by lazy { StoreLink(components.appStore, components.store) } + private val lockObserver by lazy { LockObserver(this, components.store, components.appStore) } + + @OptIn(DelicateCoroutinesApi::class) + override fun onCreate() { + super.onCreate() + + Log.addSink(AndroidLogSink("Focus")) + components.crashReporter.install(this) + + if (isMainProcess()) { + initializeNimbus() + + PreferenceManager.setDefaultValues(this, R.xml.settings, false) + + setTheme(this) + components.engine.warmUp() + + components.metrics.initialize(this) + FactsProcessor.initialize() + finishSetupMegazord() + + ProfilerMarkerFactProcessor.create { components.engine.profiler }.register() + + enableStrictMode() + + AdjustHelper.setupAdjustIfNeeded(this@FocusApplication) + + visibilityLifeCycleCallback = VisibilityLifeCycleCallback(this@FocusApplication) + registerActivityLifecycleCallbacks(visibilityLifeCycleCallback) + registerComponentCallbacks(visibilityLifeCycleCallback) + + storeLink.start() + + initializeWebExtensionSupport() + + setupLeakCanary() + + components.appStartReasonProvider.registerInAppOnCreate(this) + components.startupActivityLog.registerInAppOnCreate(this) + + ProcessLifecycleOwner.get().lifecycle.addObserver(lockObserver) + GlobalScope.launch(Dispatchers.IO) { + // Remove stale temporary uploaded files. + components.fileUploadsDirCleaner.cleanUploadsDirectory() + } + } + } + + override fun onConfigurationChanged(config: android.content.res.Configuration) { + applicationContext.resources.configuration.uiMode = config.uiMode + super.onConfigurationChanged(config) + } + + protected open fun setupLeakCanary() { + // no-op, LeakCanary is disabled by default + } + + open fun updateLeakCanaryState(isEnabled: Boolean) { + // no-op, LeakCanary is disabled by default + } + + protected open fun initializeNimbus() { + beginSetupMegazord() + + // This lazily constructs the Nimbus object… + val nimbus = components.experiments + // … which we then can populate the feature configuration. + FocusNimbus.initialize { nimbus } + } + + /** + * Initiate Megazord sequence! Megazord Battle Mode! + * + * The application-services combined libraries are known as the "megazord". We use the default `full` + * megazord - it contains everything that fenix needs, and (currently) nothing more. + * + * Documentation on what megazords are, and why they're needed: + * - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md + * - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html + * + * This is the initialization of the megazord without setting up networking, i.e. needing the + * engine for networking. This should do the minimum work necessary as it is done on the main + * thread, early in the app startup sequence. + */ + private fun beginSetupMegazord() { + // Note: Megazord.init() must be called as soon as possible ... + // Megazord.init() + + // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later. + + RustLog.enable() + } + + @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage + private fun finishSetupMegazord() { + GlobalScope.launch(Dispatchers.IO) { + // We need to use an unwrapped client because native components do not support private + // requests. + @Suppress("Deprecation") + RustHttpConfig.setClient(lazy { components.client.unwrap() }) + + // Now viaduct (the RustHttp client) is initialized we can ask Nimbus to fetch + // experiments recipes from the server. + finishNimbusInitialization(components.experiments) + } + } + + private fun setTheme(context: Context) { + val settings = context.settings + when { + settings.lightThemeSelected -> { + AppCompatDelegate.setDefaultNightMode( + AppCompatDelegate.MODE_NIGHT_NO, + ) + } + + settings.darkThemeSelected -> { + AppCompatDelegate.setDefaultNightMode( + AppCompatDelegate.MODE_NIGHT_YES, + ) + } + + settings.useDefaultThemeSelected -> { + setDefaultTheme() + } + + // No theme setting selected, select the default value, follow device theme. + else -> { + setDefaultTheme() + settings.useDefaultThemeSelected = true + } + } + } + + private fun setDefaultTheme() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + AppCompatDelegate.setDefaultNightMode( + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + ) + } else { + AppCompatDelegate.setDefaultNightMode( + AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY, + ) + } + } + + private fun enableStrictMode() { + // Android/WebView sometimes commit strict mode violations, see e.g. + // https://github.com/mozilla-mobile/focus-android/issues/660 + if (AppConstants.isReleaseBuild || AppConstants.isBetaBuild) { + return + } + + val threadPolicyBuilder = StrictMode.ThreadPolicy.Builder().detectAll() + val vmPolicyBuilder = StrictMode.VmPolicy.Builder() + .detectActivityLeaks() + .detectFileUriExposure() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() + .detectLeakedSqlLiteObjects() + + threadPolicyBuilder.penaltyLog() + vmPolicyBuilder.penaltyLog() + + StrictMode.setThreadPolicy(threadPolicyBuilder.build()) + StrictMode.setVmPolicy(vmPolicyBuilder.build()) + } + + private fun initializeWebExtensionSupport() { + WebExtensionSupport.initialize( + components.engine, + components.store, + onNewTabOverride = { _, engineSession, url -> + components.tabsUseCases.addTab( + url = url, + selectTab = true, + engineSession = engineSession, + private = true, + ) + }, + ) + } + + override val workManagerConfiguration = Builder().setMinimumLoggingLevel(INFO).build() +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/CrashListActivity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/CrashListActivity.kt new file mode 100644 index 0000000000..136e12cabe --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/CrashListActivity.kt @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.content.Intent +import androidx.core.net.toUri +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.lib.crash.ui.AbstractCrashListActivity +import org.mozilla.focus.ext.components + +class CrashListActivity : AbstractCrashListActivity() { + override val crashReporter: CrashReporter by lazy { components.crashReporter } + + override fun onCrashServiceSelected(url: String) { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = url.toUri() + `package` = packageName + } + startActivity(intent) + finish() + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/CustomTabActivity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/CustomTabActivity.kt new file mode 100644 index 0000000000..df2ae98de2 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/CustomTabActivity.kt @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import android.view.View +import mozilla.components.browser.state.selector.findCustomTab +import mozilla.components.concept.engine.EngineView +import mozilla.components.support.locale.LocaleAwareAppCompatActivity +import mozilla.components.support.utils.SafeIntent +import org.mozilla.focus.R +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.updateSecureWindowFlags +import org.mozilla.focus.fragment.BrowserFragment +import org.mozilla.focus.telemetry.startuptelemetry.StartupPathProvider +import org.mozilla.focus.telemetry.startuptelemetry.StartupTypeTelemetry + +/** + * The main entry point for "custom tabs" opened by third-party apps. + */ +class CustomTabActivity : LocaleAwareAppCompatActivity() { + private lateinit var customTabId: String + private lateinit var browserFragment: BrowserFragment + + private val startupPathProvider = StartupPathProvider() + private lateinit var startupTypeTelemetry: StartupTypeTelemetry + + override fun onCreate(savedInstanceState: Bundle?) { + updateSecureWindowFlags() + super.onCreate(savedInstanceState) + + val intent = SafeIntent(intent) + val customTabId = intent.getStringExtra(CUSTOM_TAB_ID) + + // The session for this ID, no longer exists. This usually happens because we were gc-d + // and since we do not save custom tab sessions, the activity is re-created and we no longer + // have a session with us to restore. It's safer to finish the activity instead. + if (customTabId == null || components.store.state.findCustomTab(customTabId) == null) { + finish() + return + } + + this.customTabId = customTabId + + @Suppress("DEPRECATION") // https://github.com/mozilla-mobile/focus-android/issues/5016 + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + + setContentView(R.layout.activity_customtab) + + if (savedInstanceState == null || !this::browserFragment.isInitialized) { + browserFragment = BrowserFragment.createForTab(customTabId) + supportFragmentManager.beginTransaction() + .add(R.id.container, browserFragment) + .commit() + } + + startupPathProvider.attachOnActivityOnCreate(lifecycle, intent.unsafe) + startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply { + attachOnMainActivityOnCreate(lifecycle) + } + } + + override fun onPause() { + super.onPause() + + if (isFinishing) { + components.customTabsUseCases.remove(customTabId) + } + } + + override fun onBackPressed() { + if (browserFragment.sessionFeature.onBackPressed()) { + return + } else { + onBackPressedDispatcher.onBackPressed() + } + } + + override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? { + return if (name == EngineView::class.java.name) { + val engineView = components.engine.createView(context, attrs) + engineView.asView() + } else { + super.onCreateView(parent, name, context, attrs) + } + } + + companion object { + const val CUSTOM_TAB_ID = "custom_tab_id" + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/EraseAndOpenShortcutActivity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/EraseAndOpenShortcutActivity.kt new file mode 100644 index 0000000000..47a914553a --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/EraseAndOpenShortcutActivity.kt @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import mozilla.components.browser.state.selector.privateTabs +import org.mozilla.focus.GleanMetrics.AppShortcuts +import org.mozilla.focus.ext.components + +class EraseAndOpenShortcutActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + components.tabsUseCases.removeAllTabs() + + val tabCount = components.store.state.privateTabs.size + AppShortcuts.eraseOpenButtonTapped.record(AppShortcuts.EraseOpenButtonTappedExtra(tabCount)) + + val intent = Intent(this, MainActivity::class.java) + intent.action = MainActivity.ACTION_OPEN + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + + finish() + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/EraseShortcutActivity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/EraseShortcutActivity.kt new file mode 100644 index 0000000000..4f7f160911 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/EraseShortcutActivity.kt @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.activity + +import android.app.Activity +import android.os.Bundle +import mozilla.components.browser.state.selector.privateTabs +import org.mozilla.focus.GleanMetrics.AppShortcuts +import org.mozilla.focus.ext.components + +class EraseShortcutActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + components.tabsUseCases.removeAllTabs() + + val tabCount = components.store.state.privateTabs.size + AppShortcuts.justEraseButtonTapped.record(AppShortcuts.JustEraseButtonTappedExtra(tabCount)) + + finishAndRemoveTask() + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/InstallFirefoxActivity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/InstallFirefoxActivity.kt new file mode 100644 index 0000000000..5e85af2670 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/InstallFirefoxActivity.kt @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.webkit.WebView +import androidx.core.net.toUri +import mozilla.components.service.glean.private.NoExtras +import mozilla.components.support.utils.Browsers +import mozilla.components.support.utils.ext.resolveActivityCompat +import org.mozilla.focus.GleanMetrics.OpenWith +import org.mozilla.focus.utils.AppConstants + +/** + * Helper activity that will open the Google Play store by following a redirect URL. + */ +class InstallFirefoxActivity : Activity() { + + private var webView: WebView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + webView = WebView(this) + + setContentView(webView) + + webView!!.loadUrl(REDIRECT_URL) + } + + override fun onPause() { + super.onPause() + + if (webView != null) { + webView!!.onPause() + } + + finish() + } + + override fun onDestroy() { + super.onDestroy() + + if (webView != null) { + webView!!.destroy() + } + } + + companion object { + private const val REDIRECT_URL = "https://app.adjust.com/gs1ao4" + + fun resolveAppStore(context: Context): ActivityInfo? { + val resolveInfo = context.packageManager.resolveActivityCompat(createStoreIntent(), 0) + + if (resolveInfo?.activityInfo == null) { + return null + } + + return if (!resolveInfo.activityInfo.exported) { + // We are not allowed to launch this activity. + null + } else { + resolveInfo.activityInfo + } + } + + private fun createStoreIntent(): Intent { + return Intent( + Intent.ACTION_VIEW, + ("market://details?id=" + Browsers.KnownBrowser.FIREFOX.packageName).toUri(), + ) + } + + fun open(context: Context) { + if (AppConstants.isKlarBuild) { + // Redirect to Google Play directly + context.startActivity(createStoreIntent()) + } else { + // Start this activity to load the redirect URL in a WebView. + val intent = Intent(context, InstallFirefoxActivity::class.java) + context.startActivity(intent) + } + + OpenWith.installFirefox.record(NoExtras()) + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/IntentReceiverActivity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/IntentReceiverActivity.kt new file mode 100644 index 0000000000..027ade16bc --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/IntentReceiverActivity.kt @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import mozilla.components.feature.intent.ext.sanitize +import mozilla.components.feature.search.widget.BaseVoiceSearchActivity +import mozilla.components.support.utils.toSafeIntent +import org.mozilla.focus.ext.components +import org.mozilla.focus.session.IntentProcessor +import org.mozilla.focus.utils.SupportUtils + +/** + * This activity receives VIEW intents and either forwards them to MainActivity or CustomTabActivity. + */ +class IntentReceiverActivity : Activity() { + private val intentProcessor by lazy { + IntentProcessor(this, components.tabsUseCases, components.customTabsUseCases) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val intent = intent.sanitize().toSafeIntent() + + if (intent.dataString.equals(SupportUtils.OPEN_WITH_DEFAULT_BROWSER_URL)) { + dispatchNormalIntent() + return + } + + val result = intentProcessor.handleIntent(this, intent, savedInstanceState) + if (result is IntentProcessor.Result.CustomTab) { + dispatchCustomTabsIntent(result.id) + } else { + dispatchNormalIntent() + } + + finish() + } + + private fun dispatchCustomTabsIntent(tabId: String) { + val intent = Intent(intent) + + intent.setClassName(applicationContext, CustomTabActivity::class.java.name) + + // We are adding a generated custom tab ID to the intent here. CustomTabActivity will + // use this ID to later decide what session to display once it is created. + intent.putExtra(CustomTabActivity.CUSTOM_TAB_ID, tabId) + + startActivity(intent) + } + + private fun dispatchNormalIntent() { + val intent = Intent(intent) + intent.setClassName(applicationContext, MainActivity::class.java.name) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + intent.putExtra(SEARCH_WIDGET_EXTRA, intent.getBooleanExtra(SEARCH_WIDGET_EXTRA, false)) + intent.putExtra( + BaseVoiceSearchActivity.SPEECH_PROCESSING, + intent.getStringExtra(BaseVoiceSearchActivity.SPEECH_PROCESSING), + ) + startActivity(intent) + } + + companion object { + const val SEARCH_WIDGET_EXTRA = "search_widget_extra" + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt new file mode 100644 index 0000000000..f81e0fb926 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/MainActivity.kt @@ -0,0 +1,473 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.Manifest.permission.POST_NOTIFICATIONS +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.util.AttributeSet +import android.view.MenuItem +import android.view.View +import android.view.ViewTreeObserver +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.ActionBar +import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.isVisible +import androidx.preference.PreferenceManager +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.concept.engine.EngineView +import mozilla.components.feature.search.widget.BaseVoiceSearchActivity +import mozilla.components.lib.auth.canUseBiometricFeature +import mozilla.components.lib.crash.Crash +import mozilla.components.service.glean.private.NoExtras +import mozilla.components.support.base.feature.UserInteractionHandler +import mozilla.components.support.ktx.android.content.getColorFromAttr +import mozilla.components.support.ktx.android.view.createWindowInsetsController +import mozilla.components.support.locale.LocaleAwareAppCompatActivity +import mozilla.components.support.utils.SafeIntent +import mozilla.components.support.utils.StatusBarUtils +import org.mozilla.experiments.nimbus.initializeTooling +import org.mozilla.focus.GleanMetrics.AppOpened +import org.mozilla.focus.GleanMetrics.Notifications +import org.mozilla.focus.R +import org.mozilla.focus.appreview.AppReviewUtils +import org.mozilla.focus.databinding.ActivityMainBinding +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.setNavigationIcon +import org.mozilla.focus.ext.settings +import org.mozilla.focus.ext.updateSecureWindowFlags +import org.mozilla.focus.fragment.BrowserFragment +import org.mozilla.focus.fragment.UrlInputFragment +import org.mozilla.focus.navigation.MainActivityNavigation +import org.mozilla.focus.navigation.Navigator +import org.mozilla.focus.searchwidget.ExternalIntentNavigation +import org.mozilla.focus.session.IntentProcessor +import org.mozilla.focus.session.PrivateNotificationFeature +import org.mozilla.focus.shortcut.HomeScreen +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.state.Screen +import org.mozilla.focus.telemetry.startuptelemetry.StartupPathProvider +import org.mozilla.focus.telemetry.startuptelemetry.StartupTypeTelemetry +import org.mozilla.focus.utils.SupportUtils + +private const val REQUEST_TIME_OUT = 2000L + +@Suppress("TooManyFunctions", "LargeClass") +open class MainActivity : LocaleAwareAppCompatActivity() { + private var isToolbarInflated = false + private val intentProcessor by lazy { + IntentProcessor(this, components.tabsUseCases, components.customTabsUseCases) + } + + private val navigator by lazy { Navigator(components.appStore, MainActivityNavigation(this)) } + private val tabCount: Int + get() = components.store.state.privateTabs.size + + private val startupPathProvider = StartupPathProvider() + private lateinit var startupTypeTelemetry: StartupTypeTelemetry + private var _binding: ActivityMainBinding? = null + private val binding get() = _binding!! + private lateinit var privateNotificationFeature: PrivateNotificationFeature + private val notificationPermission = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + when { + granted -> { + privateNotificationFeature.start() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + components.experiments.initializeTooling(applicationContext, intent) + installSplashScreen() + + updateSecureWindowFlags() + + super.onCreate(savedInstanceState) + _binding = ActivityMainBinding.inflate(layoutInflater) + // Checks if Activity is currently in PiP mode if launched from external intents, then exits it + checkAndExitPiP() + + if (!isTaskRoot) { + if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN == intent.action) { + finish() + return + } + } + + @Suppress("DEPRECATION") // https://github.com/mozilla-mobile/focus-android/issues/5016 + window.decorView.systemUiVisibility = + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + + window.statusBarColor = ContextCompat.getColor(this, android.R.color.transparent) + when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_UNDEFINED, // We assume light here per Android doc's recommendation + Configuration.UI_MODE_NIGHT_NO, + -> { + updateLightSystemBars() + } + Configuration.UI_MODE_NIGHT_YES -> { + clearLightSystemBars() + } + } + setContentView(binding.root) + + startupPathProvider.attachOnActivityOnCreate(lifecycle, intent) + startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply { + attachOnMainActivityOnCreate(lifecycle) + } + + val safeIntent = SafeIntent(intent) + + lifecycle.addObserver(navigator) + + if (savedInstanceState == null) { + handleAppNavigation(safeIntent) + } + + if (savedInstanceState == null && intent.hasExtra(HomeScreen.ADD_TO_HOMESCREEN_TAG)) { + intentProcessor.handleNewIntent(this, safeIntent) + } + + if (safeIntent.isLauncherIntent) { + AppOpened.fromIcons.record(AppOpened.FromIconsExtra(AppOpenType.LAUNCH.type)) + } + + val launchCount = settings.getAppLaunchCount() + PreferenceManager.getDefaultSharedPreferences(this) + .edit() + .putInt(getString(R.string.app_launch_count), launchCount + 1) + .apply() + + AppReviewUtils.showAppReview(this) + + privateNotificationFeature = PrivateNotificationFeature( + context = applicationContext, + browserStore = components.store, + permissionRequestHandler = { requestNotificationPermission() }, + ).also { + it.start() + } + + components.notificationsDelegate.bindToActivity(this) + } + + private fun requestNotificationPermission() { + privateNotificationFeature.stop() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermission.launch(POST_NOTIFICATIONS) + } + } + + private fun setSplashScreenPreDrawListener(safeIntent: SafeIntent) { + val endTime = System.currentTimeMillis() + REQUEST_TIME_OUT + binding.container.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + return if (System.currentTimeMillis() >= endTime) { + ExternalIntentNavigation.handleAppNavigation( + bundle = safeIntent.extras, + context = this@MainActivity, + ) + binding.container.viewTreeObserver.removeOnPreDrawListener(this) + true + } else { + false + } + } + }, + ) + } + + private fun checkAndExitPiP() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode && intent != null) { + // Exit PiP mode + moveTaskToBack(false) + startActivity(Intent(this, this::class.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)) + } + } + + final override fun onUserLeaveHint() { + // The notification permission prompt will trigger onUserLeaveHint too. + // We shouldn't treat this situation as user leaving. + if (!components.notificationsDelegate.isRequestingPermission) { + val browserFragment = + supportFragmentManager.findFragmentByTag(BrowserFragment.FRAGMENT_TAG) as BrowserFragment? + if (browserFragment is UserInteractionHandler && browserFragment.onHomePressed()) { + return + } + } + super.onUserLeaveHint() + } + + override fun onResume() { + super.onResume() + checkBiometricStillValid() + } + + override fun onPause() { + val fragmentManager = supportFragmentManager + val browserFragment = + fragmentManager.findFragmentByTag(BrowserFragment.FRAGMENT_TAG) as BrowserFragment? + browserFragment?.cancelAnimation() + + val urlInputFragment = + fragmentManager.findFragmentByTag(UrlInputFragment.FRAGMENT_TAG) as UrlInputFragment? + urlInputFragment?.cancelAnimation() + + super.onPause() + } + + override fun onStop() { + super.onStop() + } + + override fun onNewIntent(unsafeIntent: Intent) { + if (Crash.isCrashIntent(unsafeIntent)) { + val browserFragment = supportFragmentManager + .findFragmentByTag(BrowserFragment.FRAGMENT_TAG) as BrowserFragment? + val crash = Crash.fromIntent(unsafeIntent) + + browserFragment?.handleTabCrash(crash) + } + startupPathProvider.onIntentReceived(intent) + val intent = SafeIntent(unsafeIntent) + + handleAppRestoreFromBackground(intent) + + if (intent.dataString.equals(SupportUtils.OPEN_WITH_DEFAULT_BROWSER_URL)) { + components.appStore.dispatch( + AppAction.OpenSettings( + page = Screen.Settings.Page.General, + ), + ) + super.onNewIntent(unsafeIntent) + return + } + + val action = intent.action + + if (intent.hasExtra(HomeScreen.ADD_TO_HOMESCREEN_TAG)) { + intentProcessor.handleNewIntent(this, intent) + } + + if (ACTION_OPEN == action) { + Notifications.openButtonTapped.record(NoExtras()) + } + + if (ACTION_ERASE == action) { + processEraseAction(intent) + } + + if (intent.isLauncherIntent) { + AppOpened.fromIcons.record(AppOpened.FromIconsExtra(AppOpenType.RESUME.type)) + } + + super.onNewIntent(unsafeIntent) + } + + private fun handleAppRestoreFromBackground(intent: SafeIntent) { + if (!intent.extras?.getString(BaseVoiceSearchActivity.SPEECH_PROCESSING).isNullOrEmpty()) { + handleAppNavigation(intent) + return + } + when (components.appStore.state.screen) { + is Screen.Settings -> components.appStore.dispatch( + AppAction.OpenSettings( + page = + (components.appStore.state.screen as Screen.Settings).page, + ), + ) + is Screen.SitePermissionOptionsScreen -> components.appStore.dispatch( + AppAction.OpenSitePermissionOptionsScreen( + sitePermission = + (components.appStore.state.screen as Screen.SitePermissionOptionsScreen).sitePermission, + ), + ) + else -> { + handleAppNavigation(intent) + } + } + } + + private fun handleAppNavigation(intent: SafeIntent) { + if (components.appStore.state.screen == Screen.Locked()) { + components.appStore.dispatch(AppAction.Lock(intent.extras)) + } else if (settings.getAppLaunchCount() == 0) { + setSplashScreenPreDrawListener(intent) + } else { + ExternalIntentNavigation.handleAppNavigation( + bundle = intent.extras, + context = this, + ) + } + } + + private fun processEraseAction(intent: SafeIntent) { + val fromNotificationAction = intent.getBooleanExtra(EXTRA_NOTIFICATION, false) + + components.tabsUseCases.removeAllTabs() + + if (fromNotificationAction) { + Notifications.eraseOpenButtonTapped.record(Notifications.EraseOpenButtonTappedExtra(tabCount)) + } + } + + override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? { + return if (name == EngineView::class.java.name) { + components.engine.createView(context, attrs).asView() + } else { + super.onCreateView(parent, name, context, attrs) + } + } + + override fun onBackPressed() { + val fragmentManager = supportFragmentManager + + val urlInputFragment = + fragmentManager.findFragmentByTag(UrlInputFragment.FRAGMENT_TAG) as UrlInputFragment? + if (urlInputFragment != null && + urlInputFragment.isVisible && + urlInputFragment.onBackPressed() + ) { + // The URL input fragment has handled the back press. It does its own animations so + // we do not try to remove it from outside. + return + } + + val browserFragment = + fragmentManager.findFragmentByTag(BrowserFragment.FRAGMENT_TAG) as BrowserFragment? + if (browserFragment != null && + browserFragment.isVisible && + browserFragment.onBackPressed() + ) { + // The Browser fragment handles back presses on its own because it might just go back + // in the browsing history. + return + } + + val appStore = components.appStore + if (appStore.state.screen is Screen.Settings || appStore.state.screen is Screen.SitePermissionOptionsScreen) { + // When on a settings screen we want the same behavior as navigating "up" via the toolbar + // and therefore dispatch the `NavigateUp` action on the app store. + val selectedTabId = components.store.state.selectedTabId + appStore.dispatch(AppAction.NavigateUp(selectedTabId)) + return + } + + onBackPressedDispatcher.onBackPressed() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + // We forward an up action to the app store with the NavigateUp action to let the reducer + // decide to show a different screen. + val selectedTabId = components.store.state.selectedTabId + components.appStore.dispatch(AppAction.NavigateUp(selectedTabId)) + return true + } + + return super.onOptionsItemSelected(item) + } + + // Handles the edge case of a user removing all enrolled prints while auth was enabled + private fun checkBiometricStillValid() { + // Disable biometrics if the user is no longer eligible due to un-enrolling fingerprints: + if (!canUseBiometricFeature()) { + PreferenceManager.getDefaultSharedPreferences(this) + .edit().putBoolean( + getString(R.string.pref_key_biometric), + false, + ).apply() + } + } + + private fun updateLightSystemBars() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + window.statusBarColor = getColorFromAttr(android.R.attr.statusBarColor) + window.createWindowInsetsController().isAppearanceLightStatusBars = true + } else { + window.statusBarColor = Color.BLACK + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // API level can display handle light navigation bar color + window.createWindowInsetsController().isAppearanceLightNavigationBars = true + window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + window.navigationBarDividerColor = + ContextCompat.getColor(this, android.R.color.transparent) + } + } + } + + private fun clearLightSystemBars() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + window.createWindowInsetsController().isAppearanceLightStatusBars = false + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // API level can display handle light navigation bar color + window.createWindowInsetsController().isAppearanceLightNavigationBars = false + } + } + + fun getToolbar(): ActionBar { + if (!isToolbarInflated) { + val toolbar = binding.toolbar.inflate() as Toolbar + setSupportActionBar(toolbar) + setNavigationIcon(R.drawable.ic_back_button) + isToolbarInflated = true + } + return supportActionBar!! + } + + fun customizeStatusBar(backgroundColorId: Int? = null) { + with(binding.statusBarBackground) { + binding.statusBarBackground.isVisible = true + StatusBarUtils.getStatusBarHeight(this) { statusBarHeight -> + layoutParams.height = statusBarHeight + backgroundColorId?.let { color -> + setBackgroundColor(ContextCompat.getColor(context, color)) + } + } + } + } + + fun hideStatusBarBackground() { + binding.statusBarBackground.isVisible = false + } + + override fun onDestroy() { + super.onDestroy() + _binding = null + + if (this::privateNotificationFeature.isInitialized) { + privateNotificationFeature.stop() + } + + components.notificationsDelegate.unBindActivity(this) + } + + enum class AppOpenType(val type: String) { + LAUNCH("Launch"), + RESUME("Resume"), + } + + companion object { + const val ACTION_ERASE = "erase" + const val ACTION_OPEN = "open" + + const val EXTRA_NOTIFICATION = "notification" + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/TextActionActivity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/TextActionActivity.kt new file mode 100644 index 0000000000..2181ded3ae --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/activity/TextActionActivity.kt @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.activity + +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.annotation.RequiresApi +import androidx.core.net.toUri +import mozilla.components.feature.search.ext.buildSearchUrl +import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine +import mozilla.components.support.utils.SafeIntent +import org.mozilla.focus.ext.components + +/** + * Activity for receiving and processing an ACTION_PROCESS_TEXT intent. + */ +class TextActionActivity : Activity() { + @RequiresApi(api = Build.VERSION_CODES.M) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val intent = SafeIntent(intent) + + val searchTextCharSequence = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT) + val searchText = searchTextCharSequence?.toString() ?: "" + + components.store.waitForSelectedOrDefaultSearchEngine { searchEngine -> + val searchUrl = searchEngine?.buildSearchUrl(searchText).toString() + val searchIntent = Intent(this, IntentReceiverActivity::class.java) + searchIntent.action = Intent.ACTION_VIEW + searchIntent.putExtra(EXTRA_TEXT_SELECTION, true) + searchIntent.data = searchUrl.toUri() + searchIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + + startActivity(searchIntent) + + finish() + } + } + + companion object { + const val EXTRA_TEXT_SELECTION = "text_selection" + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/animation/TransitionDrawableGroup.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/animation/TransitionDrawableGroup.kt new file mode 100644 index 0000000000..3c1e30d44e --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/animation/TransitionDrawableGroup.kt @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.animation + +import android.graphics.drawable.TransitionDrawable + +/** + * A class to allow [TransitionDrawable]'s animations to play together: similar to [android.animation.AnimatorSet]. + */ +class TransitionDrawableGroup(private vararg val transitionDrawables: TransitionDrawable) { + fun startTransition(durationMillis: Int) { + // In theory, there are no guarantees these will play together. + // In practice, I haven't noticed any problems. + for (transitionDrawable in transitionDrawables) { + transitionDrawable.startTransition(durationMillis) + } + } + + fun resetTransition() { + for (transitionDrawable in transitionDrawables) { + transitionDrawable.resetTransition() + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/appreview/AppReviewStep.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/appreview/AppReviewStep.kt new file mode 100644 index 0000000000..47089dc410 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/appreview/AppReviewStep.kt @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.appreview + +enum class AppReviewStep { + Pending, + ReviewNeeded, + Reviewed, +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/appreview/AppReviewUtils.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/appreview/AppReviewUtils.kt new file mode 100644 index 0000000000..d180bc6bac --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/appreview/AppReviewUtils.kt @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.appreview + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri +import androidx.preference.PreferenceManager +import com.google.android.play.core.review.ReviewInfo +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.android.play.core.tasks.Task +import mozilla.components.browser.state.state.SessionState +import org.mozilla.focus.R +import org.mozilla.focus.ext.components +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.utils.SupportUtils +import java.util.concurrent.TimeUnit + +class AppReviewUtils { + companion object { + /** + * Number of app openings until In App Review is triggered. + */ + private const val APP_OPENINGS_REVIEW_TRIGGER = 3 + private val APP_REVIEW_TIME_TRIGGER = TimeUnit.DAYS.toMillis(90) + + /** + * Shows in app review or opens play store if Review Info task is not successful. + * + * @property activity where In App review is triggered + */ + fun showAppReview(activity: Activity) { + if (shouldShowInAppReview(activity)) { + val manager = ReviewManagerFactory.create(activity) + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task: Task -> + if (task.isSuccessful) { + // We can get the ReviewInfo object + val reviewInfo = task.result + val flow = manager.launchReviewFlow(activity, reviewInfo) + flow.addOnCompleteListener { + // The flow has finished. The API does not indicate whether the user + // reviewed or not, or even whether the review dialog was shown. Thus, no + // matter the result, we continue our app flow. + setAppReviewed(activity) + } + } else { + setAppReviewed(activity) + // There was some problem, open PlayStore + openPlayStore(activity = activity) + } + } + } + } + + /** + * Set the number of app openings and the flag when In App Review is needed. + * + * @property context needed for SharePref + */ + fun addAppOpenings(context: Context) { + val preferenceManage = PreferenceManager.getDefaultSharedPreferences(context) + val currentOpeningsNumber = preferenceManage.getInt( + context.getString( + R.string.pref_in_app_review_openings, + ), + 0, + ) + 1 + val appReviewStep = preferenceManage.getString( + context.getString( + R.string.pref_in_app_review_step, + ), + AppReviewStep.Pending.name, + ) + + preferenceManage + .edit().putInt( + context.getString(R.string.pref_in_app_review_openings), + currentOpeningsNumber, + ).apply() + if (currentOpeningsNumber == APP_OPENINGS_REVIEW_TRIGGER && + appReviewStep == AppReviewStep.Pending.name + ) { + setAppReviewStep(context, AppReviewStep.ReviewNeeded) + } + } + + private fun shouldShowInAppReview(context: Context): Boolean { + val inAppReviewStep = PreferenceManager.getDefaultSharedPreferences(context).getString( + context.getString(R.string.pref_in_app_review_step), + AppReviewStep.Pending.name, + ) + val lastReviewedTime = PreferenceManager.getDefaultSharedPreferences(context).getLong( + context.getString(R.string.pref_in_app_review_time), + 0L, + ) + + return inAppReviewStep == AppReviewStep.ReviewNeeded.name || ( + lastReviewedTime + + APP_REVIEW_TIME_TRIGGER <= System.currentTimeMillis() && + inAppReviewStep == AppReviewStep.Reviewed.name + ) + } + + private fun openPlayStore(activity: Activity) { + try { + activity.startActivity( + Intent( + Intent.ACTION_VIEW, + SupportUtils.RATE_APP_URL.toUri(), + ), + ) + } catch (e: ActivityNotFoundException) { + // Device without the play store installed. + // Opening the play store website. + val tabId = activity.components.tabsUseCases.addTab( + url = SupportUtils.FOCUS_PLAY_STORE_URL, + source = SessionState.Source.Internal.NewTab, + selectTab = true, + private = true, + ) + activity.components.appStore.dispatch(AppAction.OpenTab(tabId)) + } + } + + private fun setAppReviewed(activity: Activity) { + setAppReviewStep(activity, AppReviewStep.Reviewed) + setLastReviewedTime(activity) + } + + private fun setAppReviewStep(context: Context, appReviewStep: AppReviewStep) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString( + context.getString(R.string.pref_in_app_review_step), + appReviewStep.name, + ).apply() + } + + private fun setLastReviewedTime(context: Context) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putLong( + context.getString(R.string.pref_in_app_review_time), + System.currentTimeMillis(), + ).apply() + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteAddFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteAddFragment.kt new file mode 100644 index 0000000000..4da800d468 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteAddFragment.kt @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.autocomplete + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import mozilla.components.browser.domains.CustomDomains +import org.mozilla.focus.GleanMetrics.Autocomplete +import org.mozilla.focus.R +import org.mozilla.focus.databinding.FragmentAutocompleteAddDomainBinding +import org.mozilla.focus.ext.requireComponents +import org.mozilla.focus.ext.showToolbar +import org.mozilla.focus.settings.BaseSettingsLikeFragment +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.utils.ViewUtils +import kotlin.coroutines.CoroutineContext + +/** + * Fragment showing settings UI to add custom autocomplete domains. + */ +class AutocompleteAddFragment : BaseSettingsLikeFragment(), CoroutineScope { + private var job = Job() + override val coroutineContext: CoroutineContext + get() = job + Main + private var _binding: FragmentAutocompleteAddDomainBinding? = null + private val binding get() = _binding!! + + override fun onResume() { + super.onResume() + + if (job.isCancelled) { + job = Job() + } + + showToolbar(getString(R.string.preference_autocomplete_title_add)) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentAutocompleteAddDomainBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + ViewUtils.showKeyboard(binding.domainView) + } + + override fun onPause() { + job.cancel() + ViewUtils.hideKeyboard(activity?.currentFocus) + super.onPause() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_autocomplete_add, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.save -> { + val domain = binding.domainView.text.toString().trim() + + launch(IO) { + val domains = CustomDomains.load(requireActivity()) + val error = when { + domain.isEmpty() -> getString(R.string.preference_autocomplete_add_error) + domains.contains(domain) -> getString(R.string.preference_autocomplete_duplicate_url_error) + else -> null + } + + launch(Main) { + if (error != null) { + binding.domainView.error = error + } else { + saveDomainAndClose(requireActivity().applicationContext, domain) + } + } + } + true + } + // other options are not handled by this menu provider + else -> false + } + + private fun saveDomainAndClose(context: Context, domain: String) { + launch(IO) { + CustomDomains.add(context, domain) + Autocomplete.domainAdded.add() + } + + ViewUtils.showBrandedSnackbar(view, R.string.preference_autocomplete_add_confirmation, 0) + + requireComponents.appStore.dispatch( + AppAction.NavigateUp( + requireComponents.store.state.selectedTabId, + ), + ) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteCustomDomainsPreference.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteCustomDomainsPreference.kt new file mode 100644 index 0000000000..3a71b24790 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteCustomDomainsPreference.kt @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.autocomplete + +import android.content.Context +import android.util.AttributeSet +import org.mozilla.focus.settings.LearnMoreSwitchPreference +import org.mozilla.focus.utils.SupportUtils + +/** + * Switch preference for enabling/disabling autocompletion for custom domains entered by the user. + */ +class AutocompleteCustomDomainsPreference( + context: Context, + attrs: AttributeSet?, +) : LearnMoreSwitchPreference(context, attrs) { + override fun getLearnMoreUrl() = SupportUtils.getSumoURLForTopic( + SupportUtils.getAppVersion(context), + SupportUtils.SumoTopic.AUTOCOMPLETE, + ) +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteDefaultDomainsPreference.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteDefaultDomainsPreference.kt new file mode 100644 index 0000000000..1a8509d1f3 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteDefaultDomainsPreference.kt @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.autocomplete + +import android.content.Context +import android.util.AttributeSet +import org.mozilla.focus.settings.LearnMoreSwitchPreference +import org.mozilla.focus.utils.SupportUtils + +/** + * Switch preference for enabling/disabling autocompletion for default domains that ship with the app. + */ +class AutocompleteDefaultDomainsPreference( + context: Context, + attrs: AttributeSet?, +) : LearnMoreSwitchPreference(context, attrs) { + override fun getLearnMoreUrl() = SupportUtils.getSumoURLForTopic( + SupportUtils.getAppVersion(context), + SupportUtils.SumoTopic.AUTOCOMPLETE, + ) +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteDomainFormatter.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteDomainFormatter.kt new file mode 100644 index 0000000000..d62b3a8b35 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteDomainFormatter.kt @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.autocomplete + +object AutocompleteDomainFormatter { + private const val HOST_INDEX = 3 + private val urlMatcher = Regex("""(https?://)?(www.)?(.+)?""") + + fun format(url: String): String { + val result = urlMatcher.find(url) + + return result?.let { + it.groups[HOST_INDEX]?.value + } ?: url + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteListFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteListFragment.kt new file mode 100644 index 0000000000..7d2f793746 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteListFragment.kt @@ -0,0 +1,349 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.autocomplete + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.browser.domains.CustomDomains +import org.mozilla.focus.GleanMetrics.Autocomplete +import org.mozilla.focus.R +import org.mozilla.focus.databinding.FragmentAutocompleteCustomdomainsBinding +import org.mozilla.focus.ext.requireComponents +import org.mozilla.focus.ext.showToolbar +import org.mozilla.focus.settings.BaseSettingsLikeFragment +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.state.Screen +import org.mozilla.focus.utils.ViewUtils +import java.util.Collections +import kotlin.coroutines.CoroutineContext + +typealias DomainFormatter = (String) -> String + +/** + * Fragment showing settings UI listing all custom autocomplete domains entered by the user. + */ +open class AutocompleteListFragment : BaseSettingsLikeFragment(), CoroutineScope { + private var job = Job() + override val coroutineContext: CoroutineContext + get() = job + Main + private var _binding: FragmentAutocompleteCustomdomainsBinding? = null + protected val binding get() = _binding!! + + /** + * ItemTouchHelper for reordering items in the domain list. + */ + val itemTouchHelper: ItemTouchHelper = ItemTouchHelper( + object : SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean { + val from = viewHolder.bindingAdapterPosition + val to = target.bindingAdapterPosition + + (recyclerView.adapter as DomainListAdapter).move(from, to) + + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + if (viewHolder is AddActionViewHolder) { + return ItemTouchHelper.Callback.makeMovementFlags(0, 0) + } + + return super.getMovementFlags(recyclerView, viewHolder) + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + + if (viewHolder is DomainViewHolder) { + viewHolder.onSelected() + } + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + if (viewHolder is DomainViewHolder) { + viewHolder.onCleared() + } + } + + override fun canDropOver( + recyclerView: RecyclerView, + current: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean { + if (target is AddActionViewHolder) { + return false + } + + return super.canDropOver(recyclerView, current, target) + } + }, + ) + + /** + * In selection mode the user can select and remove items. In non-selection mode the list can + * be reordered by the user. + */ + open fun isSelectionMode() = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentAutocompleteCustomdomainsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.domainList.apply { + layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) + adapter = DomainListAdapter() + setHasFixedSize(true) + + if (!isSelectionMode()) { + itemTouchHelper.attachToRecyclerView(this) + } + } + } + + override fun onResume() { + super.onResume() + + if (job.isCancelled) { + job = Job() + } + + showToolbar(getString(R.string.preference_autocomplete_subitem_manage_sites)) + + (binding.domainList.adapter as DomainListAdapter).refresh(requireActivity()) { + activity?.invalidateOptionsMenu() + } + } + + override fun onPause() { + job.cancel() + super.onPause() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_autocomplete_list, menu) + } + + override fun onPrepareMenu(menu: Menu) { + val removeItem = menu.findItem(R.id.remove) + + removeItem?.let { + it.isVisible = isSelectionMode() || binding.domainList.adapter!!.itemCount > 1 + val isEnabled = + !isSelectionMode() || (binding.domainList.adapter as DomainListAdapter).selection() + .isNotEmpty() + ViewUtils.setMenuItemEnabled(it, isEnabled) + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.remove -> { + requireComponents.appStore.dispatch( + AppAction.OpenSettings(page = Screen.Settings.Page.SearchAutocompleteRemove), + ) + true + } + else -> false + } + + /** + * Adapter implementation for the list of custom autocomplete domains. + */ + inner class DomainListAdapter : RecyclerView.Adapter() { + private val domains: MutableList = mutableListOf() + private val selectedDomains: MutableList = mutableListOf() + + fun refresh(context: Context, body: (() -> Unit)? = null) { + launch(Main) { + val updatedDomains = + withContext(Dispatchers.Default) { + CustomDomains.load(context) + } + + domains.clear() + domains.addAll(updatedDomains) + + notifyDataSetChanged() + + body?.invoke() + } + } + + override fun getItemViewType(position: Int) = + when (position) { + domains.size -> AddActionViewHolder.LAYOUT_ID + else -> DomainViewHolder.LAYOUT_ID + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + when (viewType) { + AddActionViewHolder.LAYOUT_ID -> + AddActionViewHolder( + this@AutocompleteListFragment, + LayoutInflater.from(parent.context).inflate(viewType, parent, false), + ) + DomainViewHolder.LAYOUT_ID -> + DomainViewHolder( + LayoutInflater.from(parent.context).inflate(viewType, parent, false), + ) { AutocompleteDomainFormatter.format(it) } + else -> throw IllegalArgumentException("Unknown view type: $viewType") + } + + override fun getItemCount(): Int = domains.size + if (isSelectionMode()) 0 else 1 + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is DomainViewHolder) { + holder.bind( + domains[position], + isSelectionMode(), + selectedDomains, + itemTouchHelper, + this@AutocompleteListFragment, + ) + } + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + if (holder is DomainViewHolder) { + holder.checkBoxView.setOnCheckedChangeListener(null) + } + } + + fun selection(): List = selectedDomains + + fun move(from: Int, to: Int) { + Collections.swap(domains, from, to) + notifyItemMoved(from, to) + + launch(IO) { + CustomDomains.save(activity!!.applicationContext, domains) + Autocomplete.listOrderChanged.add() + } + } + } + + /** + * ViewHolder implementation for a domain item in the list. + */ + private class DomainViewHolder( + itemView: View, + val domainFormatter: DomainFormatter? = null, + ) : RecyclerView.ViewHolder(itemView) { + val domainView: TextView = itemView.findViewById(R.id.domainView) + val checkBoxView: CheckBox = itemView.findViewById(R.id.checkbox) + val handleView: View = itemView.findViewById(R.id.handleView) + + companion object { + val LAYOUT_ID = R.layout.item_custom_domain + } + + fun bind( + domain: String, + isSelectionMode: Boolean, + selectedDomains: MutableList, + itemTouchHelper: ItemTouchHelper, + fragment: AutocompleteListFragment, + ) { + domainView.text = domainFormatter?.invoke(domain) ?: domain + + checkBoxView.isVisible = isSelectionMode + checkBoxView.isChecked = selectedDomains.contains(domain) + checkBoxView.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean -> + if (isChecked) { + selectedDomains.add(domain) + } else { + selectedDomains.remove(domain) + } + + fragment.activity?.invalidateOptionsMenu() + } + + handleView.isVisible = !isSelectionMode + handleView.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + itemTouchHelper.startDrag(this) + } + false + } + + if (isSelectionMode) { + itemView.setOnClickListener { + checkBoxView.isChecked = !checkBoxView.isChecked + } + } + } + + fun onSelected() { + itemView.setBackgroundColor(ContextCompat.getColor(itemView.context, R.color.disabled)) + } + + fun onCleared() { + itemView.setBackgroundColor(0) + } + } + + /** + * ViewHolder implementation for a "Add custom domain" item at the bottom of the list. + */ + private class AddActionViewHolder( + val fragment: AutocompleteListFragment, + itemView: View, + ) : RecyclerView.ViewHolder(itemView) { + init { + itemView.setOnClickListener { + fragment.requireComponents.appStore.dispatch( + AppAction.OpenSettings(page = Screen.Settings.Page.SearchAutocompleteAdd), + ) + } + } + + companion object { + val LAYOUT_ID = R.layout.item_add_custom_domain + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteRemoveFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteRemoveFragment.kt new file mode 100644 index 0000000000..248a514ed7 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteRemoveFragment.kt @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.autocomplete + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.browser.domains.CustomDomains +import org.mozilla.focus.GleanMetrics.Autocomplete +import org.mozilla.focus.R +import org.mozilla.focus.ext.requireComponents +import org.mozilla.focus.ext.showToolbar +import org.mozilla.focus.state.AppAction +import kotlin.coroutines.CoroutineContext + +class AutocompleteRemoveFragment : AutocompleteListFragment(), CoroutineScope { + private var job = Job() + override val coroutineContext: CoroutineContext + get() = job + Main + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_autocomplete_remove, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.remove -> { + removeSelectedDomains(requireActivity().applicationContext) + true + } + else -> false + } + + private fun removeSelectedDomains(context: Context) { + val domains = (binding.domainList.adapter as DomainListAdapter).selection() + if (domains.isNotEmpty()) { + launch(Main) { + withContext(Dispatchers.Default) { + CustomDomains.remove(context, domains) + Autocomplete.domainRemoved.add() + } + + requireComponents.appStore.dispatch( + AppAction.NavigateUp(requireComponents.store.state.selectedTabId), + ) + } + } + } + + override fun isSelectionMode() = true + + override fun onResume() { + super.onResume() + + if (job.isCancelled) { + job = Job() + } + + showToolbar(getString(R.string.preference_autocomplete_title_remove)) + } + + override fun onPause() { + job.cancel() + super.onPause() + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteSettingsFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteSettingsFragment.kt new file mode 100644 index 0000000000..58c4eb13a8 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/autocomplete/AutocompleteSettingsFragment.kt @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.autocomplete + +import android.content.SharedPreferences +import android.os.Bundle +import androidx.preference.Preference +import org.mozilla.focus.GleanMetrics.Autocomplete +import org.mozilla.focus.R +import org.mozilla.focus.ext.requireComponents +import org.mozilla.focus.ext.requirePreference +import org.mozilla.focus.ext.showToolbar +import org.mozilla.focus.settings.BaseSettingsFragment +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.state.Screen + +/** + * Settings UI for configuring autocomplete. + */ +class AutocompleteSettingsFragment : BaseSettingsFragment(), SharedPreferences.OnSharedPreferenceChangeListener { + + private lateinit var topSitesAutocomplete: AutocompleteDefaultDomainsPreference + private lateinit var favoriteSitesAutocomplete: AutocompleteCustomDomainsPreference + + override fun onCreatePreferences(p0: Bundle?, p1: String?) { + addPreferencesFromResource(R.xml.autocomplete) + val appName = requireContext().getString(R.string.app_name) + + topSitesAutocomplete = + requirePreference(R.string.pref_key_autocomplete_preinstalled).apply { + summary = + context.getString(R.string.preference_autocomplete_topsite_summary2, appName) + } + favoriteSitesAutocomplete = + requirePreference(R.string.pref_key_autocomplete_custom).apply { + summary = + context.getString(R.string.preference_autocomplete_user_list_summary2, appName) + } + } + + override fun onResume() { + super.onResume() + + showToolbar(getString(R.string.preference_subitem_autocomplete)) + + preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + + preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + if (preference.key == getString(R.string.pref_key_screen_custom_domains)) { + requireComponents.appStore.dispatch( + AppAction.OpenSettings(page = Screen.Settings.Page.SearchAutocompleteList), + ) + } + + return super.onPreferenceTreeClick(preference) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + if (key == null || sharedPreferences == null) { + return + } + + when (key) { + topSitesAutocomplete.key -> + Autocomplete.topSitesSettingChanged.record( + Autocomplete.TopSitesSettingChangedExtra(sharedPreferences.all[key] as Boolean), + ) + + favoriteSitesAutocomplete.key -> + Autocomplete.favoriteSitesSettingChanged.record( + Autocomplete.FavoriteSitesSettingChangedExtra(sharedPreferences.all[key] as Boolean), + ) + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragment.kt new file mode 100644 index 0000000000..8f86e53ee0 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragment.kt @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.biometrics + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import mozilla.components.lib.auth.AuthenticationDelegate +import mozilla.components.lib.auth.BiometricPromptAuth +import mozilla.components.lib.auth.canUseBiometricFeature +import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import org.mozilla.focus.R +import org.mozilla.focus.ext.hideToolbar +import org.mozilla.focus.ext.requireComponents +import org.mozilla.focus.searchwidget.ExternalIntentNavigation +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.ui.theme.FocusTheme + +/** + * Fragment used to display biometric authentication when the app is locked. + */ +class BiometricAuthenticationFragment : Fragment(), AuthenticationDelegate { + @VisibleForTesting + internal val biometricPromptAuth = ViewBoundFeatureWrapper() + + @VisibleForTesting + internal val biometricErrorText = mutableStateOf("") + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return ComposeView(requireContext()).apply { + setBiometricPrompt(this) + setContent { + FocusTheme { + val biometricErrorText by biometricErrorText + BiometricPromptContent(biometricErrorText) { + showBiometricPrompt( + biometricPromptAuth.get(), + getString(R.string.biometric_prompt_title), + getString(R.string.biometric_prompt_subtitle), + ) + } + } + } + isTransitionGroup = true + } + } + + override fun onResume() { + super.onResume() + hideToolbar() + } + override fun onAuthError(errorText: String) { + biometricErrorText.value = errorText + } + + override fun onAuthFailure() { + // onAuthFailure + } + + override fun onAuthSuccess() { + onAuthenticated() + } + + @VisibleForTesting + internal fun showBiometricPrompt( + biometricPromptAuth: BiometricPromptAuth?, + title: String, + subtitle: String, + ) { + if (context?.canUseBiometricFeature() == true) { + biometricPromptAuth?.requestAuthentication( + title = title, + subtitle = subtitle, + ) + } + } + + private fun setBiometricPrompt(view: View) { + biometricPromptAuth.set( + feature = BiometricPromptAuth( + context = requireContext(), + fragment = this, + authenticationDelegate = this, + ), + owner = this, + view = view, + ) + } + + @VisibleForTesting + internal fun onAuthenticated() { + ExternalIntentNavigation.handleAppNavigation( + bundle = arguments, + context = requireContext(), + ) + + val tabId = requireComponents.store.state.selectedTabId + requireComponents.appStore.dispatch(AppAction.Unlock(tabId)) + dismiss() + } + + @VisibleForTesting + internal fun dismiss() { + requireActivity().supportFragmentManager.beginTransaction().remove(this).commitAllowingStateLoss() + } + + companion object { + const val FRAGMENT_TAG = "biometric-authentication-fragment" + + /** + * Creates a [BiometricAuthenticationFragment] with redirection to a destination from @param [bundle]. + */ + fun createWithDestinationData(bundle: Bundle? = null): BiometricAuthenticationFragment { + val fragment = BiometricAuthenticationFragment() + fragment.arguments = bundle + return fragment + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentCompose.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentCompose.kt new file mode 100644 index 0000000000..378e4b7150 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentCompose.kt @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.biometrics + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import mozilla.components.ui.colors.PhotonColors +import org.mozilla.focus.R +import org.mozilla.focus.ui.theme.FocusTheme +import org.mozilla.focus.ui.theme.focusTypography + +@Composable +@Preview +private fun BiometricPromptContentPreview() { + FocusTheme { + BiometricPromptContent("Fingerprint operation canceled by user.") {} + } +} + +/** + * Content of the biometric authentication prompt. + * @param biometricErrorText Text for an authentication error + * @param showBiometricPrompt callback for displaying the OS biometric authentication prompt + */ +@Composable +fun BiometricPromptContent(biometricErrorText: String, showBiometricPrompt: () -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .background( + brush = Brush.linearGradient( + colors = listOf( + colorResource(R.color.home_screen_modal_gradient_one), + colorResource(R.color.home_screen_modal_gradient_two), + colorResource(R.color.home_screen_modal_gradient_three), + colorResource(R.color.home_screen_modal_gradient_four), + colorResource(R.color.home_screen_modal_gradient_five), + colorResource(R.color.home_screen_modal_gradient_six), + ), + end = Offset(0f, Float.POSITIVE_INFINITY), + start = Offset(Float.POSITIVE_INFINITY, 0f), + ), + ), + ) { + Image( + painter = painterResource(R.drawable.wordmark2), + contentDescription = LocalContext.current.getString(R.string.app_name), + modifier = Modifier + .padding(start = 24.dp, end = 24.dp), + ) + Text( + style = focusTypography.onboardingButton, + color = Color.Red, + text = biometricErrorText, + modifier = Modifier.padding(top = 16.dp, bottom = 16.dp), + ) + ComponentShowBiometricPromptButton { + showBiometricPrompt() + } + } +} + +@Composable +private fun ComponentShowBiometricPromptButton(showBiometricPrompt: () -> Unit) { + Button( + onClick = showBiometricPrompt, + colors = ButtonDefaults.textButtonColors( + backgroundColor = colorResource(R.color.biometric_show_button_background), + ), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + ) { + Image( + painter = painterResource(R.drawable.ic_fingerprint), + contentDescription = LocalContext.current.getString(R.string.biometric_auth_image_description), + modifier = Modifier + .padding(end = 10.dp), + ) + Text( + color = PhotonColors.White, + text = AnnotatedString( + LocalContext.current.resources.getString( + R.string.show_biometric_button_text, + ), + ), + ) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/LockObserver.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/LockObserver.kt new file mode 100644 index 0000000000..7625effa9c --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/biometrics/LockObserver.kt @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.biometrics + +import android.content.Context +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.lib.auth.canUseBiometricFeature +import org.mozilla.focus.GleanMetrics.TabCount +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.settings +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.state.AppStore +import org.mozilla.focus.topsites.DefaultTopSitesStorage + +class LockObserver( + private val context: Context, + private val browserStore: BrowserStore, + private val appStore: AppStore, +) : DefaultLifecycleObserver { + + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + triggerAppLock() + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + triggerAppLock() + } + + @OptIn(DelicateCoroutinesApi::class) + private fun triggerAppLock() { + GlobalScope.launch(Dispatchers.IO) { + val tabCount = browserStore.state.privateTabs.size.toLong() + TabCount.appBackgrounded.accumulateSamples(listOf(tabCount)) + val topSitesList = context.components.topSitesStorage.getTopSites( + totalSites = DefaultTopSitesStorage.TOP_SITES_MAX_LIMIT, + frecencyConfig = null, + ) + if (tabCount == 0L && topSitesList.isEmpty()) { + return@launch + } + if (context.settings.shouldUseBiometrics() && + context.canUseBiometricFeature() + ) { + appStore.dispatch(AppAction.Lock()) + } + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/BlockedTrackersMiddleware.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/BlockedTrackersMiddleware.kt new file mode 100644 index 0000000000..4fc2de851f --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/BlockedTrackersMiddleware.kt @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.browser + +import android.content.Context +import androidx.preference.PreferenceManager +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.TrackingProtectionAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import org.mozilla.focus.R +import org.mozilla.focus.ext.settings + +/** + * [Middleware] to record the number of blocked trackers in response to [BrowserAction]s. + * @param context The application context. + */ +class BlockedTrackersMiddleware( + private val context: Context, +) : Middleware { + + private val settings = context.settings + private val preferences = PreferenceManager.getDefaultSharedPreferences(context) + + override fun invoke( + context: MiddlewareContext, + next: (BrowserAction) -> Unit, + action: BrowserAction, + ) { + when (action) { + is TrackingProtectionAction.TrackerBlockedAction -> { + incrementCount() + } + else -> { + // no-op + } + } + + next(action) + } + + private fun incrementCount() { + val blockedTrackersCount = settings.getTotalBlockedTrackersCount() + preferences + .edit() + .putInt( + context.getString(R.string.pref_key_privacy_total_trackers_blocked_count), + blockedTrackersCount + 1, + ) + .apply() + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/LocalizedContent.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/LocalizedContent.kt new file mode 100644 index 0000000000..304fe4b281 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/LocalizedContent.kt @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.browser + +import android.content.Context +import android.content.pm.PackageManager +import android.view.View +import androidx.collection.ArrayMap +import mozilla.components.support.utils.ext.getPackageInfoCompat +import org.mozilla.focus.R +import org.mozilla.focus.locale.Locales +import org.mozilla.focus.utils.HtmlLoader +import org.mozilla.focus.utils.SupportUtils.manifestoURL +import org.mozilla.geckoview.BuildConfig +import java.util.Locale + +object LocalizedContent { + // We can't use "about:" because webview silently swallows about: pages, hence we use + // a custom scheme. + const val URL_ABOUT = "focus:about" + const val URL_RIGHTS = "focus:rights" + const val URL_GPL = "focus:gpl" + const val URL_LICENSES = "focus:licenses" + + /** + * Load the content for focus:about + */ + fun loadAbout(context: Context): String { + val resources = Locales.getLocalizedResources(context) + val substitutionMap: MutableMap = ArrayMap() + val appName = context.resources.getString(R.string.app_name) + val learnMoreURL = manifestoURL + var aboutVersion = "" + try { + val engineIndicator = " \uD83E\uDD8E " + BuildConfig.MOZ_APP_VERSION + "-" + + BuildConfig.MOZ_APP_BUILDID + val packageInfo = context.packageManager.getPackageInfoCompat(context.packageName, 0) + @Suppress("DEPRECATION") + aboutVersion = String.format( + Locale.US, + "%s (Build #%s)", + packageInfo.versionName, + packageInfo.versionCode.toString() + engineIndicator, + ) + } catch (e: PackageManager.NameNotFoundException) { + // Nothing to do if we can't find the package name. + } + substitutionMap["%about-version%"] = aboutVersion + val aboutContent = resources.getString(R.string.about_content, appName, learnMoreURL) + substitutionMap["%about-content%"] = aboutContent + val wordmark = HtmlLoader.loadPngAsDataURI(context, R.drawable.wordmark2) + substitutionMap["%wordmark%"] = wordmark + putLayoutDirectionIntoMap(substitutionMap, context) + return HtmlLoader.loadResourceFile(context, R.raw.about, substitutionMap) + } + + /** + * Load the content for focus:rights + */ + fun loadRights(context: Context): String { + val resources = Locales.getLocalizedResources(context) + val substitutionMap: MutableMap = ArrayMap() + val appName = context.resources.getString(R.string.app_name) + val mplUrl = "https://www.mozilla.org/en-US/MPL/" + val trademarkPolicyUrl = "https://www.mozilla.org/foundation/trademarks/policy/" + val gplUrl = "focus:gpl" + val trackingProtectionUrl = "https://wiki.mozilla.org/Security/Tracking_protection#Lists" + val licensesUrl = "focus:licenses" + val content1 = resources.getString(R.string.your_rights_content1, appName) + substitutionMap["%your-rights-content1%"] = content1 + val content2 = resources.getString(R.string.your_rights_content2, appName, mplUrl) + substitutionMap["%your-rights-content2%"] = content2 + val content3 = resources.getString(R.string.your_rights_content3, appName, trademarkPolicyUrl) + substitutionMap["%your-rights-content3%"] = content3 + val content4 = resources.getString(R.string.your_rights_content4, appName, licensesUrl) + substitutionMap["%your-rights-content4%"] = content4 + val content5 = resources.getString(R.string.your_rights_content5, appName, gplUrl, trackingProtectionUrl) + substitutionMap["%your-rights-content5%"] = content5 + putLayoutDirectionIntoMap(substitutionMap, context) + return HtmlLoader.loadResourceFile(context, R.raw.rights, substitutionMap) + } + + fun loadLicenses(context: Context): String { + return HtmlLoader.loadResourceFile(context, R.raw.licenses, emptyMap()) + } + + fun loadGPL(context: Context): String { + return HtmlLoader.loadResourceFile(context, R.raw.gpl, emptyMap()) + } + + private fun putLayoutDirectionIntoMap(substitutionMap: MutableMap, context: Context) { + val direction: String = when (context.resources.configuration.layoutDirection) { + View.LAYOUT_DIRECTION_LTR -> { + "ltr" + } + View.LAYOUT_DIRECTION_RTL -> { + "rtl" + } + else -> { + "auto" + } + } + substitutionMap["%dir%"] = direction + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserMenuController.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserMenuController.kt new file mode 100644 index 0000000000..7deb9d9c99 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserMenuController.kt @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.browser.integration + +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.top.sites.TopSitesUseCases +import org.mozilla.focus.GleanMetrics.BrowserMenu +import org.mozilla.focus.GleanMetrics.CustomTabsToolbar +import org.mozilla.focus.GleanMetrics.Shortcuts +import org.mozilla.focus.ext.titleOrDomain +import org.mozilla.focus.menu.ToolbarMenu +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.state.AppStore +import org.mozilla.focus.state.Screen + +@Suppress("LongParameterList") +class BrowserMenuController( + private val sessionUseCases: SessionUseCases, + private val appStore: AppStore, + private val store: BrowserStore, + private val topSitesUseCases: TopSitesUseCases, + private val currentTabId: String, + private val shareCallback: () -> Unit, + private val requestDesktopCallback: (isChecked: Boolean) -> Unit, + private val addToHomeScreenCallback: () -> Unit, + private val showFindInPageCallback: () -> Unit, + private val openInCallback: () -> Unit, + private val openInBrowser: () -> Unit, + private val showShortcutAddedSnackBar: () -> Unit, +) { + @VisibleForTesting + private val currentTab: SessionState? + get() = store.state.findTabOrCustomTabOrSelectedTab(currentTabId) + + @VisibleForTesting + internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO) + + @Suppress("ComplexMethod") + fun handleMenuInteraction(item: ToolbarMenu.Item) { + recordBrowserMenuTelemetry(item) + + when (item) { + is ToolbarMenu.Item.Back, ToolbarMenu.CustomTabItem.Back -> sessionUseCases.goBack( + currentTabId, + ) + is ToolbarMenu.Item.Forward, ToolbarMenu.CustomTabItem.Forward -> sessionUseCases.goForward( + currentTabId, + ) + is ToolbarMenu.Item.Reload, ToolbarMenu.CustomTabItem.Reload -> { + sessionUseCases.reload(currentTabId) + } + is ToolbarMenu.Item.Stop, ToolbarMenu.CustomTabItem.Stop -> sessionUseCases.stopLoading( + currentTabId, + ) + is ToolbarMenu.Item.Share -> shareCallback() + is ToolbarMenu.Item.FindInPage, ToolbarMenu.CustomTabItem.FindInPage -> showFindInPageCallback() + is ToolbarMenu.Item.AddToShortcuts -> { + ioScope.launch { + currentTab?.let { state -> + topSitesUseCases.addPinnedSites( + title = state.content.titleOrDomain, + url = state.content.url, + ) + } + } + showShortcutAddedSnackBar() + } + is ToolbarMenu.Item.RemoveFromShortcuts -> { + ioScope.launch { + currentTab?.let { state -> + appStore.state.topSites.find { it.url == state.content.url } + ?.let { topSite -> + topSitesUseCases.removeTopSites(topSite) + } + } + } + } + is ToolbarMenu.Item.RequestDesktop -> requestDesktopCallback(item.isChecked) + is ToolbarMenu.CustomTabItem.RequestDesktop -> requestDesktopCallback(item.isChecked) + is ToolbarMenu.Item.AddToHomeScreen, ToolbarMenu.CustomTabItem.AddToHomeScreen -> addToHomeScreenCallback() + is ToolbarMenu.CustomTabItem.OpenInBrowser -> openInBrowser() + is ToolbarMenu.Item.OpenInApp, ToolbarMenu.CustomTabItem.OpenInApp -> openInCallback() + is ToolbarMenu.Item.Settings -> appStore.dispatch(AppAction.OpenSettings(page = Screen.Settings.Page.Start)) + } + } + + @Suppress("LongMethod") + @VisibleForTesting + internal fun recordBrowserMenuTelemetry(item: ToolbarMenu.Item) { + when (item) { + is ToolbarMenu.Item.Back -> BrowserMenu.navigationToolbarAction.record( + BrowserMenu.NavigationToolbarActionExtra("back"), + ) + is ToolbarMenu.Item.Forward -> BrowserMenu.navigationToolbarAction.record( + BrowserMenu.NavigationToolbarActionExtra("forward"), + ) + is ToolbarMenu.Item.Reload -> { + BrowserMenu.navigationToolbarAction.record( + BrowserMenu.NavigationToolbarActionExtra("reload"), + ) + } + is ToolbarMenu.Item.Stop -> BrowserMenu.navigationToolbarAction.record( + BrowserMenu.NavigationToolbarActionExtra("stop"), + ) + is ToolbarMenu.Item.Share -> BrowserMenu.navigationToolbarAction.record( + BrowserMenu.NavigationToolbarActionExtra("share"), + ) + is ToolbarMenu.Item.FindInPage -> BrowserMenu.browserMenuAction.record( + BrowserMenu.BrowserMenuActionExtra("find_in_page"), + ) + is ToolbarMenu.Item.AddToShortcuts -> + Shortcuts.shortcutAddedCounter.add() + is ToolbarMenu.Item.RemoveFromShortcuts -> + Shortcuts.shortcutRemovedCounter["removed_from_browser_menu"].add() + + is ToolbarMenu.Item.RequestDesktop -> { + if (item.isChecked) { + BrowserMenu.browserMenuAction.record( + BrowserMenu.BrowserMenuActionExtra("desktop_view_on"), + ) + } else { + BrowserMenu.browserMenuAction.record( + BrowserMenu.BrowserMenuActionExtra("desktop_view_off"), + ) + } + } + is ToolbarMenu.Item.AddToHomeScreen -> BrowserMenu.browserMenuAction.record( + BrowserMenu.BrowserMenuActionExtra("add_to_home_screen"), + ) + + is ToolbarMenu.Item.OpenInApp -> BrowserMenu.browserMenuAction.record( + BrowserMenu.BrowserMenuActionExtra("open_in_app"), + ) + is ToolbarMenu.Item.Settings -> BrowserMenu.browserMenuAction.record( + BrowserMenu.BrowserMenuActionExtra("settings"), + ) + + // custom tabs + ToolbarMenu.CustomTabItem.Back -> CustomTabsToolbar.navigationToolbarAction.record( + CustomTabsToolbar.NavigationToolbarActionExtra("back"), + ) + ToolbarMenu.CustomTabItem.Forward -> CustomTabsToolbar.navigationToolbarAction.record( + CustomTabsToolbar.NavigationToolbarActionExtra("forward"), + ) + ToolbarMenu.CustomTabItem.Stop -> CustomTabsToolbar.navigationToolbarAction.record( + CustomTabsToolbar.NavigationToolbarActionExtra("stop"), + ) + + ToolbarMenu.CustomTabItem.Reload -> { + CustomTabsToolbar.navigationToolbarAction.record( + CustomTabsToolbar.NavigationToolbarActionExtra("reload"), + ) + } + + ToolbarMenu.CustomTabItem.AddToHomeScreen -> CustomTabsToolbar.browserMenuAction.record( + CustomTabsToolbar.BrowserMenuActionExtra("add_to_home_screen"), + ) + ToolbarMenu.CustomTabItem.OpenInApp -> CustomTabsToolbar.browserMenuAction.record( + CustomTabsToolbar.BrowserMenuActionExtra("open_in_app"), + ) + ToolbarMenu.CustomTabItem.OpenInBrowser -> CustomTabsToolbar.browserMenuAction.record( + CustomTabsToolbar.BrowserMenuActionExtra("open_in_browser"), + ) + + ToolbarMenu.CustomTabItem.FindInPage -> CustomTabsToolbar.browserMenuAction.record( + CustomTabsToolbar.BrowserMenuActionExtra("find_in_page"), + ) + is ToolbarMenu.CustomTabItem.RequestDesktop -> { + if (item.isChecked) { + CustomTabsToolbar.browserMenuAction.record( + CustomTabsToolbar.BrowserMenuActionExtra("desktop_view_on"), + ) + } else { + CustomTabsToolbar.browserMenuAction.record( + CustomTabsToolbar.BrowserMenuActionExtra("desktop_view_off"), + ) + } + } + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegration.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegration.kt new file mode 100644 index 0000000000..e3f8db2f89 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegration.kt @@ -0,0 +1,501 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.browser.integration + +import android.graphics.Color +import android.widget.LinearLayout +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.AppCompatEditText +import androidx.compose.material.Text +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.children +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.mapNotNull +import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.browser.toolbar.display.DisplayToolbar.Indicators +import mozilla.components.compose.cfr.CFRPopup +import mozilla.components.compose.cfr.CFRPopupProperties +import mozilla.components.feature.customtabs.CustomTabsToolbarFeature +import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.tabs.CustomTabsUseCases +import mozilla.components.feature.tabs.toolbar.TabCounterToolbarButton +import mozilla.components.feature.toolbar.ToolbarBehaviorController +import mozilla.components.feature.toolbar.ToolbarPresenter +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.ktx.android.view.hideKeyboard +import org.mozilla.focus.GleanMetrics.TabCount +import org.mozilla.focus.GleanMetrics.TrackingProtection +import org.mozilla.focus.R +import org.mozilla.focus.cookiebanner.CookieBannerOption +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.isCustomTab +import org.mozilla.focus.ext.isTablet +import org.mozilla.focus.ext.requireComponents +import org.mozilla.focus.ext.settings +import org.mozilla.focus.fragment.BrowserFragment +import org.mozilla.focus.menu.browser.CustomTabMenu +import org.mozilla.focus.nimbus.FocusNimbus +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.state.Screen +import org.mozilla.focus.ui.theme.focusTypography +import org.mozilla.focus.utils.ClickableSubstringLink + +@Suppress("LongParameterList", "LargeClass", "TooManyFunctions") +class BrowserToolbarIntegration( + private val store: BrowserStore, + private val toolbar: BrowserToolbar, + private val fragment: BrowserFragment, + controller: BrowserMenuController, + sessionUseCases: SessionUseCases, + customTabsUseCases: CustomTabsUseCases, + private val onUrlLongClicked: () -> Boolean, + private val eraseActionListener: () -> Unit, + private val tabCounterListener: () -> Unit, + private val customTabId: String? = null, + inTesting: Boolean = false, +) : LifecycleAwareFeature { + private val presenter = ToolbarPresenter( + toolbar, + store, + customTabId, + ) + + @VisibleForTesting + internal var securityIndicatorScope: CoroutineScope? = null + + @VisibleForTesting + internal var eraseTabsCfrScope: CoroutineScope? = null + + @VisibleForTesting + internal var trackingProtectionCfrScope: CoroutineScope? = null + + @VisibleForTesting + internal var cookieBannerCfrScope: CoroutineScope? = null + + private var tabsCounterScope: CoroutineScope? = null + private var customTabsFeature: CustomTabsToolbarFeature? = null + private var navigationButtonsIntegration: NavigationButtonsIntegration? = null + private val eraseAction = BrowserToolbar.Button( + imageDrawable = AppCompatResources.getDrawable( + toolbar.context, + R.drawable.mozac_ic_delete_24, + )!!, + contentDescription = toolbar.context.getString(R.string.content_description_erase), + iconTintColorResource = R.color.primaryText, + listener = { + val openedTabs = store.state.tabs.size + TabCount.eraseButtonTapped.record(TabCount.EraseButtonTappedExtra(openedTabs)) + + eraseActionListener.invoke() + }, + ) + private val tabsAction = TabCounterToolbarButton( + lifecycleOwner = fragment, + showTabs = { + toolbar.hideKeyboard() + tabCounterListener.invoke() + }, + store = store, + ) + + @VisibleForTesting + internal var toolbarController = ToolbarBehaviorController(toolbar, store, customTabId) + + init { + val context = toolbar.context + + toolbar.display.apply { + colors = colors.copy( + hint = ContextCompat.getColor(toolbar.context, R.color.urlBarHintText), + securityIconInsecure = Color.TRANSPARENT, + text = ContextCompat.getColor(toolbar.context, R.color.primaryText), + menu = ContextCompat.getColor(toolbar.context, R.color.primaryText), + ) + + addTrackingProtectionIndicator() + + displayIndicatorSeparator = false + + setOnSiteSecurityClickedListener { + TrackingProtection.toolbarShieldClicked.add() + fragment.initCookieBanner() + fragment.showTrackingProtectionPanel() + } + + onUrlClicked = { + fragment.edit() + false // Do not switch to edit mode + } + + setOnUrlLongClickListener { onUrlLongClicked() } + + icons = icons.copy( + trackingProtectionTrackersBlocked = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_shield_24, + )!!, + trackingProtectionNothingBlocked = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_shield_24, + )!!, + trackingProtectionException = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_shield_slash_24, + )!!, + ) + } + + toolbar.display.setOnTrackingProtectionClickedListener { + TrackingProtection.toolbarShieldClicked.add() + fragment.initCookieBanner() + fragment.showTrackingProtectionPanel() + } + + if (customTabId != null) { + val menu = CustomTabMenu( + context = fragment.requireContext(), + store = store, + currentTabId = customTabId, + onItemTapped = { controller.handleMenuInteraction(it) }, + ) + customTabsFeature = CustomTabsToolbarFeature( + store, + toolbar, + sessionId = customTabId, + useCases = customTabsUseCases, + menuBuilder = menu.menuBuilder, + window = fragment.activity?.window, + menuItemIndex = menu.menuBuilder.items.size - 1, + closeListener = { fragment.closeCustomTab() }, + updateTheme = true, + forceActionButtonTinting = false, + ) + } + + val isCustomTab = store.state.findCustomTabOrSelectedTab(customTabId)?.isCustomTab() + + if (context.isTablet() && isCustomTab == false) { + navigationButtonsIntegration = NavigationButtonsIntegration( + context, + store, + toolbar, + sessionUseCases, + customTabId, + ) + } + + if (isCustomTab == false) { + toolbar.addNavigationAction(eraseAction) + if (!inTesting) { + setUrlBackground() + } + } + } + + // Use the same background for display/edit modes. + private fun setUrlBackground() { + val urlBackground = ResourcesCompat.getDrawable( + fragment.resources, + R.drawable.toolbar_url_background, + fragment.context?.theme, + ) + toolbar.display.setUrlBackground(urlBackground) + } + + private fun setBrowserActionButtons() { + tabsCounterScope = store.flowScoped { flow -> + flow.distinctUntilChangedBy { state -> state.tabs.size > 1 } + .collect { state -> + if (state.tabs.size > 1) { + toolbar.addBrowserAction(tabsAction) + } else { + toolbar.removeBrowserAction(tabsAction) + } + } + } + } + + override fun start() { + presenter.start() + toolbarController.start() + customTabsFeature?.start() + navigationButtonsIntegration?.start() + observerSecurityIndicatorChanges() + if (store.state.findCustomTabOrSelectedTab(customTabId)?.isCustomTab() == false) { + setBrowserActionButtons() + observeEraseCfr() + } + + if (fragment.requireContext().settings.shouldShowCookieBannerCfr && + fragment.requireContext().settings.isCookieBannerEnable && + fragment.requireContext().settings.getCurrentCookieBannerOptionFromSharePref() == + CookieBannerOption.CookieBannerRejectAll() + ) { + observeCookieBannerCfr() + } + + observeTrackingProtectionCfr() + } + + @VisibleForTesting + internal fun observeEraseCfr() { + eraseTabsCfrScope = fragment.components?.appStore?.flowScoped { flow -> + flow.mapNotNull { state -> state.showEraseTabsCfr } + .distinctUntilChanged() + .collect { showEraseCfr -> + if (showEraseCfr) { + val eraseActionView = + toolbar.findViewById(R.id.mozac_browser_toolbar_navigation_actions) + .children + .last() + CFRPopup( + anchor = eraseActionView, + properties = CFRPopupProperties( + popupWidth = 256.dp, + popupAlignment = CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR, + popupBodyColors = listOf( + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_end_color, + ), + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_start_color, + ), + ), + dismissButtonColor = ContextCompat.getColor( + fragment.requireContext(), + R.color.cardview_light_background, + ), + popupVerticalOffset = 0.dp, + ), + onDismiss = { onDismissEraseTabsCfr() }, + text = { + Text( + style = focusTypography.cfrTextStyle, + text = fragment.getString(R.string.cfr_for_toolbar_delete_icon2), + color = colorResource(R.color.cfr_text_color), + ) + }, + ).apply { + show() + } + } + } + } + } + + private fun onDismissEraseTabsCfr() { + fragment.components?.appStore?.dispatch(AppAction.ShowEraseTabsCfrChange(false)) + } + + @VisibleForTesting + internal fun observeCookieBannerCfr() { + cookieBannerCfrScope = fragment.components?.appStore?.flowScoped { flow -> + flow.mapNotNull { state -> state.showCookieBannerCfr } + .distinctUntilChanged() + .collect { showCookieBannerCfr -> + if (showCookieBannerCfr) { + CFRPopup( + anchor = toolbar.findViewById(R.id.mozac_browser_toolbar_background), + properties = CFRPopupProperties( + popupWidth = 256.dp, + popupAlignment = CFRPopup.PopupAlignment.BODY_TO_ANCHOR_START, + popupBodyColors = listOf( + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_end_color, + ), + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_start_color, + ), + ), + dismissButtonColor = ContextCompat.getColor( + fragment.requireContext(), + R.color.cardview_light_background, + ), + popupVerticalOffset = 0.dp, + indicatorArrowStartOffset = 10.dp, + ), + onDismiss = { onDismissCookieBannerCfr() }, + text = { + val textCookieBannerCfr = stringResource( + id = R.string.cfr_cookie_banner, + LocalContext.current.getString(R.string.onboarding_short_app_name), + LocalContext.current.getString(R.string.cfr_cookie_banner_link), + ) + ClickableSubstringLink( + text = textCookieBannerCfr, + style = focusTypography.cfrCookieBannerTextStyle, + linkTextDecoration = TextDecoration.Underline, + clickableStartIndex = textCookieBannerCfr.indexOf( + LocalContext.current.getString( + R.string.cfr_cookie_banner_link, + ), + ), + clickableEndIndex = textCookieBannerCfr.length, + onClick = { + fragment.requireComponents.appStore.dispatch( + AppAction.OpenSettings(Screen.Settings.Page.CookieBanner), + ) + onDismissCookieBannerCfr() + }, + ) + }, + ).apply { + show() + stopObserverCookieBannerCfrChanges() + } + } + } + } + } + + @VisibleForTesting + internal fun observeTrackingProtectionCfr() { + trackingProtectionCfrScope = fragment.components?.appStore?.flowScoped { flow -> + flow.mapNotNull { state -> state.showTrackingProtectionCfrForTab } + .distinctUntilChanged() + .collect { showTrackingProtectionCfrForTab -> + if (showTrackingProtectionCfrForTab[store.state.selectedTabId] == true) { + CFRPopup( + anchor = toolbar.findViewById( + R.id.mozac_browser_toolbar_tracking_protection_indicator, + ), + properties = CFRPopupProperties( + popupWidth = 256.dp, + popupAlignment = CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR, + popupBodyColors = listOf( + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_end_color, + ), + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_start_color, + ), + ), + dismissButtonColor = ContextCompat.getColor( + fragment.requireContext(), + R.color.cardview_light_background, + ), + popupVerticalOffset = 0.dp, + ), + onDismiss = { onDismissTrackingProtectionCfr() }, + text = { + Text( + style = focusTypography.cfrTextStyle, + text = fragment.getString(R.string.cfr_for_toolbar_shield_icon2), + color = colorResource(R.color.cfr_text_color), + ) + }, + ).apply { + show() + } + } + } + } + } + + private fun onDismissCookieBannerCfr() { + fragment.components?.appStore?.dispatch( + AppAction.ShowCookieBannerCfrChange( + false, + ), + ) + fragment.requireContext().settings.shouldShowCookieBannerCfr = false + } + + private fun onDismissTrackingProtectionCfr() { + store.state.selectedTabId?.let { + fragment.components?.appStore?.dispatch( + AppAction.ShowTrackingProtectionCfrChange( + mapOf( + it to false, + ), + ), + ) + } + fragment.requireContext().settings.shouldShowCfrForTrackingProtection = false + FocusNimbus.features.onboarding.recordExposure() + fragment.components?.appStore?.dispatch(AppAction.ShowEraseTabsCfrChange(true)) + } + + @VisibleForTesting + internal fun observerSecurityIndicatorChanges() { + securityIndicatorScope = store.flowScoped { flow -> + flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(customTabId) } + .distinctUntilChangedBy { tab -> tab.content.securityInfo } + .collect { + val secure = it.content.securityInfo.secure + val url = it.content.url + if (secure && Indicators.SECURITY in toolbar.display.indicators) { + addTrackingProtectionIndicator() + } else if (!secure && Indicators.SECURITY !in toolbar.display.indicators && + !url.trim().startsWith("about:") + ) { + addSecurityIndicator() + } + } + } + } + + override fun stop() { + presenter.stop() + toolbarController.stop() + customTabsFeature?.stop() + navigationButtonsIntegration?.stop() + stopObserverSecurityIndicatorChanges() + toolbar.removeBrowserAction(tabsAction) + tabsCounterScope?.cancel() + stopObserverEraseTabsCfrChanges() + stopObserverTrackingProtectionCfrChanges() + stopObserverCookieBannerCfrChanges() + } + + @VisibleForTesting + internal fun stopObserverTrackingProtectionCfrChanges() { + trackingProtectionCfrScope?.cancel() + } + + @VisibleForTesting + internal fun stopObserverEraseTabsCfrChanges() { + eraseTabsCfrScope?.cancel() + } + + @VisibleForTesting + internal fun stopObserverSecurityIndicatorChanges() { + securityIndicatorScope?.cancel() + } + + @VisibleForTesting + internal fun stopObserverCookieBannerCfrChanges() { + cookieBannerCfrScope?.cancel() + } + + @VisibleForTesting + internal fun addSecurityIndicator() { + toolbar.display.indicators = listOf(Indicators.SECURITY) + } + + @VisibleForTesting + internal fun addTrackingProtectionIndicator() { + toolbar.display.indicators = listOf(Indicators.TRACKING_PROTECTION) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/FindInPageIntegration.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/FindInPageIntegration.kt new file mode 100644 index 0000000000..18742f2994 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/FindInPageIntegration.kt @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.browser.integration + +import androidx.core.view.isVisible +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.concept.engine.EngineView +import mozilla.components.feature.findinpage.FindInPageFeature +import mozilla.components.feature.findinpage.view.FindInPageBar +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.feature.UserInteractionHandler + +class FindInPageIntegration( + store: BrowserStore, + private val findInPageView: FindInPageBar, + private val browserToolbar: BrowserToolbar, + engineView: EngineView, +) : LifecycleAwareFeature, UserInteractionHandler { + private val feature = FindInPageFeature( + store, + findInPageView, + engineView, + ::hide, + ) + + override fun start() { + feature.start() + } + + override fun stop() { + feature.stop() + } + + override fun onBackPressed(): Boolean { + return feature.onBackPressed() + } + + fun show(sessionState: SessionState) { + findInPageView.isVisible = true + // Hiding the toolbar prevents Talkback from dictating its elements. + browserToolbar.isVisible = false + feature.bind(sessionState) + } + + fun hide() { + findInPageView.isVisible = false + browserToolbar.isVisible = true + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/FullScreenIntegration.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/FullScreenIntegration.kt new file mode 100644 index 0000000000..a003636912 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/FullScreenIntegration.kt @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.browser.integration + +import android.app.Activity +import android.os.Build +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.concept.engine.EngineView +import mozilla.components.feature.prompts.dialog.FullScreenNotification +import mozilla.components.feature.prompts.dialog.FullScreenNotificationDialog +import mozilla.components.feature.session.FullScreenFeature +import mozilla.components.feature.session.SessionUseCases +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.base.feature.UserInteractionHandler +import mozilla.components.support.ktx.android.view.enterImmersiveMode +import mozilla.components.support.ktx.android.view.exitImmersiveMode +import org.mozilla.focus.R +import org.mozilla.focus.ext.disableDynamicBehavior +import org.mozilla.focus.ext.enableDynamicBehavior +import org.mozilla.focus.ext.hide +import org.mozilla.focus.ext.showAsFixed +import org.mozilla.focus.utils.Settings + +@Suppress("LongParameterList") +class FullScreenIntegration( + val activity: Activity, + val store: BrowserStore, + tabId: String?, + sessionUseCases: SessionUseCases, + private val settings: Settings, + private val toolbarView: BrowserToolbar, + private val statusBar: View, + private val engineView: EngineView, + private val parentFragmentManager: FragmentManager, +) : LifecycleAwareFeature, UserInteractionHandler { + @VisibleForTesting + internal var feature = FullScreenFeature( + store, + sessionUseCases, + tabId, + ::viewportFitChanged, + ::fullScreenChanged, + ) + + override fun start() { + feature.start() + } + + override fun stop() { + feature.stop() + } + + @VisibleForTesting + internal fun fullScreenChanged( + enabled: Boolean, + fullScreenNotification: FullScreenNotification = + FullScreenNotificationDialog(R.layout.dialog_full_screen_notification), + ) { + if (enabled) { + enterBrowserFullscreen() + statusBar.isVisible = false + + fullScreenNotification.show(parentFragmentManager) + + switchToImmersiveMode() + } else { + // If the video is in PiP, but is not in fullscreen anymore we should move the task containing + // this activity to the back of the activity stack + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInPictureInPictureMode) { + activity.moveTaskToBack(false) + } + statusBar.isVisible = true + exitBrowserFullscreen() + + exitImmersiveMode() + } + } + + override fun onBackPressed(): Boolean { + return feature.onBackPressed() + } + + @VisibleForTesting + internal fun viewportFitChanged(viewportFit: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + activity.window.attributes.layoutInDisplayCutoutMode = viewportFit + } + } + + /** + * Hide system bars. They can be revealed temporarily with system gestures, such as swiping from + * the top of the screen. These transient system bars will overlay app’s content, may have some + * degree of transparency, and will automatically hide after a short timeout. + */ + @VisibleForTesting + internal fun switchToImmersiveMode() { + activity.enterImmersiveMode() + } + + /** + * Show the system bars again. + */ + fun exitImmersiveMode() { + activity.exitImmersiveMode() + } + + @VisibleForTesting + internal fun enterBrowserFullscreen() { + if (settings.isAccessibilityEnabled()) { + toolbarView.hide(engineView) + } else { + toolbarView.collapse() + toolbarView.disableDynamicBehavior(engineView) + } + } + + @VisibleForTesting + internal fun exitBrowserFullscreen() { + if (settings.isAccessibilityEnabled()) { + toolbarView.showAsFixed(activity, engineView) + } else { + toolbarView.enableDynamicBehavior(activity, engineView) + toolbarView.expand() + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/NavigationButtonsIntegration.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/NavigationButtonsIntegration.kt new file mode 100644 index 0000000000..056d8bfb3a --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/NavigationButtonsIntegration.kt @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.browser.integration + +import android.content.Context +import androidx.core.content.ContextCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.toolbar.BrowserToolbar +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.kotlinx.coroutines.flow.ifAnyChanged +import mozilla.components.support.utils.ColorUtils +import org.mozilla.focus.R +import org.mozilla.focus.ext.ifCustomTab +import org.mozilla.focus.theme.resolveAttribute + +class NavigationButtonsIntegration( + val context: Context, + val store: BrowserStore, + val toolbar: BrowserToolbar, + private val sessionUseCases: SessionUseCases, + private val customTabId: String?, +) : LifecycleAwareFeature { + private var scope: CoroutineScope? = null + + private var enabledColorRes = context.theme.resolveAttribute(R.attr.primaryText) + private var disabledColorRes = context.theme.resolveAttribute(R.attr.disabled) + + init { + store.state.findCustomTabOrSelectedTab(customTabId)?.ifCustomTab()?.let { sessionState -> + sessionState.config.colorSchemes?.defaultColorSchemeParams?.toolbarColor?.let { color -> + if (!ColorUtils.isDark(color)) { + enabledColorRes = R.color.enabled_button_tint + disabledColorRes = R.color.disabled + } + } + } + + val backButton = BrowserToolbar.TwoStateButton( + primaryImage = ContextCompat.getDrawable(context, R.drawable.mozac_ic_back_24)!!, + primaryContentDescription = context.getString(R.string.content_description_back), + primaryImageTintResource = enabledColorRes, + isInPrimaryState = { + store.state.findCustomTabOrSelectedTab(customTabId)?.content?.canGoBack + ?: false + }, + secondaryImageTintResource = disabledColorRes, + disableInSecondaryState = true, + longClickListener = null, + listener = { + sessionUseCases.goBack(store.state.findCustomTabOrSelectedTab(customTabId)?.id) + }, + ) + toolbar.addNavigationAction(backButton) + + val forwardButton = BrowserToolbar.TwoStateButton( + primaryImage = ContextCompat.getDrawable(context, R.drawable.mozac_ic_forward_24)!!, + primaryContentDescription = context.getString(R.string.content_description_forward), + primaryImageTintResource = enabledColorRes, + isInPrimaryState = { + store.state.findCustomTabOrSelectedTab(customTabId)?.content?.canGoForward + ?: false + }, + secondaryImageTintResource = disabledColorRes, + disableInSecondaryState = true, + longClickListener = null, + listener = { + sessionUseCases.goForward(store.state.findCustomTabOrSelectedTab(customTabId)?.id) + }, + ) + toolbar.addNavigationAction(forwardButton) + + val reloadOrStopButton = BrowserToolbar.TwoStateButton( + primaryImage = ContextCompat.getDrawable(context, R.drawable.mozac_ic_stop)!!, + secondaryImage = ContextCompat.getDrawable(context, R.drawable.mozac_ic_arrow_clockwise_24)!!, + primaryContentDescription = context.getString(R.string.content_description_stop), + secondaryContentDescription = context.getString(R.string.content_description_reload), + primaryImageTintResource = enabledColorRes, + isInPrimaryState = { + store.state.findCustomTabOrSelectedTab(customTabId)?.content?.loading ?: false + }, + secondaryImageTintResource = enabledColorRes, + disableInSecondaryState = false, + longClickListener = null, + listener = { + val tab = store.state.findCustomTabOrSelectedTab(customTabId) + ?: return@TwoStateButton + if (tab.content.loading) { + sessionUseCases.stopLoading(tab.id) + } else { + sessionUseCases.reload(tab.id) + } + }, + ) + toolbar.addNavigationAction(reloadOrStopButton) + } + + override fun start() { + scope = store.flowScoped { flow -> + flow.map { state -> state.findCustomTabOrSelectedTab(customTabId) } + .ifAnyChanged { tab -> + arrayOf( + tab?.content?.canGoBack, + tab?.content?.canGoForward, + tab?.content?.loading, + ) + } + .collect { toolbar.invalidateActions() } + } + } + + override fun stop() { + scope?.cancel() + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cfr/CfrMiddleware.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cfr/CfrMiddleware.kt new file mode 100644 index 0000000000..16ea8393f6 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cfr/CfrMiddleware.kt @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.cfr + +import android.content.Context +import androidx.core.net.toUri +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.CookieBannerAction +import mozilla.components.browser.state.action.TrackingProtectionAction +import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.concept.engine.EngineSession +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.service.glean.private.NoExtras +import org.mozilla.focus.GleanMetrics.CookieBanner +import org.mozilla.focus.cookiebanner.CookieBannerOption +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.settings +import org.mozilla.focus.ext.truncatedHost +import org.mozilla.focus.nimbus.FocusNimbus +import org.mozilla.focus.nimbus.Onboarding +import org.mozilla.focus.state.AppAction + +/** + * Middleware used to intercept browser store actions in order to decide when should we display a specific CFR + */ +class CfrMiddleware(private val appContext: Context) : Middleware { + private val onboardingFeature = FocusNimbus.features.onboarding + private lateinit var onboardingConfig: Onboarding + private val components = appContext.components + private var isCurrentTabSecure = false + private var tpExposureAlreadyRecorded = false + + override fun invoke( + context: MiddlewareContext, + next: (BrowserAction) -> Unit, + action: BrowserAction, + ) { + onboardingConfig = onboardingFeature.value() + if (onboardingConfig.isCfrEnabled) { + next(action) + showCookieBannerCfr(action) + showTrackingProtectionCfr(action, context) + } else { + next(action) + } + } + + private fun showCookieBannerCfr( + action: BrowserAction, + ) { + if (action is CookieBannerAction.UpdateStatusAction && + shouldShowCookieBannerCfr(action) && + otherCfrHasBeenShown() + ) { + CookieBanner.cookieBannerCfrShown.record(NoExtras()) + components.appStore.dispatch( + AppAction.ShowCookieBannerCfrChange(true), + ) + } + } + + private fun showTrackingProtectionCfr( + action: BrowserAction, + context: MiddlewareContext, + ) { + if (action is ContentAction.UpdateSecurityInfoAction) { + isCurrentTabSecure = action.securityInfo.secure + } + if (shouldShowCfrForTrackingProtection(action = action, browserState = context.state)) { + if (!tpExposureAlreadyRecorded) { + FocusNimbus.features.onboarding.recordExposure() + tpExposureAlreadyRecorded = true + } + + components.appStore.dispatch( + AppAction.ShowTrackingProtectionCfrChange( + mapOf((action as TrackingProtectionAction.TrackerBlockedAction).tabId to true), + ), + ) + } + } + + private fun isMozillaUrl(browserState: BrowserState): Boolean { + return browserState.findTabOrCustomTabOrSelectedTab( + browserState.selectedTabId, + )?.content?.url?.toUri()?.truncatedHost()?.substringBefore(".") == ("mozilla") + } + + private fun isActionSecure(action: BrowserAction) = + action is TrackingProtectionAction.TrackerBlockedAction && isCurrentTabSecure + + private fun shouldShowCfrForTrackingProtection( + action: BrowserAction, + browserState: BrowserState, + ) = ( + isActionSecure(action = action) && + !isMozillaUrl(browserState = browserState) && + components.settings.shouldShowCfrForTrackingProtection && + !components.appStore.state.showEraseTabsCfr + ) + + private fun otherCfrHasBeenShown(): Boolean { + return ( + !appContext.settings.shouldShowCfrForTrackingProtection && + !components.appStore.state.showEraseTabsCfr + ) + } + + private fun shouldShowCookieBannerCfr(action: CookieBannerAction.UpdateStatusAction): Boolean { + return ( + !appContext.settings.isFirstRun && + appContext.settings.shouldShowCookieBannerCfr && + appContext.settings.isCookieBannerEnable && + appContext.settings.getCurrentCookieBannerOptionFromSharePref() == + CookieBannerOption.CookieBannerRejectAll() && + action.status == EngineSession.CookieBannerHandlingStatus.HANDLED + ) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/components/EngineProvider.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/components/EngineProvider.kt new file mode 100644 index 0000000000..3b747b7711 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/components/EngineProvider.kt @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.components + +import android.content.Context +import androidx.datastore.preferences.preferencesDataStore +import mozilla.components.browser.engine.gecko.GeckoEngine +import mozilla.components.browser.engine.gecko.cookiebanners.GeckoCookieBannersStorage +import mozilla.components.browser.engine.gecko.cookiebanners.ReportSiteDomainsRepository +import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient +import mozilla.components.concept.engine.DefaultSettings +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.fetch.Client +import mozilla.components.lib.crash.handler.CrashHandlerService +import org.mozilla.focus.utils.AppConstants +import org.mozilla.geckoview.GeckoRuntime +import org.mozilla.geckoview.GeckoRuntimeSettings + +object EngineProvider { + private var runtime: GeckoRuntime? = null + private val Context.dataStore by preferencesDataStore( + name = ReportSiteDomainsRepository.REPORT_SITE_DOMAINS_REPOSITORY_NAME, + ) + + @Synchronized + private fun getOrCreateRuntime(context: Context): GeckoRuntime { + if (runtime == null) { + val builder = GeckoRuntimeSettings.Builder() + + builder.crashHandler(CrashHandlerService::class.java) + builder.aboutConfigEnabled( + AppConstants.isDevOrNightlyBuild || AppConstants.isBetaBuild, + ) + + runtime = GeckoRuntime.create(context, builder.build()) + } + + return runtime!! + } + + fun createEngine(context: Context, defaultSettings: DefaultSettings): Engine { + val runtime = getOrCreateRuntime(context) + + return GeckoEngine(context, defaultSettings, runtime) + } + + fun createCookieBannerStorage(context: Context): GeckoCookieBannersStorage { + val runtime = getOrCreateRuntime(context) + + return GeckoCookieBannersStorage(runtime, ReportSiteDomainsRepository(context.dataStore)) + } + + fun createClient(context: Context): Client { + val runtime = getOrCreateRuntime(context) + return GeckoViewFetchClient(context, runtime) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/contextmenu/ContextMenuCandidates.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/contextmenu/ContextMenuCandidates.kt new file mode 100644 index 0000000000..0620e67699 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/contextmenu/ContextMenuCandidates.kt @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.contextmenu + +import android.content.Context +import android.view.View +import mozilla.components.feature.app.links.AppLinksUseCases +import mozilla.components.feature.contextmenu.ContextMenuCandidate +import mozilla.components.feature.contextmenu.ContextMenuUseCases +import mozilla.components.feature.tabs.TabsUseCases +import mozilla.components.ui.widgets.DefaultSnackbarDelegate +import mozilla.components.ui.widgets.SnackbarDelegate + +object ContextMenuCandidates { + @Suppress("LongParameterList", "UndocumentedPublicFunction") + fun get( + context: Context, + tabsUseCases: TabsUseCases, + contextMenuUseCases: ContextMenuUseCases, + appLinksUseCases: AppLinksUseCases, + snackBarParentView: View, + snackbarDelegate: SnackbarDelegate = DefaultSnackbarDelegate(), + isCustomTab: Boolean, + ): List { + return if (isCustomTab) { + // the context menu candidates list is the same as in a Fenix custom tab. + listOf( + ContextMenuCandidate.createCopyLinkCandidate( + context, + snackBarParentView, + snackbarDelegate, + ), + ContextMenuCandidate.createShareLinkCandidate(context), + ContextMenuCandidate.createSaveImageCandidate(context, contextMenuUseCases), + ContextMenuCandidate.createSaveVideoAudioCandidate(context, contextMenuUseCases), + ContextMenuCandidate.createCopyImageLocationCandidate( + context, + snackBarParentView, + snackbarDelegate, + ), + ) + } else { + listOf( + ContextMenuCandidate.createOpenInPrivateTabCandidate( + context, + tabsUseCases, + snackBarParentView, + snackbarDelegate, + ), + ContextMenuCandidate.createCopyLinkCandidate( + context, + snackBarParentView, + snackbarDelegate, + ), + ContextMenuCandidate.createDownloadLinkCandidate(context, contextMenuUseCases), + ContextMenuCandidate.createShareLinkCandidate(context), + ContextMenuCandidate.createShareImageCandidate(context, contextMenuUseCases), + ContextMenuCandidate.createOpenImageInNewTabCandidate( + context, + tabsUseCases, + snackBarParentView, + snackbarDelegate, + ), + ContextMenuCandidate.createSaveImageCandidate(context, contextMenuUseCases), + ContextMenuCandidate.createSaveVideoAudioCandidate(context, contextMenuUseCases), + ContextMenuCandidate.createCopyImageLocationCandidate( + context, + snackBarParentView, + snackbarDelegate, + ), + ContextMenuCandidate.createAddContactCandidate(context), + ContextMenuCandidate.createShareEmailAddressCandidate(context), + ContextMenuCandidate.createCopyEmailAddressCandidate( + context, + snackBarParentView, + snackbarDelegate, + ), + ContextMenuCandidate.createOpenInExternalAppCandidate( + context, + appLinksUseCases, + ), + ) + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerFragment.kt new file mode 100644 index 0000000000..78f3d4b0f0 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerFragment.kt @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.cookiebanner + +import android.os.Bundle +import org.mozilla.focus.GleanMetrics.CookieBanner +import org.mozilla.focus.R +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.requirePreference +import org.mozilla.focus.ext.settings +import org.mozilla.focus.ext.showToolbar +import org.mozilla.focus.settings.BaseSettingsFragment + +class CookieBannerFragment : BaseSettingsFragment() { + private lateinit var rejectAllCookies: CookieBannerRejectAllPreference + + override fun onStart() { + super.onStart() + showToolbar(getString(R.string.preferences_cookie_banner)) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.cookie_banner_settings) + setupPreferences() + setupInitialState() + setupOnPreferenceChangeListener() + } + + private fun setupPreferences() { + rejectAllCookies = requirePreference(R.string.pref_key_cookie_banner_reject_all) + } + + private fun setupInitialState() { + when (requireContext().settings.getCurrentCookieBannerOptionFromSharePref()) { + is CookieBannerOption.CookieBannerDisabled -> { + rejectAllCookies.isChecked = false + } + is CookieBannerOption.CookieBannerRejectAll -> { + rejectAllCookies.isChecked = true + } + } + } + + private fun setupOnPreferenceChangeListener() { + rejectAllCookies.setOnPreferenceChangeListener { _, newValue -> + val enableRejectAllCookies = newValue as Boolean + + val cookieBannerOption: CookieBannerOption = if (enableRejectAllCookies) { + CookieBannerOption.CookieBannerRejectAll() + } else { + CookieBannerOption.CookieBannerDisabled() + } + + handleCookieBannerChange(cookieBannerOption) + true + } + } + + private fun handleCookieBannerChange(cookieBannerOption: CookieBannerOption) { + CookieBanner.settingChanged.record(CookieBanner.SettingChangedExtra(cookieBannerOption.metricTag)) + requireContext().settings.saveCurrentCookieBannerOptionInSharePref(cookieBannerOption) + requireContext().components.engine.settings.cookieBannerHandlingModePrivateBrowsing = cookieBannerOption.mode + requireContext().components.sessionUseCases.reload() + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerOption.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerOption.kt new file mode 100644 index 0000000000..69a85a2bc7 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerOption.kt @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.cookiebanner + +import mozilla.components.concept.engine.EngineSession +import org.mozilla.focus.R + +sealed class CookieBannerOption( + open val prefKeyId: Int, + open val mode: EngineSession.CookieBannerHandlingMode, + open val metricTag: String, +) { + + data class CookieBannerRejectAll( + override val prefKeyId: Int = R.string.pref_key_cookie_banner_reject_all, + override val mode: EngineSession.CookieBannerHandlingMode = + EngineSession.CookieBannerHandlingMode.REJECT_ALL, + override val metricTag: String = "reject_all", + ) : CookieBannerOption(prefKeyId = prefKeyId, mode = mode, metricTag = metricTag) + + data class CookieBannerDisabled( + override val prefKeyId: Int = R.string.pref_key_cookie_banner_disabled, + override val mode: EngineSession.CookieBannerHandlingMode = + EngineSession.CookieBannerHandlingMode.DISABLED, + override val metricTag: String = "disabled", + ) : CookieBannerOption(prefKeyId = prefKeyId, mode = mode, metricTag = metricTag) +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerRejectAllPreference.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerRejectAllPreference.kt new file mode 100644 index 0000000000..4d96848c43 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebanner/CookieBannerRejectAllPreference.kt @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.focus.cookiebanner + +import android.content.Context +import android.util.AttributeSet +import org.mozilla.focus.settings.LearnMoreSwitchPreference +import org.mozilla.focus.utils.SupportUtils + +class CookieBannerRejectAllPreference(context: Context, attrs: AttributeSet?) : + LearnMoreSwitchPreference(context, attrs) { + + override fun getLearnMoreUrl(): String { + return SupportUtils.getSumoURLForTopic( + SupportUtils.getAppVersion(context), + SupportUtils.SumoTopic.COOKIE_BANNER, + ) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerExceptionDetailsSwitch.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerExceptionDetailsSwitch.kt new file mode 100644 index 0000000000..084d258bf6 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerExceptionDetailsSwitch.kt @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.cookiebannerreducer + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import org.mozilla.focus.R +import org.mozilla.focus.databinding.SwitchWithDescriptionBinding + +class CookieBannerExceptionDetailsSwitch @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : ConstraintLayout(context, attrs, defStyleAttr) { + + internal var binding: SwitchWithDescriptionBinding + + init { + val view = + LayoutInflater.from(context).inflate(R.layout.switch_with_description, this, true) + binding = SwitchWithDescriptionBinding.bind(view) + setTitle() + } + + private fun setTitle() { + binding.title.text = context.getString(R.string.cookie_banner_exception_panel_switch_title) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerDetailsPanel.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerDetailsPanel.kt new file mode 100644 index 0000000000..9cf1aa5111 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerDetailsPanel.kt @@ -0,0 +1,181 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.cookiebannerreducer + +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.core.net.toUri +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mozilla.components.support.ktx.kotlin.toShortUrl +import mozilla.telemetry.glean.private.NoExtras +import org.mozilla.focus.GleanMetrics.CookieBanner +import org.mozilla.focus.R +import org.mozilla.focus.databinding.CookieBannerReducerDetailsBinding +import org.mozilla.focus.ext.components + +/** + * Cookie banner reducer details panel that will be visible when the user + * clicks on cookie banner reducer item. + */ +class CookieBannerReducerDetailsPanel( + context: Context, + cookieBannerReducerStore: CookieBannerReducerStore, + private val ioScope: CoroutineScope, + private val tabUrl: String, + private val goBack: () -> Unit, + private val defaultCookieBannerInteractor: DefaultCookieBannerReducerInteractor, +) : BottomSheetDialog(context) { + + private var binding: CookieBannerReducerDetailsBinding = + CookieBannerReducerDetailsBinding.inflate(layoutInflater, null, false) + private val cookieBannerExceptionStatus = + cookieBannerReducerStore.state.cookieBannerReducerStatus + private var siteDomain: String? = null + + init { + setContentView(binding.root) + initSiteDomain() + expandBottomSheet() + setListeners() + bindSwitchItem() + updateViews() + } + + private fun initSiteDomain() { + ioScope.launch { + val host = tabUrl.toUri().host.orEmpty() + siteDomain = context.components.publicSuffixList.getPublicSuffixPlusOne(host).await() + } + } + + private fun updateViews() { + bindTitle() + bindDescription() + bindItemAction() + } + + private fun expandBottomSheet() { + val bottomSheet = + findViewById(R.id.design_bottom_sheet) as FrameLayout + BottomSheetBehavior.from(bottomSheet).state = BottomSheetBehavior.STATE_EXPANDED + } + + private fun updateSwitchItem() { + binding.cookieBannerExceptionDetailsSwitch.visibility = View.VISIBLE + binding.cookieBannerExceptionDetailsSwitch.binding.switchWidget.setOnClickListener { + val isChecked = + binding.cookieBannerExceptionDetailsSwitch.binding.switchWidget.isChecked + defaultCookieBannerInteractor.handleToggleCookieBannerException(isChecked) + updateViews() + dismiss() + } + } + + private fun bindItemAction() { + when (cookieBannerExceptionStatus) { + CookieBannerReducerStatus.HasException -> { + binding.cookieBannerExceptionDetailsSwitch.binding.description.text = + context.getString(R.string.cookie_banner_exception_panel_switch_state_off) + updateSwitchItem() + } + CookieBannerReducerStatus.NoException -> { + binding.cookieBannerExceptionDetailsSwitch.binding.description.text = + context.getString(R.string.cookie_banner_exception_panel_switch_state_on) + updateSwitchItem() + } + CookieBannerReducerStatus.CookieBannerSiteNotSupported -> updateSiteNotSupportedItem() + else -> {} + } + } + + private fun updateSiteNotSupportedItem() { + binding.requestSupport.visibility = View.VISIBLE + binding.cancelButton.visibility = View.VISIBLE + } + + private fun bindTitle() { + val titleText = when (cookieBannerExceptionStatus) { + CookieBannerReducerStatus.HasException -> { + setExceptionTitle( + siteDomain, + R.string.cookie_banner_exception_panel_title_state_on_for_site, + ) + } + CookieBannerReducerStatus.NoException -> { + setExceptionTitle( + siteDomain, + R.string.cookie_banner_exception_panel_title_state_off_for_site, + ) + } + CookieBannerReducerStatus.CookieBannerSiteNotSupported -> { + context.getString(R.string.cookie_banner_exception_panel_switch_title) + } + else -> "" + } + binding.title.text = titleText + } + + private fun bindDescription() { + val detailsText = when (cookieBannerExceptionStatus) { + CookieBannerReducerStatus.HasException -> context.getString( + R.string.cookie_banner_exception_panel_description_state_off_for_site2, + context.getString(R.string.app_name), + ) + CookieBannerReducerStatus.CookieBannerSiteNotSupported -> context.getString( + R.string.cookie_banner_exception_panel_description_site_is_not_supported, + ) + CookieBannerReducerStatus.NoException -> context.getString( + R.string.cookie_banner_exception_panel_description_state_on_for_site, + context.getString(R.string.app_name), + ) + else -> "" + } + binding.details.text = detailsText + } + + private fun setListeners() { + binding.detailsBack.setOnClickListener { + goBack.invoke() + dismiss() + } + binding.cancelButton.setOnClickListener { + CookieBanner.reportSiteCancelButton.record(NoExtras()) + goBack.invoke() + dismiss() + } + binding.requestSupport.setOnClickListener { + if (!siteDomain.isNullOrEmpty()) { + defaultCookieBannerInteractor.handleRequestReportSiteDomain(siteDomain!!) + } + dismiss() + } + } + + private fun setExceptionTitle(domain: String?, titleRes: Int): String { + val data = domain ?: tabUrl + val shortUrl = data.toShortUrl(context.components.publicSuffixList) + return context.getString( + titleRes, + shortUrl, + ) + } + + private fun bindSwitchItem() { + when (cookieBannerExceptionStatus) { + CookieBannerReducerStatus.HasException -> { + binding.cookieBannerExceptionDetailsSwitch.binding.switchWidget.isChecked = false + } + CookieBannerReducerStatus.NoException -> { + binding.cookieBannerExceptionDetailsSwitch.binding.switchWidget.isChecked = true + } + else -> { + } + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerItem.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerItem.kt new file mode 100644 index 0000000000..6dbc2a5e77 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerItem.kt @@ -0,0 +1,158 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.cookiebannerreducer + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.mozilla.focus.R +import org.mozilla.focus.ui.theme.FocusTheme +import org.mozilla.focus.ui.theme.focusColors + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun CookieBannerReducerItemPreviewSiteIsNotSupported() { + FocusTheme { + CookieBannerReducerItem(cookieBannerReducerStatus = CookieBannerReducerStatus.CookieBannerSiteNotSupported) {} + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun CookieBannerReducerItemPreviewHasException() { + FocusTheme { + CookieBannerReducerItem(cookieBannerReducerStatus = CookieBannerReducerStatus.HasException) {} + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun CookieBannerReducerItemPreviewHasNotException() { + FocusTheme { + CookieBannerReducerItem(cookieBannerReducerStatus = CookieBannerReducerStatus.NoException) {} + } +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun CookieBannerReducerItemPreviewUnsupportedSiteRequestWasSubmitted() { + FocusTheme { + CookieBannerReducerItem( + cookieBannerReducerStatus = + CookieBannerReducerStatus.CookieBannerUnsupportedSiteRequestWasSubmitted, + ) {} + } +} + +/** + * Displays the cookie banner exception item from Tracking Protection panel. + * + * @param cookieBannerReducerStatus if the site has a cookie banner, an exception or is not supported + * @param preferenceOnClickListener Callback that will redirect the user to cookie banner item details. + */ +@Composable +fun CookieBannerReducerItem( + cookieBannerReducerStatus: CookieBannerReducerStatus, + preferenceOnClickListener: (() -> Unit)? = null, +) { + var rowModifier = Modifier + .defaultMinSize(minHeight = 48.dp) + .background( + colorResource(R.color.settings_background), + shape = RectangleShape, + ) + + if (cookieBannerReducerStatus !is CookieBannerReducerStatus.CookieBannerUnsupportedSiteRequestWasSubmitted + ) { + rowModifier = rowModifier.then( + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { preferenceOnClickListener?.invoke() }, + ) + } + + Row( + modifier = rowModifier, + verticalAlignment = Alignment.CenterVertically, + ) { + val painter = + if (cookieBannerReducerStatus is CookieBannerReducerStatus.NoException) { + painterResource(id = R.drawable.mozac_ic_cookies_24) + } else { + painterResource(id = R.drawable.ic_cookies_disable) + } + Icon( + painter = painter, + contentDescription = null, + tint = focusColors.onPrimary, + modifier = Modifier.padding(end = 20.dp), + ) + + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(R.string.cookie_banner_exception_item_title), + maxLines = 1, + color = focusColors.settingsTextColor, + fontSize = 14.sp, + lineHeight = 20.sp, + ) + val summary = when (cookieBannerReducerStatus) { + CookieBannerReducerStatus.HasException -> + stringResource(id = R.string.cookie_banner_exception_item_description_state_off) + CookieBannerReducerStatus.NoException -> + stringResource(id = R.string.cookie_banner_exception_item_description_state_on) + CookieBannerReducerStatus.CookieBannerSiteNotSupported -> + stringResource(id = R.string.cookie_banner_exception_site_not_supported) + CookieBannerReducerStatus.CookieBannerUnsupportedSiteRequestWasSubmitted -> + stringResource(id = R.string.cookie_banner_the_site_was_reported) + } + Text( + text = summary, + maxLines = 1, + color = colorResource(R.color.disabled), + fontSize = 12.sp, + lineHeight = 16.sp, + ) + } + if ( + cookieBannerReducerStatus !is CookieBannerReducerStatus.CookieBannerUnsupportedSiteRequestWasSubmitted + ) { + Icon( + modifier = Modifier + .padding(end = 0.dp) + .size(24.dp), + tint = focusColors.onPrimary, + painter = painterResource(id = R.drawable.mozac_ic_chevron_right_24), + contentDescription = null, + ) + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerMiddleware.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerMiddleware.kt new file mode 100644 index 0000000000..74388311a3 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerMiddleware.kt @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.cookiebannerreducer + +import android.content.Context +import androidx.core.net.toUri +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.browser.state.state.SessionState +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.cookiehandling.CookieBannersStorage +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import mozilla.telemetry.glean.private.NoExtras +import org.mozilla.focus.GleanMetrics.CookieBanner +import org.mozilla.focus.GleanMetrics.Pings +import org.mozilla.focus.cookiebanner.CookieBannerOption +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.settings + +/** + * Middleware for cookie banner reduction. + */ +class CookieBannerReducerMiddleware( + private val ioScope: CoroutineScope, + private val cookieBannersStorage: CookieBannersStorage, + private val appContext: Context, + private val currentTab: SessionState, +) : + Middleware { + + override fun invoke( + context: MiddlewareContext, + next: (CookieBannerReducerAction) -> Unit, + action: CookieBannerReducerAction, + ) { + when (action) { + is CookieBannerReducerAction.InitCookieBannerReducer -> { + /** + * The initial CookieBannerReducerState when the user enters first in the screen + */ + initCookieBannerReducer(context) + } + + is CookieBannerReducerAction.ToggleCookieBannerException -> { + handleCookieBannerToggle(action, context) + next(action) + } + is CookieBannerReducerAction.RequestReportSite -> { + reportSite(action, context) + next(action) + } + else -> { + next(action) + } + } + } + + private fun handleCookieBannerToggle( + action: CookieBannerReducerAction.ToggleCookieBannerException, + context: MiddlewareContext, + ) { + ioScope.launch { + if (action.isCookieBannerHandlingExceptionEnabled) { + cookieBannersStorage.removeException(currentTab.content.url, true) + CookieBanner.exceptionRemoved.record(NoExtras()) + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerStatus( + CookieBannerReducerStatus.NoException, + ), + ) + } else { + clearSiteData() + cookieBannersStorage.addPersistentExceptionInPrivateMode(currentTab.content.url) + CookieBanner.exceptionAdded.record(NoExtras()) + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerStatus( + CookieBannerReducerStatus.HasException, + ), + ) + } + appContext.components.sessionUseCases.reload() + } + } + + private fun reportSite( + action: CookieBannerReducerAction.RequestReportSite, + context: MiddlewareContext, + ) { + CookieBanner.reportSiteDomain.set(action.siteToReport) + Pings.cookieBannerReportSite.submit() + context.store.dispatch( + CookieBannerReducerAction.ShowSnackBarForSiteToReport( + true, + ), + ) + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerStatus( + CookieBannerReducerStatus.CookieBannerUnsupportedSiteRequestWasSubmitted, + ), + ) + ioScope.launch { cookieBannersStorage.saveSiteDomain(action.siteToReport) } + } + + private fun initCookieBannerReducer( + context: MiddlewareContext, + ) { + val shouldShowCookieBannerItem = shouldShowCookieBannerReducerItem() + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerVisibility( + shouldShowCookieBannerItem = shouldShowCookieBannerItem, + ), + ) + + if (!shouldShowCookieBannerItem) { + return + } + ioScope.launch { + if (isSiteDomainReported(context)) { + return@launch + } + val hasException = + cookieBannersStorage.hasException(currentTab.content.url, true) + withContext(Dispatchers.Main) { + if (hasException == null) { + // An error occurred while querying the exception, let's hide the item. + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerStatus( + null, + ), + ) + return@withContext + } else if (!hasException) { + showUnsupportedSiteIfNeeded(context) + } else { + showExceptionStatus(context, true) + } + } + } + } + + private fun showUnsupportedSiteIfNeeded( + context: MiddlewareContext, + ) { + currentTab.engineState.engineSession?.hasCookieBannerRuleForSession( + onResult = { result -> + if (result) { + showExceptionStatus(context, false) + } else { + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerStatus( + CookieBannerReducerStatus.CookieBannerSiteNotSupported, + ), + ) + } + }, + onException = { + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerVisibility( + shouldShowCookieBannerItem = false, + ), + ) + }, + ) + } + + private fun showExceptionStatus( + context: MiddlewareContext, + hasException: Boolean, + ) { + if (hasException) { + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerStatus( + CookieBannerReducerStatus.HasException, + ), + ) + } else { + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerStatus( + CookieBannerReducerStatus.NoException, + ), + ) + } + } + + /** + * It returns the cookie banner reducer item visibility from tracking protection panel . + * If the item is invisible item details should also be invisible. + */ + private fun shouldShowCookieBannerReducerItem(): Boolean { + return appContext.settings.isCookieBannerEnable && + appContext.settings.getCurrentCookieBannerOptionFromSharePref() != + CookieBannerOption.CookieBannerDisabled() + } + + private suspend fun isSiteDomainReported( + context: MiddlewareContext, + ): Boolean { + val host = currentTab.content.url.toUri().host.orEmpty() + val siteDomain = + appContext.components.publicSuffixList.getPublicSuffixPlusOne(host).await() + if (siteDomain != null && cookieBannersStorage.isSiteDomainReported(siteDomain)) { + context.store.dispatch( + CookieBannerReducerAction.UpdateCookieBannerReducerStatus( + CookieBannerReducerStatus.CookieBannerUnsupportedSiteRequestWasSubmitted, + ), + ) + return true + } + return false + } + + private suspend fun clearSiteData() { + val host = currentTab.content.url.toUri().host.orEmpty() + val domain = appContext.components.publicSuffixList.getPublicSuffixPlusOne(host).await() + withContext(Dispatchers.Main) { + appContext.components.engine.clearData( + host = domain, + data = Engine.BrowsingData.select( + Engine.BrowsingData.AUTH_SESSIONS, + Engine.BrowsingData.ALL_SITE_DATA, + ), + ) + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerStatus.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerStatus.kt new file mode 100644 index 0000000000..978a0baf50 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerStatus.kt @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.cookiebannerreducer + +/** + * Sealed class for the cookie banner exception Gui item + * from Tracking Protection panel. + */ +sealed class CookieBannerReducerStatus { + + /** + * If the site is excepted from cookie banner reduction. + */ + object HasException : CookieBannerReducerStatus() + + /** + * If the site is not excepted from cookie banner reduction. + */ + object NoException : CookieBannerReducerStatus() + + /** + * If the cookie banner reducer is not supported on the site. + */ + object CookieBannerSiteNotSupported : CookieBannerReducerStatus() + + /** + * If the user reports with success a site that wasn't supported by cookie banner reducer. + */ + object CookieBannerUnsupportedSiteRequestWasSubmitted : CookieBannerReducerStatus() +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerStore.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerStore.kt new file mode 100644 index 0000000000..b6225bb028 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/CookieBannerReducerStore.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 org.mozilla.focus.cookiebannerreducer + +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store + +/** + * The [CookieBannerReducerStore] holds the [CookieBannerReducerState] (state tree). + * + * The only way to change the [CookieBannerReducerState] inside + * [CookieBannerReducerStore] is to dispatch an [CookieBannerReducerAction] on it. + */ +class CookieBannerReducerStore( + initialState: CookieBannerReducerState, + middlewares: List> = emptyList(), +) : Store( + initialState, + ::cookieBannerStateReducer, + middlewares, +) { + init { + dispatch(CookieBannerReducerAction.InitCookieBannerReducer) + } +} + +/** + * The state of the cookie banner reducer + * + * @property isCookieBannerToggleEnabled Current status of cookie banner toggle from details exception. + * @property shouldShowCookieBannerItem Visibility of cookie banner reducer item. + * @property isCookieBannerDetected If the site has a cookie banner. + * @property showSnackBarForSiteToReport When cookie banner reducer doesn't work + * on a website and the user reports that site + * @property cookieBannerReducerStatus Current status of cookie banner reducer. + * @property siteToReport Site to report when cookie banner reducer doesn't work + */ +data class CookieBannerReducerState( + val isCookieBannerToggleEnabled: Boolean = false, + val shouldShowCookieBannerItem: Boolean = false, + val isCookieBannerDetected: Boolean = false, + val showSnackBarForSiteToReport: Boolean = false, + val cookieBannerReducerStatus: CookieBannerReducerStatus? = CookieBannerReducerStatus.NoException, + val siteToReport: String = "", +) : State + +/** + * Action to dispatch through the `CookieBannerReducerStore` to modify cookie banner reducer item and item details + * from Tracking protection panel through the reducer. + */ +@Suppress("UndocumentedPublicClass") +sealed class CookieBannerReducerAction : Action { + object InitCookieBannerReducer : CookieBannerReducerAction() + + data class ToggleCookieBannerException( + val isCookieBannerHandlingExceptionEnabled: Boolean, + ) : CookieBannerReducerAction() + + data class UpdateCookieBannerReducerVisibility( + val shouldShowCookieBannerItem: Boolean, + ) : CookieBannerReducerAction() + + data class UpdateCookieBannerReducerStatus( + val cookieBannerReducerStatus: CookieBannerReducerStatus?, + ) : CookieBannerReducerAction() + + data class RequestReportSite( + val siteToReport: String, + ) : CookieBannerReducerAction() + + data class ShowSnackBarForSiteToReport( + val isSnackBarVisible: Boolean, + ) : CookieBannerReducerAction() +} + +/** + * Reduces the cookie banner state from the current state and an action performed on it. + * + * @param state the current cookie banner item state + * @param action the action to perform + * @return the new cookie banner reducer state + */ +private fun cookieBannerStateReducer( + state: CookieBannerReducerState, + action: CookieBannerReducerAction, +): CookieBannerReducerState { + return when (action) { + is CookieBannerReducerAction.ToggleCookieBannerException -> { + state.copy(isCookieBannerToggleEnabled = action.isCookieBannerHandlingExceptionEnabled) + } + is CookieBannerReducerAction.UpdateCookieBannerReducerVisibility -> { + state.copy(shouldShowCookieBannerItem = action.shouldShowCookieBannerItem) + } + is CookieBannerReducerAction.UpdateCookieBannerReducerStatus -> { + state.copy(cookieBannerReducerStatus = action.cookieBannerReducerStatus) + } + is CookieBannerReducerAction.RequestReportSite -> { + state.copy(siteToReport = action.siteToReport) + } + is CookieBannerReducerAction.ShowSnackBarForSiteToReport -> { + state.copy(showSnackBarForSiteToReport = action.isSnackBarVisible) + } + CookieBannerReducerAction.InitCookieBannerReducer -> { + throw IllegalStateException( + "You need to add CookieBannerReducerMiddleware to your CookieBannerReducerStore. ($action)", + ) + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/DefaultCookieBannerReducerInteractor.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/DefaultCookieBannerReducerInteractor.kt new file mode 100644 index 0000000000..f0ee6f7305 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/cookiebannerreducer/DefaultCookieBannerReducerInteractor.kt @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.cookiebannerreducer + +import mozilla.telemetry.glean.private.NoExtras +import org.mozilla.focus.GleanMetrics.CookieBanner + +/** + * Interactor class for cookie banner reducer feature . + */ +class DefaultCookieBannerReducerInteractor(val store: CookieBannerReducerStore) { + + /** + * Method that gets called when the user changes the cookie banner exception toggle state. + * @param isCookieBannerHandlingExceptionEnabled - the state of the toggle + */ + fun handleToggleCookieBannerException(isCookieBannerHandlingExceptionEnabled: Boolean) { + store.dispatch( + CookieBannerReducerAction.ToggleCookieBannerException( + isCookieBannerHandlingExceptionEnabled, + ), + ) + } + + /** + * Method that gets called when the user sends the url of the site that he wants to report. + * @param siteDomain - the site domain that will be sent to nimbus. + */ + fun handleRequestReportSiteDomain(siteDomain: String) { + CookieBanner.reportDomainSiteButton.record(NoExtras()) + store.dispatch(CookieBannerReducerAction.RequestReportSite(siteToReport = siteDomain)) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/customtabs/CustomTabsService.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/customtabs/CustomTabsService.kt new file mode 100644 index 0000000000..73a453cc89 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/customtabs/CustomTabsService.kt @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.customtabs + +import mozilla.components.concept.engine.Engine +import mozilla.components.feature.customtabs.AbstractCustomTabsService +import mozilla.components.feature.customtabs.store.CustomTabsServiceStore +import org.mozilla.focus.ext.components + +class CustomTabsService : AbstractCustomTabsService() { + override val customTabsServiceStore: CustomTabsServiceStore by lazy { components.customTabsStore } + override val engine: Engine by lazy { components.engine } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/downloads/DownloadService.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/downloads/DownloadService.kt new file mode 100644 index 0000000000..b5041f573e --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/downloads/DownloadService.kt @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.downloads + +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.fetch.Client +import mozilla.components.feature.downloads.AbstractFetchDownloadService +import mozilla.components.support.base.android.NotificationsDelegate +import org.mozilla.focus.ext.components + +class DownloadService : AbstractFetchDownloadService() { + override val httpClient: Client by lazy { components.client } + override val store: BrowserStore by lazy { components.store } + override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/AppContentInterceptor.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/AppContentInterceptor.kt new file mode 100644 index 0000000000..2f6e4c5147 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/AppContentInterceptor.kt @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.engine + +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import mozilla.components.browser.errorpages.ErrorPages +import mozilla.components.browser.errorpages.ErrorType +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.request.RequestInterceptor +import org.mozilla.focus.R +import org.mozilla.focus.activity.CrashListActivity +import org.mozilla.focus.browser.LocalizedContent +import org.mozilla.focus.ext.components +import org.mozilla.focus.utils.SupportUtils + +class AppContentInterceptor( + private val context: Context, +) : RequestInterceptor { + override fun onLoadRequest( + engineSession: EngineSession, + uri: String, + lastUri: String?, + hasUserGesture: Boolean, + isSameDomain: Boolean, + isRedirect: Boolean, + isDirectNavigation: Boolean, + isSubframeRequest: Boolean, + ): RequestInterceptor.InterceptionResponse? { + return when (uri) { + LocalizedContent.URL_ABOUT -> RequestInterceptor.InterceptionResponse.Content( + LocalizedContent.loadAbout(context), + encoding = "base64", + ) + + LocalizedContent.URL_RIGHTS -> RequestInterceptor.InterceptionResponse.Content( + LocalizedContent.loadRights(context), + encoding = "base64", + ) + + LocalizedContent.URL_GPL -> RequestInterceptor.InterceptionResponse.Content( + LocalizedContent.loadGPL(context), + encoding = "base64", + ) + + LocalizedContent.URL_LICENSES -> RequestInterceptor.InterceptionResponse.Content( + LocalizedContent.loadLicenses(context), + encoding = "base64", + ) + + "about:crashes" -> { + val intent = Intent(context, CrashListActivity::class.java) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + + RequestInterceptor.InterceptionResponse.Url("about:blank") + } + + else -> context.components.appLinksInterceptor.onLoadRequest( + engineSession, + uri, + lastUri, + hasUserGesture, + isSameDomain, + isRedirect, + isDirectNavigation, + isSubframeRequest, + ) + } + } + + override fun onErrorRequest( + session: EngineSession, + errorType: ErrorType, + uri: String?, + ): RequestInterceptor.ErrorResponse { + val errorPage = ErrorPages.createUrlEncodedErrorPage( + context, + errorType, + uri, + titleOverride = { type -> getErrorPageTitle(context, type) }, + descriptionOverride = { type -> getErrorPageDescription(context, type) }, + ) + return RequestInterceptor.ErrorResponse(errorPage) + } + + override fun interceptsAppInitiatedRequests() = true +} + +private fun getErrorPageTitle(context: Context, type: ErrorType): String? { + if (type == ErrorType.ERROR_HTTPS_ONLY) { + return context.getString(R.string.errorpage_httpsonly_title2) + } + // Returning `null` here will let the component use its default title for this error type + return null +} + +private fun getErrorPageDescription(context: Context, type: ErrorType): String? { + if (type == ErrorType.ERROR_HTTPS_ONLY) { + return context.getString( + R.string.errorpage_httpsonly_message2, + context.getString(R.string.app_name), + SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.HTTPS_ONLY), + ) + } + // Returning `null` here will let the component use its default description for this error type + return null +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/ClientWrapper.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/ClientWrapper.kt new file mode 100644 index 0000000000..092744cd52 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/ClientWrapper.kt @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.engine + +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.fetch.Request +import mozilla.components.concept.fetch.Response + +/** + * A wrapper around [Client] preventing [Request]s without the private flag set. + */ +class ClientWrapper( + private val actual: Client, +) : Client() { + override fun fetch(request: Request): Response { + if (!request.private) { + throw IllegalStateException("Non-private request") + } + + return actual.fetch(request) + } + + @Deprecated("Non-private Client usage should be prevented") + fun unwrap(): Client { + return actual + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/EngineSharedPreferencesListener.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/EngineSharedPreferencesListener.kt new file mode 100644 index 0000000000..abd64b8e92 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/EngineSharedPreferencesListener.kt @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.engine + +import android.content.Context +import androidx.preference.Preference +import org.mozilla.focus.GleanMetrics.TrackingProtection +import org.mozilla.focus.R +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.settings + +/** + * SharedPreference listener that will update the engine whenever the user changes settings. + */ +class EngineSharedPreferencesListener( + private val context: Context, +) : Preference.OnPreferenceChangeListener { + + override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { + when (preference.key) { + context.getString(R.string.pref_key_performance_enable_cookies) -> + updateTrackingProtectionPolicy(shouldBlockCookiesValue = newValue as String) + + context.getString(R.string.pref_key_safe_browsing) -> + updateSafeBrowsingPolicy(newValue as Boolean) + + context.getString(R.string.pref_key_performance_block_javascript) -> + updateJavaScriptSetting(newValue as Boolean) + + context.getString(R.string.pref_key_performance_block_webfonts) -> + updateWebFontsBlocking(newValue as Boolean) + } + + return true + } + + internal fun updateTrackingProtectionPolicy( + source: String? = null, + tracker: String? = null, + isEnabled: Boolean = false, + shouldBlockCookiesValue: String = context.settings.shouldBlockCookiesValue(), + ) { + val policy = context.settings.createTrackingProtectionPolicy(shouldBlockCookiesValue) + val components = context.components + + components.engineDefaultSettings.trackingProtectionPolicy = policy + components.settingsUseCases.updateTrackingProtection(policy) + + if (source != null && tracker != null) { + TrackingProtection.trackerSettingChanged.record( + TrackingProtection.TrackerSettingChangedExtra( + sourceOfChange = source, + trackerChanged = tracker, + isEnabled = isEnabled, + ), + ) + } + components.sessionUseCases.reload() + } + + private fun updateSafeBrowsingPolicy(newValue: Boolean) { + context.settings.setupSafeBrowsing(context.components.engine, newValue) + context.components.sessionUseCases.reload() + } + + private fun updateJavaScriptSetting(newValue: Boolean) { + val components = context.components + + components.engineDefaultSettings.javascriptEnabled = !newValue + components.engine.settings.javascriptEnabled = !newValue + components.sessionUseCases.reload() + } + + private fun updateWebFontsBlocking(newValue: Boolean) { + val components = context.components + + components.engineDefaultSettings.webFontsEnabled = !newValue + components.engine.settings.webFontsEnabled = !newValue + components.sessionUseCases.reload() + } + + enum class ChangeSource(val source: String) { + SETTINGS("Settings"), + PANEL("Panel"), + } + + enum class TrackerChanged(val tracker: String) { + ADVERTISING("Advertising"), + ANALYTICS("Analytics"), + SOCIAL("Social"), + CONTENT("Content"), + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/SanityCheckMiddleware.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/SanityCheckMiddleware.kt new file mode 100644 index 0000000000..c84c7b2fc6 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/engine/SanityCheckMiddleware.kt @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.engine + +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.InitAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext + +/** + * Middleware preventing creating non-private tabs. + */ +class SanityCheckMiddleware : Middleware { + override fun invoke( + context: MiddlewareContext, + next: (BrowserAction) -> Unit, + action: BrowserAction, + ) { + next(action) + + if (action is TabListAction || action is InitAction) { + verifyNoNonPrivateTabs(context.state) + } + } + + private fun verifyNoNonPrivateTabs(state: BrowserState) { + if (state.normalTabs.isNotEmpty()) { + throw IllegalStateException("State contains non-private tabs") + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/exceptions/ExceptionsListFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/exceptions/ExceptionsListFragment.kt new file mode 100644 index 0000000000..eecbc7f517 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/exceptions/ExceptionsListFragment.kt @@ -0,0 +1,323 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.exceptions + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import mozilla.components.concept.engine.content.blocking.TrackingProtectionException +import org.mozilla.focus.GleanMetrics.TrackingProtectionExceptions +import org.mozilla.focus.R +import org.mozilla.focus.autocomplete.AutocompleteDomainFormatter +import org.mozilla.focus.databinding.FragmentExceptionsDomainsBinding +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.requireComponents +import org.mozilla.focus.ext.showToolbar +import org.mozilla.focus.settings.BaseSettingsLikeFragment +import org.mozilla.focus.state.AppAction +import org.mozilla.focus.state.Screen +import org.mozilla.focus.utils.ViewUtils +import java.util.Collections +import kotlin.coroutines.CoroutineContext + +private const val REMOVE_EXCEPTIONS_DISABLED_ALPHA = 0.5f +typealias DomainFormatter = (String) -> String + +/** + * Fragment showing settings UI listing all exception domains. + */ +open class ExceptionsListFragment : BaseSettingsLikeFragment(), CoroutineScope { + private var job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + private var _binding: FragmentExceptionsDomainsBinding? = null + protected val binding get() = _binding!! + + /** + * ItemTouchHelper for reordering items in the domain list. + */ + val itemTouchHelper: ItemTouchHelper = ItemTouchHelper( + object : SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean { + val from = viewHolder.bindingAdapterPosition + val to = target.bindingAdapterPosition + + (recyclerView.adapter as DomainListAdapter).move(from, to) + + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + + if (viewHolder is DomainViewHolder) { + viewHolder.onSelected() + } + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ) { + super.clearView(recyclerView, viewHolder) + + if (viewHolder is DomainViewHolder) { + viewHolder.onCleared() + } + } + }, + ) + + /** + * In selection mode the user can select and remove items. In non-selection mode the list can + * be reordered by the user. + */ + open fun isSelectionMode() = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentExceptionsDomainsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.exceptionList.apply { + layoutManager = + LinearLayoutManager(activity, RecyclerView.VERTICAL, false) + adapter = DomainListAdapter() + setHasFixedSize(true) + } + + binding.removeAllExceptions.isVisible = !isSelectionMode() + + if (!isSelectionMode()) { + itemTouchHelper.attachToRecyclerView(binding.exceptionList) + } + + binding.removeAllExceptions.setOnClickListener { removeButton -> + removeButton.apply { + isEnabled = false + alpha = REMOVE_EXCEPTIONS_DISABLED_ALPHA + } + requireComponents.trackingProtectionUseCases.removeAllExceptions { + val exceptionsListSize = + (binding.exceptionList.adapter as DomainListAdapter).itemCount + TrackingProtectionExceptions.allowListCleared.record( + TrackingProtectionExceptions.AllowListClearedExtra(exceptionsListSize), + ) + + requireComponents.appStore.dispatch( + AppAction.NavigateUp( + requireComponents.store.state.selectedTabId, + ), + ) + } + } + } + + override fun onResume() { + super.onResume() + + job = Job() + + showToolbar(getString(R.string.preference_exceptions)) + + (binding.exceptionList.adapter as DomainListAdapter).refresh(requireActivity()) { + // check if the exceptions list is empty only if fragment is still attached. + context?.let { + if ((binding.exceptionList.adapter as DomainListAdapter).itemCount == 0) { + requireComponents.appStore.dispatch( + AppAction.NavigateUp(requireComponents.store.state.selectedTabId), + ) + } + activity?.invalidateOptionsMenu() + } + } + } + + override fun onStop() { + job.cancel() + super.onStop() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_exceptions_list, menu) + } + + override fun onPrepareMenu(menu: Menu) { + val removeItem = menu.findItem(R.id.remove) + + removeItem?.let { + it.isVisible = isSelectionMode() || binding.exceptionList.adapter!!.itemCount > 0 + val isEnabled = + !isSelectionMode() || (binding.exceptionList.adapter as DomainListAdapter).selection().isNotEmpty() + ViewUtils.setMenuItemEnabled(it, isEnabled) + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.remove -> { + requireComponents.appStore.dispatch( + AppAction.OpenSettings(page = Screen.Settings.Page.PrivacyExceptionsRemove), + ) + true + } + else -> false + } + + /** + * Adapter implementation for the list of exception domains. + */ + inner class DomainListAdapter : RecyclerView.Adapter() { + private var exceptions: List = emptyList() + private val selectedExceptions: MutableList = mutableListOf() + + fun refresh(context: Context, body: (() -> Unit)? = null) { + this@ExceptionsListFragment.launch(Dispatchers.Main) { + context.components.trackingProtectionUseCases.fetchExceptions { + exceptions = it + notifyDataSetChanged() + body?.invoke() + } + } + } + + override fun getItemViewType(position: Int) = DomainViewHolder.LAYOUT_ID + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = + when (viewType) { + DomainViewHolder.LAYOUT_ID -> + DomainViewHolder( + LayoutInflater.from(parent.context).inflate(viewType, parent, false), + ) { AutocompleteDomainFormatter.format(it) } + else -> throw IllegalArgumentException("Unknown view type: $viewType") + } + + override fun getItemCount(): Int = exceptions.size + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is DomainViewHolder) { + holder.bind( + exceptions[position], + isSelectionMode(), + selectedExceptions, + itemTouchHelper, + this@ExceptionsListFragment, + ) + } + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + if (holder is DomainViewHolder) { + holder.checkBoxView.setOnCheckedChangeListener(null) + } + } + + fun selection(): List = selectedExceptions + + fun move(from: Int, to: Int) { + Collections.swap(exceptions, from, to) + notifyItemMoved(from, to) + + // The underlying storage in GeckoView doesn't support ordering - and ordering is also + // not necessary. We may just need to remove this feature from this list. + } + } + + /** + * ViewHolder implementation for a domain item in the list. + */ + private class DomainViewHolder( + itemView: View, + val domainFormatter: DomainFormatter? = null, + ) : RecyclerView.ViewHolder(itemView) { + val domainView: TextView = itemView.findViewById(R.id.domainView) + val checkBoxView: CheckBox = itemView.findViewById(R.id.checkbox) + val handleView: View = itemView.findViewById(R.id.handleView) + + companion object { + val LAYOUT_ID = R.layout.item_custom_domain + } + + fun bind( + exception: TrackingProtectionException, + isSelectionMode: Boolean, + selectedExceptions: MutableList, + itemTouchHelper: ItemTouchHelper, + fragment: ExceptionsListFragment, + ) { + domainView.text = domainFormatter?.invoke(exception.url) ?: exception.url + + checkBoxView.isVisible = isSelectionMode + checkBoxView.isChecked = selectedExceptions.contains(exception) + checkBoxView.setOnCheckedChangeListener { _: CompoundButton, isChecked: Boolean -> + if (isChecked) { + selectedExceptions.add(exception) + } else { + selectedExceptions.remove(exception) + } + + fragment.activity?.invalidateOptionsMenu() + } + + handleView.isVisible = isSelectionMode + handleView.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + itemTouchHelper.startDrag(this) + } + false + } + + if (isSelectionMode) { + itemView.setOnClickListener { + checkBoxView.isChecked = !checkBoxView.isChecked + } + } + } + + fun onSelected() { + itemView.setBackgroundColor(ContextCompat.getColor(itemView.context, R.color.disabled)) + } + + fun onCleared() { + itemView.setBackgroundColor(0) + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/exceptions/ExceptionsRemoveFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/exceptions/ExceptionsRemoveFragment.kt new file mode 100644 index 0000000000..c228877556 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/exceptions/ExceptionsRemoveFragment.kt @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.exceptions + +import android.content.Context +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import org.mozilla.focus.GleanMetrics.TrackingProtectionExceptions +import org.mozilla.focus.R +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.requireComponents +import org.mozilla.focus.ext.showToolbar +import org.mozilla.focus.state.AppAction +import kotlin.collections.forEach as withEach + +class ExceptionsRemoveFragment : ExceptionsListFragment() { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.menu_autocomplete_remove, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.remove -> { + removeSelectedDomains(requireActivity().applicationContext) + true + } + else -> false + } + + private fun removeSelectedDomains(context: Context) { + val exceptions = (binding.exceptionList.adapter as DomainListAdapter).selection() + TrackingProtectionExceptions.selectedItemsRemoved.record( + TrackingProtectionExceptions.SelectedItemsRemovedExtra(exceptions.size), + ) + + if (exceptions.isNotEmpty()) { + launch(Main) { + exceptions.withEach { exception -> + context.components.trackingProtectionUseCases.removeException(exception) + } + + requireComponents.appStore.dispatch( + AppAction.NavigateUp(requireComponents.store.state.selectedTabId), + ) + } + } + } + + override fun isSelectionMode() = true + + override fun onResume() { + super.onResume() + + showToolbar(getString(R.string.preference_autocomplete_title_remove)) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/experiments/NimbusSetup.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/experiments/NimbusSetup.kt new file mode 100644 index 0000000000..07b788d2d6 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/experiments/NimbusSetup.kt @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.experiments + +import android.content.Context +import mozilla.components.service.nimbus.NimbusApi +import mozilla.components.service.nimbus.NimbusAppInfo +import mozilla.components.service.nimbus.NimbusBuilder +import mozilla.components.support.base.log.logger.Logger +import org.json.JSONObject +import org.mozilla.experiments.nimbus.NimbusInterface +import org.mozilla.experiments.nimbus.internal.NimbusException +import org.mozilla.focus.BuildConfig +import org.mozilla.focus.R +import org.mozilla.focus.ext.components +import org.mozilla.focus.ext.settings +import org.mozilla.focus.nimbus.FocusNimbus + +/** + * The maximum amount of time the app launch will be blocked to load experiments from disk. + * + * ⚠️ This value was decided from analyzing the Focus metrics (nimbus_initial_fetch) for the ideal + * timeout. We should NOT change this value without collecting more metrics first. + */ +private const val TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS = 200L + +/** + * Create the Nimbus singleton object for the Focus/Klar apps. + */ +fun createNimbus(context: Context, urlString: String?): NimbusApi { + val isAppFirstRun = context.settings.isFirstRun + + // These values can be used in the JEXL expressions when targeting experiments. + val customTargetingAttributes = JSONObject().apply { + // By convention, we should use snake case. + put("is_first_run", isAppFirstRun) + + // This camelCase attribute is a boolean value represented as a string. + // This is left for backwards compatibility. + put("isFirstRun", isAppFirstRun.toString()) + } + + // The name "focus-android" or "klar-android" here corresponds to the app_name defined + // for the family of apps that encompasses all of the channels for the Focus app. + // This is defined upstream in the telemetry system. For more context on where the + // app_name come from see: + // https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings + // and + // https://github.com/mozilla/probe-scraper/blob/master/repositories.yaml + val appInfo = NimbusAppInfo( + appName = getNimbusAppName(), + // Note: Using BuildConfig.BUILD_TYPE is important here so that it matches the value + // passed into Glean. `Config.channel.toString()` turned out to be non-deterministic + // and would mostly produce the value `Beta` and rarely would produce `beta`. + channel = BuildConfig.BUILD_TYPE, + customTargetingAttributes = customTargetingAttributes, + ) + + return NimbusBuilder(context).apply { + url = urlString + errorReporter = { message, e -> + Logger.error("Nimbus error: $message", e) + if (e !is NimbusException || e.isReportableError()) { + context.components.crashReporter.submitCaughtException(e) + } + } + initialExperiments = R.raw.initial_experiments + timeoutLoadingExperiment = TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS + usePreviewCollection = context.settings.shouldUseNimbusPreview + sharedPreferences = context.settings.preferences + isFirstRun = isAppFirstRun + featureManifest = FocusNimbus + }.build(appInfo) +} + +internal fun finishNimbusInitialization(experiments: NimbusApi) = + experiments.run { + // We fetch experiments in all cases, + if (context.settings.isFirstRun) { + // … however on first run, we immediately apply pending experiments. + // We also want to measure how long this will take, with Glean. + register( + object : NimbusInterface.Observer { + override fun onExperimentsFetched() { + applyPendingExperiments() + // Remove lingering observer when we're done fetching experiments on startup. + unregister(this) + } + }, + ) + } + fetchExperiments() + } + +fun getNimbusAppName(): String { + return if (BuildConfig.FLAVOR.contains("focus")) { + "focus_android" + } else { + "klar_android" + } +} + +/** + * Classifies which errors we should forward to our crash reporter or not. We want to filter out the + * non-reportable ones if we know there is no reasonable action that we can perform. + * + * This fix should be upstreamed as part of: https://github.com/mozilla/application-services/issues/4333 + */ +fun NimbusException.isReportableError(): Boolean { + return when (this) { + is NimbusException.ClientException -> false + else -> true + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Activity.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Activity.kt new file mode 100644 index 0000000000..ee391ceb0c --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Activity.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 org.mozilla.focus.ext + +import android.app.Activity +import android.os.Build +import android.view.WindowManager +import androidx.annotation.DrawableRes +import androidx.appcompat.app.AppCompatActivity + +/** + * Sets the icon for the back (up) navigation button. + * @param icon The resource id of the icon. + */ +fun Activity.setNavigationIcon( + @DrawableRes icon: Int, +) { + (this as? AppCompatActivity)?.supportActionBar?.let { + it.setDisplayHomeAsUpEnabled(true) + it.setHomeAsUpIndicator(icon) + } +} + +/** + * Sets or clears the secure flags for the activity's window. + */ +fun Activity.updateSecureWindowFlags() { + if (this.settings.shouldUseSecureMode()) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + setRecentsScreenshotEnabled(false) + } + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + setRecentsScreenshotEnabled(true) + } + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/AndroidViewModel.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/AndroidViewModel.kt new file mode 100644 index 0000000000..216dc009e7 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/AndroidViewModel.kt @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import androidx.lifecycle.AndroidViewModel +import org.mozilla.focus.Components +import org.mozilla.focus.FocusApplication + +val AndroidViewModel.components: Components + get() = getApplication().components diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserStore.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserStore.kt new file mode 100644 index 0000000000..e037f3f368 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserStore.kt @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import mozilla.components.browser.state.search.SearchEngine +import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine +import mozilla.components.browser.state.store.BrowserStore + +/** + * Returns the default search engine name or "custom" string if the engine is added by the user. + */ +fun BrowserStore.defaultSearchEngineName(): String { + val defaultSearchEngine = state.search.selectedOrDefaultSearchEngine + return if (defaultSearchEngine?.type == SearchEngine.Type.CUSTOM) { + "custom" + } else { + defaultSearchEngine?.name ?: "" + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt new file mode 100644 index 0000000000..e719986a9d --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/BrowserToolbar.kt @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import android.content.Context +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.isVisible +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.concept.engine.EngineView +import mozilla.components.ui.widgets.behavior.EngineViewClippingBehavior +import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior +import org.mozilla.focus.R +import mozilla.components.ui.widgets.behavior.ToolbarPosition as engineToolbarPosition +import mozilla.components.ui.widgets.behavior.ViewPosition as browserToolbarPosition + +/** + * Collapse the toolbar and block it from appearing until calling [enableDynamicBehavior]. + * Useful in situations like entering fullscreen. + * + * @param engineView [EngineView] previously set to react to toolbar's dynamic behavior. + * Will now go through a bit of cleanup to ensure everything will be displayed nicely even without a toolbar. + */ +fun BrowserToolbar.disableDynamicBehavior(engineView: EngineView) { + (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = null + + engineView.setDynamicToolbarMaxHeight(0) + engineView.asView().translationY = 0f + (engineView.asView().layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = null +} + +/** + * Expand the toolbar and reenable the dynamic behavior. + * Useful after [disableDynamicBehavior] for situations like exiting fullscreen. + * + * @param context [Context] used in setting up the dynamic behavior. + * @param engineView [EngineView] that should react to toolbar's dynamic behavior. + */ +fun BrowserToolbar.enableDynamicBehavior(context: Context, engineView: EngineView) { + (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = EngineViewScrollingBehavior( + context, + null, + browserToolbarPosition.TOP, + ) + + val toolbarHeight = context.resources.getDimension(R.dimen.browser_toolbar_height).toInt() + engineView.setDynamicToolbarMaxHeight(toolbarHeight) + (engineView.asView().layoutParams as? CoordinatorLayout.LayoutParams)?.apply { + topMargin = 0 + behavior = EngineViewClippingBehavior( + context, + null, + engineView.asView(), + toolbarHeight, + engineToolbarPosition.TOP, + ) + } +} + +/** + * Show this toolbar at the top of the screen, fixed in place, with the EngineView immediately below it. + * + * @param context [Context] used for various system interactions + * @param engineView [EngineView] that must be shown immediately below the toolbar. + */ +fun BrowserToolbar.showAsFixed(context: Context, engineView: EngineView) { + isVisible = true + + engineView.setDynamicToolbarMaxHeight(0) + + val toolbarHeight = context.resources.getDimension(R.dimen.browser_toolbar_height).toInt() + (engineView.asView().layoutParams as? CoordinatorLayout.LayoutParams)?.topMargin = toolbarHeight +} + +/** + * Remove this toolbar from the screen and allow the EngineView to occupy the entire screen. + * + * @param engineView [EngineView] that will be configured to occupy the entire screen. + */ +fun BrowserToolbar.hide(engineView: EngineView) { + engineView.setDynamicToolbarMaxHeight(0) + (engineView.asView().layoutParams as? CoordinatorLayout.LayoutParams)?.topMargin = 0 + + isVisible = false +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/ContentState.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/ContentState.kt new file mode 100644 index 0000000000..86ecc538b0 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/ContentState.kt @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import mozilla.components.browser.state.state.ContentState + +val ContentState.hasSearchTerms: Boolean + get() = searchTerms.isNotEmpty() + +/** + * Returns the tab site title or domain name if title is empty. + */ +val ContentState.titleOrDomain: String + get() = title.ifEmpty { url.tryGetRootDomain } diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Context.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Context.kt new file mode 100644 index 0000000000..e4b8f89bb3 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Context.kt @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import android.app.Activity +import android.content.Context +import android.view.ContextThemeWrapper +import android.view.accessibility.AccessibilityManager +import mozilla.components.support.utils.ext.getPackageInfoCompat +import org.mozilla.focus.Components +import org.mozilla.focus.FocusApplication +import org.mozilla.focus.utils.Settings +import org.mozilla.gecko.util.HardwareUtils +import java.text.DateFormat + +/** + * Get the FocusApplication object from a context. + */ +val Context.application: FocusApplication + get() = applicationContext as FocusApplication + +/** + * Get the components of this application. + */ +val Context.components: Components + get() = application.components + +/** + * Get the settings of this application. + */ +val Context.settings: Settings + get() = application.components.settings + +/** + * System's [AccessibilityManager]. + */ +val Context.accessibilityManager: AccessibilityManager + get() = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager + +/** + * Get the app install date. + */ +val Context.installedDate: String + get() { + val installTime = this.packageManager.getPackageInfoCompat(this.packageName, 0).firstInstallTime + return DateFormat.getDateInstance().format(installTime) + } + +/** + * Checks if the current device is a tablet. + */ +fun Context.isTablet(): Boolean = HardwareUtils.isTablet(this) + +/** + * Casts [Context] to [Activity]. + */ +fun Context.tryAsActivity() = + (this as? ContextThemeWrapper)?.baseContext as? Activity ?: this as? Activity diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Fragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Fragment.kt new file mode 100644 index 0000000000..e0694eb0be --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Fragment.kt @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import org.mozilla.focus.Components +import org.mozilla.focus.activity.MainActivity + +/** + * Get the components of this application or null if this Fragment is not attached to a Context. + */ +val Fragment.components: Components? + get() = context?.components + +/** + * Get the components of this application. + * + * This method will throw an exception if this Fragment is not attached to a Context. + */ +val Fragment.requireComponents: Components + get() = requireContext().components + +/** + * Get the preference key. + * @param preferenceId Resource ID from preference_keys + */ +fun Fragment.getPreferenceKey(@StringRes preferenceId: Int): String = getString(preferenceId) + +/** + * Displays the toolbar with the given [title] if the parent activity + * can be casted to [AppCompatActivity] and [MainActivity] + */ +fun Fragment.showToolbar(title: String) { + (requireActivity() as? AppCompatActivity)?.title = title + (requireActivity() as? MainActivity)?.getToolbar()?.show() +} + +/** + * Hides the activity toolbar if the fragment is attached to an [AppCompatActivity]. + */ +fun Fragment.hideToolbar() { + (requireActivity() as? AppCompatActivity)?.supportActionBar?.hide() +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/PreferenceFragmentCompat.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/PreferenceFragmentCompat.kt new file mode 100644 index 0000000000..a1349c5bdc --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/PreferenceFragmentCompat.kt @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import androidx.annotation.StringRes +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat + +/** + * Find a preference with the corresponding key and throw if it does not exist. + * @param preferenceId Resource ID from preference_keys + */ +fun PreferenceFragmentCompat.requirePreference(@StringRes preferenceId: Int) = + requireNotNull(findPreference(getPreferenceKey(preferenceId))) diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/SessionState.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/SessionState.kt new file mode 100644 index 0000000000..f211bc6b45 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/SessionState.kt @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.browser.state.state.SessionState + +/** + * Returns this [SessionState] cast to [CustomTabSessionState] if possible. Otherwise returns `null`. + */ +fun SessionState.ifCustomTab(): CustomTabSessionState? { + if (this is CustomTabSessionState) { + return this + } + return null +} + +/** + * Returns `true` if this [SessionState] is a custom tab (an instance of [CustomTabSessionState]). + */ +fun SessionState.isCustomTab(): Boolean { + return this is CustomTabSessionState +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/String.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/String.kt new file mode 100644 index 0000000000..d0711ab15d --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/String.kt @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import androidx.compose.ui.graphics.Color +import androidx.core.net.toUri +import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes +import mozilla.components.support.ktx.util.URLStringUtils + +// Extension functions for the String class + +/** + * Beautify a URL by truncating it in a way that highlights important parts of the URL. + * + * Spec: https://github.com/mozilla-mobile/focus-android/issues/1231#issuecomment-326237077 + */ +fun String.beautifyUrl(): String { + if (isNullOrEmpty() || !URLStringUtils.isHttpOrHttps(this)) { + return this + } + + val beautifulUrl = StringBuilder() + + val uri = this.toUri() + + // Use only the truncated host name + + val truncatedHost = uri.truncatedHost() + if (truncatedHost.isNullOrEmpty()) { + return this + } + + beautifulUrl.append(truncatedHost) + + // Append the truncated path + + val truncatedPath = uri.truncatedPath() + if (truncatedPath.isNotEmpty()) { + beautifulUrl.append(truncatedPath) + } + + // And then append (only) the first query parameter + + val query = uri.query + if (!query.isNullOrEmpty()) { + beautifulUrl.append("?") + beautifulUrl.append(query.split("&").first()) + } + + // We always append a fragment if there's one + + val fragment = uri.fragment + if (!fragment.isNullOrEmpty()) { + beautifulUrl.append("#") + beautifulUrl.append(fragment) + } + + return beautifulUrl.toString() +} + +/** + * Tries to parse and get root domain part if [String] is a valid URL. + */ +val String.tryGetRootDomain: String + get() = + this.toUri().hostWithoutCommonPrefixes?.replaceAfter(".", "")?.removeSuffix(".") + ?.replaceFirstChar { it.uppercase() } ?: this + +/** + * Tries to parse a color string and return a [Color] + */ +val String.color: Color + get() = Color(android.graphics.Color.parseColor(this)) diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Uri.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Uri.kt new file mode 100644 index 0000000000..b13857f84c --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ext/Uri.kt @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.ext + +import android.net.Uri +import mozilla.components.support.ktx.kotlin.ELLIPSIS + +// Extension functions for the android.net.Uri class + +/** + * Return the truncated host of this Uri. The truncated host will only contain up to 2-3 segments of + * the original host. The original host will be returned if it's null or and empty String. + * + * Examples: + * mail.google.com -> google.com + * www.tomshardware.co.uk -> tomshardware.co.uk + * + * Spec: https://github.com/mozilla-mobile/focus-android/issues/1231#issuecomment-326237077 + */ +fun Uri.truncatedHost(): String? { + if (host.isNullOrEmpty()) { + return host + } + + // We start with the host name: + // * Pick the last two segments: + // en.m.wikipedia.org -> wikipedia.org + // * If the last element has a length of 2 or less than take another segment: + // www.tomshardware.co.uk -> tomshardware.co.uk + // * If there's no host then just remove the URL as is. + + val hostSegments = host!!.split(".") + val usedHostSegments = mutableListOf() + + for (segment in hostSegments.reversed()) { + if (usedHostSegments.size < 2) { + // We always pick the first two segments + usedHostSegments.add(0, segment) + } else if (usedHostSegments.first().length <= 2) { + // The last segment has a length of 2 or less -> pick this segment too + usedHostSegments.add(0, segment) + } else { + // We have everything we need. Bail out. + break + } + } + + return usedHostSegments.joinToString(separator = ".") +} + +/** + * Return the truncated path only containing the first and last segment of the full path. If the + * Uri does not have a path an empty String will be returned. + * + * Example: + * /foo/bar/test/index.html -> /foo/…/index.html + */ +fun Uri.truncatedPath(): String { + val segments = pathSegments + + return when (segments.size) { + 0 -> "" + 1 -> "/" + segments[0] + 2 -> "/" + segments.joinToString(separator = "/") + else -> "/" + listOf(segments.first(), Char.ELLIPSIS, segments.last()).joinToString(separator = "/") + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/firstrun/FirstrunCardView.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/firstrun/FirstrunCardView.kt new file mode 100644 index 0000000000..eea14dc1ff --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/firstrun/FirstrunCardView.kt @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.firstrun + +import android.content.Context +import android.util.AttributeSet +import org.mozilla.focus.R +import kotlin.math.min + +class FirstrunCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.cardViewStyle, +) : androidx.cardview.widget.CardView(context, attrs, defStyleAttr) { + + private val maxWidth = resources.getDimensionPixelSize(R.dimen.firstrun_card_width) + private val maxHeight = resources.getDimensionPixelSize(R.dimen.firstrun_card_height) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + // The view is set to match_parent in the layout file. So width and height should be the + // value needed to fill the whole parent view. + val availableWidth = MeasureSpec.getSize(widthMeasureSpec) + val availableHeight = MeasureSpec.getSize(heightMeasureSpec) + + // Now let's use those sizes to measure - but let's not exceed our defined max sizes (We do + // not want to have gigantic cards on large devices like tablets.) + val measuredWidth = min(availableWidth, maxWidth) + val measuredHeight = min(availableHeight, maxHeight) + + // Let's use the measured values to hand them to the super class to measure the child views etc. + val newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY) + val newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + + super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/firstrun/FirstrunPagerAdapter.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/firstrun/FirstrunPagerAdapter.kt new file mode 100644 index 0000000000..e3a60d271c --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/firstrun/FirstrunPagerAdapter.kt @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.firstrun + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.viewpager.widget.PagerAdapter +import androidx.viewpager.widget.ViewPager +import org.mozilla.focus.R + +class FirstrunPagerAdapter( + private val context: Context, + private val listener: View.OnClickListener, +) : PagerAdapter() { + + private data class FirstrunPage(val title: String, val text: String, val imageResource: Int) { + val contentDescription = title + text + } + + private val pages: Array + + init { + val appName = context.getString(R.string.app_name) + pages = arrayOf( + FirstrunPage( + context.getString(R.string.firstrun_defaultbrowser_title), + context.getString(R.string.firstrun_defaultbrowser_text2), + R.drawable.onboarding_img1, + ), + FirstrunPage( + context.getString(R.string.firstrun_search_title), + context.getString(R.string.firstrun_search_text), + R.drawable.onboarding_img4, + ), + FirstrunPage( + context.getString(R.string.firstrun_shortcut_title), + context.getString(R.string.firstrun_shortcut_text, appName), + R.drawable.onboarding_img3, + ), + FirstrunPage( + context.getString(R.string.firstrun_privacy_title), + context.getString(R.string.firstrun_privacy_text, appName), + R.drawable.onboarding_img2, + ), + ) + } + + private fun getView(position: Int, pager: ViewPager): View { + val view = LayoutInflater.from(context).inflate(R.layout.firstrun_page, pager, false) + + val page = pages[position] + + val titleView: TextView = view.findViewById(R.id.title) + titleView.text = page.title + + val textView: TextView = view.findViewById(R.id.text) + textView.text = page.text + + val imageView: ImageView = view.findViewById(R.id.image) + imageView.setImageResource(page.imageResource) + + val buttonView: Button = view.findViewById(R.id.button) + buttonView.setOnClickListener(listener) + if (position == pages.size - 1) { + buttonView.setText(R.string.firstrun_close_button) + buttonView.id = R.id.finish + buttonView.contentDescription = buttonView.text.toString().lowercase() + } else { + buttonView.setText(R.string.firstrun_next_button) + buttonView.id = R.id.next + } + + return view + } + + fun getPageAccessibilityDescription(position: Int): String = + pages[position].contentDescription + + override fun isViewFromObject(view: View, any: Any) = view === any + + override fun getCount() = pages.size + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + container as ViewPager + + val view = getView(position, container) + container.addView(view) + + return view + } + + override fun destroyItem(container: ViewGroup, position: Int, view: Any) { + view as View + container.removeView(view) + } +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/AddToHomescreenDialogFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/AddToHomescreenDialogFragment.kt new file mode 100644 index 0000000000..f06d789299 --- /dev/null +++ b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/AddToHomescreenDialogFragment.kt @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.fragment + +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.Button +import android.widget.EditText +import android.widget.ImageView +import androidx.appcompat.app.AlertDialog +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import androidx.preference.PreferenceManager +import mozilla.components.browser.icons.IconRequest +import mozilla.components.service.glean.private.NoExtras +import org.mozilla.focus.GleanMetrics.AddToHomeScreen +import org.mozilla.focus.R +import org.mozilla.focus.ext.components +import org.mozilla.focus.shortcut.HomeScreen +import org.mozilla.focus.shortcut.IconGenerator + +/** + * Fragment displaying a dialog where a user can change the title for a homescreen shortcut + */ +class AddToHomescreenDialogFragment : DialogFragment() { + + @Suppress("LongMethod") + override fun onCreateDialog(bundle: Bundle?): AlertDialog { + AddToHomeScreen.dialogDisplayed.record(NoExtras()) + val url = requireArguments().getString(URL)!! + val title = requireArguments().getString(TITLE) + val blockingEnabled = requireArguments().getBoolean(BLOCKING_ENABLED) + val requestDesktop = requireArguments().getBoolean(REQUEST_DESKTOP) + + val builder = AlertDialog.Builder(requireActivity(), R.style.DialogStyle) + builder.setCancelable(true) + val inflater = requireActivity().layoutInflater + val dialogView = inflater.inflate(R.layout.dialog_add_to_homescreen2, null) + builder.setView(dialogView) + + val iconView = dialogView.findViewById(R.id.homescreen_icon) + requireContext().components.icons.loadIntoView( + iconView, + IconRequest(url, isPrivate = true), + ) + + val blockIcon = dialogView.findViewById(R.id.homescreen_dialog_block_icon) + blockIcon.setImageResource(R.drawable.mozac_ic_shield_slash_24) + val warning = + dialogView.findViewById(R.id.homescreen_dialog_warning_layout) + warning.isVisible = !blockingEnabled + + val editableTitle = dialogView.findViewById(R.id.edit_title) + + if (!TextUtils.isEmpty(title)) { + editableTitle.setText(title) + } + + setButtons(dialogView, editableTitle, url, blockingEnabled, requestDesktop, title) + + return builder.create() + } + + @Suppress("LongParameterList") + private fun setButtons( + parentView: View, + editableTitle: EditText, + iconUrl: String, + blockingEnabled: Boolean, + requestDesktop: Boolean, + initialTitle: String?, + ) { + val addToHomescreenDialogCancelButton = + parentView.findViewById