diff --git a/.experimenter.yaml b/.experimenter.yaml index d1790ace8..6ae24eaa4 100644 --- a/.experimenter.yaml +++ b/.experimenter.yaml @@ -1,4 +1,12 @@ --- +cookie-banners: + description: Features for cookie banner handling. + hasExposure: true + exposureDescription: "" + variables: + sections-enabled: + type: json + description: This property provides a lookup table of whether or not the given section should be enabled. growth-data: description: A feature measuring campaign growth data hasExposure: true @@ -63,6 +71,14 @@ nimbus-validation: settings-title: type: string description: The title of displayed in the Settings screen and app menu. +re-engagement-notification: + description: A feature that shows the re-enagement notification if the user is inactive. + hasExposure: true + exposureDescription: "" + variables: + enabled: + type: boolean + description: "If true, the re-engagement notification is shown to the inactive user." search-term-groups: description: A feature allowing the grouping of URLs around the search term that it came from. hasExposure: true diff --git a/.taskcluster.yml b/.taskcluster.yml index e803a2845..f1fd7be3e 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -122,7 +122,7 @@ tasks: name: "Decision Task for cron job ${cron.job_name}" description: 'Created by a [cron task](https://firefox-ci-tc.services.mozilla.com/tasks/${cron.task_id})' provisionerId: "${trustDomain}-${level}" - workerType: "decision" + workerType: "decision-gcp" tags: $if: 'tasks_for in ["github-push", "github-pull-request"]' then: diff --git a/app/build.gradle b/app/build.gradle index b8a30714c..97ed32ad1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -687,6 +687,7 @@ dependencies { testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3' implementation Deps.mozilla_support_rusthttp + androidTestImplementation Deps.mockk_android testImplementation Deps.mockk // For the initial release of Glean 19, we require consumer applications to diff --git a/app/metrics.yaml b/app/metrics.yaml index afc6256d4..18050c1b6 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -206,6 +206,32 @@ events: - https://github.com/mozilla-mobile/fenix/issues/27779 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/27780 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 122 + re_engagement_notif_tapped: + type: event + description: | + User tapped on the re-engagement notification + bugs: + - https://github.com/mozilla-mobile/fenix/issues/27949 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/27978 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 122 + re_engagement_notif_shown: + type: event + description: | + Re-engagement notification was shown to the user + bugs: + - https://github.com/mozilla-mobile/fenix/issues/27949 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/27978 data_sensitivity: - technical notification_emails: @@ -1105,6 +1131,7 @@ metrics: to identify installs from Mozilla Online. send_in_pings: - metrics + - baseline bugs: - https://github.com/mozilla-mobile/fenix/issues/16075 data_reviews: @@ -1116,6 +1143,8 @@ metrics: - android-probes@mozilla.com - kbrosnan@mozilla.com expires: never + no_lint: + - BASELINE_PING metadata: tags: - China @@ -1906,12 +1935,14 @@ customize_home: An indication of whether Contile is enabled to be displayed send_in_pings: - metrics + - topsites-impression bugs: - https://github.com/mozilla-mobile/fenix/issues/24467 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/24468 data_sensitivity: - interaction + lifetime: application notification_emails: - android-probes@mozilla.com expires: 112 @@ -6200,18 +6231,28 @@ browser.search: type: labeled_counter description: | Records counts of SERP pages with adverts displayed. - The key format is ``. + 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 bugs: - https://github.com/mozilla-mobile/fenix/issues/6558 + - https://github.com/mozilla-mobile/fenix/issues/28010 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1799049 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/10112 - https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068 - https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789 - https://github.com/mozilla-mobile/fenix/pull/20230#issuecomment-879244938 - https://github.com/mozilla-mobile/fenix/pull/21038#issuecomment-906757301 + - https://github.com/mozilla-mobile/fenix/pull/28012#issuecomment-1330822281 data_sensitivity: - interaction notification_emails: @@ -6224,18 +6265,27 @@ browser.search: type: labeled_counter description: | Records clicks of adverts on SERP pages. - The key format is ``. + 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 bugs: - https://github.com/mozilla-mobile/fenix/issues/6558 + - https://github.com/mozilla-mobile/fenix/issues/28010 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/10112 - https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068 - https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789 - https://github.com/mozilla-mobile/fenix/pull/20230#issuecomment-879244938 - https://github.com/mozilla-mobile/fenix/pull/21038#issuecomment-906757301 + - https://github.com/mozilla-mobile/fenix/pull/28012#issuecomment-1330822281 data_sensitivity: - interaction notification_emails: @@ -6795,6 +6845,93 @@ autoplay: tags: - SitePermissions +cookie_banners: + visited_setting: + type: event + description: A user visited the cookie banner handling screen + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1796146 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/27561 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 118 + 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 banners: disabled,reject_all, + or reject_or_accept_all. + type: string + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1796146 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/27561 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 118 + 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=1797577 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 118 + 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=1797577 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 118 + metadata: + tags: + - Privacy&Security + visited_panel: + type: event + description: A user visited the cookie banner toolbar panel + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797577 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 118 + metadata: + tags: + - Privacy&Security site_permissions: prompt_shown: type: event diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt index d0e69cb86..f404e990e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt @@ -18,6 +18,7 @@ object Constants { } const val SPEECH_RECOGNITION = "android.speech.action.RECOGNIZE_SPEECH" + const val POCKET_RECOMMENDED_STORIES_UTM_PARAM = "utm_source=pocket-newtab-android" const val LONG_CLICK_DURATION: Long = 5000 const val LISTS_MAXSWIPES: Int = 3 const val RETRY_COUNT = 3 diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/Experimentation.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/Experimentation.kt new file mode 100644 index 000000000..3b4ba4ee1 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/Experimentation.kt @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.helpers + +import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.helpers.TestHelper.appContext + +object Experimentation { + val experiments = + appContext.components.analytics.experiments + + fun withHelper(block: GleanPlumbMessageHelper.() -> Unit) { + val helper = experiments.createMessageHelper() + block(helper) + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt index efda02186..ec3ef1959 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.helpers +import android.content.Intent import android.view.ViewConfiguration.getLongPressTimeout import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.rule.ActivityTestRule @@ -161,6 +162,8 @@ class HomeActivityIntentTestRule internal constructor( private val longTapUserPreference = getLongPressTimeout() + private lateinit var intent: Intent + /** * Update settings after the activity was created. */ @@ -171,6 +174,19 @@ class HomeActivityIntentTestRule internal constructor( } } + override fun getActivityIntent(): Intent? { + return if (this::intent.isInitialized) { + this.intent + } else { + super.getActivityIntent() + } + } + + fun withIntent(intent: Intent): HomeActivityIntentTestRule { + this.intent = intent + return this + } + override fun beforeActivityLaunched() { super.beforeActivityLaunched() setLongTapTimeout(3000) diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt index 368cd8244..6d9282635 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.helpers +import android.Manifest import android.app.ActivityManager import android.app.PendingIntent import android.content.ActivityNotFoundException @@ -20,8 +21,12 @@ import android.graphics.Canvas import android.graphics.Color import android.net.Uri import android.os.Build +import android.os.storage.StorageManager +import android.os.storage.StorageVolume import android.provider.Settings +import android.util.Log import android.view.View +import androidx.annotation.RequiresApi import androidx.browser.customtabs.CustomTabsIntent import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView @@ -38,10 +43,10 @@ import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule +import androidx.test.runner.permission.PermissionRequester import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject -import androidx.test.uiautomator.UiObjectNotFoundException import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until @@ -52,12 +57,13 @@ import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matcher import org.junit.Assert +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity import org.mozilla.fenix.ext.components -import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.ext.waitNotNull @@ -65,6 +71,7 @@ import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource import org.mozilla.fenix.ui.robots.BrowserRobot import org.mozilla.fenix.utils.IntentUtils import org.mozilla.gecko.util.ThreadUtils +import java.io.File import java.util.Locale import java.util.regex.Pattern @@ -147,24 +154,21 @@ object TestHelper { } } - // Remove test file from Google Photos (AOSP) on Firebase - fun deleteDownloadFromStorage() { - val deleteButton = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/trash")) - deleteButton.waitForExists(waitingTime) - deleteButton.click() - - // Sometimes there's a secondary confirmation + @RequiresApi(Build.VERSION_CODES.R) + fun deleteDownloadedFileOnStorage(fileName: String) { + val storageManager: StorageManager? = appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager? + val storageVolumes = storageManager!!.storageVolumes + val storageVolume: StorageVolume = storageVolumes[0] + val file = File(storageVolume.directory!!.path + "/Download/" + fileName) try { - val deleteConfirm = mDevice.findObject(UiSelector().text("Got it")) - deleteConfirm.waitForExists(waitingTime) - deleteConfirm.click() - } catch (e: UiObjectNotFoundException) { - // Do nothing + file.delete() + Log.d("TestLog", "File delete try 1") + assertFalse("The file was not deleted", file.exists()) + } catch (e: AssertionError) { + file.delete() + Log.d("TestLog", "File delete retried") + assertFalse("The file was not deleted", file.exists()) } - - val trashIt = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/move_to_trash")) - trashIt.waitForExists(waitingTime) - trashIt.click() } fun setNetworkEnabled(enabled: Boolean) { @@ -388,20 +392,31 @@ object TestHelper { /** * Changes the default language of the entire device, not just the app. + * Runs on Debug variant as we don't want to adjust Release permission manifests * Runs the test in its testBlock. - * Cleans up and sets the default locale after it's are done. + * Cleans up and sets the default locale after it's done. */ fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule, testBlock: () -> Unit) { - val defaultLocale = Locale.getDefault() + if (Config.channel.isDebug) { + /* Sets permission to change device language */ + PermissionRequester().apply { + addPermissions( + Manifest.permission.CHANGE_CONFIGURATION, + ) + requestPermissions() + } - try { - setSystemLocale(locale) - testBlock() - ThreadUtils.runOnUiThread { testRule.activity.recreate() } - } catch (e: Exception) { - e.printStackTrace() - } finally { - setSystemLocale(defaultLocale) + val defaultLocale = Locale.getDefault() + + try { + setSystemLocale(locale) + testBlock() + ThreadUtils.runOnUiThread { testRule.activity.recreate() } + } catch (e: Exception) { + e.printStackTrace() + } finally { + setSystemLocale(defaultLocale) + } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt b/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt index e2aae7321..7e8a10263 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt @@ -33,7 +33,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule * * Say no to main thread IO! 🙅 */ -private const val EXPECTED_SUPPRESSION_COUNT = 19 +private const val EXPECTED_SUPPRESSION_COUNT = 18 /** * The number of times we call the `runBlocking` coroutine method on the main thread during this diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt index 087121e39..e55b93eff 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt @@ -128,6 +128,28 @@ class ContextMenusTest { } } + @Test + fun verifyContextCopyLinkNotDisplayedAfterApplied() { + val pageLinks = TestAssetHelper.getGenericAsset(mockWebServer, 4) + val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 3) + + navigationToolbar { + }.enterURLAndEnterToBrowser(pageLinks.url) { + longClickMatchingText("Link 3") + verifyLinkContextMenuItems(genericURL.url) + clickContextCopyLink() + verifySnackBarText("Link copied to clipboard") + }.openNavigationToolbar { + }.visitLinkFromClipboard { + verifyUrl(genericURL.url.toString()) + }.openTabDrawer { + }.openNewTab { + } + navigationToolbar { + verifyClipboardSuggestionsAreDisplayed(shouldBeDisplayed = false) + } + } + @Test fun verifyContextShareLink() { val pageLinks = diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt index d71c2b089..7678aa973 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt @@ -5,20 +5,14 @@ package org.mozilla.fenix.ui import androidx.core.net.toUri -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.permission.PermissionRequester -import androidx.test.uiautomator.UiDevice import org.junit.After import org.junit.Before -import org.junit.Ignore 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.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.HomeActivityIntentTestRule -import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage +import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage +import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.downloadRobot import org.mozilla.fenix.ui.robots.navigationToolbar @@ -33,8 +27,6 @@ import org.mozilla.fenix.ui.robots.notificationShade * - Verifies managing downloads inside the Downloads listing. **/ class DownloadTest { - private lateinit var mDevice: UiDevice - /* Remote test page managed by Mozilla Mobile QA team at https://github.com/mozilla-mobile/testapp */ private val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html" private var downloadFile: String = "" @@ -42,25 +34,8 @@ class DownloadTest { @get:Rule val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides() - // Making sure to grant storage access for this test running on API 28 - @get: Rule - var watcher: TestRule = object : TestWatcher() { - override fun starting(description: Description) { - if (description.methodName == "pauseResumeCancelDownloadTest") { - PermissionRequester().apply { - addPermissions( - android.Manifest.permission.WRITE_EXTERNAL_STORAGE, - android.Manifest.permission.READ_EXTERNAL_STORAGE, - ) - requestPermissions() - } - } - } - } - @Before fun setUp() { - mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) // clear all existing notifications notificationShade { mDevice.openNotification() @@ -157,13 +132,13 @@ class DownloadTest { /* Verifies downloads in the Downloads Menu: - downloads appear in the list - deleting a download from device storage, removes it from the Downloads Menu too - */ - @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/27220") + */ @SmokeTest @Test fun manageDownloadsInDownloadsMenuTest() { // a long filename to verify it's correctly displayed on the prompt and in the Downloads menu - downloadFile = "tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg" + downloadFile = + "tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg" navigationToolbar { }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { @@ -179,14 +154,34 @@ class DownloadTest { waitForDownloadsListToExist() verifyDownloadedFileName(downloadFile) verifyDownloadedFileIcon() - openDownloadedFile(downloadFile) - verifyPhotosAppOpens() - deleteDownloadFromStorage() - waitForDownloadsListToExist() + deleteDownloadedFileOnStorage(downloadFile) }.exitDownloadsManagerToBrowser { }.openThreeDotMenu { }.openDownloadsManager { verifyEmptyDownloadsList() } } + + @SmokeTest + @Test + fun openDownloadedFileTest() { + downloadFile = "web_icon.png" + + navigationToolbar { + }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { + waitForPageToLoad() + }.clickDownloadLink(downloadFile) { + verifyDownloadPrompt(downloadFile) + }.clickDownload { + verifyDownloadNotificationPopup() + } + browserScreen { + }.openThreeDotMenu { + }.openDownloadsManager { + verifyDownloadedFileName(downloadFile) + openDownloadedFile(downloadFile) + verifyPhotosAppOpens() + mDevice.pressBack() + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt index e2bb8e865..e28888505 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.ui +import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice @@ -11,9 +12,11 @@ import androidx.test.uiautomator.Until 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.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.Constants.POCKET_RECOMMENDED_STORIES_UTM_PARAM import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper @@ -34,11 +37,13 @@ class HomeScreenTest { private lateinit var mDevice: UiDevice private lateinit var mockWebServer: MockWebServer + private lateinit var firstPocketStoryPublisher: String - @get:Rule - val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides() + @get:Rule(order = 0) + val activityTestRule = + AndroidComposeTestRule(HomeActivityTestRule.withDefaultSettingsOverrides()) { it.activity } - @Rule + @Rule(order = 1) @JvmField val retryTestRule = RetryTestRule(3) @@ -62,21 +67,26 @@ class HomeScreenTest { homeScreen { }.dismissOnboarding() homeScreen { - verifyHomeScreen() - verifyNavigationToolbar() - verifyHomePrivateBrowsingButton() - verifyHomeMenu() verifyHomeWordmark() - verifyTabButton() - verifyCollectionsHeader() - verifyHomeToolbar() - verifyHomeComponent() - - // Verify Top Sites - verifyExistingTopSitesList() + verifyHomePrivateBrowsingButton() verifyExistingTopSitesTabs("Wikipedia") verifyExistingTopSitesTabs("Top Articles") verifyExistingTopSitesTabs("Google") + verifyCollectionsHeader() + verifyNoCollectionsText() + scrollToPocketProvokingStories() + swipePocketProvokingStories() + verifyPocketRecommendedStoriesItems(activityTestRule, 1, 3, 4, 5, 6, 7) + verifyPocketSponsoredStoriesItems(activityTestRule, 2, 8) + verifyDiscoverMoreStoriesButton(activityTestRule, 9) + verifyStoriesByTopicItems() + verifyPoweredByPocket(activityTestRule) + verifyCustomizeHomepageButton(true) + verifyNavigationToolbar() + verifyDefaultSearchEngine("Google") + verifyHomeMenuButton() + verifyTabButton() + verifyTabCounter("0") } } @@ -89,11 +99,11 @@ class HomeScreenTest { verifyHomeScreen() verifyNavigationToolbar() verifyHomePrivateBrowsingButton() - verifyHomeMenu() + verifyHomeMenuButton() verifyHomeWordmark() verifyTabButton() verifyPrivateSessionMessage() - verifyHomeToolbar() + verifyNavigationToolbar() verifyHomeComponent() } @@ -104,15 +114,47 @@ class HomeScreenTest { verifyHomeScreen() verifyNavigationToolbar() verifyHomePrivateBrowsingButton() - verifyHomeMenu() + verifyHomeMenuButton() verifyHomeWordmark() verifyTabButton() verifyPrivateSessionMessage() - verifyHomeToolbar() + verifyNavigationToolbar() verifyHomeComponent() } } + @Test + fun verifyJumpBackInSectionTest() { + val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4) + val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + navigationToolbar { + }.enterURLAndEnterToBrowser(firstWebPage.url) { + }.goToHomescreen { + verifyJumpBackInSectionIsDisplayed() + verifyJumpBackInItemTitle(firstWebPage.title) + verifyJumpBackInItemWithUrl(firstWebPage.url.toString()) + verifyJumpBackInShowAllButton() + }.clickJumpBackInShowAllButton { + verifyExistingOpenTabs(firstWebPage.title) + }.closeTabDrawer() { + } + homeScreen { + }.clickJumpBackInItemWithTitle(firstWebPage.title) { + verifyUrl(firstWebPage.url.toString()) + clickLinkMatchingText("Link 1") + }.goToHomescreen { + verifyJumpBackInSectionIsDisplayed() + verifyJumpBackInItemTitle(secondWebPage.title) + verifyJumpBackInItemWithUrl(secondWebPage.url.toString()) + }.openTabDrawer { + closeTab() + } + homeScreen { + verifyJumpBackInSectionIsNotDisplayed() + } + } + @Test fun dismissOnboardingUsingSettingsTest() { homeScreen { @@ -141,7 +183,7 @@ class HomeScreenTest { @Test fun dismissOnboardingUsingHelpTest() { - activityTestRule.applySettingsExceptions { + activityTestRule.activityRule.applySettingsExceptions { it.isJumpBackInCFREnabled = false it.isWallpaperOnboardingEnabled = false } @@ -156,6 +198,25 @@ class HomeScreenTest { } } + @Test + fun dismissOnboardingWithPageLoadTest() { + activityTestRule.activityRule.applySettingsExceptions { + it.isJumpBackInCFREnabled = false + it.isWallpaperOnboardingEnabled = false + } + + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + homeScreen { + verifyWelcomeHeader() + } + navigationToolbar { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.goToHomescreen { + verifyExistingTopSitesList() + } + } + @Test fun toolbarTapDoesntDismissOnboardingTest() { homeScreen { @@ -171,7 +232,7 @@ class HomeScreenTest { @Test fun verifyPocketHomepageStoriesTest() { - activityTestRule.applySettingsExceptions { + activityTestRule.activityRule.applySettingsExceptions { it.isRecentTabsFeatureEnabled = false it.isRecentlyVisitedFeatureEnabled = false } @@ -181,6 +242,11 @@ class HomeScreenTest { homeScreen { verifyThoughtProvokingStories(true) + scrollToPocketProvokingStories() + swipePocketProvokingStories() + verifyPocketRecommendedStoriesItems(activityTestRule, 1, 3, 4, 5, 6, 7) + verifyPocketSponsoredStoriesItems(activityTestRule, 2, 8) + verifyDiscoverMoreStoriesButton(activityTestRule, 9) verifyStoriesByTopic(true) }.openThreeDotMenu { }.openCustomizeHome { @@ -191,6 +257,79 @@ class HomeScreenTest { } } + @Test + fun openPocketStoryItemTest() { + activityTestRule.activityRule.applySettingsExceptions { + it.isRecentTabsFeatureEnabled = false + it.isRecentlyVisitedFeatureEnabled = false + } + + homeScreen { + }.dismissOnboarding() + + homeScreen { + verifyThoughtProvokingStories(true) + scrollToPocketProvokingStories() + firstPocketStoryPublisher = getProvokingStoryPublisher(1) + }.clickPocketStoryItem(firstPocketStoryPublisher, 1) { + verifyUrl(POCKET_RECOMMENDED_STORIES_UTM_PARAM) + } + } + + @Ignore("Failed, see: https://github.com/mozilla-mobile/fenix/issues/28098") + @Test + fun openPocketDiscoverMoreTest() { + activityTestRule.activityRule.applySettingsExceptions { + it.isRecentTabsFeatureEnabled = false + it.isRecentlyVisitedFeatureEnabled = false + } + + homeScreen { + }.dismissOnboarding() + + homeScreen { + scrollToPocketProvokingStories() + swipePocketProvokingStories() + verifyDiscoverMoreStoriesButton(activityTestRule, 9) + }.clickPocketDiscoverMoreButton(activityTestRule, 9) { + verifyUrl("getpocket.com/explore") + } + } + + @Test + fun selectStoriesByTopicItemTest() { + activityTestRule.activityRule.applySettingsExceptions { + it.isRecentTabsFeatureEnabled = false + it.isRecentlyVisitedFeatureEnabled = false + } + + homeScreen { + }.dismissOnboarding() + + homeScreen { + verifyStoriesByTopicItemState(activityTestRule, false, 1) + clickStoriesByTopicItem(activityTestRule, 1) + verifyStoriesByTopicItemState(activityTestRule, true, 1) + } + } + + @Test + fun verifyPocketLearnMoreLinkTest() { + activityTestRule.activityRule.applySettingsExceptions { + it.isRecentTabsFeatureEnabled = false + it.isRecentlyVisitedFeatureEnabled = false + } + + homeScreen { + }.dismissOnboarding() + + homeScreen { + verifyPoweredByPocket(activityTestRule) + }.clickPocketLearnMoreLink(activityTestRule) { + verifyUrl("mozilla.org/en-US/firefox/pocket") + } + } + @Test fun verifyCustomizeHomepageTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt index d0629660a..3f602610f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -12,11 +12,14 @@ import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar +import java.util.Locale /** * Tests for verifying basic functionality of browser navigation and page related interactions @@ -95,6 +98,46 @@ class NavigationToolbarTest { } } + // Swipes the nav bar left/right to switch between tabs + @SmokeTest + @Test + fun swipeToSwitchTabTest() { + val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) + + navigationToolbar { + }.enterURLAndEnterToBrowser(firstWebPage.url) { + }.openTabDrawer { + }.openNewTab { + }.submitQuery(secondWebPage.url.toString()) { + swipeNavBarRight(secondWebPage.url.toString()) + verifyUrl(firstWebPage.url.toString()) + swipeNavBarLeft(firstWebPage.url.toString()) + verifyUrl(secondWebPage.url.toString()) + } + } + + // Because it requires changing system prefs, this test will run only on Debug builds + @Test + fun swipeToSwitchTabInRTLTest() { + val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) + val arabicLocale = Locale("ar", "AR") + + runWithSystemLocaleChanged(arabicLocale, activityTestRule) { + navigationToolbar { + }.enterURLAndEnterToBrowser(firstWebPage.url) { + }.openTabDrawer { + }.openNewTab { + }.submitQuery(secondWebPage.url.toString()) { + swipeNavBarLeft(secondWebPage.url.toString()) + verifyUrl(firstWebPage.url.toString()) + swipeNavBarRight(firstWebPage.url.toString()) + verifyUrl(secondWebPage.url.toString()) + } + } + } + // Test running on beta/release builds in CI: // caution when making changes to it, so they don't block the builds @Test diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusEventTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusEventTest.kt new file mode 100644 index 000000000..e3e33bf08 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NimbusEventTest.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.fenix.ui + +import android.content.Intent +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import io.mockk.mockk +import mozilla.components.concept.sync.AuthType +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.mozilla.fenix.components.TelemetryAccountObserver +import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.Experimentation +import org.mozilla.fenix.helpers.HomeActivityIntentTestRule +import org.mozilla.fenix.helpers.RetryTestRule +import org.mozilla.fenix.helpers.TestHelper.appContext +import org.mozilla.fenix.ui.robots.homeScreen + +class NimbusEventTest { + private lateinit var mDevice: UiDevice + private lateinit var mockWebServer: MockWebServer + + @get:Rule + val homeActivityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides() + .withIntent( + Intent().apply { + action = Intent.ACTION_VIEW + }, + ) + + @Rule + @JvmField + val retryTestRule = RetryTestRule(3) + + @Before + fun setUp() { + mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + mockWebServer = MockWebServer().apply { + dispatcher = AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + mockWebServer.shutdown() + } + + @Test + fun homeScreenNimbusEventsTest() { + homeScreen { }.dismissOnboarding() + + Experimentation.withHelper { + assertTrue(evalJexl("'app_opened'|eventSum('Days', 28, 0) > 0")) + } + } + + @Test + fun telemetryAccountObserverTest() { + val observer = TelemetryAccountObserver(appContext) + observer.onAuthenticated(mockk(), AuthType.Signin) + + Experimentation.withHelper { + assertTrue(evalJexl("'sync_auth.sign_in'|eventSum('Days', 28, 0) > 0")) + } + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt index 28a49e45b..9a2adea03 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt @@ -160,6 +160,7 @@ class SearchTest { } } + @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @SmokeTest @Test fun searchGroupShowsInRecentlyVisitedTest() { @@ -194,6 +195,7 @@ class SearchTest { } } + @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @Test fun verifySearchGroupHistoryWithNoDuplicatesTest() { val firstPageUrl = getGenericAsset(searchMockServer, 1).url @@ -275,6 +277,7 @@ class SearchTest { } } + @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @SmokeTest @Test fun noSearchGroupFromPrivateBrowsingTest() { @@ -313,6 +316,7 @@ class SearchTest { } } + @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @SmokeTest @Test fun deleteItemsFromSearchGroupHistoryTest() { @@ -361,6 +365,7 @@ class SearchTest { } } + @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @Test fun deleteSearchGroupFromHistoryTest() { queryString = "test search" @@ -407,6 +412,7 @@ class SearchTest { } } + @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @Test fun reopenTabsFromSearchGroupTest() { val firstPageUrl = getGenericAsset(searchMockServer, 1).url @@ -460,6 +466,7 @@ class SearchTest { } } + @Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704") @Test fun sharePageFromASearchGroupTest() { val firstPageUrl = getGenericAsset(searchMockServer, 1).url diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt index 24a6fc292..63c4bb7b1 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt @@ -4,7 +4,6 @@ package org.mozilla.fenix.ui -import android.content.res.Configuration import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Before @@ -71,17 +70,6 @@ class SettingsBasicsTest { mockWebServer.shutdown() } - private fun getUiTheme(): Boolean { - val mode = - activityIntentTestRule.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 - } - } - @Test fun settingsGeneralItemsTests() { homeScreen { @@ -101,21 +89,6 @@ class SettingsBasicsTest { } } - @Test - fun changeThemeSetting() { - // Goes through the settings and changes the default search engine, then verifies it changes. - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openCustomizeSubMenu { - verifyThemes() - selectDarkMode() - verifyDarkThemeApplied(getUiTheme()) - selectLightMode() - verifyLightThemeApplied(getUiTheme()) - } - } - @Test fun changeAccessibiltySettings() { // Goes through the settings and changes the default text on a webpage, then verifies if the text has changed. @@ -302,6 +275,7 @@ class SettingsBasicsTest { } } + // Because it requires changing system prefs, this test will run only on Debug builds @Ignore("Failing due to app translation bug, see: https://github.com/mozilla-mobile/fenix/issues/26729") @Test fun frenchSystemLocaleTest() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsCustomizeTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsCustomizeTest.kt new file mode 100644 index 000000000..0176c12ff --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsCustomizeTest.kt @@ -0,0 +1,122 @@ +package org.mozilla.fenix.ui + +import android.content.res.Configuration +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.HomeActivityIntentTestRule +import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.helpers.TestHelper.exitMenu +import org.mozilla.fenix.ui.robots.homeScreen +import org.mozilla.fenix.ui.robots.navigationToolbar + +class SettingsCustomizeTest { + private lateinit var mockWebServer: MockWebServer + + @get:Rule + val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides() + + @Before + fun setUp() { + mockWebServer = MockWebServer().apply { + dispatcher = AndroidAssetDispatcher() + start() + } + } + + @After + fun tearDown() { + mockWebServer.shutdown() + } + + private fun getUiTheme(): Boolean { + val mode = + activityIntentTestRule.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 + } + } + + @Test + fun changeThemeSettingTest() { + // Goes through the settings and changes the default search engine, then verifies it changes. + homeScreen { + }.openThreeDotMenu { + }.openSettings { + }.openCustomizeSubMenu { + verifyThemes() + selectDarkMode() + verifyDarkThemeApplied(getUiTheme()) + selectLightMode() + verifyLightThemeApplied(getUiTheme()) + } + } + + @Test + fun setToolbarPositionTest() { + homeScreen { + }.openThreeDotMenu { + }.openSettings { + }.openCustomizeSubMenu { + verifyToolbarPositionPreference("Bottom") + clickTopToolbarToggle() + verifyToolbarPositionPreference("Top") + }.goBack { + }.goBack { + verifyToolbarPosition(defaultPosition = false) + }.openThreeDotMenu { + }.openSettings { + }.openCustomizeSubMenu { + clickBottomToolbarToggle() + verifyToolbarPositionPreference("Bottom") + exitMenu() + } + homeScreen { + verifyToolbarPosition(defaultPosition = true) + } + } + + @Test + fun swipeToolbarGesturePreferenceOffTest() { + val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) + + homeScreen { + }.openThreeDotMenu { + }.openSettings { + }.openCustomizeSubMenu { + verifySwipeToolbarGesturePrefState(true) + clickSwipeToolbarToSwitchTabToggle() + verifySwipeToolbarGesturePrefState(false) + exitMenu() + } + navigationToolbar { + }.enterURLAndEnterToBrowser(firstWebPage.url) { + }.openTabDrawer { + }.openNewTab { + }.submitQuery(secondWebPage.url.toString()) { + swipeNavBarRight(secondWebPage.url.toString()) + verifyUrl(secondWebPage.url.toString()) + swipeNavBarLeft(secondWebPage.url.toString()) + verifyUrl(secondWebPage.url.toString()) + } + } + + @Test + fun pullToRefreshPreferenceTest() { + homeScreen { + }.openThreeDotMenu { + }.openSettings { + }.openCustomizeSubMenu { + verifyPullToRefreshGesturePrefState(isEnabled = true) + clickPullToRefreshToggle() + verifyPullToRefreshGesturePrefState(isEnabled = false) + } + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt index 42ee74c5f..1ec759d2e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsHomepageTest.kt @@ -15,6 +15,8 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset +import org.mozilla.fenix.helpers.TestHelper.mDevice +import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink import org.mozilla.fenix.helpers.TestHelper.restartApp import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen @@ -180,19 +182,6 @@ class SettingsHomepageTest { fun startOnLastTabTest() { val firstWebPage = getGenericAsset(mockWebServer, 1) - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openHomepageSubMenu { - clickStartOnHomepageButton() - } - - restartApp(activityIntentTestRule) - - homeScreen { - verifyHomeScreen() - } - navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { }.goToHomescreen { @@ -208,6 +197,31 @@ class SettingsHomepageTest { } } + @Test + fun ignoreStartOnHomeWhenLaunchedByExternalLinkTest() { + val genericPage = getGenericAsset(mockWebServer, 1) + + homeScreen { + }.openThreeDotMenu { + }.openSettings { + }.openHomepageSubMenu { + clickStartOnHomepageButton() + }.goBack {} + + with(activityIntentTestRule) { + finishActivity() + mDevice.waitForIdle() + this.applySettingsExceptions { + it.isTCPCFREnabled = false + } + openAppFromExternalLink(genericPage.url.toString()) + } + + browserScreen { + verifyPageContent(genericPage.content) + } + } + @SmokeTest @Test @Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/26559") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt index bedae77e2..5708ff37c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSearchTest.kt @@ -420,12 +420,11 @@ class SettingsSearchTest { "Bing", "Amazon.com", "DuckDuckGo", - "eBay", - /* Disabled Arabic Wikipedia verification - until https://github.com/mozilla-mobile/fenix/issues/12236 gets fixed - "ويكيبيديا (ar)" - */ + "ويكيبيديا (ar)", ) + changeDefaultSearchEngine(activityTestRule, "ويكيبيديا (ar)") + }.submitQuery("firefox") { + verifyUrl("ar.m.wikipedia.org") } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt index d0a85e27a..8d0077f7e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -37,7 +37,6 @@ import org.mozilla.fenix.helpers.TestHelper.assertNativeAppOpens import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent import org.mozilla.fenix.helpers.TestHelper.generateRandomString import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources -import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.customTabScreen @@ -46,7 +45,6 @@ import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.notificationShade import org.mozilla.fenix.ui.robots.openEditURLView import org.mozilla.fenix.ui.robots.searchScreen -import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER /** * Test Suite that contains a part of the Smoke and Sanity tests defined in TestRail: @@ -112,41 +110,19 @@ class SmokeTest { @Test fun firstRunScreenTest() { homeScreen { - verifyHomeScreen() - verifyNavigationToolbar() - verifyHomePrivateBrowsingButton() - verifyHomeMenu() - verifyHomeWordmark() - - verifyWelcomeHeader() - // Sign in to Firefox - verifyStartSyncHeader() - verifyAccountsSignInButton() - - // Always-on privacy - scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER) - verifyAutomaticPrivacyHeader() - verifyAutomaticPrivacyText() - - // Choose your theme - verifyChooseThemeHeader() - verifyChooseThemeText() - verifyDarkThemeDescription() - verifyDarkThemeToggle() - verifyLightThemeDescription() - verifyLightThemeToggle() - - // Pick your toolbar placement - verifyTakePositionHeader() - verifyTakePositionElements() - - // Your privacy - verifyYourPrivacyHeader() - verifyYourPrivacyText() - verifyPrivacyNoticeButton() - - // Start Browsing - verifyStartBrowsingButton() + verifyHomeScreenAppBarItems() + verifyHomeScreenWelcomeItems() + verifyChooseYourThemeCard( + isDarkThemeChecked = false, + isLightThemeChecked = false, + isAutomaticThemeChecked = true, + ) + verifyToolbarPlacementCard(isBottomChecked = true, isTopChecked = false) + verifySignInToSyncCard() + verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false) + verifyPrivacyNoticeCard() + verifyStartBrowsingSection() + verifyNavigationToolbarItems("0") } } @@ -421,24 +397,6 @@ class SmokeTest { } } - // Swipes the nav bar left/right to switch between tabs - @Test - fun swipeToSwitchTabTest() { - val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) - - navigationToolbar { - }.enterURLAndEnterToBrowser(firstWebPage.url) { - }.openTabDrawer { - }.openNewTab { - }.submitQuery(secondWebPage.url.toString()) { - swipeNavBarRight(secondWebPage.url.toString()) - verifyUrl(firstWebPage.url.toString()) - swipeNavBarLeft(firstWebPage.url.toString()) - verifyUrl(secondWebPage.url.toString()) - } - } - // Saves a login, then changes it and verifies the update @Test fun updateSavedLoginTest() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt index 1be555141..69761496f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt @@ -122,7 +122,7 @@ class TabbedBrowsingTest { verifyShareTabButton() verifySelectTabs() }.closeAllTabs { - verifyNoTabsOpened() + verifyTabCounter("0") } // Repeat for Private Tabs @@ -137,7 +137,7 @@ class TabbedBrowsingTest { }.openTabsListThreeDotMenu { verifyCloseAllTabsButton() }.closeAllTabs { - verifyNoTabsOpened() + verifyTabCounter("0") } } @@ -153,7 +153,7 @@ class TabbedBrowsingTest { closeTab() } homeScreen { - verifyNoTabsOpened() + verifyTabCounter("0") }.openNavigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { }.openTabDrawer { @@ -161,7 +161,7 @@ class TabbedBrowsingTest { swipeTabRight("Test_Page_1") } homeScreen { - verifyNoTabsOpened() + verifyTabCounter("0") }.openNavigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { }.openTabDrawer { @@ -169,7 +169,7 @@ class TabbedBrowsingTest { swipeTabLeft("Test_Page_1") } homeScreen { - verifyNoTabsOpened() + verifyTabCounter("0") } } @@ -213,7 +213,7 @@ class TabbedBrowsingTest { closeTab() } homeScreen { - verifyNoTabsOpened() + verifyTabCounter("0") }.openNavigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { }.openTabDrawer { @@ -221,7 +221,7 @@ class TabbedBrowsingTest { swipeTabRight("Test_Page_1") } homeScreen { - verifyNoTabsOpened() + verifyTabCounter("0") }.openNavigationToolbar { }.enterURLAndEnterToBrowser(genericURL.url) { }.openTabDrawer { @@ -229,7 +229,7 @@ class TabbedBrowsingTest { swipeTabLeft("Test_Page_1") } homeScreen { - verifyNoTabsOpened() + verifyTabCounter("0") } } @@ -354,7 +354,7 @@ class TabbedBrowsingTest { // dismiss search dialog homeScreen { }.pressBack() verifyPrivateSessionMessage() - verifyHomeToolbar() + verifyNavigationToolbar() } navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { @@ -365,7 +365,7 @@ class TabbedBrowsingTest { // dismiss search dialog homeScreen { }.pressBack() verifyHomeWordmark() - verifyHomeToolbar() + verifyNavigationToolbar() } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt index e0c94db21..12fef0855 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt @@ -9,6 +9,7 @@ import androidx.test.uiautomator.UiDevice 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.mozilla.fenix.R @@ -19,6 +20,7 @@ import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestHelper.generateRandomString import org.mozilla.fenix.helpers.TestHelper.getStringResource +import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar @@ -227,6 +229,8 @@ class TopSitesTest { verifyExistingTopSitesTabs(defaultWebPage.title) }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) { }.deleteTopSiteFromHistory { + verifySnackBarText(getStringResource(R.string.snackbar_top_site_removed)) + waitUntilSnackbarGone() }.openThreeDotMenu { }.openHistory { verifyEmptyHistoryView() @@ -275,6 +279,7 @@ class TopSitesTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/25926") @Test fun verifySponsoredShortcutsSponsorsAndPrivacyOptionTest() { var sponsoredShortcutTitle = "" diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt index c3d6bf437..b74d1b313 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt @@ -8,15 +8,23 @@ package org.mozilla.fenix.ui.robots import android.graphics.Bitmap import android.widget.EditText +import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onChildAt +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove +import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.matcher.RootMatchers @@ -57,9 +65,6 @@ import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.withBitmapDrawable -import org.mozilla.fenix.ui.util.STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER -import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER -import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER /** * Implementation of Robot Pattern for the home screen menu. @@ -71,19 +76,93 @@ class HomeScreenRobot { " service provider, it makes it easier to keep what you do online private from anyone" + " else who uses this device." - fun verifyNavigationToolbar() = assertNavigationToolbar() + fun verifyNavigationToolbar() = assertAppItemsWithResourceId(navigationToolbar) fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar() - fun verifyHomeScreen() = assertHomeScreen() - fun verifyHomePrivateBrowsingButton() = assertHomePrivateBrowsingButton() - fun verifyHomeMenu() = assertHomeMenu() + fun verifyHomeScreen() = assertAppItemsWithResourceId(homeScreen) + + fun verifyHomeScreenAppBarItems() = + assertAppItemsWithResourceId(homeScreen, privateBrowsingButton, homepageWordmark) + + fun verifyHomeScreenWelcomeItems() = + assertAppItemsContainingText(welcomeHeader, welcomeSubHeader) + + fun verifyChooseYourThemeCard( + isDarkThemeChecked: Boolean, + isLightThemeChecked: Boolean, + isAutomaticThemeChecked: Boolean, + ) { + scrollToElementByText(getStringResource(R.string.onboarding_theme_picker_header)) + assertAppItemsContainingText( + chooseThemeHeader, + chooseThemeText, + darkThemeDescription, + lightThemeDescription, + ) + assertAppItemsStateWithResourceId( + darkThemeToggle(isDarkThemeChecked), + lightThemeToggle(isLightThemeChecked), + automaticThemeToggle(isAutomaticThemeChecked), + ) + assertAppItemsWithResourceIdAndDescription(automaticThemeDescription) + } + + fun verifyToolbarPlacementCard(isBottomChecked: Boolean, isTopChecked: Boolean) { + scrollToElementByText(getStringResource(R.string.onboarding_toolbar_placement_header_1)) + assertAppItemsContainingText(toolbarPlacementHeader, toolbarPlacementDescription) + assertAppItemsStateWithResourceId( + toolbarPlacementBottomRadioButton(isBottomChecked), + toolbarPlacementTopRadioButton(isTopChecked), + ) + assertAppItemsWithResourceId( + toolbarPlacementBottomImage, + toolbarPlacementBottomTitle, + toolbarPlacementTopImage, + toolbarPlacementTopTitle, + ) + } + + fun verifySignInToSyncCard() { + scrollToElementByText(getStringResource(R.string.onboarding_account_sign_in_header)) + assertAppItemsContainingText(startSyncHeader, startSyncDescription) + assertAppItemsWithResourceId(signInButton) + } + + fun verifyPrivacyProtectionCard(isStandardChecked: Boolean, isStrictChecked: Boolean) { + scrollToElementByText(getStringResource(R.string.onboarding_tracking_protection_header)) + assertAppItemsContainingText(privacyProtectionHeader, privacyProtectionDescription) + assertAppItemsStateWithResourceId( + standardTrackingProtectionToggle(isStandardChecked), + strictTrackingProtectionToggle(isStrictChecked), + ) + } + + fun verifyPrivacyNoticeCard() { + scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_header_1)) + assertAppItemsContainingText(privacyNoticeHeader, privacyNoticeDescription) + assertAppItemsWithResourceId(privacyNoticeButton) + } + + fun verifyStartBrowsingSection() { + scrollToElementByText(getStringResource(R.string.onboarding_finish)) + assertAppItemsWithResourceId(startBrowsingButton) + assertAppItemsContainingText(conclusionHeader) + } + + fun verifyNavigationToolbarItems(numberOfOpenTabs: String) { + assertAppItemsWithResourceId(navigationToolbar, menuButton) + assertAppItemsWithResourceIdAndText(tabCounter(numberOfOpenTabs)) + } + + fun verifyHomePrivateBrowsingButton() = assertAppItemsWithResourceId(privateBrowsingButton) + fun verifyHomeMenuButton() = assertAppItemsWithResourceId(menuButton) fun verifyTabButton() = assertTabButton() fun verifyCollectionsHeader() = assertCollectionsHeader() fun verifyNoCollectionsText() = assertNoCollectionsText() - fun verifyHomeWordmark() = assertHomeWordmark() - fun verifyHomeToolbar() = assertHomeToolbar() + fun verifyHomeWordmark() = assertAppItemsWithResourceId(homepageWordmark) fun verifyHomeComponent() = assertHomeComponent() fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine) - fun verifyNoTabsOpened() = assertNoTabsOpened() + fun verifyTabCounter(numberOfOpenTabs: String) = + assertAppItemsWithResourceIdAndText(tabCounter(numberOfOpenTabs)) fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true) fun verifyWallpaperImageApplied(isEnabled: Boolean) { @@ -105,33 +184,12 @@ class HomeScreenRobot { } // First Run elements - fun verifyWelcomeHeader() = assertWelcomeHeader() - - fun verifyStartSyncHeader() = assertStartSyncHeader() - fun verifyAccountsSignInButton() = assertAccountsSignInButton() - fun verifyChooseThemeHeader() = assertChooseThemeHeader() - fun verifyChooseThemeText() = assertChooseThemeText() - fun verifyLightThemeToggle() = assertLightThemeToggle() - fun verifyLightThemeDescription() = assertLightThemeDescription() - fun verifyDarkThemeToggle() = assertDarkThemeToggle() - fun verifyDarkThemeDescription() = assertDarkThemeDescription() - fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle() - fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription() - fun verifyAutomaticPrivacyHeader() = assertAutomaticPrivacyHeader() - fun verifyAutomaticPrivacyText() = assertAlwaysPrivacyText() - - // Pick your toolbar placement - fun verifyTakePositionHeader() = assertTakePlacementHeader() - fun verifyTakePositionElements() { - assertTakePlacementBottomRadioButton() - assertTakePacementTopRadioButton() - } - - // Your privacy - fun verifyYourPrivacyHeader() = assertYourPrivacyHeader() - fun verifyYourPrivacyText() = assertYourPrivacyText() - fun verifyPrivacyNoticeButton() = assertPrivacyNoticeButton() - fun verifyStartBrowsingButton() = assertStartBrowsingButton() + fun verifyWelcomeHeader() = assertAppItemsContainingText(welcomeHeader) + fun verifyAccountsSignInButton() = assertAppItemsWithResourceId(signInButton) + fun verifyStartBrowsingButton() { + scrollToElementByText(getStringResource(R.string.onboarding_finish)) + assertAppItemsWithResourceId(startBrowsingButton) + } // Upgrading users onboarding dialog fun verifyUpgradingUserOnboardingFirstScreen(testRule: ComposeTestRule) { @@ -182,6 +240,9 @@ class HomeScreenRobot { fun verifyJumpBackInSectionIsDisplayed() = assertJumpBackInSectionIsDisplayed() fun verifyJumpBackInSectionIsNotDisplayed() = assertJumpBackInSectionIsNotDisplayed() + fun verifyJumpBackInItemTitle(itemTitle: String) = assertJumpBackInItemTitle(itemTitle) + fun verifyJumpBackInItemWithUrl(itemUrl: String) = assertJumpBackInItemWithUrl(itemUrl) + fun verifyJumpBackInShowAllButton() = assertJumpBackInShowAllButton() fun verifyRecentlyVisitedSectionIsDisplayed() = assertRecentlyVisitedSectionIsDisplayed() fun verifyRecentlyVisitedSectionIsNotDisplayed() = assertRecentlyVisitedSectionIsNotDisplayed() fun verifyRecentBookmarksSectionIsDisplayed() = assertRecentBookmarksSectionIsDisplayed() @@ -267,6 +328,39 @@ class HomeScreenRobot { } } + fun scrollToPocketProvokingStories() = + scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header)) + + fun swipePocketProvokingStories() { + UiScrollable(UiSelector().resourceId("pocket.stories")).setAsHorizontalList() + .swipeLeft(3) + } + + fun verifyPocketRecommendedStoriesItems(composeTestRule: ComposeTestRule, vararg positions: Int) { + composeTestRule.onNodeWithTag("pocket.stories").assertIsDisplayed() + positions.forEach { + composeTestRule.onNodeWithTag("pocket.stories") + .onChildAt(it - 1) + .assert(hasTestTag("pocket.recommended.story")) + } + } + + fun verifyPocketSponsoredStoriesItems(composeTestRule: ComposeTestRule, vararg positions: Int) { + composeTestRule.onNodeWithTag("pocket.stories").assertIsDisplayed() + positions.forEach { + composeTestRule.onNodeWithTag("pocket.stories") + .onChildAt(it - 1) + .assert(hasTestTag("pocket.sponsored.story")) + } + } + + fun verifyDiscoverMoreStoriesButton(composeTestRule: ComposeTestRule, position: Int) { + composeTestRule.onNodeWithTag("pocket.stories") + .assertIsDisplayed() + .onChildAt(position - 1) + .assert(hasTestTag("pocket.discover.more.story")) + } + fun verifyStoriesByTopic(enabled: Boolean) { if (enabled) { scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header)) @@ -291,6 +385,30 @@ class HomeScreenRobot { } } + fun verifyStoriesByTopicItems() = + assertTrue(mDevice.findObject(UiSelector().resourceId("pocket.categories")).childCount > 1) + + fun verifyStoriesByTopicItemState(composeTestRule: ComposeTestRule, isSelected: Boolean, position: Int) { + homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header"))) + + if (isSelected) { + composeTestRule.onNodeWithTag("pocket.categories").assertIsDisplayed() + storyByTopicItem(composeTestRule, position).assertIsSelected() + } else { + composeTestRule.onNodeWithTag("pocket.categories").assertIsDisplayed() + storyByTopicItem(composeTestRule, position).assertIsNotSelected() + } + } + + fun clickStoriesByTopicItem(composeTestRule: ComposeTestRule, position: Int) = + storyByTopicItem(composeTestRule, position).performClick() + + fun verifyPoweredByPocket(rule: ComposeTestRule) { + homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header"))) + rule.onNodeWithTag("pocket.header.title", true).assertIsDisplayed() + rule.onNodeWithTag("pocket.header.subtitle", true).assertIsDisplayed() + } + fun verifyCustomizeHomepageButton(enabled: Boolean) { if (enabled) { scrollToElementByText(getStringResource(R.string.browser_menu_customize_home_1)) @@ -334,6 +452,31 @@ class HomeScreenRobot { ) } + fun getProvokingStoryPublisher(position: Int): String { + val publisher = mDevice.findObject( + UiSelector() + .className("android.view.View") + .index(position - 1), + ).getChild( + UiSelector() + .className("android.widget.TextView") + .index(1), + ).text + + return publisher + } + + fun verifyToolbarPosition(defaultPosition: Boolean) { + onView(withId(R.id.toolbarLayout)) + .check( + if (defaultPosition) { + isPartiallyBelow(withId(R.id.sessionControlRecyclerView)) + } else { + isCompletelyAbove(withId(R.id.homeAppBar)) + }, + ) + } + class Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { @@ -366,8 +509,8 @@ class HomeScreenRobot { } fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition { - navigationToolbar().waitForExists(waitingTime) - navigationToolbar().click() + navigationToolbar.waitForExists(waitingTime) + navigationToolbar.click() SearchRobot().interact() return SearchRobot.Transition() @@ -378,7 +521,7 @@ class HomeScreenRobot { } fun clickStartBrowsingButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition { - startBrowsingButton().click() + startBrowsingButton.click() SearchRobot().interact() return SearchRobot.Transition() @@ -399,8 +542,7 @@ class HomeScreenRobot { .waitForExists( waitingTime, ) - privateBrowsingButton() - .perform(click()) + privateBrowsingButton.click() } fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition { @@ -411,8 +553,7 @@ class HomeScreenRobot { waitingTime, ) - privateBrowsingButton() - .perform(click()) + privateBrowsingButton.click() } AddToHomeScreenRobot().interact() @@ -426,7 +567,7 @@ class HomeScreenRobot { fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition { mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")) .waitForExists(waitingTime) - navigationToolbar().click() + navigationToolbar.click() NavigationToolbarRobot().interact() return NavigationToolbarRobot.Transition() @@ -582,6 +723,63 @@ class HomeScreenRobot { SettingsSubMenuHomepageRobot().interact() return SettingsSubMenuHomepageRobot.Transition() } + + fun clickJumpBackInShowAllButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { + mDevice + .findObject( + UiSelector() + .textContains(getStringResource(R.string.recent_tabs_show_all)), + ).clickAndWaitForNewWindow(waitingTime) + + TabDrawerRobot().interact() + return TabDrawerRobot.Transition() + } + + fun clickJumpBackInItemWithTitle(itemTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + mDevice + .findObject( + UiSelector() + .resourceId("recent.tab.title") + .textContains(itemTitle), + ).clickAndWaitForNewWindow(waitingTime) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun clickPocketStoryItem(publisher: String, position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + mDevice.findObject( + UiSelector() + .className("android.view.View") + .index(position - 1), + ).getChild( + UiSelector() + .className("android.widget.TextView") + .index(1) + .textContains(publisher), + ).clickAndWaitForNewWindow(waitingTime) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun clickPocketDiscoverMoreButton(composeTestRule: ComposeTestRule, position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + composeTestRule.onNodeWithTag("pocket.stories") + .assertIsDisplayed() + .onChildAt(position - 1) + .assert(hasTestTag("pocket.discover.more.story")) + .performClick() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun clickPocketLearnMoreLink(composeTestRule: ComposeTestRule, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + composeTestRule.onNodeWithTag("pocket.header.subtitle", true).performClick() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } } } @@ -605,34 +803,10 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) = .contains("mInputShown=true"), ) -private fun navigationToolbar() = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")) - -private fun assertNavigationToolbar() = assertTrue(navigationToolbar().waitForExists(waitingTime)) - private fun assertFocusedNavigationToolbar() = onView(allOf(withHint("Search or enter address"))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun assertHomeScreen() { - mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout")).waitForExists(waitingTime) - onView(ViewMatchers.withResourceName("homeLayout")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertHomeMenu() = onView(ViewMatchers.withResourceName("menuButton")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - -private fun assertHomePrivateBrowsingButton() = - privateBrowsingButton() - .check(matches(isDisplayed())) - -private val homepageWordmark = onView(ViewMatchers.withResourceName("wordmark")) -private fun assertHomeWordmark() = - homepageWordmark.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - -private fun assertHomeToolbar() = onView(ViewMatchers.withResourceName("toolbar")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - private fun assertTabButton() = onView(allOf(withId(R.id.tab_button), isDisplayed())) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) @@ -655,8 +829,6 @@ private fun assertHomeComponent() = onView(ViewMatchers.withResourceName("sessionControlRecyclerView")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun assertNoTabsOpened() = onView(withId(R.id.counter_text)).check(matches(withText("0"))) - private fun threeDotButton() = onView(allOf(withId(R.id.menuButton))) private fun verifySearchEngineIcon(searchEngineIcon: Bitmap, searchEngineName: String) { @@ -668,141 +840,9 @@ private fun getSearchEngine(searchEngineName: String) = appContext.components.core.store.state.search.searchEngines.find { it.name == searchEngineName } private fun verifySearchEngineIcon(searchEngineName: String) { - val ddgSearchEngine = getSearchEngine(searchEngineName) + val defaultSearchEngine = getSearchEngine(searchEngineName) ?: throw AssertionError("No search engine with name $searchEngineName") - verifySearchEngineIcon(ddgSearchEngine.icon, ddgSearchEngine.name) -} - -// First Run elements -private fun assertWelcomeHeader() = - assertTrue( - mDevice.findObject( - UiSelector().textContains( - getStringResource(R.string.onboarding_header_2), - ), - ).waitForExists(waitingTime), - ) - -private fun assertStartSyncHeader() { - scrollToElementByText(STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER) - onView(allOf(withText(R.string.onboarding_account_sign_in_header))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} -private fun assertAccountsSignInButton() { - scrollToElementByText(getStringResource(R.string.onboarding_firefox_account_sign_in)) - onView(ViewMatchers.withResourceName("fxa_sign_in_button")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertChooseThemeHeader() { - scrollToElementByText("Choose your theme") - onView(withText("Choose your theme")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} -private fun assertChooseThemeText() { - scrollToElementByText("Choose your theme") - onView(allOf(withText("Save some battery and your eyesight with dark mode."))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertLightThemeToggle() { - scrollToElementByText("Choose your theme") - onView(ViewMatchers.withResourceName("theme_light_radio_button")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertLightThemeDescription() { - scrollToElementByText("Choose your theme") - onView(allOf(withText("Light theme"))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertDarkThemeToggle() { - scrollToElementByText("Choose your theme") - onView(ViewMatchers.withResourceName("theme_dark_radio_button")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertDarkThemeDescription() { - scrollToElementByText("Choose your theme") - onView(allOf(withText("Dark theme"))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} -private fun assertAutomaticThemeToggle() { - scrollToElementByText("Choose your theme") - onView(withId(R.id.theme_automatic_radio_button)) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertAutomaticThemeDescription() { - scrollToElementByText("Choose your theme") - onView(allOf(withText("Automatic"))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertAutomaticPrivacyHeader() { - scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER) - onView(allOf(withText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertAlwaysPrivacyText() { - scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER) - onView( - allOf( - withText( - "Featuring Total Cookie Protection to stop trackers from using cookies to stalk you across sites.", - ), - ), - ) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertYourPrivacyHeader() { - scrollToElementByText("You control your data") - onView(allOf(withText("You control your data"))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertYourPrivacyText() { - scrollToElementByText("You control your data") - onView( - allOf( - withText( - "Firefox gives you control over what you share online and what you share with us.", - ), - ), - ) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertPrivacyNoticeButton() { - scrollToElementByText("You control your data") - onView(allOf(withText("Read our privacy notice"))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertStartBrowsingButton() { - assertTrue(startBrowsingButton().waitForExists(waitingTime)) -} - -// Pick your toolbar placement -private fun assertTakePlacementHeader() { - scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER) - onView(allOf(withText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertTakePacementTopRadioButton() { - scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER) - onView(ViewMatchers.withResourceName("toolbar_top_radio_button")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertTakePlacementBottomRadioButton() { - scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER) - onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + verifySearchEngineIcon(defaultSearchEngine.icon, defaultSearchEngine.name) } private fun assertPrivateSessionMessage() = @@ -901,6 +941,35 @@ private fun assertJumpBackInSectionIsDisplayed() = assertTrue(jumpBackInSection( private fun assertJumpBackInSectionIsNotDisplayed() = assertFalse(jumpBackInSection().waitForExists(waitingTimeShort)) +private fun assertJumpBackInItemTitle(itemTitle: String) = + assertTrue( + mDevice + .findObject( + UiSelector() + .resourceId("recent.tab.title") + .textContains(itemTitle), + ).waitForExists(waitingTime), + ) + +private fun assertJumpBackInItemWithUrl(itemUrl: String) = + assertTrue( + mDevice + .findObject( + UiSelector() + .resourceId("recent.tab.url") + .textContains(itemUrl), + ).waitForExists(waitingTime), + ) + +private fun assertJumpBackInShowAllButton() = + assertTrue( + mDevice + .findObject( + UiSelector() + .textContains(getStringResource(R.string.recent_tabs_show_all)), + ).waitForExists(waitingTime), + ) + private fun assertRecentlyVisitedSectionIsDisplayed() = assertTrue(recentlyVisitedSection().waitForExists(waitingTime)) private fun assertRecentlyVisitedSectionIsNotDisplayed() = assertFalse(recentlyVisitedSection().waitForExists(waitingTime)) @@ -915,8 +984,6 @@ private fun assertPocketSectionIsDisplayed() = assertTrue(pocketSection().waitFo private fun assertPocketSectionIsNotDisplayed() = assertFalse(pocketSection().waitForExists(waitingTime)) -private fun privateBrowsingButton() = onView(withId(R.id.privateBrowsingButton)) - private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button)) private fun tabsCounter() = onView(withId(R.id.tab_button)) @@ -933,15 +1000,6 @@ private fun recentBookmarksSection() = private fun pocketSection() = mDevice.findObject(UiSelector().textContains(getStringResource(R.string.pocket_stories_header_1))) -private fun startBrowsingButton(): UiObject { - val startBrowsingButton = mDevice.findObject(UiSelector().resourceId("$packageName:id/finish_button")) - homeScreenList() - .scrollIntoView(startBrowsingButton) - homeScreenList() - .ensureFullyVisible(startBrowsingButton) - return startBrowsingButton -} - private fun sponsoredShortcut(sponsoredShortcutTitle: String) = mDevice.findObject( By @@ -949,6 +1007,129 @@ private fun sponsoredShortcut(sponsoredShortcutTitle: String) = .textContains(sponsoredShortcutTitle), ) +private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) = + composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1) + +private fun appItemWithResourceId(resourceId: String) = + mDevice.findObject(UiSelector().resourceId(resourceId)) + +private fun appItemContainingText(itemText: String) = + mDevice.findObject(UiSelector().textContains(itemText)) + +private fun appItemStateWithResourceId(resourceId: String, state: Boolean) = + mDevice.findObject(UiSelector().resourceId(resourceId).checked(state)) + +private fun appItemWithResourceIdAndDescription(resourceId: String, description: String) = + mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description)) + +private fun appItemWithResourceIdAndText(resourceId: String, text: String) = + mDevice.findObject(UiSelector().resourceId(resourceId).text(text)) + +private fun assertAppItemsWithResourceId(vararg appItems: UiObject) { + for (appItem in appItems) { + assertTrue(appItem.waitForExists(waitingTime)) + } +} + +private fun assertAppItemsContainingText(vararg appItems: UiObject) { + for (appItem in appItems) { + assertTrue(appItem.waitForExists(waitingTime)) + } +} + +private fun assertAppItemsStateWithResourceId(vararg appItems: UiObject) { + for (appItem in appItems) { + assertTrue(appItem.waitForExists(waitingTime)) + } +} + +private fun assertAppItemsWithResourceIdAndDescription(vararg appItems: UiObject) { + for (appItem in appItems) { + assertTrue(appItem.waitForExists(waitingTime)) + } +} + +private fun assertAppItemsWithResourceIdAndText(vararg appItems: UiObject) { + for (appItem in appItems) { + assertTrue(appItem.waitForExists(waitingTime)) + } +} + +private val homeScreen = + appItemWithResourceId("$packageName:id/homeLayout") +private val privateBrowsingButton = + appItemWithResourceId("$packageName:id/privateBrowsingButton") +private val homepageWordmark = + appItemWithResourceId("$packageName:id/wordmark") +private val welcomeHeader = appItemContainingText(getStringResource(R.string.onboarding_header_2)) +private val welcomeSubHeader = + appItemContainingText(getStringResource(R.string.onboarding_message)) +private val chooseThemeHeader = + appItemContainingText(getStringResource(R.string.onboarding_theme_picker_header)) +private val chooseThemeText = + appItemContainingText(getStringResource(R.string.onboarding_theme_picker_description_2)) +private val darkThemeDescription = + appItemContainingText(getStringResource(R.string.onboarding_theme_dark_title)) +private val lightThemeDescription = + appItemContainingText(getStringResource(R.string.onboarding_theme_light_title)) +private val automaticThemeDescription = + appItemWithResourceIdAndDescription( + "$packageName:id/clickable_region_automatic", + "${getStringResource(R.string.onboarding_theme_automatic_title)} ${getStringResource(R.string.onboarding_theme_automatic_summary)}", + ) +private fun darkThemeToggle(isChecked: Boolean) = + appItemStateWithResourceId("$packageName:id/theme_dark_radio_button", isChecked) +private fun lightThemeToggle(isChecked: Boolean) = + appItemStateWithResourceId("$packageName:id/theme_light_radio_button", isChecked) +private fun automaticThemeToggle(isChecked: Boolean) = + appItemStateWithResourceId("$packageName:id/theme_automatic_radio_button", isChecked) +private val toolbarPlacementHeader = + appItemContainingText(getStringResource(R.string.onboarding_toolbar_placement_header_1)) +private val toolbarPlacementDescription = + appItemContainingText(getStringResource(R.string.onboarding_toolbar_placement_description)) +private fun toolbarPlacementBottomRadioButton(isChecked: Boolean) = + appItemStateWithResourceId("$packageName:id/toolbar_bottom_radio_button", isChecked) +private fun toolbarPlacementTopRadioButton(isChecked: Boolean) = + appItemStateWithResourceId("$packageName:id/toolbar_top_radio_button", isChecked) +private val toolbarPlacementBottomImage = + appItemWithResourceId("$packageName:id/toolbar_bottom_image") +private val toolbarPlacementBottomTitle = + appItemWithResourceId("$packageName:id/toolbar_bottom_title") +private val toolbarPlacementTopTitle = + appItemWithResourceId("$packageName:id/toolbar_top_title") +private val toolbarPlacementTopImage = + appItemWithResourceId("$packageName:id/toolbar_top_image") +private val startSyncHeader = + appItemContainingText(getStringResource(R.string.onboarding_account_sign_in_header)) +private val startSyncDescription = + appItemContainingText(getStringResource(R.string.onboarding_manual_sign_in_description)) +private val signInButton = + appItemWithResourceId("$packageName:id/fxa_sign_in_button") +private val privacyProtectionHeader = + appItemContainingText(getStringResource(R.string.onboarding_tracking_protection_header)) +private val privacyProtectionDescription = + appItemContainingText(getStringResource(R.string.onboarding_tracking_protection_description)) +private fun standardTrackingProtectionToggle(isChecked: Boolean) = + appItemStateWithResourceId("$packageName:id/tracking_protection_standard_option", isChecked) +private fun strictTrackingProtectionToggle(isChecked: Boolean) = + appItemStateWithResourceId("$packageName:id/tracking_protection_strict_default", isChecked) +private val privacyNoticeHeader = + appItemContainingText(getStringResource(R.string.onboarding_privacy_notice_header_1)) +private val privacyNoticeDescription = + appItemContainingText(getStringResource(R.string.onboarding_privacy_notice_description)) +private val privacyNoticeButton = + appItemWithResourceId("$packageName:id/read_button") +private val startBrowsingButton = + appItemWithResourceId("$packageName:id/finish_button") +private val conclusionHeader = + appItemContainingText(getStringResource(R.string.onboarding_conclusion_header)) +private val navigationToolbar = + appItemWithResourceId("$packageName:id/toolbar") +private val menuButton = + appItemWithResourceId("$packageName:id/menuButton") +private fun tabCounter(numberOfOpenTabs: String) = + appItemWithResourceIdAndText("$packageName:id/counter_text", numberOfOpenTabs) + val deleteFromHistory = onView( allOf( diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt index ed09b5a9c..3d5818e64 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt @@ -65,7 +65,7 @@ class NavigationToolbarRobot { readerViewToggle().click() } - fun verifyClipboardSuggestionsAreDisplayed(link: String, shouldBeDisplayed: Boolean) { + fun verifyClipboardSuggestionsAreDisplayed(link: String = "", shouldBeDisplayed: Boolean) { when (shouldBeDisplayed) { true -> { assertTrue( diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt index 592ce4d9d..eae28eb86 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt @@ -537,3 +537,5 @@ private val awesomeBar = mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")) private val voiceSearchButton = mDevice.findObject(UiSelector().description("Voice search")) + +private fun goBackButton() = onView(allOf(withContentDescription("Navigate up"))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt index 0ce2f375d..e22b01640 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt @@ -147,12 +147,12 @@ class SettingsRobot { return SettingsSubMenuSearchRobot.Transition() } - fun openCustomizeSubMenu(interact: SettingsSubMenuThemeRobot.() -> Unit): SettingsSubMenuThemeRobot.Transition { + fun openCustomizeSubMenu(interact: SettingsSubMenuCustomizeRobot.() -> Unit): SettingsSubMenuCustomizeRobot.Transition { fun customizeButton() = onView(withText("Customize")) customizeButton().click() - SettingsSubMenuThemeRobot().interact() - return SettingsSubMenuThemeRobot.Transition() + SettingsSubMenuCustomizeRobot().interact() + return SettingsSubMenuCustomizeRobot.Transition() } fun openTabsSubMenu(interact: SettingsSubMenuTabsRobot.() -> Unit): SettingsSubMenuTabsRobot.Transition { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt index e9d5c1bcd..fa57b8e05 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt @@ -23,6 +23,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector +import mozilla.components.support.utils.ext.getPackageInfoCompat import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString import org.junit.Assert.assertTrue @@ -97,7 +98,7 @@ private fun assertAboutToolbar() = private fun assertVersionNumber() { val context = InstrumentationRegistry.getInstrumentation().targetContext - val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + val packageInfo = context.packageManager.getPackageInfoCompat(context.packageName, 0) val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString() val buildNVersion = "${packageInfo.versionName} (Build #$versionCode)\n" diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.kt new file mode 100644 index 000000000..96616f114 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuCustomizeRobot.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/. */ + +@file:Suppress("TooManyFunctions") + +package org.mozilla.fenix.ui.robots + +import android.os.Build +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.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withClassName +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.Matchers.endsWith +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.TestHelper.getStringResource +import org.mozilla.fenix.helpers.TestHelper.hasCousin +import org.mozilla.fenix.helpers.TestHelper.mDevice +import org.mozilla.fenix.helpers.click + +/** + * Implementation of Robot Pattern for the settings Theme sub menu. + */ +class SettingsSubMenuCustomizeRobot { + + fun verifyThemes() = assertThemes() + + fun verifyLightThemeApplied(expected: Boolean) = + assertFalse("Light theme not selected", expected) + + fun verifyDarkThemeApplied(expected: Boolean) = assertTrue("Dark theme not selected", expected) + + fun selectDarkMode() = darkModeToggle().click() + + fun selectLightMode() = lightModeToggle().click() + + fun clickTopToolbarToggle() = topToolbarToggle().click() + + fun clickBottomToolbarToggle() = bottomToolbarToggle().click() + + fun verifyToolbarPositionPreference(selectedPosition: String) { + onView(withText(selectedPosition)) + .check(matches(hasSibling(allOf(withId(R.id.radio_button), isChecked())))) + } + + fun clickSwipeToolbarToSwitchTabToggle() = swipeToolbarToggle.click() + + fun clickPullToRefreshToggle() = pullToRefreshToggle.click() + + fun verifySwipeToolbarGesturePrefState(isEnabled: Boolean) { + swipeToolbarToggle + .check( + matches( + hasCousin( + allOf( + withClassName(endsWith("Switch")), + if (isEnabled) { + isChecked() + } else { + isNotChecked() + }, + ), + ), + ), + ) + } + + fun verifyPullToRefreshGesturePrefState(isEnabled: Boolean) { + pullToRefreshToggle + .check( + matches( + hasCousin( + allOf( + withClassName(endsWith("Switch")), + if (isEnabled) { + isChecked() + } else { + isNotChecked() + }, + ), + ), + ), + ) + } + + class Transition { + fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { + mDevice.waitForIdle() + goBackButton().perform(ViewActions.click()) + + SettingsRobot().interact() + return SettingsRobot.Transition() + } + } +} + +private fun assertThemes() { + lightModeToggle() + .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + darkModeToggle() + .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + deviceModeToggle() + .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) +} + +private fun darkModeToggle() = onView(withText("Dark")) + +private fun lightModeToggle() = onView(withText("Light")) + +private fun topToolbarToggle() = onView(withText("Top")) + +private fun bottomToolbarToggle() = onView(withText("Bottom")) + +private fun deviceModeToggle(): ViewInteraction { + val followDeviceThemeText = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) "Follow device theme" else "Set by Battery Saver" + return onView(withText(followDeviceThemeText)) +} + +private val swipeToolbarToggle = + onView(withText(getStringResource(R.string.preference_gestures_swipe_toolbar_switch_tabs))) + +private val pullToRefreshToggle = + onView(withText(getStringResource(R.string.preference_gestures_website_pull_to_refresh))) + +private fun goBackButton() = + onView(allOf(ViewMatchers.withContentDescription("Navigate up"))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuThemeRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuThemeRobot.kt deleted file mode 100644 index 71985c22d..000000000 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuThemeRobot.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -@file:Suppress("TooManyFunctions") - -package org.mozilla.fenix.ui.robots - -import android.os.Build -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.withText -import org.hamcrest.CoreMatchers.allOf -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.mozilla.fenix.helpers.TestHelper.mDevice -import org.mozilla.fenix.helpers.click - -/** - * Implementation of Robot Pattern for the settings Theme sub menu. - */ -class SettingsSubMenuThemeRobot { - - fun verifyThemes() = assertThemes() - - fun verifyLightThemeApplied(expected: Boolean) = - assertFalse("Light theme not selected", expected) - - fun verifyDarkThemeApplied(expected: Boolean) = assertTrue("Dark theme not selected", expected) - - fun selectDarkMode() = darkModeToggle().click() - - fun selectLightMode() = lightModeToggle().click() - - fun clickTopToolbarToggle() = topToolbarToggle().click() - - fun clickBottomToolbarToggle() = bottomToolbarToggle().click() - - class Transition { - fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { - mDevice.waitForIdle() - goBackButton().perform(ViewActions.click()) - - SettingsRobot().interact() - return SettingsRobot.Transition() - } - } -} - -private fun assertThemes() { - lightModeToggle() - .check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) - darkModeToggle() - .check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) - deviceModeToggle() - .check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) -} - -private fun darkModeToggle() = onView(withText("Dark")) - -private fun lightModeToggle() = onView(withText("Light")) - -private fun topToolbarToggle() = onView(withText("Top")) - -private fun bottomToolbarToggle() = onView(withText("Bottom")) - -private fun deviceModeToggle(): ViewInteraction { - val followDeviceThemeText = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) "Follow device theme" else "Set by Battery Saver" - return onView(withText(followDeviceThemeText)) -} - -private fun goBackButton() = - onView(allOf(ViewMatchers.withContentDescription("Navigate up"))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt index 729200c54..bc9544f72 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt @@ -4,9 +4,6 @@ package org.mozilla.fenix.ui.util -const val STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER = "Pick up where you left off" -const val STRING_ONBOARDING_TRACKING_PROTECTION_HEADER = "Privacy protection by default" -const val STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER = "Pick your toolbar placement" const val FRENCH_LANGUAGE_HEADER = "Langues" const val ROMANIAN_LANGUAGE_HEADER = "Limbă" const val ARABIC_LANGUAGE_HEADER = "اللغة" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e433e95c6..1d9b8b7c9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ + + + = Build.VERSION_CODES.TIRAMISU) { + queue.runIfReadyOrQueue { + ensureMarketingChannelExists(this) + } + } + } + initQueue() // We init these items in the visual completeness queue to avoid them initing in the critical @@ -368,6 +387,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { queueReviewPrompt() queueRestoreLocale() queueStorageMaintenance() + queueNotificationPermissionRequest() } private fun startMetricsIfEnabled() { @@ -415,6 +435,15 @@ open class FenixApplication : LocaleAwareApplication(), Provider { .install(this) } + protected open fun initializeNimbus() { + beginSetupMegazord() + + // This lazily constructs the Nimbus object… + val nimbus = components.analytics.experiments + // … which we then can populate the feature configuration. + FxNimbus.initialize { nimbus } + } + /** * Initiate Megazord sequence! Megazord Battle Mode! * @@ -424,54 +453,43 @@ open class FenixApplication : LocaleAwareApplication(), Provider { * 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. */ - @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage - private fun setupMegazord(): Deferred { + private fun beginSetupMegazord() { // Note: Megazord.init() must be called as soon as possible ... Megazord.init() - // Give the generated FxNimbus a closure to lazily get the Nimbus object - FxNimbus.initialize { components.analytics.experiments } + + initializeRustErrors(components.analytics.crashReporter) + // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later. + + // Once application-services has switched to using the new + // error reporting system, RustLog shouldn't input a CrashReporter + // anymore. + // (https://github.com/mozilla/application-services/issues/4981). + RustLog.enable(components.analytics.crashReporter) + } + + @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage + private fun finishSetupMegazord(): Deferred { return GlobalScope.async(Dispatchers.IO) { - initializeRustErrors(components.analytics.crashReporter) - // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later. - RustHttpConfig.setClient(lazy { components.core.client }) - // Once application-services has switched to using the new - // error reporting system, RustLog shouldn't input a CrashReporter - // anymore. - // (https://github.com/mozilla/application-services/issues/4981). - RustLog.enable(components.analytics.crashReporter) - // We want to ensure Nimbus is initialized as early as possible so we can - // experiment on features close to startup. - // But we need viaduct (the RustHttp client) to be ready before we do. - components.analytics.experiments.apply { - setupNimbusObserver(this) + if (Config.channel.isDebug) { + RustHttpConfig.allowEmulatorLoopback() } - } - } + RustHttpConfig.setClient(lazy { components.core.client }) - private fun setupNimbusObserver(nimbus: Observable) { - nimbus.register( - object : NimbusInterface.Observer { - override fun onUpdatesApplied(updated: List) { - onNimbusStartupAndUpdate() - } - }, - ) + // Now viaduct (the RustHttp client) is initialized we can ask Nimbus to fetch + // experiments recipes from the server. + components.analytics.experiments.fetchExperiments() + } } - private fun onNimbusStartupAndUpdate() { - // When Nimbus has successfully started up, we can apply our engine settings experiment. - // Any previous value that was set on the engine will be overridden from those set in - // Core.Engine.DefaultSettings. - // NOTE ⚠️: Any startup experiment we want to run needs to have it's value re-applied here. - components.core.engine.settings.trackingProtectionPolicy = - components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy() - - val settings = settings() - if (FeatureFlags.messagingFeature && settings.isExperimentationEnabled) { + private fun restoreMessaging() { + if (settings().isExperimentationEnabled) { components.appStore.dispatch(AppAction.MessagingAction.Restore) } - reportHomeScreenSectionMetrics(settings) } override fun onTrimMemory(level: Int) { diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 9f849d6f5..e58f8f542 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.SearchAction import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.state.SessionState @@ -103,6 +104,7 @@ import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDir import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker import org.mozilla.fenix.onboarding.FenixOnboarding +import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.Performance @@ -275,7 +277,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { val safeIntent = intent?.toSafeIntent() safeIntent ?.let(::getIntentSource) - ?.also { Events.appOpened.record(Events.AppOpenedExtra(it)) } + ?.also { + Events.appOpened.record(Events.AppOpenedExtra(it)) + // This will record an event in Nimbus' internal event store. Used for behavioral targeting + components.analytics.experiments.recordEvent("app_opened") + } } supportActionBar?.hide() @@ -377,7 +383,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { components.appStore.dispatch(AppAction.ResumedMetricsAction) DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext) + ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext) } + + // This was done in order to refresh search engines when app is running in background + // and the user changes the system language + // More details here: https://github.com/mozilla-mobile/fenix/pull/27793#discussion_r1029892536 + components.core.store.dispatch(SearchAction.RefreshSearchEnginesAction) } override fun onStart() { @@ -618,7 +630,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { return } } - super.onBackPressed() + super.getOnBackPressedDispatcher().onBackPressed() } @Suppress("DEPRECATION") diff --git a/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt b/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt index d633d3160..33af4e1eb 100644 --- a/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt @@ -15,6 +15,7 @@ import mozilla.components.feature.intent.ext.sanitize import mozilla.components.feature.intent.processing.IntentProcessor import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE +import mozilla.components.support.utils.ext.getApplicationInfoCompat import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE import org.mozilla.fenix.components.IntentProcessorType @@ -125,7 +126,7 @@ class IntentReceiverActivity : Activity() { // Category is supported for API>=26. r.host?.let { host -> try { - val category = packageManager.getApplicationInfo(host, 0).category + val category = packageManager.getApplicationInfoCompat(host, 0).category intent.putExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, category) } catch (e: PackageManager.NameNotFoundException) { // At least we tried. diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt index 7904a8169..f8eefbd1b 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -411,17 +411,22 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) } private fun announceForAccessibility(announcementText: CharSequence) { - val event = AccessibilityEvent.obtain( - AccessibilityEvent.TYPE_ANNOUNCEMENT, - ) + val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) + } else { + @Suppress("DEPRECATION") + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT) + } binding?.addonProgressOverlay?.overlayCardView?.onInitializeAccessibilityEvent(event) event.text.add(announcementText) event.contentDescription = null - binding?.addonProgressOverlay?.overlayCardView?.parent?.requestSendAccessibilityEvent( - binding?.addonProgressOverlay?.overlayCardView, - event, - ) + binding?.addonProgressOverlay?.overlayCardView?.let { + it.parent?.requestSendAccessibilityEvent( + it, + event, + ) + } } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index 1be9447b3..12c12a2fb 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -33,7 +33,6 @@ import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch @@ -1251,7 +1250,7 @@ abstract class BaseBrowserFragment : viewLifecycleOwner.lifecycleScope.launch(Main) { val sitePermissions: SitePermissions? = tab.content.url.getOrigin()?.let { origin -> val storage = requireComponents.core.permissionStorage - storage.findSitePermissionsBy(origin) + storage.findSitePermissionsBy(origin, tab.content.private) } view?.let { @@ -1370,6 +1369,7 @@ abstract class BaseBrowserFragment : .setText(getString(R.string.full_screen_notification)) .show() activity?.enterToImmersiveMode() + (view as? SwipeGestureLayout)?.isSwipeEnabled = false browserToolbarView.collapse() browserToolbarView.view.isVisible = false val browserEngine = binding.swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams @@ -1384,6 +1384,7 @@ abstract class BaseBrowserFragment : MediaState.fullscreen.record(NoExtras()) } else { activity?.exitImmersiveMode() + (view as? SwipeGestureLayout)?.isSwipeEnabled = true (activity as? HomeActivity)?.let { activity -> activity.themeManager.applyStatusBarTheme(activity) } @@ -1398,6 +1399,11 @@ abstract class BaseBrowserFragment : binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(inFullScreen) } + @CallSuper + internal open fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) { + toolbar.dismissMenu() + } + /* * Dereference these views when the fragment view is destroyed to prevent memory leaks */ @@ -1470,7 +1476,9 @@ abstract class BaseBrowserFragment : override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - _browserToolbarView?.dismissMenu() + _browserToolbarView?.let { + onUpdateToolbarForConfigurationChange(it) + } } // This method is called in response to native web extension messages from diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index f4e02b49a..a76cb5a64 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -11,8 +11,12 @@ import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.TabSessionState @@ -31,12 +35,14 @@ import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.TabCollectionStorage +import org.mozilla.fenix.components.toolbar.BrowserToolbarView import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.shortcut.PwaOnboardingObserver import org.mozilla.fenix.theme.ThemeManager @@ -52,6 +58,11 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { private var readerModeAvailable = false private var pwaOnboardingObserver: PwaOnboardingObserver? = null + private var forwardAction: BrowserToolbar.TwoStateButton? = null + private var backAction: BrowserToolbar.TwoStateButton? = null + private var refreshAction: BrowserToolbar.TwoStateButton? = null + private var isTablet: Boolean = false + @Suppress("LongMethod") override fun initializeUI(view: View, tab: SessionState) { super.initializeUI(view, tab) @@ -84,86 +95,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { browserToolbarView.view.addNavigationAction(homeAction) - if (resources.getBoolean(R.bool.tablet)) { - val enableTint = ThemeManager.resolveAttribute(R.attr.textPrimary, context) - val disableTint = ThemeManager.resolveAttribute(R.attr.textDisabled, context) - val backAction = BrowserToolbar.TwoStateButton( - primaryImage = AppCompatResources.getDrawable( - context, - R.drawable.mozac_ic_back, - )!!, - primaryContentDescription = context.getString(R.string.browser_menu_back), - primaryImageTintResource = enableTint, - isInPrimaryState = { getCurrentTab()?.content?.canGoBack ?: false }, - secondaryImageTintResource = disableTint, - disableInSecondaryState = true, - longClickListener = { - browserToolbarInteractor.onBrowserToolbarMenuItemTapped( - ToolbarMenu.Item.Back(viewHistory = true), - ) - }, - listener = { - browserToolbarInteractor.onBrowserToolbarMenuItemTapped( - ToolbarMenu.Item.Back(viewHistory = false), - ) - }, - ) - browserToolbarView.view.addNavigationAction(backAction) - val forwardAction = BrowserToolbar.TwoStateButton( - primaryImage = AppCompatResources.getDrawable( - context, - R.drawable.mozac_ic_forward, - )!!, - primaryContentDescription = context.getString(R.string.browser_menu_forward), - primaryImageTintResource = enableTint, - isInPrimaryState = { getCurrentTab()?.content?.canGoForward ?: false }, - secondaryImageTintResource = disableTint, - disableInSecondaryState = true, - longClickListener = { - browserToolbarInteractor.onBrowserToolbarMenuItemTapped( - ToolbarMenu.Item.Forward(viewHistory = true), - ) - }, - listener = { - browserToolbarInteractor.onBrowserToolbarMenuItemTapped( - ToolbarMenu.Item.Forward(viewHistory = false), - ) - }, - ) - browserToolbarView.view.addNavigationAction(forwardAction) - val refreshAction = BrowserToolbar.TwoStateButton( - primaryImage = AppCompatResources.getDrawable( - context, - R.drawable.mozac_ic_refresh, - )!!, - primaryContentDescription = context.getString(R.string.browser_menu_refresh), - primaryImageTintResource = enableTint, - isInPrimaryState = { - getCurrentTab()?.content?.loading == false - }, - secondaryImage = AppCompatResources.getDrawable( - context, - R.drawable.mozac_ic_stop, - )!!, - secondaryContentDescription = context.getString(R.string.browser_menu_stop), - disableInSecondaryState = false, - longClickListener = { - browserToolbarInteractor.onBrowserToolbarMenuItemTapped( - ToolbarMenu.Item.Reload(bypassCache = true), - ) - }, - listener = { - if (getCurrentTab()?.content?.loading == true) { - browserToolbarInteractor.onBrowserToolbarMenuItemTapped(ToolbarMenu.Item.Stop) - } else { - browserToolbarInteractor.onBrowserToolbarMenuItemTapped( - ToolbarMenu.Item.Reload(bypassCache = false), - ) - } - }, - ) - browserToolbarView.view.addNavigationAction(refreshAction) - } + updateToolbarActions(isTablet = resources.getBoolean(R.bool.tablet)) val readerModeAction = BrowserToolbar.ToggleButton( @@ -243,6 +175,141 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { } } + override fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) { + super.onUpdateToolbarForConfigurationChange(toolbar) + + updateToolbarActions(isTablet = resources.getBoolean(R.bool.tablet)) + } + + @VisibleForTesting + internal fun updateToolbarActions(isTablet: Boolean) { + if (isTablet == this.isTablet) return + + if (isTablet) { + addTabletActions(requireContext()) + } else { + removeTabletActions() + } + + this.isTablet = isTablet + } + + @Suppress("LongMethod") + private fun addTabletActions(context: Context) { + val enableTint = ThemeManager.resolveAttribute(R.attr.textPrimary, context) + val disableTint = ThemeManager.resolveAttribute(R.attr.textDisabled, context) + + if (backAction == null) { + backAction = BrowserToolbar.TwoStateButton( + primaryImage = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_back, + )!!, + primaryContentDescription = context.getString(R.string.browser_menu_back), + primaryImageTintResource = enableTint, + isInPrimaryState = { getCurrentTab()?.content?.canGoBack ?: false }, + secondaryImageTintResource = disableTint, + disableInSecondaryState = true, + longClickListener = { + browserToolbarInteractor.onBrowserToolbarMenuItemTapped( + ToolbarMenu.Item.Back(viewHistory = true), + ) + }, + listener = { + browserToolbarInteractor.onBrowserToolbarMenuItemTapped( + ToolbarMenu.Item.Back(viewHistory = false), + ) + }, + ) + } + + backAction?.let { + browserToolbarView.view.addNavigationAction(it) + } + + if (forwardAction == null) { + forwardAction = BrowserToolbar.TwoStateButton( + primaryImage = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_forward, + )!!, + primaryContentDescription = context.getString(R.string.browser_menu_forward), + primaryImageTintResource = enableTint, + isInPrimaryState = { getCurrentTab()?.content?.canGoForward ?: false }, + secondaryImageTintResource = disableTint, + disableInSecondaryState = true, + longClickListener = { + browserToolbarInteractor.onBrowserToolbarMenuItemTapped( + ToolbarMenu.Item.Forward(viewHistory = true), + ) + }, + listener = { + browserToolbarInteractor.onBrowserToolbarMenuItemTapped( + ToolbarMenu.Item.Forward(viewHistory = false), + ) + }, + ) + } + + forwardAction?.let { + browserToolbarView.view.addNavigationAction(it) + } + + if (refreshAction == null) { + refreshAction = BrowserToolbar.TwoStateButton( + primaryImage = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_refresh, + )!!, + primaryContentDescription = context.getString(R.string.browser_menu_refresh), + primaryImageTintResource = enableTint, + isInPrimaryState = { + getCurrentTab()?.content?.loading == false + }, + secondaryImage = AppCompatResources.getDrawable( + context, + R.drawable.mozac_ic_stop, + )!!, + secondaryContentDescription = context.getString(R.string.browser_menu_stop), + disableInSecondaryState = false, + longClickListener = { + browserToolbarInteractor.onBrowserToolbarMenuItemTapped( + ToolbarMenu.Item.Reload(bypassCache = true), + ) + }, + listener = { + if (getCurrentTab()?.content?.loading == true) { + browserToolbarInteractor.onBrowserToolbarMenuItemTapped(ToolbarMenu.Item.Stop) + } else { + browserToolbarInteractor.onBrowserToolbarMenuItemTapped( + ToolbarMenu.Item.Reload(bypassCache = false), + ) + } + }, + ) + } + + refreshAction?.let { + browserToolbarView.view.addNavigationAction(it) + } + + browserToolbarView.view.invalidateActions() + } + + private fun removeTabletActions() { + forwardAction?.let { + browserToolbarView.view.removeNavigationAction(it) + } + backAction?.let { + browserToolbarView.view.removeNavigationAction(it) + } + refreshAction?.let { + browserToolbarView.view.removeNavigationAction(it) + } + + browserToolbarView.view.invalidateActions() + } + override fun onStart() { super.onStart() val context = requireContext() @@ -261,6 +328,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { } subscribeToTabCollections() + updateLastBrowseActivity() } override fun onStop() { @@ -297,22 +365,35 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { } override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) { - requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> - runIfFragmentIsAttached { - val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains - val directions = - BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment( - sessionId = tab.id, - url = tab.content.url, - title = tab.content.title, - isSecured = tab.content.securityInfo.secure, - sitePermissions = sitePermissions, - gravity = getAppropriateLayoutGravity(), - certificateName = tab.content.securityInfo.issuer, - permissionHighlights = tab.content.permissionHighlights, - isTrackingProtectionEnabled = isTrackingProtectionEnabled, + val useCase = requireComponents.useCases.trackingProtectionUseCases + FxNimbus.features.cookieBanners.recordExposure() + useCase.containsException(tab.id) { hasTrackingProtectionException -> + lifecycleScope.launch(Dispatchers.Main) { + val cookieBannersStorage = requireComponents.core.cookieBannersStorage + val hasCookieBannerException = withContext(Dispatchers.IO) { + cookieBannersStorage.hasException( + tab.content.url, + tab.content.private, ) - nav(R.id.browserFragment, directions) + } + runIfFragmentIsAttached { + val isTrackingProtectionEnabled = + tab.trackingProtection.enabled && !hasTrackingProtectionException + val directions = + BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment( + sessionId = tab.id, + url = tab.content.url, + title = tab.content.title, + isSecured = tab.content.securityInfo.secure, + sitePermissions = sitePermissions, + gravity = getAppropriateLayoutGravity(), + certificateName = tab.content.securityInfo.issuer, + permissionHighlights = tab.content.permissionHighlights, + isTrackingProtectionEnabled = isTrackingProtectionEnabled, + isCookieHandlingEnabled = !hasCookieBannerException, + ) + nav(R.id.browserFragment, directions) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt b/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt index c34279ed1..a9ad9b7ec 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt @@ -56,19 +56,24 @@ class SwipeGestureLayout @JvmOverloads constructor( defStyleAttr: Int = 0, ) : FrameLayout(context, attrs, defStyleAttr) { + /** + * Controls whether the swiping functionality is active or not. + */ + var isSwipeEnabled = true + private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { - override fun onDown(e: MotionEvent?): Boolean { + override fun onDown(e: MotionEvent): Boolean { return true } override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent?, + e1: MotionEvent, + e2: MotionEvent, distanceX: Float, distanceY: Float, ): Boolean { - val start = e1?.let { event -> PointF(event.rawX, event.rawY) } ?: return false - val next = e2?.let { event -> PointF(event.rawX, event.rawY) } ?: return false + val start = e1.let { event -> PointF(event.rawX, event.rawY) } + val next = e2.let { event -> PointF(event.rawX, event.rawY) } if (activeListener == null && !handledInitialScroll) { activeListener = listeners.firstOrNull { listener -> @@ -81,8 +86,8 @@ class SwipeGestureLayout @JvmOverloads constructor( } override fun onFling( - e1: MotionEvent?, - e2: MotionEvent?, + e1: MotionEvent, + e2: MotionEvent, velocityX: Float, velocityY: Float, ): Boolean { @@ -107,6 +112,10 @@ class SwipeGestureLayout @JvmOverloads constructor( } override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + if (!isSwipeEnabled) { + return false + } + return when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { handledInitialScroll = false diff --git a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt index ef5cedf52..6cde3be55 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt @@ -258,7 +258,7 @@ class ToolbarGestureHandler( .setDuration(shortAnimationDuration.toLong()) .setListener( object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { tabPreview.isVisible = false } }, diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt index b0432090d..7772d5d1c 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt @@ -13,8 +13,8 @@ import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.state.Action import mozilla.components.lib.state.State import mozilla.components.lib.state.Store +import mozilla.components.support.ktx.kotlin.toShortUrl import org.mozilla.fenix.components.TabCollectionStorage -import org.mozilla.fenix.ext.toShortUrl class CollectionCreationStore( initialState: CollectionCreationState, diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt index bf47fbd16..cd98d98de 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt @@ -20,12 +20,12 @@ import androidx.transition.TransitionManager import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.showKeyboard +import mozilla.components.support.ktx.kotlin.toShortUrl import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.R import org.mozilla.fenix.databinding.ComponentCollectionCreationBinding import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.toShortUrl class CollectionCreationView( private val container: ViewGroup, diff --git a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt index 508fdd246..1d849614f 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -46,7 +46,6 @@ import org.mozilla.fenix.ext.settings import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.sync.SyncedTabsIntegration -import org.mozilla.fenix.utils.Settings /** * Component group for background services. These are the components that need to be accessed from within a @@ -127,7 +126,7 @@ class BackgroundServices( } private val telemetryAccountObserver = TelemetryAccountObserver( - context.settings(), + context, ) val accountAbnormalities = AccountAbnormalities(context, crashReporter, strictMode) @@ -219,13 +218,16 @@ private class AccountManagerReadyObserver( @VisibleForTesting(otherwise = PRIVATE) internal class TelemetryAccountObserver( - private val settings: Settings, + private val context: Context, ) : AccountObserver { override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { - settings.signedInFxaAccount = true + context.settings().signedInFxaAccount = true when (authType) { // User signed-in into an existing FxA account. - AuthType.Signin -> SyncAuth.signIn.record(NoExtras()) + AuthType.Signin -> { + SyncAuth.signIn.record(NoExtras()) + context.components.analytics.experiments.recordEvent("sync_auth.sign_in") + } // User created a new FxA account. AuthType.Signup -> SyncAuth.signUp.record(NoExtras()) @@ -254,6 +256,6 @@ internal class TelemetryAccountObserver( override fun onLoggedOut() { SyncAuth.signOut.record(NoExtras()) - settings.signedInFxaAccount = false + context.settings().signedInFxaAccount = false } } diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 8ef188e3f..fd3b644c3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -12,6 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import mozilla.components.browser.engine.gecko.GeckoEngine +import mozilla.components.browser.engine.gecko.cookiebanners.GeckoCookieBannersStorage import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage import mozilla.components.browser.icons.BrowserIcons @@ -132,6 +133,8 @@ class Core( R.color.fx_mobile_layer_color_1, ), httpsOnlyMode = context.settings().getHttpsOnlyMode(), + cookieBannerHandlingModePrivateBrowsing = context.settings().getCookieBannerHandling(), + cookieBannerHandlingMode = context.settings().getCookieBannerHandling(), ) GeckoEngine( @@ -181,6 +184,8 @@ class Core( ) } + val cookieBannersStorage by lazyMonitored { GeckoCookieBannersStorage(geckoRuntime) } + val geckoSitePermissionsStorage by lazyMonitored { GeckoSitePermissionsStorage(geckoRuntime, OnDiskSitePermissionsStorage(context)) } diff --git a/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt b/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt index 5a5efcaf4..cbc70d0e8 100644 --- a/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt @@ -20,26 +20,58 @@ class PermissionStorage( context.components.core.geckoSitePermissionsStorage, ) { + /** + * Persists the [sitePermissions] provided as a parameter. + * @param sitePermissions the [sitePermissions] to be stored. + */ suspend fun add(sitePermissions: SitePermissions) = withContext(dispatcher) { - permissionsStorage.save(sitePermissions) + permissionsStorage.save(sitePermissions, private = false) } - suspend fun findSitePermissionsBy(origin: String): SitePermissions? = withContext(dispatcher) { - permissionsStorage.findSitePermissionsBy(origin) - } + /** + * Finds all SitePermissions that match the [origin]. + * @param origin the site to be used as filter in the search. + * @param private indicates if the [origin] belongs to a private session. + */ + suspend fun findSitePermissionsBy(origin: String, private: Boolean): SitePermissions? = + withContext(dispatcher) { + permissionsStorage.findSitePermissionsBy(origin, private = private) + } - suspend fun updateSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) { - permissionsStorage.update(sitePermissions) - } + /** + * Replaces an existing SitePermissions with the values of [sitePermissions] provided as a parameter. + * @param sitePermissions the sitePermissions to be updated. + * @param private indicates if the [SitePermissions] belongs to a private session. + */ + suspend fun updateSitePermissions(sitePermissions: SitePermissions, private: Boolean) = + withContext(dispatcher) { + permissionsStorage.update(sitePermissions, private = private) + } + /** + * Returns all saved [SitePermissions] instances as a [DataSource.Factory]. + * + * A consuming app can transform the data source into a `LiveData` of when using RxJava2 into a + * `Flowable` or `Observable`, that can be observed. + * + * - https://developer.android.com/topic/libraries/architecture/paging/data + * - https://developer.android.com/topic/libraries/architecture/paging/ui + */ suspend fun getSitePermissionsPaged(): DataSource.Factory { return permissionsStorage.getSitePermissionsPaged() } + /** + * Deletes all sitePermissions that match the sitePermissions provided as a parameter. + * @param sitePermissions the sitePermissions to be deleted from the storage. + */ suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) { - permissionsStorage.remove(sitePermissions) + permissionsStorage.remove(sitePermissions, private = false) } + /** + * Deletes all sitePermissions sitePermissions. + */ suspend fun deleteAllSitePermissions() = withContext(dispatcher) { permissionsStorage.removeAll() } diff --git a/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt b/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt index b76fc778b..7b5a5160c 100644 --- a/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt @@ -19,8 +19,8 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollectionStorage import mozilla.components.support.base.observer.Observable import mozilla.components.support.base.observer.ObserverRegistry +import mozilla.components.support.ktx.kotlin.toShortUrl import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.perf.StrictModeManager private const val COLLECTION_MAX_TITLE_LENGTH = 20 diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index 77fedea9a..9fec84140 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -23,14 +23,9 @@ sealed class Event { object SetAsDefault : GrowthData("xgpcgt") /** - * Event recording the first time Firefox has been resumed in a 24 hour period. + * Event recording that an ad was clicked in a search engine results page. */ - object FirstAppOpenForDay : GrowthData("41hl22") - - /** - * Event recording the first time a URI is loaded in Firefox in a 24 hour period. - */ - object FirstUriLoadForDay : GrowthData("ja86ek") + object SerpAdClicked : GrowthData("e2x17e") /** * Event recording the first time Firefox is used 3 days in a row in the first week of install. diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt index d5590efe7..6b1f94994 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt @@ -262,6 +262,7 @@ internal class ReleaseMetricController( Component.FEATURE_SEARCH to AdsTelemetry.SERP_ADD_CLICKED -> { BrowserSearch.adClicks[value!!].add() + track(Event.GrowthData.SerpAdClicked) } Component.FEATURE_SEARCH to AdsTelemetry.SERP_SHOWN_WITH_ADDS -> { BrowserSearch.withAds[value!!].add() diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsMiddleware.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsMiddleware.kt index e7d7cf2a1..83a1320f0 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsMiddleware.kt @@ -1,3 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + package org.mozilla.fenix.components.metrics import mozilla.components.lib.state.Middleware @@ -23,7 +27,6 @@ class MetricsMiddleware( private fun handleAction(action: AppAction) = when (action) { is AppAction.ResumedMetricsAction -> { metrics.track(Event.GrowthData.SetAsDefault) - metrics.track(Event.GrowthData.FirstAppOpenForDay) metrics.track(Event.GrowthData.FirstWeekSeriesActivity) } else -> Unit diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsStorage.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsStorage.kt index 6b06e40ca..7cb678a8a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsStorage.kt @@ -8,6 +8,7 @@ import android.content.Context import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import mozilla.components.support.utils.ext.getPackageInfoCompat import org.mozilla.fenix.ext.settings import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.utils.Settings @@ -46,21 +47,20 @@ internal class DefaultMetricsStorage( */ override suspend fun shouldTrack(event: Event): Boolean = withContext(dispatcher) { - // The side-effect of storing days of use needs to happen during the first two days after - // install, which would normally be skipped by shouldSendGenerally. + // The side-effect of storing days of use always needs to happen. updateDaysOfUse() + val currentTime = System.currentTimeMillis() shouldSendGenerally() && when (event) { Event.GrowthData.SetAsDefault -> { - !settings.setAsDefaultGrowthSent && checkDefaultBrowser() - } - Event.GrowthData.FirstAppOpenForDay -> { - settings.resumeGrowthLastSent.hasBeenMoreThanDaySince() - } - Event.GrowthData.FirstUriLoadForDay -> { - settings.uriLoadGrowthLastSent.hasBeenMoreThanDaySince() + currentTime.duringFirstMonth() && + !settings.setAsDefaultGrowthSent && + checkDefaultBrowser() } Event.GrowthData.FirstWeekSeriesActivity -> { - shouldTrackFirstWeekActivity() + currentTime.duringFirstMonth() && shouldTrackFirstWeekActivity() + } + Event.GrowthData.SerpAdClicked -> { + currentTime.duringFirstMonth() && !settings.adClickGrowthSent } } } @@ -70,15 +70,12 @@ internal class DefaultMetricsStorage( Event.GrowthData.SetAsDefault -> { settings.setAsDefaultGrowthSent = true } - Event.GrowthData.FirstAppOpenForDay -> { - settings.resumeGrowthLastSent = System.currentTimeMillis() - } - Event.GrowthData.FirstUriLoadForDay -> { - settings.uriLoadGrowthLastSent = System.currentTimeMillis() - } Event.GrowthData.FirstWeekSeriesActivity -> { settings.firstWeekSeriesGrowthSent = true } + Event.GrowthData.SerpAdClicked -> { + settings.adClickGrowthSent = true + } } } @@ -86,13 +83,13 @@ internal class DefaultMetricsStorage( val daysOfUse = settings.firstWeekDaysOfUseGrowthData val currentDate = Calendar.getInstance(Locale.US) val currentDateString = dateFormatter.format(currentDate.time) - if (currentDate.timeInMillis.withinFirstWeek() && daysOfUse.none { it == currentDateString }) { + if (currentDate.timeInMillis.duringFirstWeek() && daysOfUse.none { it == currentDateString }) { settings.firstWeekDaysOfUseGrowthData = daysOfUse + currentDateString } } private fun shouldTrackFirstWeekActivity(): Boolean = Result.runCatching { - if (!System.currentTimeMillis().withinFirstWeek() || settings.firstWeekSeriesGrowthSent) { + if (!System.currentTimeMillis().duringFirstWeek() || settings.firstWeekSeriesGrowthSent) { return false } @@ -120,14 +117,13 @@ internal class DefaultMetricsStorage( return false }.getOrDefault(false) - private fun Long.hasBeenMoreThanDaySince(): Boolean = - System.currentTimeMillis() - this > dayMillis - private fun Long.toCalendar(): Calendar = Calendar.getInstance(Locale.US).also { calendar -> calendar.timeInMillis = this } - private fun Long.withinFirstWeek() = this < getInstalledTime() + fullWeekMillis + private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis + + private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis private fun Calendar.createNextDay() = (this.clone() as Calendar).also { calendar -> calendar.add(Calendar.DAY_OF_MONTH, 1) @@ -135,8 +131,7 @@ internal class DefaultMetricsStorage( companion object { private const val dayMillis: Long = 1000 * 60 * 60 * 24 - private const val windowStartMillis: Long = dayMillis * 2 - private const val windowEndMillis: Long = dayMillis * 28 + private const val shortestMonthMillis: Long = dayMillis * 28 // Note this is 8 so that recording of FirstWeekSeriesActivity happens throughout the length // of the 7th day after install @@ -145,21 +140,15 @@ internal class DefaultMetricsStorage( /** * Determines whether events should be tracked based on some general criteria: * - user has installed as a result of a campaign - * - user is within 2-28 days of install * - tracking is still enabled through Nimbus */ fun shouldSendGenerally(context: Context): Boolean { - val installedTime = getInstalledTime(context) - val timeDifference = System.currentTimeMillis() - installedTime - val withinWindow = timeDifference in windowStartMillis..windowEndMillis - return context.settings().adjustCampaignId.isNotEmpty() && - FxNimbus.features.growthData.value().enabled && - withinWindow + FxNimbus.features.growthData.value().enabled } fun getInstalledTime(context: Context): Long = context.packageManager - .getPackageInfo(context.packageName, 0) + .getPackageInfoCompat(context.packageName, 0) .firstInstallTime } } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt index 388b0e01a..fc42937b7 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.components.metrics import android.content.Context import android.content.pm.PackageManager +import mozilla.components.support.utils.ext.getPackageInfoCompat import org.mozilla.fenix.utils.BrowsersCache object MozillaProductDetector { @@ -45,7 +46,7 @@ object MozillaProductDetector { fun packageIsInstalled(context: Context, packageName: String): Boolean { try { - context.packageManager.getPackageInfo(packageName, 0) + context.packageManager.getPackageInfoCompat(packageName, 0) } catch (e: PackageManager.NameNotFoundException) { return false } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt index 91aee1c34..5bbcc9489 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -22,7 +22,7 @@ import mozilla.components.browser.state.state.ExternalAppType import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.behavior.BrowserToolbarBehavior import mozilla.components.browser.toolbar.display.DisplayToolbar -import mozilla.components.support.utils.URLStringUtils +import mozilla.components.support.ktx.util.URLStringUtils import org.mozilla.fenix.R import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/MenuPresenter.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/MenuPresenter.kt index 80b12d786..522d03df8 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/MenuPresenter.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/MenuPresenter.kt @@ -49,11 +49,11 @@ class MenuPresenter( menuToolbar.invalidateActions() } - override fun onViewDetachedFromWindow(v: View?) { + override fun onViewDetachedFromWindow(v: View) { menuToolbar.onStop() } - override fun onViewAttachedToWindow(v: View?) { + override fun onViewAttachedToWindow(v: View) { // no-op } } diff --git a/app/src/main/java/org/mozilla/fenix/compose/ClickableSubstringLink.kt b/app/src/main/java/org/mozilla/fenix/compose/ClickableSubstringLink.kt index 33a43fa7b..9a0429702 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ClickableSubstringLink.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ClickableSubstringLink.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * [Text] containing a substring styled as an URL informing when this is clicked. @@ -95,7 +94,7 @@ fun ClickableSubstringLink( private fun ClickableSubstringTextPreview() { val text = "This text contains a link" - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) { ClickableSubstringLink( text = text, diff --git a/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt b/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt index 0501ee448..870440758 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/Favicon.kt @@ -24,7 +24,6 @@ import mozilla.components.browser.icons.compose.Placeholder import mozilla.components.browser.icons.compose.WithIcon import org.mozilla.fenix.components.components import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Load and display the favicon of a particular website. @@ -98,7 +97,7 @@ private fun FaviconPlaceholder( @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) private fun FaviconPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { Favicon( url = "www.mozilla.com", diff --git a/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLarge.kt b/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLarge.kt index 60c2743e3..dd3f1feeb 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLarge.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLarge.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Default layout of a large tab shown in a list taking String arguments for title and caption. @@ -171,7 +170,7 @@ fun ListItemTabSurface( @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ListItemTabLargePreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { ListItemTabLarge( imageUrl = "", title = "This is a very long title for a tab but needs to be so for this preview", @@ -184,7 +183,7 @@ private fun ListItemTabLargePreview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ListItemTabSurfacePreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { ListItemTabSurface( imageUrl = "", ) { @@ -201,7 +200,7 @@ private fun ListItemTabSurfacePreview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ListItemTabSurfaceWithCustomBackgroundPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { ListItemTabSurface( imageUrl = "", backgroundColor = Color.Cyan, diff --git a/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLargePlaceholder.kt b/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLargePlaceholder.kt index 8d93032b9..31055da79 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLargePlaceholder.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLargePlaceholder.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Placeholder of a [ListItemTabLarge] with the same dimensions but only a centered text. @@ -74,7 +73,7 @@ fun ListItemTabLargePlaceholder( @Composable @Preview private fun ListItemTabLargePlaceholderPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { ListItemTabLargePlaceholder(text = "Item placeholder") } } diff --git a/app/src/main/java/org/mozilla/fenix/compose/SelectableChip.kt b/app/src/main/java/org/mozilla/fenix/compose/SelectableChip.kt index ae2e04aba..fc715d55e 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/SelectableChip.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/SelectableChip.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Default layout of a selectable chip. @@ -78,7 +77,7 @@ fun SelectableChip( @Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_NO) private fun SelectableChipPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Row( modifier = Modifier .fillMaxWidth() @@ -95,7 +94,7 @@ private fun SelectableChipPreview() { @Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_NO) private fun SelectableChipWithCustomColorsPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Row( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/org/mozilla/fenix/compose/StaggeredHorizontalGrid.kt b/app/src/main/java/org/mozilla/fenix/compose/StaggeredHorizontalGrid.kt index f9182d295..145a5701c 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/StaggeredHorizontalGrid.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/StaggeredHorizontalGrid.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Displays a list of items as a staggered horizontal grid placing them on ltr rows and continuing @@ -121,7 +120,7 @@ fun StaggeredHorizontalGrid( @Composable @Preview private fun StaggeredHorizontalGridPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer2)) { StaggeredHorizontalGrid( horizontalItemsSpacing = 8.dp, diff --git a/app/src/main/java/org/mozilla/fenix/compose/TabSubtitleWithInterdot.kt b/app/src/main/java/org/mozilla/fenix/compose/TabSubtitleWithInterdot.kt index 06fbdfd46..0ff1f1907 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/TabSubtitleWithInterdot.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/TabSubtitleWithInterdot.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Special caption text for a tab layout shown on one line. @@ -106,7 +105,7 @@ fun TabSubtitleWithInterdot( @Composable @Preview private fun TabSubtitleWithInterdotPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer2)) { TabSubtitleWithInterdot( firstText = "firstText", diff --git a/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt b/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt index 167460a33..722c647e7 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt @@ -34,7 +34,6 @@ import mozilla.components.concept.base.images.ImageLoadRequest import org.mozilla.fenix.R import org.mozilla.fenix.components.components import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Card which will display a thumbnail. If a thumbnail is not available for [url], the favicon @@ -138,7 +137,7 @@ private fun ThumbnailImage( @Preview @Composable private fun ThumbnailCardPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { ThumbnailCard( url = "https://mozilla.com", key = "123", diff --git a/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt b/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt index d344fb681..7a9d802df 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/button/Button.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.mozilla.fenix.R import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Base component for buttons. @@ -187,7 +186,7 @@ fun DestructiveButton( @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ButtonPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Column( modifier = Modifier .background(FirefoxTheme.colors.layer1) diff --git a/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt b/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt index 1ba676695..78118946f 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/button/TextButton.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme import java.util.Locale /** @@ -48,7 +47,7 @@ fun TextButton( @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun TextButtonPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { TextButton( text = "label", diff --git a/app/src/main/java/org/mozilla/fenix/compose/home/HomeSectionHeader.kt b/app/src/main/java/org/mozilla/fenix/compose/home/HomeSectionHeader.kt index 8313c17e7..cd4ee8e60 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/home/HomeSectionHeader.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/home/HomeSectionHeader.kt @@ -28,7 +28,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.components import org.mozilla.fenix.compose.inComposePreview import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme import org.mozilla.fenix.wallpapers.Wallpaper /** @@ -124,7 +123,7 @@ private fun HomeSectionHeaderContent( @Composable @Preview private fun HomeSectionsHeaderPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { HomeSectionHeader( headerText = stringResource(R.string.recently_saved_title), description = stringResource(R.string.recently_saved_show_all_content_description_2), diff --git a/app/src/main/java/org/mozilla/fenix/compose/list/ExpandableListHeader.kt b/app/src/main/java/org/mozilla/fenix/compose/list/ExpandableListHeader.kt index e7324ea32..2befdd624 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/list/ExpandableListHeader.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/list/ExpandableListHeader.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.mozilla.fenix.R import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Expandable header for sections of lists @@ -96,7 +95,7 @@ fun ExpandableListHeader( @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) private fun TextOnlyHeaderPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { ExpandableListHeader(headerText = "Section title") } @@ -106,7 +105,7 @@ private fun TextOnlyHeaderPreview() { @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) private fun CollapsibleHeaderPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { ExpandableListHeader( headerText = "Collapsible section title", @@ -122,7 +121,7 @@ private fun CollapsibleHeaderPreview() { @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun HeaderWithClickableIconPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { ExpandableListHeader(headerText = "Section title") { Box( @@ -145,7 +144,7 @@ private fun HeaderWithClickableIconPreview() { @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun CollapsibleHeaderWithClickableIconPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { ExpandableListHeader( headerText = "Section title", diff --git a/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt b/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt index 2a1afad42..af357f7ea 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.unit.dp import org.mozilla.fenix.R import org.mozilla.fenix.compose.Favicon import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme private val LIST_ITEM_HEIGHT = 56.dp @@ -250,7 +249,7 @@ private fun ListItem( @Composable @Preview(name = "TextListItem", uiMode = Configuration.UI_MODE_NIGHT_YES) private fun TextListItemPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { TextListItem(label = "Label only") } @@ -260,7 +259,7 @@ private fun TextListItemPreview() { @Composable @Preview(name = "TextListItem with a description", uiMode = Configuration.UI_MODE_NIGHT_YES) private fun TextListItemWithDescriptionPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { TextListItem( label = "Label + description", @@ -273,7 +272,7 @@ private fun TextListItemWithDescriptionPreview() { @Composable @Preview(name = "TextListItem with a right icon", uiMode = Configuration.UI_MODE_NIGHT_YES) private fun TextListItemWithIconPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { TextListItem( label = "Label + right icon", @@ -288,7 +287,7 @@ private fun TextListItemWithIconPreview() { @Composable @Preview(name = "IconListItem", uiMode = Configuration.UI_MODE_NIGHT_YES) private fun IconListItemPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { IconListItem( label = "Left icon list item", @@ -305,7 +304,7 @@ private fun IconListItemPreview() { uiMode = Configuration.UI_MODE_NIGHT_YES, ) private fun IconListItemWithRightIconPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { IconListItem( label = "Left icon list item + right icon", @@ -325,7 +324,7 @@ private fun IconListItemWithRightIconPreview() { uiMode = Configuration.UI_MODE_NIGHT_YES, ) private fun FaviconListItemPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { Box(Modifier.background(FirefoxTheme.colors.layer1)) { FaviconListItem( label = "Favicon + right icon + clicks", diff --git a/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt b/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt index fad96153a..6ad763025 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt @@ -22,7 +22,6 @@ import mozilla.components.browser.state.state.createTab import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState import org.mozilla.fenix.R import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * Controller buttons for the media (play/pause) state for the given [tab]. @@ -59,7 +58,7 @@ fun MediaImage( @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun ImagePreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { MediaImage( tab = createTab(url = "https://mozilla.com"), onMediaIconClicked = {}, diff --git a/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt b/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt index a704d3a3d..c1b72665f 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt @@ -34,7 +34,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.compose.ThumbnailCard import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.theme.FirefoxTheme -import org.mozilla.fenix.theme.Theme /** * List item used to display a tab that supports clicks, @@ -172,7 +171,7 @@ private fun Thumbnail( @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun TabListItemPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { TabListItem( tab = createTab(url = "www.mozilla.com", title = "Mozilla"), onCloseClick = {}, @@ -187,7 +186,7 @@ private fun TabListItemPreview() { @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) private fun SelectedTabListItemPreview() { - FirefoxTheme(theme = Theme.getTheme()) { + FirefoxTheme { TabListItem( tab = createTab(url = "www.mozilla.com", title = "Mozilla"), onCloseClick = {}, diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index 07a9b46d8..7657256f6 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -9,7 +9,11 @@ import android.content.Intent import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.concept.engine.manifest.WebAppManifestParser @@ -29,6 +33,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.browser.BaseBrowserFragment import org.mozilla.fenix.browser.CustomTabContextMenuCandidate import org.mozilla.fenix.browser.FenixSnackbarDelegate +import org.mozilla.fenix.components.components import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents @@ -159,21 +164,29 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler } override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) { + val cookieBannersStorage = requireComponents.core.cookieBannersStorage requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> - runIfFragmentIsAttached { - val directions = ExternalAppBrowserFragmentDirections - .actionGlobalQuickSettingsSheetDialogFragment( - sessionId = tab.id, - url = tab.content.url, - title = tab.content.title, - isSecured = tab.content.securityInfo.secure, - sitePermissions = sitePermissions, - gravity = getAppropriateLayoutGravity(), - certificateName = tab.content.securityInfo.issuer, - permissionHighlights = tab.content.permissionHighlights, - isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains, - ) - nav(R.id.externalAppBrowserFragment, directions) + lifecycleScope.launch(Dispatchers.IO) { + val hasException = + cookieBannersStorage.hasException(tab.content.url, tab.content.private) + withContext(Dispatchers.Main) { + runIfFragmentIsAttached { + val directions = ExternalAppBrowserFragmentDirections + .actionGlobalQuickSettingsSheetDialogFragment( + sessionId = tab.id, + url = tab.content.url, + title = tab.content.title, + isSecured = tab.content.securityInfo.secure, + sitePermissions = sitePermissions, + gravity = getAppropriateLayoutGravity(), + certificateName = tab.content.securityInfo.issuer, + permissionHighlights = tab.content.permissionHighlights, + isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains, + isCookieHandlingEnabled = !hasException, + ) + nav(R.id.externalAppBrowserFragment, directions) + } + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt b/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt index 72b9cac81..99dff84fe 100644 --- a/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt +++ b/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt @@ -5,36 +5,17 @@ package org.mozilla.fenix.experiments import android.content.Context -import android.net.Uri -import android.os.StrictMode -import mozilla.components.service.nimbus.Nimbus import mozilla.components.service.nimbus.NimbusApi import mozilla.components.service.nimbus.NimbusAppInfo -import mozilla.components.service.nimbus.NimbusDisabled -import mozilla.components.service.nimbus.NimbusServerSettings +import mozilla.components.service.nimbus.NimbusBuilder import mozilla.components.support.base.log.logger.Logger -import org.mozilla.experiments.nimbus.NimbusInterface -import org.mozilla.experiments.nimbus.internal.EnrolledExperiment +import org.json.JSONObject import org.mozilla.experiments.nimbus.internal.NimbusException -import org.mozilla.experiments.nimbus.joinOrTimeout import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.nimbus.FxNimbus -import org.mozilla.fenix.perf.runBlockingIncrement - -/** - * Fenix specific observer of Nimbus events. - * - * The generated code `FxNimbus` provides a cache which should be invalidated - * when the experiments recipes are updated. - */ -private val observer = object : NimbusInterface.Observer { - override fun onUpdatesApplied(updated: List) { - FxNimbus.invalidateCachedValues() - } -} /** * The maximum amount of time the app launch will be blocked to load experiments from disk. @@ -44,93 +25,59 @@ private val observer = object : NimbusInterface.Observer { */ private const val TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS = 200L -@Suppress("TooGenericExceptionCaught") -fun createNimbus(context: Context, url: String?): NimbusApi { - val errorReporter: ((String, Throwable) -> Unit) = reporter@{ message, e -> - Logger.error("Nimbus error: $message", e) - - if (e is NimbusException && !e.isReportableError()) { - return@reporter - } - - context.components.analytics.crashReporter.submitCaughtException(e) +/** + * Create the Nimbus singleton object for the Fenix app. + */ +fun createNimbus(context: Context, urlString: String?): NimbusApi { + val isAppFirstRun = context.settings().isFirstNimbusRun + if (isAppFirstRun) { + context.settings().isFirstNimbusRun = false } - return try { - // Eventually we'll want to use `NimbusDisabled` when we have no NIMBUS_ENDPOINT. - // but we keep this here to not mix feature flags and how we configure Nimbus. - val serverSettings = if (!url.isNullOrBlank()) { - if (context.settings().nimbusUsePreview) { - NimbusServerSettings(url = Uri.parse(url), collection = "nimbus-preview") - } else { - NimbusServerSettings(url = Uri.parse(url)) - } - } else { - null - } - - // Global opt out state is stored in Nimbus, and shouldn't be toggled to `true` - // from the app unless the user does so from a UI control. - // However, the user may have opt-ed out of mako experiments already, so - // we should respect that setting here. - val enabled = - context.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { - context.settings().isExperimentationEnabled - } - // The name "fenix" here corresponds to the app_name defined for the family of apps - // that encompasses all of the channels for the Fenix 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 = "fenix", - // 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 = mapOf( - "isFirstRun" to context.settings().isFirstNimbusRun.toString(), - ), - ) - Nimbus(context, appInfo, serverSettings, errorReporter).apply { - // We register our own internal observer for housekeeping the Nimbus SDK and - // generated code. - register(observer) + // 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) - val isFirstNimbusRun = context.settings().isFirstNimbusRun + // This camelCase attribute is a boolean value represented as a string. + // This is left for backwards compatibility. + put("isFirstRun", isAppFirstRun.toString()) + } - // We always want `Nimbus.initialize` to happen ASAP and before any features (engine/UI) - // have been initialized. For that reason, we use runBlocking here to avoid - // inconsistency in the experiments. - // We can safely do this because Nimbus does most of it's work on background threads, - // except for loading the initial experiments from disk. For this reason, we have a - // `joinOrTimeout` to limit the blocking until TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS. - runBlockingIncrement { - val job = initialize( - isFirstNimbusRun || url.isNullOrBlank(), - R.raw.initial_experiments, - ) - // We only read from disk when loading first-run experiments. This is the only time - // that we should join and block. Otherwise, we don't want to wait. - if (isFirstNimbusRun) { - context.settings().isFirstNimbusRun = false - job.joinOrTimeout(TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS) - } - } + // The name "fenix" here corresponds to the app_name defined for the family of apps + // that encompasses all of the channels for the Fenix 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 = "fenix", + // 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.let { if (it == "debug") "developer" else it }, + customTargetingAttributes = customTargetingAttributes, + ) - if (!enabled) { - // This opts out of nimbus experiments. It involves writing to disk, so does its - // work on the db thread. - globalUserParticipation = enabled + return NimbusBuilder(context).apply { + url = urlString + errorReporter = { message, e -> + Logger.error("Nimbus error: $message", e) + if (e !is NimbusException || e.isReportableError()) { + context.components.analytics.crashReporter.submitCaughtException(e) } } - } catch (e: Throwable) { - // Something went wrong. We'd like not to, but stability of the app is more important than - // failing fast here. - errorReporter("Failed to initialize Nimbus", e) - NimbusDisabled(context) - } + initialExperiments = R.raw.initial_experiments + timeoutLoadingExperiment = TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS + usePreviewCollection = context.settings().nimbusUsePreview + isFirstRun = isAppFirstRun + onCreateCallback = { nimbus -> + FxNimbus.initialize { nimbus } + } + onApplyCallback = { + FxNimbus.invalidateCachedValues() + } + }.build(appInfo) } /** diff --git a/app/src/main/java/org/mozilla/fenix/ext/String.kt b/app/src/main/java/org/mozilla/fenix/ext/String.kt index 618172d8e..c390ef9fc 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/String.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/String.kt @@ -4,76 +4,12 @@ package org.mozilla.fenix.ext -import android.net.InetAddresses -import android.os.Build import android.text.Editable -import android.util.Patterns -import android.webkit.URLUtil import androidx.compose.runtime.Composable -import androidx.core.net.toUri -import mozilla.components.lib.publicsuffixlist.PublicSuffixList -import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH +import mozilla.components.support.ktx.kotlin.toShortUrl import org.mozilla.fenix.components.components import org.mozilla.fenix.compose.inComposePreview -import java.net.IDN -import java.util.Locale - -const val FILE_PREFIX = "file://" -const val MAX_VALID_PORT = 65_535 - -/** - * Shortens URLs to be more user friendly. - * - * The algorithm used to generate these strings is a combination of FF desktop 'top sites', - * feedback from the security team, and documentation regarding url elision. See - * StringTest.kt for details. - * - * This method is complex because URLs have a lot of edge cases. Be sure to thoroughly unit - * test any changes you make to it. - */ -// Unused Parameter: We may resume stripping eTLD, depending on conversations between security and UX -// Return count: This is a complex method, but it would not be more understandable if broken up -// ComplexCondition: Breaking out the complex condition would make this logic harder to follow -@Suppress("UNUSED_PARAMETER", "ReturnCount", "ComplexCondition") -fun String.toShortUrl(publicSuffixList: PublicSuffixList): String { - val inputString = this - val uri = inputString.toUri() - - if ( - inputString.isEmpty() || - !URLUtil.isValidUrl(inputString) || - inputString.startsWith(FILE_PREFIX) || - uri.port !in -1..MAX_VALID_PORT - ) { - return inputString - } - - if (uri.host?.isIpv4OrIpv6() == true || - // If inputString is just a hostname and not a FQDN, use the entire hostname. - uri.host?.contains(".") == false - ) { - return uri.host ?: inputString - } - - fun String.stripUserInfo(): String { - val userInfo = this.toUri().encodedUserInfo - return if (userInfo != null) { - val infoIndex = this.indexOf(userInfo) - this.removeRange(infoIndex..infoIndex + userInfo.length) - } else { - this - } - } - fun String.stripPrefixes(): String = this.toUri().hostWithoutCommonPrefixes ?: this - fun String.toUnicode() = IDN.toUnicode(this) - - return inputString - .stripUserInfo() - .lowercase(Locale.getDefault()) - .stripPrefixes() - .toUnicode() -} /** * Shortens URLs to be more user friendly, by applying [String.toShortUrl] @@ -94,29 +30,6 @@ fun String.toShortUrl(): String { } } -// impl via FFTV https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/main/java/org/mozilla/focus/utils/FormattedDomain.java#129 -@Suppress("DEPRECATION") -internal fun String.isIpv4(): Boolean = Patterns.IP_ADDRESS.matcher(this).matches() - -// impl via FFiOS: https://github.com/mozilla-mobile/firefox-ios/blob/deb9736c905cdf06822ecc4a20152df7b342925d/Shared/Extensions/NSURLExtensions.swift#L292 -// True IPv6 validation is difficult. This is slightly better than nothing -internal fun String.isIpv6(): Boolean { - return this.isNotEmpty() && this.contains(":") -} - -/** - * Returns true if the string represents a valid Ipv4 or Ipv6 IP address. - * Note: does not validate a dual format Ipv6 ( "y:y:y:y:y:y:x.x.x.x" format). - * - */ -fun String.isIpv4OrIpv6(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - InetAddresses.isNumericAddress(this) - } else { - this.isIpv4() || this.isIpv6() - } -} - /** * Trims a URL string of its scheme and common prefixes. * diff --git a/app/src/main/java/org/mozilla/fenix/ext/View.kt b/app/src/main/java/org/mozilla/fenix/ext/View.kt index f8891e35f..83a02b7c1 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/View.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/View.kt @@ -43,11 +43,11 @@ fun View.removeTouchDelegate() { fun View.setNewAccessibilityParent(newParent: View) { this.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( - host: View?, - info: AccessibilityNodeInfo?, + host: View, + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) - info?.setParent(newParent) + info.setParent(newParent) } } } @@ -64,11 +64,22 @@ fun View.updateAccessibilityCollectionItemInfo( ) { this.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( - host: View?, - info: AccessibilityNodeInfo?, + host: View, + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) - info?.collectionItemInfo = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + info.collectionItemInfo = + AccessibilityNodeInfo.CollectionItemInfo( + rowIndex, + rowSpan, + columnIndex, + columnSpan, + false, + isSelected, + ) + } else { + @Suppress("DEPRECATION") AccessibilityNodeInfo.CollectionItemInfo.obtain( rowIndex, rowSpan, @@ -77,6 +88,7 @@ fun View.updateAccessibilityCollectionItemInfo( false, isSelected, ) + } } } } @@ -90,15 +102,24 @@ fun View.updateAccessibilityCollectionInfo( ) { this.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( - host: View?, - info: AccessibilityNodeInfo?, + host: View, + info: AccessibilityNodeInfo, ) { super.onInitializeAccessibilityNodeInfo(host, info) - info?.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( - rowCount, - columnCount, - false, - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + info.collectionInfo = AccessibilityNodeInfo.CollectionInfo( + rowCount, + columnCount, + false, + ) + } else { + @Suppress("DEPRECATION") + info.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( + rowCount, + columnCount, + false, + ) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt index 9a27bb159..947fbdb4d 100644 --- a/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/MessagingFeature.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.gleanplumb import mozilla.components.support.base.feature.LifecycleAwareFeature -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction.MessagingAction @@ -15,9 +14,7 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction class MessagingFeature(val appStore: AppStore) : LifecycleAwareFeature { override fun start() { - if (FeatureFlags.messagingFeature) { - appStore.dispatch(MessagingAction.Evaluate) - } + appStore.dispatch(MessagingAction.Evaluate) } override fun stop() = Unit diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 72df445e6..2ba1f24db 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -28,6 +28,8 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toDrawable +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment @@ -50,12 +52,17 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import mozilla.components.browser.menu.view.MenuButton +import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.searchEngines import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.menu.Orientation +import mozilla.components.concept.menu.candidate.DrawableMenuIcon +import mozilla.components.concept.menu.candidate.TextMenuCandidate import mozilla.components.concept.storage.FrecencyThresholdOption import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType @@ -74,6 +81,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.Config import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.HomeScreen +import org.mozilla.fenix.GleanMetrics.UnifiedSearch import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.addons.showSnackBar @@ -88,6 +96,7 @@ import org.mozilla.fenix.databinding.FragmentHomeBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.containsQueryParameters import org.mozilla.fenix.ext.hideToolbar +import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached @@ -115,6 +124,7 @@ import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.runBlockingIncrement +import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.tabstray.TabsTrayAccessPoint import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD import org.mozilla.fenix.utils.ToolbarPopupWindow @@ -143,6 +153,13 @@ class HomeFragment : Fragment() { ToolbarPosition.TOP -> null } + private val searchSelectorMenu by lazy { + SearchSelectorMenu( + context = requireContext(), + interactor = sessionControlInteractor, + ) + } + private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager private val collectionStorageObserver = object : TabCollectionStorage.Observer { @@ -331,6 +348,24 @@ class HomeFragment : Fragment() { ) } + requireContext().settings().showUnifiedSearchFeature.let { + binding.searchSelector.isVisible = it + binding.searchEngineIcon.isGone = it + } + + binding.searchSelector.apply { + setOnClickListener { + val orientation = if (context.settings().shouldUseBottomToolbar) { + Orientation.UP + } else { + Orientation.DOWN + } + + UnifiedSearch.searchMenuTapped.record(NoExtras()) + searchSelectorMenu.menuController.show(anchor = it, orientation = orientation, forceOrientation = true) + } + } + _sessionControlInteractor = SessionControlInteractor( controller = DefaultSessionControlController( activity = activity, @@ -595,6 +630,14 @@ class HomeFragment : Fragment() { } } + consumeFlow(requireComponents.core.store) { flow -> + flow.map { state -> state.search } + .ifChanged() + .collect { search -> + updateSearchSelectorMenu(search.searchEngines) + } + } + // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL! requireComponents.core.engine.profiler?.addMarker( MarkersFragmentLifecycleCallbacks.MARKER_NAME, @@ -603,20 +646,42 @@ class HomeFragment : Fragment() { ) } + private fun updateSearchSelectorMenu(searchEngines: List) { + val searchEngineList = searchEngines + .map { + TextMenuCandidate( + text = it.name, + start = DrawableMenuIcon( + drawable = it.icon.toDrawable(resources), + ), + ) { + sessionControlInteractor.onMenuItemTapped(SearchSelectorMenu.Item.SearchEngine(it)) + } + } + + searchSelectorMenu.menuController.submitList(searchSelectorMenu.menuItems(searchEngineList)) + } + private fun observeSearchEngineChanges() { consumeFlow(store) { flow -> flow.map { state -> state.search.selectedOrDefaultSearchEngine } .ifChanged() .collect { searchEngine -> - if (searchEngine != null) { + val name = searchEngine?.name + val icon = searchEngine?.let { + // Changing dimensions doesn't not affect the icon size, not sure what the + // code is doing: https://github.com/mozilla-mobile/fenix/issues/27763 val iconSize = requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size) - val searchIcon = - BitmapDrawable(requireContext().resources, searchEngine.icon) - searchIcon.setBounds(0, 0, iconSize, iconSize) - binding.searchEngineIcon.setImageDrawable(searchIcon) + BitmapDrawable(requireContext().resources, searchEngine.icon).apply { + setBounds(0, 0, iconSize, iconSize) + } + } + + if (requireContext().settings().showUnifiedSearchFeature) { + binding.searchSelector.setIcon(icon, name) } else { - binding.searchEngineIcon.setImageDrawable(null) + binding.searchEngineIcon.setImageDrawable(icon) } } } @@ -836,6 +901,8 @@ class HomeFragment : Fragment() { true, ) layout.findViewById