diff --git a/.cron.yml b/.cron.yml index 7dffdb220..0ee4b916a 100644 --- a/.cron.yml +++ b/.cron.yml @@ -32,3 +32,9 @@ jobs: treeherder-symbol: bump-ac target-tasks-method: bump_android_components when: [{hour: 14, minute: 0}] + - name: screenshots + job: + type: decision-task + treeherder-symbol: screenshots-D + target-tasks-method: screenshots + when: [{weekday: 'Monday', hour: 10, minute: 0}] diff --git a/app/build.gradle b/app/build.gradle index 85d61d442..a710bb43f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,6 @@ plugins { apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-kapt' apply plugin: 'jacoco' apply from: "$project.rootDir/automation/gradle/versionCode.gradle" apply plugin: 'androidx.navigation.safeargs.kotlin' diff --git a/app/src/androidTest/java/org/mozilla/fenix/AppRequestInterceptor.kt b/app/src/androidTest/java/org/mozilla/fenix/AppRequestInterceptor.kt index f59bb885e..906e84f6b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/AppRequestInterceptor.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/AppRequestInterceptor.kt @@ -23,10 +23,11 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor { hasUserGesture: Boolean, isSameDomain: Boolean, isRedirect: Boolean, - isDirectNavigation: Boolean + isDirectNavigation: Boolean, + isSubframeRequest: Boolean ): RequestInterceptor.InterceptionResponse? { return appContext.components.services.accountsAuthFeature.interceptor.onLoadRequest( - engineSession, uri, hasUserGesture, isSameDomain, isRedirect, isDirectNavigation + engineSession, uri, hasUserGesture, isSameDomain, isRedirect, isDirectNavigation, isSubframeRequest ) } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt b/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt index 663e96b01..c6e1e5951 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt @@ -92,9 +92,10 @@ class MenuScreenShotTest : ScreenshotTest() { Screengrab.screenshot("SettingsSubMenuDefaultBrowserRobot_settings-default-browser") mDevice.pressBack() - settingsTP() - Screengrab.screenshot("settings-enhanced-tp") - mDevice.pressBack() + // Disabled for Pixel 2 + // settingsTP() + // Screengrab.screenshot("settings-enhanced-tp") + // mDevice.pressBack() loginsAndPassword() Screengrab.screenshot("SettingsSubMenuLoginsAndPasswords-settings-logins-passwords") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt index 1d2de8f5b..14f91034d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -234,7 +234,7 @@ class BookmarksTest { IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { }.clickOpenInNewTab { - // verifyPageContent(defaultWebPage.content) + verifyUrl(defaultWebPage.url.toString()) }.openTabDrawer { verifyNormalModeSelected() } @@ -253,7 +253,7 @@ class BookmarksTest { IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { }.clickOpenInPrivateTab { - // verifyPageContent(defaultWebPage.content) + verifyUrl(defaultWebPage.url.toString()) }.openTabDrawer { verifyPrivateModeSelected() } 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 fa22d40a2..53247bb0b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt @@ -61,7 +61,7 @@ class ContextMenusTest { navigationToolbar { }.enterURLAndEnterToBrowser(pageLinks.url) { - // verifyPageContent(pageLinks.content) + mDevice.waitForIdle() longClickMatchingText("Link 1") verifyLinkContextMenuItems(genericURL.url) clickContextOpenLinkInNewTab() @@ -83,7 +83,7 @@ class ContextMenusTest { navigationToolbar { }.enterURLAndEnterToBrowser(pageLinks.url) { - // verifyPageContent(pageLinks.content) + mDevice.waitForIdle() longClickMatchingText("Link 2") verifyLinkContextMenuItems(genericURL.url) clickContextOpenLinkInPrivateTab() @@ -105,7 +105,7 @@ class ContextMenusTest { navigationToolbar { }.enterURLAndEnterToBrowser(pageLinks.url) { - // verifyPageContent(pageLinks.content) + mDevice.waitForIdle() longClickMatchingText("Link 3") verifyLinkContextMenuItems(genericURL.url) clickContextCopyLink() @@ -125,12 +125,13 @@ class ContextMenusTest { navigationToolbar { }.enterURLAndEnterToBrowser(pageLinks.url) { - // verifyPageContent(pageLinks.content) + mDevice.waitForIdle() longClickMatchingText("Link 1") verifyLinkContextMenuItems(genericURL.url) clickContextShareLink(genericURL.url) // verify share intent is matched with associated URL } } + @Ignore("Intermittent: https://github.com/mozilla-mobile/fenix/issues/12367") @Test fun verifyContextOpenImageNewTab() { @@ -141,7 +142,7 @@ class ContextMenusTest { navigationToolbar { }.enterURLAndEnterToBrowser(pageLinks.url) { - // verifyPageContent(pageLinks.content) + mDevice.waitForIdle() longClickMatchingText("test_link_image") verifyLinkImageContextMenuItems(imageResource.url) clickContextOpenImageNewTab() @@ -161,7 +162,7 @@ class ContextMenusTest { navigationToolbar { }.enterURLAndEnterToBrowser(pageLinks.url) { - // verifyPageContent(pageLinks.content) + mDevice.waitForIdle() longClickMatchingText("test_link_image") verifyLinkImageContextMenuItems(imageResource.url) clickContextCopyImageLocation() @@ -182,7 +183,7 @@ class ContextMenusTest { navigationToolbar { }.enterURLAndEnterToBrowser(pageLinks.url) { - // verifyPageContent(pageLinks.content) + mDevice.waitForIdle() longClickMatchingText("test_link_image") verifyLinkImageContextMenuItems(imageResource.url) clickContextSaveImage() @@ -209,7 +210,7 @@ class ContextMenusTest { navigationToolbar { }.enterURLAndEnterToBrowser(pageLinks.url) { - // verifyPageContent(pageLinks.content) + mDevice.waitForIdle() longClickMatchingText("Link 1") verifyLinkContextMenuItems(genericURL.url) dismissContentContextMenu(genericURL.url) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt new file mode 100644 index 000000000..f73a452d0 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt @@ -0,0 +1,147 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.ui + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +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.ui.robots.DeepLinkRobot + +/** + * Tests for verifying basic functionality of deep links + * - fenix://home + * - fenix://open + * - fenix://settings_notifications — take the user to the notification settings page + * - fenix://settings_privacy — take the user to the privacy settings page. + * - fenix://settings_search_engine — take the user to the search engine page, to set the default search engine. + * - fenix://home_collections — take the user to the home screen to see the list of collections. + * - fenix://urls_history — take the user to the history list. + * - fenix://urls_bookmarks — take the user to the bookmarks list + * - fenix://settings_logins — take the user to the settings page to do with logins (not the saved logins). + **/ +class DeepLinkTest { + private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + private lateinit var mockWebServer: MockWebServer + + private val robot = DeepLinkRobot() + + @get:Rule + val activityIntentTestRule = HomeActivityIntentTestRule() + + @Before + fun setUp() { + mockWebServer = MockWebServer().apply { + setDispatcher(AndroidAssetDispatcher()) + start() + } + } + + @After + fun tearDown() { + mockWebServer.shutdown() + } + + @Test + fun openHomeScreen() { + robot.openHomeScreen { + verifyHomeComponent() + } + robot.openSettings { /* move away from the home screen */ } + robot.openHomeScreen { + verifyHomeComponent() + } + } + + @Test + fun openURL() { + val genericURL = + TestAssetHelper.getGenericAsset(mockWebServer, 1) + robot.openURL(genericURL.url.toString()) { + verifyUrl(genericURL.url.toString()) + } + } + + @Test + fun openBookmarks() { + robot.openBookmarks { + // verify we can see headings. + verifyFolderTitle("Desktop Bookmarks") + } + } + + @Test + fun openHistory() { + robot.openHistory { + verifyHistoryMenuView() + } + } + + @Test + fun openCollections() { + robot.openHomeScreen { /* do nothing */ }.dismissOnboarding() + robot.openCollections { + verifyCollectionsHeader() + } + } + + @Test + fun openSettings() { + robot.openSettings { + verifyBasicsHeading() + verifyAdvancedHeading() + } + } + + @Test + fun openSettingsLogins() { + robot.openSettingsLogins { + verifyDefaultView() + verifyDefaultValueSyncLogins() + verifyDefaultValueAutofillLogins() + } + } + + @Test + fun openSettingsPrivacy() { + robot.openSettingsPrivacy { + verifyPrivacyHeading() + } + } + + @Test + fun openSettingsTrackingProtection() { + robot.openSettingsTrackingProtection { + verifyEnhancedTrackingProtectionHeader() + } + } + + @Test + fun openSettingsSearchEngine() { + robot.openSettingsSearchEngine { + verifyDefaultSearchEngineHeader() + } + } + + @Test + fun openSettingsNotifications() { + robot.openSettingsNotification { + verifyNotifications() + } + } + + @Test + fun openMakeDefaultBrowser() { + robot.openMakeDefaultBrowser { + verifyMakeDefaultBrowser() + } + } +} 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 225cf62a6..45de4045a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt @@ -82,7 +82,7 @@ class DownloadTest { navigationToolbar { }.openNewTabAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() clickLinkMatchingText(defaultWebPage.content) } @@ -100,7 +100,7 @@ class DownloadTest { navigationToolbar { }.openNewTabAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() clickLinkMatchingText(defaultWebPage.content) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt index 30ab196ef..248584e3e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt @@ -19,6 +19,7 @@ import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem import org.mozilla.fenix.ui.robots.historyMenu import org.mozilla.fenix.ui.robots.homeScreen +import org.mozilla.fenix.ui.robots.mDevice import org.mozilla.fenix.ui.robots.multipleSelectionToolbar import org.mozilla.fenix.ui.robots.navigationToolbar @@ -70,7 +71,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { verifyHistoryMenuView() @@ -86,7 +87,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { }.openThreeDotMenu { @@ -101,7 +102,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { }.openThreeDotMenu { @@ -119,12 +120,12 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { }.openThreeDotMenu { }.clickOpenInNormalTab { - // verifyPageContent(firstWebPage.content) + verifyUrl(firstWebPage.url.toString()) }.openTabDrawer { verifyNormalModeSelected() } @@ -136,12 +137,12 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { }.openThreeDotMenu { }.clickOpenInPrivateTab { - // verifyPageContent(firstWebPage.content) + verifyUrl(firstWebPage.url.toString()) }.openTabDrawer { verifyPrivateModeSelected() } @@ -153,7 +154,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { }.openThreeDotMenu { @@ -168,7 +169,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { clickDeleteHistoryButton() @@ -184,7 +185,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { longTapSelectItem(firstWebPage.url) @@ -206,7 +207,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openTabDrawer { closeTab() }.openHomeScreen { }.openThreeDotMenu { @@ -228,7 +229,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { longTapSelectItem(firstWebPage.url) @@ -249,12 +250,11 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") }.openTabDrawer { }.openHomeScreen { } navigationToolbar { }.enterURLAndEnterToBrowser(secondWebPage.url) { - // verifyPageContent("Page content: 2") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { longTapSelectItem(firstWebPage.url) @@ -277,7 +277,7 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - // verifyPageContent("Page content: 1") + mDevice.waitForIdle() }.openThreeDotMenu { }.openHistory { longTapSelectItem(firstWebPage.url) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt index 2a9957aa7..1ac67e6e5 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt @@ -58,10 +58,8 @@ class MediaNotificationTest { navigationToolbar { }.enterURLAndEnterToBrowser(videoTestPage.url) { - // verifyPageContent(videoTestPage.content) + mDevice.waitForIdle() clickMediaPlayerPlayButton() - waitForPlaybackToStart() - // verifyPageContent("Media file is playing") }.openNotificationShade { verifySystemNotificationExists(videoTestPage.title) clickMediaSystemNotificationControlButton("Pause") @@ -92,7 +90,7 @@ class MediaNotificationTest { navigationToolbar { }.enterURLAndEnterToBrowser(audioTestPage.url) { - // verifyPageContent(audioTestPage.content) + mDevice.waitForIdle() clickMediaPlayerPlayButton() waitForPlaybackToStart() }.openNotificationShade { @@ -125,10 +123,9 @@ class MediaNotificationTest { navigationToolbar { }.enterURLAndEnterToBrowser(audioTestPage.url) { - // verifyPageContent(audioTestPage.content) + mDevice.waitForIdle() clickMediaPlayerPlayButton() waitForPlaybackToStart() - // verifyPageContent("Media file is playing") }.openTabDrawer { verifyTabMediaControlButtonState("Pause") clickTabMediaControlButton() @@ -146,10 +143,9 @@ class MediaNotificationTest { navigationToolbar { }.enterURLAndEnterToBrowser(audioTestPage.url) { - // verifyPageContent(audioTestPage.content) + mDevice.waitForIdle() clickMediaPlayerPlayButton() waitForPlaybackToStart() - // verifyPageContent("Media file is playing") }.openNotificationShade { verifySystemNotificationExists("A site is playing media") clickMediaSystemNotificationControlButton("Pause") 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 64b0c5511..41b623d73 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -55,18 +55,12 @@ class NavigationToolbarTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() }.openNavigationToolbar { }.enterURLAndEnterToBrowser(nextWebPage.url) { - // verifyPageContent(nextWebPage.content) - } - - // Re-open the three-dot menu for verification - navigationToolbar { - }.openThreeDotMenu { - verifyThreeDotMenuExists() - }.goBack { - // verifyPageContent(defaultWebPage.content) + verifyUrl(nextWebPage.url.toString()) + mDevice.pressBack() + verifyUrl(defaultWebPage.url.toString()) } } @@ -77,12 +71,14 @@ class NavigationToolbarTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() }.openNavigationToolbar { }.enterURLAndEnterToBrowser(nextWebPage.url) { - // verifyPageContent(nextWebPage.content) + mDevice.waitForIdle() + verifyUrl(nextWebPage.url.toString()) mDevice.pressBack() - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() + verifyUrl(defaultWebPage.url.toString()) } // Re-open the three-dot menu for verification @@ -91,7 +87,7 @@ class NavigationToolbarTest { verifyThreeDotMenuExists() verifyForwardButton() }.goForward { - // verifyPageContent(nextWebPage.content) + verifyUrl(nextWebPage.url.toString()) } } @@ -101,7 +97,7 @@ class NavigationToolbarTest { navigationToolbar { }.enterURLAndEnterToBrowser(refreshWebPage.url) { - // verifyPageContent("DEFAULT") + mDevice.waitForIdle() } // Use refresh from the three-dot menu @@ -110,7 +106,7 @@ class NavigationToolbarTest { verifyThreeDotMenuExists() verifyRefreshButton() }.refreshPage { - // verifyPageContent("REFRESHED") + verifyPageContent("REFRESHED") } } @@ -120,7 +116,7 @@ class NavigationToolbarTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + verifyUrl(defaultWebPage.url.toString()) } } @@ -131,7 +127,7 @@ class NavigationToolbarTest { navigationToolbar { }.enterURLAndEnterToBrowser(loremIpsumWebPage.url) { - // verifyPageContent(loremIpsumWebPage.content) + mDevice.waitForIdle() } navigationToolbar { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt index 8590a8091..4e08cbc41 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt @@ -19,6 +19,7 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.ui.robots.mDevice /** * Tests for verifying basic functionality of content context menus @@ -70,7 +71,7 @@ class ReaderViewTest { navigationToolbar { }.enterURLAndEnterToBrowser(readerViewPage.url) { - // verifyPageContent(readerViewPage.content) + mDevice.waitForIdle() } IdlingRegistry.getInstance().register(readerViewNotificationDot) @@ -99,7 +100,7 @@ class ReaderViewTest { navigationToolbar { }.enterURLAndEnterToBrowser(genericPage.url) { - // verifyPageContent(genericPage.content) + mDevice.waitForIdle() } readerViewRobot { @@ -120,7 +121,7 @@ class ReaderViewTest { navigationToolbar { }.enterURLAndEnterToBrowser(readerViewPage.url) { - // verifyPageContent(readerViewPage.content) + mDevice.waitForIdle() } IdlingRegistry.getInstance().register(readerViewNotificationDot) @@ -152,7 +153,7 @@ class ReaderViewTest { navigationToolbar { }.enterURLAndEnterToBrowser(readerViewPage.url) { - // verifyPageContent(readerViewPage.content) + mDevice.waitForIdle() } IdlingRegistry.getInstance().register(readerViewNotificationDot) @@ -187,7 +188,7 @@ class ReaderViewTest { navigationToolbar { }.enterURLAndEnterToBrowser(readerViewPage.url) { - // verifyPageContent(readerViewPage.content) + mDevice.waitForIdle() } IdlingRegistry.getInstance().register(readerViewNotificationDot) @@ -222,7 +223,7 @@ class ReaderViewTest { navigationToolbar { }.enterURLAndEnterToBrowser(readerViewPage.url) { - // verifyPageContent(readerViewPage.content) + mDevice.waitForIdle() } IdlingRegistry.getInstance().register(readerViewNotificationDot) @@ -263,7 +264,7 @@ class ReaderViewTest { navigationToolbar { }.enterURLAndEnterToBrowser(readerViewPage.url) { - // verifyPageContent(readerViewPage.content) + mDevice.waitForIdle() } IdlingRegistry.getInstance().register(readerViewNotificationDot) 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 6a0e76feb..ee20a2c55 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt @@ -133,27 +133,24 @@ class SettingsBasicsTest { homeScreen { }.openNavigationToolbar { }.enterURLAndEnterToBrowser(page1.url) { - // verifyPageContent(page1.content) }.openThreeDotMenu { clickAddBookmarkButton() } navigationToolbar { }.enterURLAndEnterToBrowser(page2.url) { - // verifyPageContent(page2.content) }.openThreeDotMenu { clickAddBookmarkButton() } navigationToolbar { }.enterURLAndEnterToBrowser(page3.url) { - // verifyPageContent(page3.content) + mDevice.waitForIdle() } navigationToolbar { verifyNoHistoryBookmarks() } - } @Test diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt index fdefa2e56..f8f942035 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt @@ -50,7 +50,7 @@ class ShareButtonTest { // - Visit a URL, wait until it's loaded navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() } // From the 3-dot menu next to the Select share menu 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 9265b1b8c..b7d7519ea 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -50,12 +50,10 @@ class SmokeTest { homeScreen { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() verifyNavURLBarItems() }.openNavigationToolbar { }.goBackToWebsite { - // Check disabled due to intermittent failures - // verifyPageContent(defaultWebPage.content) }.openTabDrawer { verifyExistingTabList() }.openHomeScreen { 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 d7c17e76b..19178cce3 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt @@ -66,7 +66,7 @@ class TabbedBrowsingTest { navigationToolbar { }.openNewTabAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() verifyTabCounter("1") }.openTabDrawer { verifyExistingTabList() @@ -92,7 +92,7 @@ class TabbedBrowsingTest { navigationToolbar { }.openNewTabAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) + mDevice.waitForIdle() verifyTabCounter("1") }.openTabDrawer { verifyExistingTabList() @@ -110,7 +110,6 @@ class TabbedBrowsingTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) }.openTabDrawer { verifyExistingTabList() }.openTabsListThreeDotMenu { @@ -128,7 +127,6 @@ class TabbedBrowsingTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) }.openTabDrawer { verifyPrivateModeSelected() verifyExistingTabList() @@ -149,7 +147,6 @@ class TabbedBrowsingTest { genericURLS.forEachIndexed { index, element -> navigationToolbar { }.openNewTabAndEnterToBrowser(element.url) { - // verifyPageContent(element.content) }.openTabDrawer { verifyExistingOpenTabs("Test_Page_${index + 1}") verifyCloseTabsButton("Test_Page_${index + 1}") @@ -182,7 +179,6 @@ class TabbedBrowsingTest { genericURLS.forEachIndexed { index, element -> navigationToolbar { }.openNewTabAndEnterToBrowser(element.url) { - // verifyPageContent(element.content) }.openTabDrawer { verifyExistingOpenTabs("Test_Page_${index + 1}") verifyCloseTabsButton("Test_Page_${index + 1}") 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 2081886f1..697e25ca6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt @@ -53,7 +53,6 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) }.openThreeDotMenu { verifyAddFirefoxHome() }.addToFirefoxHome { @@ -72,7 +71,6 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) }.openThreeDotMenu { verifyAddFirefoxHome() }.addToFirefoxHome { @@ -82,7 +80,6 @@ class TopSitesTest { verifyExistingTopSitesList() verifyExistingTopSitesTabs(defaultWebPageTitle) }.openTopSiteTabWithTitle(title = defaultWebPageTitle) { - // verifyPageContent(defaultWebPage.content) verifyUrl(defaultWebPage.url.toString().replace("http://", "")) }.openTabDrawer { }.openHomeScreen { @@ -103,7 +100,6 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) }.openThreeDotMenu { verifyAddFirefoxHome() }.addToFirefoxHome { @@ -126,7 +122,6 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { - // verifyPageContent(defaultWebPage.content) }.openThreeDotMenu { verifyAddFirefoxHome() }.addToFirefoxHome { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt index 63e102efb..070831d2c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt @@ -39,7 +39,6 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime -import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.ext.waitNotNull @@ -81,13 +80,11 @@ class BrowserRobot { /* Asserts that the text within DOM element with ID="testContent" has the given text, i.e. * document.querySelector('#testContent').innerText == expectedText * - * This function is not working at intended and needs a replacement. */ - /* fun verifyPageContent(expectedText: String) { - // val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - // mDevice.waitNotNull(Until.findObject(By.textContains(expectedText)), waitingTime) - }*/ + fun verifyPageContent(expectedText: String) { + assertTrue(mDevice.findObject(UiSelector().text(expectedText)).waitForExists(waitingTime)) + } fun verifyTabCounter(expectedText: String) { onView(withId(R.id.counter_text)) @@ -336,21 +333,13 @@ class BrowserRobot { } fun waitForPlaybackToStart() { - mDevice.waitNotNull( - hasObject( - text("Media file is playing") - ), waitingTimeShort - ) + val playStateMessage = mDevice.findObject(UiSelector().text("Media file is playing")) + assertTrue(playStateMessage.waitForExists(waitingTime)) } fun verifyMediaIsPaused() { - mDevice.waitNotNull( - hasObject( - text("Media file is paused") - ), waitingTimeShort - ) - - mDevice.findObject(UiSelector().text("Media file is paused")).exists() + val pausedStateMessage = mDevice.findObject(UiSelector().text("Media file is paused")) + assertTrue(pausedStateMessage.waitForExists(waitingTime)) } class Transition { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DeepLinkRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DeepLinkRobot.kt new file mode 100644 index 000000000..db4cb2903 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DeepLinkRobot.kt @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.ui.robots + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import androidx.test.platform.app.InstrumentationRegistry +import org.mozilla.fenix.BuildConfig.DEEP_LINK_SCHEME + +class DeepLinkRobot { + private fun openDeepLink(url: String) { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val intent = Intent().apply { + action = Intent.ACTION_VIEW + data = Uri.parse("$DEEP_LINK_SCHEME://$url") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + addCategory(Intent.CATEGORY_BROWSABLE) + } + try { + context.startActivity(intent) + } catch (ex: ActivityNotFoundException) { + intent.setPackage(null) + context.startActivity(intent) + } + } + + fun openURL(url: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + val deepLink = Uri.parse("open") + .buildUpon() + .appendQueryParameter("url", url) + .build() + .toString() + openDeepLink(deepLink) + return browserScreen(interact) + } + + fun openHomeScreen(interact: HomeScreenRobot.() -> Unit) = + openDeepLink("home").run { homeScreen(interact) } + + fun openBookmarks(interact: BookmarksRobot.() -> Unit) = + openDeepLink("urls_bookmarks").run { bookmarksMenu(interact) } + + fun openHistory(interact: HistoryRobot.() -> Unit) = + openDeepLink("urls_history").run { historyMenu(interact) } + + fun openCollections(interact: HomeScreenRobot.() -> Unit) = + openDeepLink("home_collections").run { homeScreen(interact) } + + fun openSettings(interact: SettingsRobot.() -> Unit) = + openDeepLink("settings").run { settings(interact) } + + fun openSettingsPrivacy(interact: SettingsRobot.() -> Unit) = + openDeepLink("settings_privacy").run { settings(interact) } + + fun openSettingsLogins(interact: SettingsSubMenuLoginsAndPasswordRobot.() -> Unit) = + openDeepLink("settings_logins").run { settingsSubMenuLoginsAndPassword(interact) } + + fun openSettingsTrackingProtection(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit) = + openDeepLink("settings_tracking_protection").run { + settingsSubMenuEnhancedTrackingProtection(interact) + } + + fun openSettingsSearchEngine(interact: SettingsSubMenuSearchRobot.() -> Unit) = + openDeepLink("settings_search_engine").run { + SettingsSubMenuSearchRobot().interact() + SettingsSubMenuSearchRobot.Transition() + } + + fun openSettingsNotification(interact: SystemSettingsRobot.() -> Unit) = + openDeepLink("settings_notifications").run { systemSettings(interact) } + + fun openMakeDefaultBrowser(interact: SystemSettingsRobot.() -> Unit) = + openDeepLink("make_default_browser").run { systemSettings(interact) } +} + +private fun settings(interact: SettingsRobot.() -> Unit) = + SettingsRobot().interact().run { SettingsRobot.Transition() } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt new file mode 100644 index 000000000..bd3847a8a --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.ui.robots + +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction + +class SystemSettingsRobot { + fun verifyNotifications() { + Intents.intended(hasAction("android.settings.APP_NOTIFICATION_SETTINGS")) + } + + fun verifyMakeDefaultBrowser() { + Intents.intended(hasAction(SettingsRobot.DEFAULT_APPS_SETTINGS_ACTION)) + } + + class Transition { + // Difficult to know where this will go + } +} + +fun systemSettings(interact: SystemSettingsRobot.() -> Unit): SystemSettingsRobot.Transition { + SystemSettingsRobot().interact() + return SystemSettingsRobot.Transition() +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 005810abe..a0869d63d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,24 +79,42 @@ + + android:host="home_collections"/> + android:host="install_search_widget"/> - - - + android:host="make_default_browser"/> + android:host="settings"/> + + + + + + + + + + + diff --git a/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt b/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt index eca6187c5..85057009a 100644 --- a/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt +++ b/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt @@ -22,10 +22,13 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor { hasUserGesture: Boolean, isSameDomain: Boolean, isRedirect: Boolean, - isDirectNavigation: Boolean + isDirectNavigation: Boolean, + isSubframeRequest: Boolean ): RequestInterceptor.InterceptionResponse? { return context.components.services.appLinksInterceptor - .onLoadRequest(engineSession, uri, hasUserGesture, isSameDomain, isRedirect, isDirectNavigation) + .onLoadRequest( + engineSession, uri, hasUserGesture, isSameDomain, isRedirect, isDirectNavigation, isSubframeRequest + ) } override fun onErrorRequest( diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 0d9a3b2c0..6089fc991 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -13,7 +13,6 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.getSystemService import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.launch @@ -67,7 +66,6 @@ open class FenixApplication : LocaleAwareApplication() { var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null private set - @ExperimentalCoroutinesApi override fun onCreate() { super.onCreate() @@ -116,7 +114,6 @@ open class FenixApplication : LocaleAwareApplication() { Log.addSink(AndroidLogSink()) } - @ExperimentalCoroutinesApi @CallSuper open fun setupInMainProcessOnly() { run { diff --git a/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt b/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt index 86987b08e..eb64e044b 100644 --- a/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt +++ b/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix import androidx.navigation.NavDirections +import mozilla.appservices.places.BookmarkRoot /** * Used with [HomeActivity] global navigation to indicate which fragment is being opened. @@ -14,6 +15,14 @@ import androidx.navigation.NavDirections */ enum class GlobalDirections(val navDirections: NavDirections, val destinationId: Int) { Home(NavGraphDirections.actionGlobalHome(), R.id.homeFragment), + Bookmarks( + NavGraphDirections.actionGlobalBookmarkFragment(BookmarkRoot.Root.id), + R.id.bookmarkFragment + ), + History( + NavGraphDirections.actionGlobalHistoryFragment(), + R.id.historyFragment + ), Settings( NavGraphDirections.actionGlobalSettingsFragment(), R.id.settingsFragment @@ -37,5 +46,13 @@ enum class GlobalDirections(val navDirections: NavDirections, val destinationId: SettingsAddonManager( NavGraphDirections.actionGlobalSettingsAddonsManagementFragment(), R.id.addonsManagementFragment + ), + SettingsLogins( + NavGraphDirections.actionGlobalSavedLoginsAuthFragment(), + R.id.saveLoginSettingFragment + ), + SettingsTrackingProtection( + NavGraphDirections.actionGlobalTrackingProtectionFragment(), + R.id.trackingProtectionFragment ) } diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 9f77509b1..ca06e6964 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -29,6 +29,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.Session @@ -104,6 +105,7 @@ import org.mozilla.fenix.utils.RunWhenReadyQueue * - home screen * - browser screen */ +@OptIn(ExperimentalCoroutinesApi::class) @SuppressWarnings("TooManyFunctions", "LargeClass") open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { 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 ce2a4188a..e6bc5e57b 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -158,7 +158,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) onPositiveButtonClicked = onPositiveButtonClicked ) dialog.show(parentFragmentManager, PERMISSIONS_DIALOG_FRAGMENT_TAG) - requireContext().components.analytics.metrics.track(Event.AddonInstalled(addon.id)) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index b93e78517..719adfd39 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -4,9 +4,9 @@ package org.mozilla.fenix.components -import android.app.Application import android.content.Context import android.content.Intent +import androidx.core.content.getSystemService import androidx.core.net.toUri import mozilla.components.feature.addons.AddonManager import mozilla.components.feature.addons.amo.AddonCollectionProvider @@ -106,5 +106,5 @@ class Components(private val context: Context) { val migrationStore by lazy { MigrationStore() } val performance by lazy { PerformanceComponent() } val push by lazy { Push(context, analytics.crashReporter) } - val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context as Application) } + val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) } } 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 752de367b..d3ec37d3a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -50,7 +50,6 @@ import org.mozilla.fenix.BuildConfig.DIGITAL_ASSET_LINKS_TOKEN import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.ReleaseChannel import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings @@ -97,11 +96,7 @@ class Core(private val context: Context) { * disabled in Fenix Release builds for now. * This is consistent with both Fennec and Firefox Desktop. */ - val shouldEnableWebcompatReporter = Config.channel !in setOf( - ReleaseChannel.FenixProduction, - ReleaseChannel.FennecProduction - ) - if (shouldEnableWebcompatReporter) { + if (Config.channel.isNightlyOrDebug || Config.channel.isBeta) { WebCompatReporterFeature.install(it) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt index d3aa0c8c3..fd00fd9ef 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt @@ -102,6 +102,15 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ val installedApps = MozillaProductDetector.getInstalledMozillaProducts(application) + val trackingProtection = application.settings().run { + when { + !shouldUseTrackingProtection -> "none" + useStandardTrackingProtection -> "standard" + useStrictTrackingProtection -> "strict" + else -> "custom" + } + } + Leanplum.start(application, hashMapOf( "default_browser" to MozillaProductDetector.getMozillaBrowserDefault(application).orEmpty(), "fennec_installed" to installedApps.contains(MozillaProducts.FIREFOX.productName), @@ -110,6 +119,8 @@ class LeanplumMetricsService(private val application: Application) : MetricsServ "fxa_signed_in" to application.settings().fxaSignedIn, "fxa_has_synced_items" to application.settings().fxaHasSyncedItems, "search_widget_installed" to application.settings().searchWidgetInstalled, + "tracking_protection_enabled" to application.settings().shouldUseTrackingProtection, + "tracking_protection_setting" to trackingProtection, "fenix" to true )) diff --git a/app/src/main/java/org/mozilla/fenix/components/searchengine/FenixSearchEngineProvider.kt b/app/src/main/java/org/mozilla/fenix/components/searchengine/FenixSearchEngineProvider.kt index 8e1c28ed8..149620a54 100644 --- a/app/src/main/java/org/mozilla/fenix/components/searchengine/FenixSearchEngineProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/components/searchengine/FenixSearchEngineProvider.kt @@ -7,7 +7,6 @@ package org.mozilla.fenix.components.searchengine import android.content.Context import androidx.annotation.VisibleForTesting import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async @@ -33,14 +32,16 @@ import java.util.Locale open class FenixSearchEngineProvider( private val context: Context ) : SearchEngineProvider, CoroutineScope by CoroutineScope(Job() + Dispatchers.IO) { - private val locationService: LocationService = if (Config.channel.isDebug) { - LocationService.dummy() - } else { - MozillaLocationService( - context, - context.components.core.client, - BuildConfig.MLS_TOKEN - ) + private val locationService = with(MozillaLocationService( + context, + context.components.core.client, + BuildConfig.MLS_TOKEN + )) { + if (Config.channel.isDebug || !this.hasRegionCached()) { + LocationService.dummy() + } else { + this + } } // We have two search engine types: one based on MLS reported region, one based only on Locale. @@ -93,17 +94,6 @@ open class FenixSearchEngineProvider( private var loadedSearchEngines = refreshAsync() - // https://github.com/mozilla-mobile/fenix/issues/9935 - // Create new getter that will return the fallback SearchEngineList if - // the main one hasn't completed yet - private val searchEngines: Deferred - get() = - if (isRegionCachedByLocationService) { - loadedSearchEngines - } else { - fallbackEngines - } - fun getDefaultEngine(context: Context): SearchEngine { val engines = installedSearchEngines(context) val selectedName = context.settings().defaultSearchEngineName @@ -117,7 +107,7 @@ open class FenixSearchEngineProvider( */ fun installedSearchEngines(context: Context): SearchEngineList = runBlocking { val installedIdentifiers = installedSearchEngineIdentifiers(context) - val engineList = searchEngines.await() + val engineList = loadedSearchEngines.await() engineList.copy( list = engineList.list.filter { @@ -188,7 +178,11 @@ open class FenixSearchEngineProvider( } private fun refreshAsync() = async { - val engineList = baseSearchEngines.await() + val engineList = if (isRegionCachedByLocationService) { + baseSearchEngines.await() + } else { + fallbackEngines.await() + } val bundledList = bundledSearchEngines.await().list val customList = customSearchEngines.await().list 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 f0a90094a..56744f49e 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/String.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/String.kt @@ -126,7 +126,6 @@ suspend fun String.toRoundedDrawable(context: Context, client: Client) = bitmapF } suspend fun bitmapForUrl(url: String, client: Client): Bitmap? = withContext(Dispatchers.IO) { - // TODO cache this image, see https://github.com/mozilla-mobile/fenix/issues/9531 // Code below will cache it in Gecko's cache, which ensures that as long as we've fetched it once, // we will be able to display this avatar as long as the cache isn't purged (e.g. via 'clear user data'). val body = try { diff --git a/app/src/main/java/org/mozilla/fenix/ext/TextView.kt b/app/src/main/java/org/mozilla/fenix/ext/TextView.kt new file mode 100644 index 000000000..262f3ce7e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/ext/TextView.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.ext + +import android.text.style.UnderlineSpan +import android.widget.TextView +import androidx.core.text.toSpannable + +/** + * Adds an underline effect to the text displayed in the TextView. + */ +fun TextView.addUnderline() { + val currentText = text + text = currentText.toSpannable().apply { + setSpan(UnderlineSpan(), 0, currentText.length, 0) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessor.kt index cb1a1922e..740307cc4 100644 --- a/app/src/main/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessor.kt @@ -4,16 +4,19 @@ package org.mozilla.fenix.home.intent +import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.os.Build.VERSION.SDK_INT -import android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS +import android.provider.Settings import androidx.navigation.NavController import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.GlobalDirections import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.SearchWidgetCreator import org.mozilla.fenix.ext.alreadyOnDestination /** @@ -24,7 +27,7 @@ class DeepLinkIntentProcessor( ) : HomeIntentProcessor { override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { - val scheme = intent.scheme?.contains("fenix") ?: return false + val scheme = intent.scheme?.equals(BuildConfig.DEEP_LINK_SCHEME, ignoreCase = true) ?: return false return if (scheme) { intent.data?.let { handleDeepLink(it, navController) } true @@ -39,12 +42,22 @@ class DeepLinkIntentProcessor( val globalDirections = when (deepLink.host) { "home", "enable_private_browsing" -> GlobalDirections.Home + "urls_bookmarks" -> GlobalDirections.Bookmarks + "urls_history" -> GlobalDirections.History "settings" -> GlobalDirections.Settings "turn_on_sync" -> GlobalDirections.Sync "settings_search_engine" -> GlobalDirections.SearchEngine "settings_accessibility" -> GlobalDirections.Accessibility "settings_delete_browsing_data" -> GlobalDirections.DeleteData "settings_addon_manager" -> GlobalDirections.SettingsAddonManager + "settings_logins" -> GlobalDirections.SettingsLogins + "settings_tracking_protection" -> GlobalDirections.SettingsTrackingProtection + // We'd like to highlight views within the fragment + // https://github.com/mozilla-mobile/fenix/issues/11856 + // The current version of UI has these features in more complex screens. + "settings_privacy" -> GlobalDirections.Settings + "home_collections" -> GlobalDirections.Home + else -> return } @@ -63,7 +76,7 @@ class DeepLinkIntentProcessor( } "make_default_browser" -> { if (SDK_INT >= Build.VERSION_CODES.N) { - val settingsIntent = Intent(ACTION_MANAGE_DEFAULT_APPS_SETTINGS) + val settingsIntent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) activity.startActivity(settingsIntent) } } @@ -76,6 +89,38 @@ class DeepLinkIntentProcessor( ) } } + "settings_notifications" -> { + val intent = notificationSettings(activity) + activity.startActivity(intent) + } + "install_search_widget" -> { + if (SDK_INT >= Build.VERSION_CODES.O) { + SearchWidgetCreator.createSearchWidget(activity) + } + } } } + + private fun notificationSettings(context: Context, channel: String? = null) = + Intent().apply { + when { + SDK_INT >= Build.VERSION_CODES.O -> { + action = channel?.let { + putExtra(Settings.EXTRA_CHANNEL_ID, it) + Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS + } ?: Settings.ACTION_APP_NOTIFICATION_SETTINGS + putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + } + SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> { + action = "android.settings.APP_NOTIFICATION_SETTINGS" + putExtra("app_package", context.packageName) + putExtra("app_uid", context.applicationInfo.uid) + } + else -> { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + addCategory(Intent.CATEGORY_DEFAULT) + data = Uri.parse("package:" + context.packageName) + } + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index d24b62662..7f1a1450d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -9,15 +9,14 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.android.extensions.LayoutContainer -import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import org.mozilla.fenix.R +import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.home.HomeFragmentState import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.OnboardingState -import org.mozilla.fenix.components.tips.Tip // This method got a little complex with the addition of the tab tray feature flag // When we remove the tabs from the home screen this will get much simpler again. @@ -110,7 +109,6 @@ private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapI AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex) } -@ExperimentalCoroutinesApi class SessionControlView( override val containerView: View?, interactor: SessionControlInteractor, diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolder.kt index 31a1facb1..4cc6a0d85 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolder.kt @@ -4,13 +4,12 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders -import android.text.SpannableString import android.text.method.LinkMovementMethod -import android.text.style.UnderlineSpan import android.view.View import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.private_browsing_description.view.* import org.mozilla.fenix.R +import org.mozilla.fenix.ext.addUnderline import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor class PrivateBrowsingDescriptionViewHolder( @@ -24,13 +23,9 @@ class PrivateBrowsingDescriptionViewHolder( view.private_session_description.text = resources.getString( R.string.private_browsing_placeholder_description_2, appName ) - val commonMythsText = view.private_session_common_myths.text.toString() - val textWithLink = SpannableString(commonMythsText).apply { - setSpan(UnderlineSpan(), 0, commonMythsText.length, 0) - } with(view.private_session_common_myths) { movementMethod = LinkMovementMethod.getInstance() - text = textWithLink + addUnderline() setOnClickListener { interactor.onPrivateBrowsingLearnMoreClicked() } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingIcon.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingIcon.kt index 5ea1b7446..62badd38a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingIcon.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingIcon.kt @@ -6,8 +6,8 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding import android.widget.TextView import androidx.annotation.DrawableRes -import androidx.appcompat.content.res.AppCompatResources import mozilla.components.support.ktx.android.content.getColorFromAttr +import mozilla.components.support.ktx.android.content.getDrawableWithTint import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelative import org.mozilla.fenix.R import org.mozilla.fenix.ext.setBounds @@ -16,9 +16,9 @@ import org.mozilla.fenix.ext.setBounds * Sets the drawableStart of a header in an onboarding card. */ fun TextView.setOnboardingIcon(@DrawableRes id: Int) { - val icon = AppCompatResources.getDrawable(context, id) - val size = context.resources.getDimensionPixelSize(R.dimen.onboarding_header_icon_height_width) - icon?.setBounds(size) - icon?.setTint(context.getColorFromAttr(R.attr.onboardingSelected)) + val icon = context.getDrawableWithTint(id, context.getColorFromAttr(R.attr.onboardingSelected))?.apply { + val size = context.resources.getDimensionPixelSize(R.dimen.onboarding_header_icon_height_width) + setBounds(size) + } putCompoundDrawablesRelative(start = icon) } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingManualSignInViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingManualSignInViewHolder.kt index 219553d49..e4fa6d5a3 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingManualSignInViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingManualSignInViewHolder.kt @@ -5,11 +5,11 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding import android.view.View -import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.navigation.Navigation import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.onboarding_manual_signin.view.* +import mozilla.components.support.ktx.android.content.getDrawableWithTint import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event @@ -34,8 +34,10 @@ class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(vie val appName = context.getString(R.string.app_name) headerText.text = context.getString(R.string.onboarding_firefox_account_header, appName) - val icon = AppCompatResources.getDrawable(context, R.drawable.ic_onboarding_firefox_accounts) - icon?.setTint(ContextCompat.getColor(context, R.color.white_color)) + val icon = context.getDrawableWithTint( + R.drawable.ic_onboarding_firefox_accounts, + ContextCompat.getColor(context, R.color.white_color) + ) headerText.putCompoundDrawablesRelativeWithIntrinsicBounds(start = icon) } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingWhatsNewViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingWhatsNewViewHolder.kt index b37181533..e6cd4567c 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingWhatsNewViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingWhatsNewViewHolder.kt @@ -4,13 +4,12 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding -import android.text.SpannableString -import android.text.style.UnderlineSpan import android.view.View import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.onboarding_whats_new.view.* import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.addUnderline import org.mozilla.fenix.ext.components import org.mozilla.fenix.home.sessioncontrol.OnboardingInteractor @@ -25,12 +24,7 @@ class OnboardingWhatsNewViewHolder( val appName = view.context.getString(R.string.app_name) view.description_text.text = view.context.getString(R.string.onboarding_whats_new_description, appName) - val getAnswersText = view.get_answers.text.toString() - val textWithLink = SpannableString(getAnswersText).apply { - setSpan(UnderlineSpan(), 0, getAnswersText.length, 0) - } - - view.get_answers.text = textWithLink + view.get_answers.addUnderline() view.get_answers.setOnClickListener { interactor.onWhatsNewGetAnswersClicked() view.context.components.analytics.metrics.track(Event.OnboardingWhatsNew) diff --git a/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt index a487f6655..1711ff071 100644 --- a/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt @@ -1,7 +1,5 @@ package org.mozilla.fenix.home.tips -import android.text.SpannableString -import android.text.style.UnderlineSpan import android.view.View import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.button_tip_item.view.* @@ -11,6 +9,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.TipType +import org.mozilla.fenix.ext.addUnderline import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor @@ -36,12 +35,7 @@ class ButtonTipViewHolder( if (tip.learnMoreURL == null) { tip_learn_more.visibility = View.GONE } else { - val learnMoreText = context.getString(R.string.search_suggestions_onboarding_learn_more_link) - val textWithLink = SpannableString(learnMoreText).apply { - setSpan(UnderlineSpan(), 0, learnMoreText.length, 0) - } - - tip_learn_more.text = textWithLink + tip_learn_more.addUnderline() tip_learn_more.setOnClickListener { (context as HomeActivity).openToBrowserAndLoad( diff --git a/app/src/main/java/org/mozilla/fenix/migration/MigrationTelemetryListener.kt b/app/src/main/java/org/mozilla/fenix/migration/MigrationTelemetryListener.kt index f792a63c1..1b5268272 100644 --- a/app/src/main/java/org/mozilla/fenix/migration/MigrationTelemetryListener.kt +++ b/app/src/main/java/org/mozilla/fenix/migration/MigrationTelemetryListener.kt @@ -2,8 +2,9 @@ * License, v. 2.0. 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 +package org.mozilla.fenix.migration +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.base.log.logger.Logger @@ -16,6 +17,8 @@ class MigrationTelemetryListener( private val metrics: MetricController, private val store: MigrationStore ) { + + @OptIn(ExperimentalCoroutinesApi::class) fun start() { // Observe for migration completed. store.flowScoped { flow -> diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupReportFullyDrawn.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupReportFullyDrawn.kt index 695359e2c..c30720fec 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/StartupReportFullyDrawn.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupReportFullyDrawn.kt @@ -7,8 +7,8 @@ package org.mozilla.fenix.perf import android.app.Activity import android.view.View import androidx.core.view.doOnPreDraw -import kotlinx.android.synthetic.main.activity_home.* import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R import org.mozilla.fenix.ext.reportFullyDrawnSafe import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSiteItemViewHolder import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.APP_LINK @@ -36,7 +36,7 @@ class StartupReportFullyDrawn { state is StartupState.Cold && state.destination == APP_LINK) { // Instrumenting the first frame drawn should be good enough for app link for now. isInstrumented = true - attachReportFullyDrawn(activity, activity.rootContainer) + attachReportFullyDrawn(activity, activity.findViewById(R.id.rootContainer)) } } @@ -59,7 +59,7 @@ class StartupReportFullyDrawn { } } - private fun attachReportFullyDrawn(activity: HomeActivity, view: View) { + private fun attachReportFullyDrawn(activity: Activity, view: View) { // For greater accuracy, we could add an onDrawListener instead of a preDrawListener but: // - single use onDrawListeners are not built-in and it's non-trivial to write one // - the difference in timing is minimal (< 7ms on Pixel 2) diff --git a/app/src/main/java/org/mozilla/fenix/search/telemetry/BaseSearchTelemetry.kt b/app/src/main/java/org/mozilla/fenix/search/telemetry/BaseSearchTelemetry.kt index 7768a6423..60712a43d 100644 --- a/app/src/main/java/org/mozilla/fenix/search/telemetry/BaseSearchTelemetry.kt +++ b/app/src/main/java/org/mozilla/fenix/search/telemetry/BaseSearchTelemetry.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.search.telemetry import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map @@ -85,6 +86,7 @@ abstract class BaseSearchTelemetry { internal fun getProviderForUrl(url: String): SearchProviderModel? = providerList.find { provider -> provider.regexp.containsMatchIn(url) } + @OptIn(ExperimentalCoroutinesApi::class) internal fun installWebExtension( engine: Engine, store: BrowserStore, diff --git a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt index 34ee23dc1..3efd85f70 100644 --- a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt @@ -15,10 +15,10 @@ import mozilla.components.concept.engine.Engine import mozilla.components.concept.storage.HistoryStorage import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature import mozilla.components.support.ktx.android.content.getColorFromAttr +import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R import org.mozilla.fenix.search.SearchFragmentState -import org.mozilla.fenix.theme.ThemeManager /** * Interface for the Toolbar Interactor. This interface is implemented by objects that want @@ -71,10 +71,9 @@ class ToolbarView( false } - background = - AppCompatResources.getDrawable( - context, ThemeManager.resolveAttribute(R.attr.foundation, context) - ) + background = AppCompatResources.getDrawable( + context, context.theme.resolveAttribute(R.attr.foundation) + ) edit.hint = context.getString(R.string.search_hint) diff --git a/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt index 43f532861..46f30d451 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt @@ -16,32 +16,44 @@ interface LocaleSettingsController { } class DefaultLocaleSettingsController( - private val context: Context, + private val activity: Activity, private val localeSettingsStore: LocaleSettingsStore ) : LocaleSettingsController { override fun handleLocaleSelected(locale: Locale) { if (localeSettingsStore.state.selectedLocale == locale && - !LocaleManager.isDefaultLocaleSelected(context)) { + !LocaleManager.isDefaultLocaleSelected(activity)) { return } localeSettingsStore.dispatch(LocaleSettingsAction.Select(locale)) - LocaleManager.setNewLocale(context, locale.toLanguageTag()) - LocaleManager.updateBaseConfiguration(context, locale) - (context as Activity).recreate() + LocaleManager.setNewLocale(activity, locale.toLanguageTag()) + LocaleManager.updateBaseConfiguration(activity, locale) + activity.recreate() } override fun handleDefaultLocaleSelected() { - if (LocaleManager.isDefaultLocaleSelected(context)) { + if (LocaleManager.isDefaultLocaleSelected(activity)) { return } localeSettingsStore.dispatch(LocaleSettingsAction.Select(localeSettingsStore.state.localeList[0])) - LocaleManager.resetToSystemDefault(context) - LocaleManager.updateBaseConfiguration(context, localeSettingsStore.state.localeList[0]) - (context as Activity).recreate() + LocaleManager.resetToSystemDefault(activity) + LocaleManager.updateBaseConfiguration(activity, localeSettingsStore.state.localeList[0]) + activity.recreate() } override fun handleSearchQueryTyped(query: String) { localeSettingsStore.dispatch(LocaleSettingsAction.Search(query)) } + + /** + * Update the locale for the configuration of the app context's resources + */ + @Suppress("Deprecation") + fun LocaleManager.updateBaseConfiguration(context: Context, locale: Locale) { + val resources = context.applicationContext.resources + val config = resources.configuration + config.setLocale(locale) + config.setLayoutDirection(locale) + resources.updateConfiguration(config, resources.displayMetrics) + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleAdapter.kt b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleAdapter.kt index 57648d87d..fcf980c8d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleAdapter.kt @@ -5,14 +5,9 @@ package org.mozilla.fenix.settings.advanced import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import androidx.annotation.VisibleForTesting -import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.locale_settings_item.view.* -import mozilla.components.support.locale.LocaleManager import org.mozilla.fenix.R import java.util.Locale @@ -95,67 +90,3 @@ class LocaleAdapter(private val interactor: LocaleSettingsViewInteractor) : DEFAULT, LOCALE; } } - -class LocaleViewHolder( - view: View, - selectedLocale: Locale, - private val interactor: LocaleSettingsViewInteractor -) : BaseLocaleViewHolder(view, selectedLocale) { - private val icon = view.locale_selected_icon - private val title = view.locale_title_text - private val subtitle = view.locale_subtitle_text - - override fun bind(locale: Locale) { - // capitalisation is done using the rules of the appropriate locale (endonym and exonym) - title.text = locale.getDisplayName(locale).capitalize(locale) - subtitle.text = locale.displayName.capitalize(Locale.getDefault()) - icon.isVisible = isCurrentLocaleSelected(locale, isDefault = false) - - itemView.setOnClickListener { - interactor.onLocaleSelected(locale) - } - } -} - -class SystemLocaleViewHolder( - view: View, - selectedLocale: Locale, - private val interactor: LocaleSettingsViewInteractor -) : BaseLocaleViewHolder(view, selectedLocale) { - private val icon = view.locale_selected_icon - private val title = view.locale_title_text - private val subtitle = view.locale_subtitle_text - - override fun bind(locale: Locale) { - title.text = itemView.context.getString(R.string.default_locale_text) - subtitle.visibility = View.GONE - icon.isVisible = isCurrentLocaleSelected(locale, isDefault = true) - itemView.setOnClickListener { - interactor.onDefaultLocaleSelected() - } - } -} - -abstract class BaseLocaleViewHolder( - view: View, - private val selectedLocale: Locale -) : RecyclerView.ViewHolder(view) { - - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - internal fun isCurrentLocaleSelected(locale: Locale, isDefault: Boolean): Boolean { - return if (isDefault) { - locale == selectedLocale && LocaleManager.isDefaultLocaleSelected(itemView.context) - } else { - locale == selectedLocale && !LocaleManager.isDefaultLocaleSelected(itemView.context) - } - } - - abstract fun bind(locale: Locale) -} - -/** - * Similar to Kotlin's capitalize with locale parameter, but that method is currently experimental - */ -private fun String.capitalize(locale: Locale): String { - return substring(0, 1).toUpperCase(locale) + substring(1) -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleManagerExtension.kt b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleManagerExtension.kt index 075ca3271..03de516e8 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleManagerExtension.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleManagerExtension.kt @@ -51,15 +51,3 @@ fun LocaleManager.getSelectedLocale( fun LocaleManager.isDefaultLocaleSelected(context: Context): Boolean { return getCurrentLocale(context) == null } - -/** - * Update the locale for the configuration of the app context's resources - */ -@Suppress("DEPRECATION") -fun LocaleManager.updateBaseConfiguration(context: Context, locale: Locale) { - val resources = context.applicationContext.resources - val config = resources.configuration - config.setLocale(locale) - config.setLayoutDirection(locale) - resources.updateConfiguration(config, resources.displayMetrics) -} diff --git a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleSettingsFragment.kt index ea5df963e..2696115b1 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleSettingsFragment.kt @@ -43,7 +43,7 @@ class LocaleSettingsFragment : Fragment() { store = getStore() interactor = LocaleSettingsInteractor( controller = DefaultLocaleSettingsController( - context = requireContext(), + activity = requireActivity(), localeSettingsStore = store ) ) diff --git a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleViewHolders.kt b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleViewHolders.kt new file mode 100644 index 000000000..cb140ce18 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleViewHolders.kt @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.advanced + +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.locale_settings_item.* +import mozilla.components.support.locale.LocaleManager +import org.mozilla.fenix.R +import org.mozilla.fenix.utils.view.ViewHolder +import java.util.Locale + +class LocaleViewHolder( + view: View, + selectedLocale: Locale, + private val interactor: LocaleSettingsViewInteractor +) : BaseLocaleViewHolder(view, selectedLocale) { + + override fun bind(locale: Locale) { + // capitalisation is done using the rules of the appropriate locale (endonym and exonym) + locale_title_text.text = locale.getDisplayName(locale).capitalize(locale) + locale_subtitle_text.text = locale.displayName.capitalize(Locale.getDefault()) + locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = false) + + itemView.setOnClickListener { + interactor.onLocaleSelected(locale) + } + } +} + +class SystemLocaleViewHolder( + view: View, + selectedLocale: Locale, + private val interactor: LocaleSettingsViewInteractor +) : BaseLocaleViewHolder(view, selectedLocale) { + + override fun bind(locale: Locale) { + locale_title_text.text = itemView.context.getString(R.string.default_locale_text) + locale_subtitle_text.visibility = View.GONE + locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = true) + itemView.setOnClickListener { + interactor.onDefaultLocaleSelected() + } + } +} + +abstract class BaseLocaleViewHolder( + view: View, + private val selectedLocale: Locale +) : ViewHolder(view) { + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + internal fun isCurrentLocaleSelected(locale: Locale, isDefault: Boolean): Boolean { + return if (isDefault) { + locale == selectedLocale && LocaleManager.isDefaultLocaleSelected(itemView.context) + } else { + locale == selectedLocale && !LocaleManager.isDefaultLocaleSelected(itemView.context) + } + } + + abstract fun bind(locale: Locale) +} + +/** + * Similar to Kotlin's capitalize with locale parameter, but that method is currently experimental + */ +private fun String.capitalize(locale: Locale): String { + return substring(0, 1).toUpperCase(locale) + substring(1) +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsFragmentStore.kt index dcb399bfc..9e10508fd 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsFragmentStore.kt @@ -54,6 +54,7 @@ sealed class LoginsAction : Action { data class UpdateLoginsList(val list: List) : LoginsAction() data class UpdateCurrentLogin(val item: SavedLogin) : LoginsAction() data class SortLogins(val sortingStrategy: SortingStrategy) : LoginsAction() + data class LoginSelected(val item: SavedLogin) : LoginsAction() } /** @@ -110,6 +111,13 @@ private fun savedLoginsStateReducer( state ) } + is LoginsAction.LoginSelected -> { + state.copy( + isLoading = true, + loginList = emptyList(), + filteredItems = emptyList() + ) + } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsListViewHolder.kt index 62183ae71..f7bd68faa 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsListViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/LoginsListViewHolder.kt @@ -34,7 +34,7 @@ class LoginsListViewHolder( updateFavIcon(item.origin) view.setOnClickListener { - interactor.itemClicked(item) + interactor.onItemClicked(item) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragment.kt index e76800c6f..cec4c1c29 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsFragment.kt @@ -37,12 +37,10 @@ import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider -import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.redirectToReAuth import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar -import org.mozilla.fenix.settings.SupportUtils @SuppressWarnings("TooManyFunctions") class SavedLoginsFragment : Fragment() { @@ -88,9 +86,14 @@ class SavedLoginsFragment : Fragment() { ) } val savedLoginsController: SavedLoginsController = - SavedLoginsController(savedLoginsStore, requireContext().settings()) - savedLoginsInteractor = - SavedLoginsInteractor(savedLoginsController, ::itemClicked, ::openLearnMore) + SavedLoginsController( + store = savedLoginsStore, + navController = findNavController(), + browserNavigator = ::openToBrowserAndLoad, + settings = requireContext().settings(), + metrics = requireContext().components.analytics.metrics + ) + savedLoginsInteractor = SavedLoginsInteractor(savedLoginsController) savedLoginsView = SavedLoginsView(view.savedLoginsLayout, savedLoginsInteractor) loadAndMapLogins() return view @@ -138,20 +141,8 @@ class SavedLoginsFragment : Fragment() { super.onPause() } - private fun itemClicked(item: SavedLogin) { - context?.components?.analytics?.metrics?.track(Event.OpenOneLogin) - val directions = - SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(item.guid) - findNavController().navigate(directions) - } - - private fun openLearnMore() { - (activity as HomeActivity).openToBrowserAndLoad( - searchTermOrURL = SupportUtils.getGenericSumoURLForTopic - (SupportUtils.SumoTopic.SYNC_SETUP), - newTab = true, - from = BrowserDirection.FromSavedLoginsFragment - ) + private fun openToBrowserAndLoad(searchTermOrURL: String, newTab: Boolean, from: BrowserDirection) { + (activity as HomeActivity).openToBrowserAndLoad(searchTermOrURL, newTab, from) } private fun loadAndMapLogins() { @@ -222,11 +213,15 @@ class SavedLoginsFragment : Fragment() { sortingStrategyMenu = SavedLoginsSortingStrategyMenu(requireContext(), itemToHighlight) { when (it) { SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> { - savedLoginsInteractor.sort(SortingStrategy.Alphabetically(requireContext().applicationContext)) + savedLoginsInteractor.onSortingStrategyChanged( + SortingStrategy.Alphabetically(requireContext().applicationContext) + ) } SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> { - savedLoginsInteractor.sort(SortingStrategy.LastUsed(requireContext().applicationContext)) + savedLoginsInteractor.onSortingStrategyChanged( + SortingStrategy.LastUsed(requireContext().applicationContext) + ) } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsView.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsView.kt index 4223fdbf6..dd1564df3 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SavedLoginsView.kt @@ -4,18 +4,21 @@ package org.mozilla.fenix.settings.logins -import android.text.SpannableString import android.text.method.LinkMovementMethod -import android.text.style.UnderlineSpan import android.view.LayoutInflater import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.view.isVisible +import androidx.navigation.NavController import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.component_saved_logins.view.* -import kotlinx.android.synthetic.main.component_saved_logins.view.progress_bar +import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.ext.addUnderline +import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.utils.Settings /** @@ -39,14 +42,10 @@ class SavedLoginsView( itemAnimator = null } - val learnMoreText = view.saved_passwords_empty_learn_more.text.toString() - val textWithLink = SpannableString(learnMoreText).apply { - setSpan(UnderlineSpan(), 0, learnMoreText.length, 0) - } with(view.saved_passwords_empty_learn_more) { movementMethod = LinkMovementMethod.getInstance() - text = textWithLink - setOnClickListener { interactor.onLearnMore() } + addUnderline() + setOnClickListener { interactor.onLearnMoreClicked() } } with(view.saved_passwords_empty_message) { @@ -60,6 +59,7 @@ class SavedLoginsView( } fun update(state: LoginsListState) { + // todo MVI views should not have logic. Needs refactoring. if (state.isLoading) { view.progress_bar.isVisible = true } else { @@ -73,29 +73,63 @@ class SavedLoginsView( /** * Interactor for the saved logins screen + * + * @param savedLoginsController [SavedLoginsController] which will be delegated for all users interactions. */ class SavedLoginsInteractor( - private val savedLoginsController: SavedLoginsController, - private val itemClicked: (SavedLogin) -> Unit, - private val learnMore: () -> Unit + private val savedLoginsController: SavedLoginsController ) { - fun itemClicked(item: SavedLogin) { - itemClicked.invoke(item) + fun onItemClicked(item: SavedLogin) { + savedLoginsController.handleItemClicked(item) } - fun onLearnMore() { - learnMore.invoke() + + fun onLearnMoreClicked() { + savedLoginsController.handleLearnMoreClicked() } - fun sort(sortingStrategy: SortingStrategy) { + + fun onSortingStrategyChanged(sortingStrategy: SortingStrategy) { savedLoginsController.handleSort(sortingStrategy) } } /** * Controller for the saved logins screen + * + * @param store Store used to hold in-memory collection state. + * @param navController NavController manages app navigation within a NavHost. + * @param browserNavigator Controller allowing browser navigation to any Uri. + * @param settings SharedPreferences wrapper for easier usage. + * @param metrics Controller that handles telemetry events. */ -class SavedLoginsController(val store: LoginsFragmentStore, val settings: Settings) { +class SavedLoginsController( + private val store: LoginsFragmentStore, + private val navController: NavController, + private val browserNavigator: ( + searchTermOrURL: String, + newTab: Boolean, + from: BrowserDirection + ) -> Unit, + private val settings: Settings, + private val metrics: MetricController +) { fun handleSort(sortingStrategy: SortingStrategy) { store.dispatch(LoginsAction.SortLogins(sortingStrategy)) settings.savedLoginsSortingStrategy = sortingStrategy } + + fun handleItemClicked(item: SavedLogin) { + store.dispatch(LoginsAction.LoginSelected(item)) + metrics.track(Event.OpenOneLogin) + navController.navigate( + SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(item.guid) + ) + } + + fun handleLearnMoreClicked() { + browserNavigator.invoke( + SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.SYNC_SETUP), + true, + BrowserDirection.FromSavedLoginsFragment + ) + } } diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt index 04459010c..b9a7068d6 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt @@ -12,6 +12,11 @@ import mozilla.components.concept.sync.OAuthAccount import mozilla.components.service.fxa.manager.FxaAccountManager import org.mozilla.fenix.ext.components +/** + * Starts and stops SyncedTabsStorage based on the authentication state. + * @param context Used to get synced tabs storage, due to cyclic dependency. + * @param accountManager Used to check and observe account authentication state. + */ class SyncedTabsIntegration( private val context: Context, private val accountManager: FxaAccountManager diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index ba3db15fb..c5bbf4d4e 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -10,6 +10,10 @@ import android.view.View import android.widget.FrameLayout import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.component_sync_tabs.view.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import mozilla.components.browser.storage.sync.SyncedDeviceTabs import mozilla.components.feature.syncedtabs.view.SyncedTabsView import org.mozilla.fenix.R @@ -23,6 +27,7 @@ class SyncedTabsLayout @JvmOverloads constructor( override var listener: SyncedTabsView.Listener? = null private val adapter = SyncedTabsAdapter { listener?.onTabClicked(it) } + private val coroutineScope = CoroutineScope(Dispatchers.Main) init { inflate(getContext(), R.layout.component_sync_tabs, this) @@ -34,42 +39,46 @@ class SyncedTabsLayout @JvmOverloads constructor( } override fun onError(error: SyncedTabsView.ErrorType) { - // We may still be displaying a "loading" spinner, hide it. - stopLoading() + coroutineScope.launch { + // We may still be displaying a "loading" spinner, hide it. + stopLoading() - val stringResId = when (error) { - SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device - SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing - SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account - SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth - SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs + val stringResId = when (error) { + SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device + SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing + SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account + SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth + SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs + } + + sync_tabs_status.text = context.getText(stringResId) + + synced_tabs_list.visibility = View.GONE + sync_tabs_status.visibility = View.VISIBLE + + synced_tabs_pull_to_refresh.isEnabled = pullToRefreshEnableState(error) } - - sync_tabs_status.text = context.getText(stringResId) - - synced_tabs_list.visibility = View.GONE - sync_tabs_status.visibility = View.VISIBLE - - synced_tabs_pull_to_refresh.isEnabled = pullToRefreshEnableState(error) } override fun displaySyncedTabs(syncedTabs: List) { - synced_tabs_list.visibility = View.VISIBLE - sync_tabs_status.visibility = View.GONE + coroutineScope.launch { + synced_tabs_list.visibility = View.VISIBLE + sync_tabs_status.visibility = View.GONE - val allDeviceTabs = emptyList().toMutableList() + val allDeviceTabs = emptyList().toMutableList() - syncedTabs.forEach { (device, tabs) -> - if (tabs.isEmpty()) { - return@forEach + syncedTabs.forEach { (device, tabs) -> + if (tabs.isEmpty()) { + return@forEach + } + + val deviceTabs = tabs.map { SyncedTabsAdapter.AdapterItem.Tab(it) } + + allDeviceTabs += listOf(SyncedTabsAdapter.AdapterItem.Device(device)) + deviceTabs } - val deviceTabs = tabs.map { SyncedTabsAdapter.AdapterItem.Tab(it) } - - allDeviceTabs += listOf(SyncedTabsAdapter.AdapterItem.Device(device)) + deviceTabs + adapter.submitList(allDeviceTabs) } - - adapter.submitList(allDeviceTabs) } override fun startLoading() { @@ -83,6 +92,11 @@ class SyncedTabsLayout @JvmOverloads constructor( synced_tabs_pull_to_refresh.isRefreshing = false } + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + coroutineScope.cancel() + } + companion object { internal fun pullToRefreshEnableState(error: SyncedTabsView.ErrorType) = when (error) { // Disable "pull-to-refresh" when we clearly can't sync tabs, and user needs to take an diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt index 240a83e8c..bcd494cf0 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt @@ -47,8 +47,8 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item private fun bindHeader(device: AdapterItem.Device) { val deviceLogoDrawable = when (device.device.deviceType) { - DeviceType.DESKTOP -> { R.drawable.mozac_ic_device_desktop } - else -> { R.drawable.mozac_ic_device_mobile } + DeviceType.DESKTOP -> R.drawable.mozac_ic_device_desktop + else -> R.drawable.mozac_ic_device_mobile } itemView.synced_tabs_group_name.text = device.device.displayName diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt index 7b1014df3..62f5cacf7 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -19,6 +19,7 @@ import kotlinx.android.synthetic.main.component_tabstray.view.* import kotlinx.android.synthetic.main.component_tabstray_fab.view.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.* +import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.selector.normalTabs @@ -110,6 +111,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { } } + @OptIn(ExperimentalCoroutinesApi::class) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt index e8ac2a019..af728d664 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt @@ -11,6 +11,7 @@ import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.AppCompatImageButton import androidx.core.content.ContextCompat +import androidx.core.view.doOnNextLayout import mozilla.components.browser.state.state.MediaState import mozilla.components.browser.tabstray.TabViewHolder import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView @@ -20,7 +21,7 @@ import mozilla.components.concept.tabstray.TabsTray import mozilla.components.feature.media.ext.pauseIfPlaying import mozilla.components.feature.media.ext.playIfPaused import mozilla.components.support.base.observer.Observable -import mozilla.components.support.images.ImageRequest +import mozilla.components.support.images.ext.loadIntoView import mozilla.components.support.images.loader.ImageLoader import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import org.mozilla.fenix.R @@ -72,7 +73,10 @@ class TabTrayViewHolder( if (tab.thumbnail != null) { thumbnailView.setImageBitmap(tab.thumbnail) } else { - imageLoader.loadIntoView(thumbnailView, ImageRequest(tab.id)) + // Make sure we have the view's dimensions so we can load the image at the correct size + thumbnailView.doOnNextLayout { + imageLoader.loadIntoView(thumbnailView, tab.id) + } } // Media state diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsView.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsView.kt index 090d9c303..fcc58f7ce 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsView.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsView.kt @@ -5,17 +5,16 @@ package org.mozilla.fenix.trackingprotectionexceptions import android.text.method.LinkMovementMethod -import android.text.style.UnderlineSpan import android.view.LayoutInflater import android.view.ViewGroup import android.widget.FrameLayout -import androidx.core.text.toSpannable import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.component_exceptions.* import mozilla.components.concept.engine.content.blocking.TrackingProtectionException import org.mozilla.fenix.R +import org.mozilla.fenix.ext.addUnderline /** * Interface for the ExceptionsViewInteractor. This interface is implemented by objects that want @@ -62,10 +61,7 @@ class ExceptionsView( } with(exceptions_learn_more) { - val learnMoreText = text - text = learnMoreText.toSpannable().apply { - setSpan(UnderlineSpan(), 0, learnMoreText.length, 0) - } + addUnderline() movementMethod = LinkMovementMethod.getInstance() setOnClickListener { interactor.onLearnMore() } diff --git a/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt b/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt index eec7d25e6..cee0a6a03 100644 --- a/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt @@ -18,29 +18,21 @@ import org.mozilla.fenix.utils.Settings class SitePermissionsWifiIntegration( private val settings: Settings, private val wifiConnectionMonitor: WifiConnectionMonitor -) : LifecycleAwareFeature { +) : LifecycleAwareFeature, WifiConnectionMonitor.Observer { /** * Adds listener for autoplay setting [AUTOPLAY_ALLOW_ON_WIFI]. Sets all autoplay to allowed when * WIFI is connected, blocked otherwise. */ - private val wifiConnectedListener: ((Boolean) -> Unit) by lazy { - { connected: Boolean -> - val setting = - if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED - if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { - settings.setSitePermissionsPhoneFeatureAction( - PhoneFeature.AUTOPLAY_AUDIBLE, - setting - ) - settings.setSitePermissionsPhoneFeatureAction( - PhoneFeature.AUTOPLAY_INAUDIBLE, - setting - ) - } else { - // The autoplay setting has changed, we can remove the listener - removeWifiConnectedListener() - } + override fun onWifiConnectionChanged(connected: Boolean) { + val setting = + if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED + if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { + settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_AUDIBLE, setting) + settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_INAUDIBLE, setting) + } else { + // The autoplay setting has changed, we can remove the listener + removeWifiConnectedListener() } } @@ -55,11 +47,11 @@ class SitePermissionsWifiIntegration( } fun addWifiConnectedListener() { - wifiConnectionMonitor.addOnWifiConnectedChangedListener(wifiConnectedListener) + wifiConnectionMonitor.register(this) } fun removeWifiConnectedListener() { - wifiConnectionMonitor.removeOnWifiConnectedChangedListener(wifiConnectedListener) + wifiConnectionMonitor.unregister(this) } // Until https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed, AUTOPLAY_ALLOW_ALL diff --git a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt index 34a6e5f7a..b8e0c2ee9 100644 --- a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt +++ b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt @@ -5,11 +5,12 @@ package org.mozilla.fenix.wifi import android.app.Application -import android.content.Context import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest +import mozilla.components.support.base.observer.Observable +import mozilla.components.support.base.observer.ObserverRegistry /** * Attaches itself to the [Application] and listens for WIFI available/not available events. This @@ -25,30 +26,28 @@ import android.net.NetworkRequest * app.components.wifiConnectionListener.start() * ``` */ -class WifiConnectionMonitor(app: Application) { - private val callbacks = mutableSetOf<(Boolean) -> Unit>() - private val connectivityManager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as - ConnectivityManager +class WifiConnectionMonitor( + private val connectivityManager: ConnectivityManager +) : Observable by ObserverRegistry() { - private var lastKnownStateWasAvailable: Boolean? = null + private var callbackReceived: Boolean = false private var isRegistered = false private val frameworkListener = object : ConnectivityManager.NetworkCallback() { override fun onLost(network: Network?) { - callbacks.forEach { it(false) } - lastKnownStateWasAvailable = false + notifyAtLeastOneObserver { onWifiConnectionChanged(connected = false) } + callbackReceived = true } override fun onAvailable(network: Network?) { - callbacks.forEach { it(true) } - lastKnownStateWasAvailable = true + notifyAtLeastOneObserver { onWifiConnectionChanged(connected = true) } + callbackReceived = true } } /** * Attaches the [WifiConnectionMonitor] to the application. After this has been called, callbacks - * added via [addOnWifiConnectedChangedListener] will be called until either the app exits, or - * [stop] is called. + * added via [register] will be called until either the app exits, or [stop] is called. * * Any existing callbacks will be called with the current state when this is called. */ @@ -62,10 +61,8 @@ class WifiConnectionMonitor(app: Application) { // AFAICT, the framework does not send an event when a new NetworkCallback is registered // while the WIFI is not connected, so we push this manually. If the WIFI is on, it will send // a follow up event shortly - val noCallbacksReceivedYet = lastKnownStateWasAvailable == null - if (noCallbacksReceivedYet) { - lastKnownStateWasAvailable = false - callbacks.forEach { it(false) } + if (!callbackReceived) { + notifyAtLeastOneObserver { onWifiConnectionChanged(connected = false) } } connectivityManager.registerNetworkCallback(request, frameworkListener) @@ -74,7 +71,7 @@ class WifiConnectionMonitor(app: Application) { /** * Detatches the [WifiConnectionMonitor] from the app. No callbacks added via - * [addOnWifiConnectedChangedListener] will be called after this has been called. + * [register] will be called after this has been called. */ fun stop() { // Framework code will throw if an unregistered listener attempts to unregister. @@ -83,25 +80,7 @@ class WifiConnectionMonitor(app: Application) { isRegistered = false } - /** - * Adds [onWifiChanged] to a list of listeners that will be called whenever WIFI connects or - * disconnects. - * - * If [onWifiChanged] is successfully added (i.e., it is a new listener), it will be immediately - * called with the last known state. - */ - fun addOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) { - val lastKnownState = lastKnownStateWasAvailable - if (callbacks.add(onWifiChanged) && lastKnownState != null) { - onWifiChanged(lastKnownState) - } - } - - /** - * Removes [onWifiChanged] from the list of listeners to be called whenever WIFI connects or - * disconnects. - */ - fun removeOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) { - callbacks.remove(onWifiChanged) + interface Observer { + fun onWifiConnectionChanged(connected: Boolean) } } diff --git a/app/src/main/res/layout/fragment_tracking_protection_blocking.xml b/app/src/main/res/layout/fragment_tracking_protection_blocking.xml index a746c009f..76a8852bd 100644 --- a/app/src/main/res/layout/fragment_tracking_protection_blocking.xml +++ b/app/src/main/res/layout/fragment_tracking_protection_blocking.xml @@ -17,8 +17,8 @@ android:fontFamily="@font/metropolis_bold" android:layout_width="wrap_content" android:layout_height="@dimen/tracking_protection_item_height" - android:layout_marginStart="72dp" - android:layout_marginEnd="16dp" + android:layout_marginStart="@dimen/top_bar_alignment_margin_start" + android:layout_marginEnd="@dimen/tracking_protection_item_margin_end" android:paddingStart="0dp" android:paddingEnd="0dp" android:text="@string/enhanced_tracking_protection_blocked" /> diff --git a/app/src/main/res/layout/layout_percentage_seek_bar.xml b/app/src/main/res/layout/layout_percentage_seek_bar.xml index bfd608953..558899134 100644 --- a/app/src/main/res/layout/layout_percentage_seek_bar.xml +++ b/app/src/main/res/layout/layout_percentage_seek_bar.xml @@ -10,9 +10,9 @@ android:baselineAligned="false" android:clipChildren="false" android:clipToPadding="false" - android:paddingStart="@dimen/radio_button_preference_horizontal" + android:layout_marginStart="@dimen/top_bar_alignment_margin_start" + android:layout_marginEnd="@dimen/radio_button_preference_horizontal" android:paddingTop="@dimen/preference_seek_bar_padding" - android:paddingEnd="@dimen/radio_button_preference_horizontal" android:paddingBottom="@dimen/preference_seek_bar_padding"> @@ -27,11 +28,10 @@ android:id="@android:id/summary" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingTop="6dp" android:textAppearance="?android:attr/textAppearanceSmall" + android:textAlignment="viewStart" android:textColor="?android:attr/textColorSecondary" app:layout_constraintTop_toBottomOf="@android:id/title" - tools:layout_editor_absoluteX="16dp" tools:text="Make text on websites larger or smaller" /> + android:layout_height="@dimen/locale_item_height"> + 56dp 16dp 12dp - 24dp + 16dp 68dp 48dp @@ -50,10 +50,14 @@ 12dp + 8dp + 72dp 28dp 48dp + 16dp + 18dp 64dp @@ -115,6 +119,7 @@ 72dp 16sp 12sp + 48dp 20dp @@ -156,6 +161,9 @@ 12dp 5dp 16dp + 4dp + 16dp + 48dp 48dp diff --git a/app/src/main/res/xml/accessibility_preferences.xml b/app/src/main/res/xml/accessibility_preferences.xml index baa17890f..125d2f147 100644 --- a/app/src/main/res/xml/accessibility_preferences.xml +++ b/app/src/main/res/xml/accessibility_preferences.xml @@ -8,8 +8,7 @@ android:defaultValue="true" android:key="@string/pref_key_accessibility_auto_size" android:summary="@string/preference_accessibility_auto_size_summary" - android:title="@string/preference_accessibility_auto_size_2" - app:iconSpaceReserved="false" /> + android:title="@string/preference_accessibility_auto_size_2" /> + app:allowDividerAbove="true" /> diff --git a/app/src/migration/java/org/mozilla/fenix/MigratingFenixApplication.kt b/app/src/migration/java/org/mozilla/fenix/MigratingFenixApplication.kt index f6de4e521..c903f5d3e 100644 --- a/app/src/migration/java/org/mozilla/fenix/MigratingFenixApplication.kt +++ b/app/src/migration/java/org/mozilla/fenix/MigratingFenixApplication.kt @@ -8,6 +8,7 @@ import android.content.Context import kotlinx.coroutines.runBlocking import mozilla.components.support.migration.FennecMigrator import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks +import org.mozilla.fenix.migration.MigrationTelemetryListener /** * An application class which knows how to migrate Fennec data. diff --git a/app/src/migration/java/org/mozilla/fenix/MigrationPushRenewer.kt b/app/src/migration/java/org/mozilla/fenix/MigrationPushRenewer.kt index 7cc5fc55c..1d8e1650e 100644 --- a/app/src/migration/java/org/mozilla/fenix/MigrationPushRenewer.kt +++ b/app/src/migration/java/org/mozilla/fenix/MigrationPushRenewer.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect import mozilla.components.concept.push.PushProcessor import mozilla.components.lib.state.ext.flowScoped @@ -18,6 +19,8 @@ class MigrationPushRenewer( private val service: PushProcessor?, private val store: MigrationStore ) { + + @OptIn(ExperimentalCoroutinesApi::class) fun start() { // Observe for migration completed. store.flowScoped { flow -> diff --git a/app/src/test/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactoryTest.kt b/app/src/test/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactoryTest.kt index b87d9cfd1..94f1ae5f2 100644 --- a/app/src/test/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactoryTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactoryTest.kt @@ -345,8 +345,7 @@ private fun EngineSession.TrackingProtectionPolicy.assertPolicyEquals( checkPrivacy: Boolean ) { assertEquals(this.cookiePolicy, actual.cookiePolicy) - // TODO Uncomment this assertion after the fix in AC#6079 lands -// assertEquals(this.strictSocialTrackingProtection, actual.strictSocialTrackingProtection) + assertEquals(this.strictSocialTrackingProtection, actual.strictSocialTrackingProtection) // E.g., atm, RECOMMENDED == AD + ANALYTICS + SOCIAL + TEST + MOZILLA_SOCIAL + CRYPTOMINING. // If all of these are set manually, the equality check should not fail if (this.trackingCategories.toInt() != actual.trackingCategories.toInt()) { diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt index d40da0e77..2a1cbd089 100644 --- a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt @@ -69,7 +69,7 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.home.Tab import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit -@ExperimentalCoroutinesApi +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(FenixRobolectricTestRunner::class) class DefaultBrowserToolbarControllerTest { diff --git a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt index c70bc2c91..2db0a9b94 100644 --- a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt @@ -9,18 +9,16 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.MainScope -import kotlinx.coroutines.newSingleThreadContext -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain +import kotlinx.coroutines.test.TestCoroutineDispatcher import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.Engine import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases -import org.junit.After +import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity @@ -35,10 +33,12 @@ import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.settings.SupportUtils import mozilla.components.feature.tab.collections.Tab as ComponentTab -@ExperimentalCoroutinesApi +@OptIn(ExperimentalCoroutinesApi::class) class DefaultSessionControlControllerTest { - private val mainThreadSurrogate = newSingleThreadContext("UI thread") + @get:Rule + val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher()) + private val activity: HomeActivity = mockk(relaxed = true) private val fragmentStore: HomeFragmentStore = mockk(relaxed = true) private val navController: NavController = mockk(relaxed = true) @@ -62,7 +62,6 @@ class DefaultSessionControlControllerTest { @Before fun setup() { - Dispatchers.setMain(mainThreadSurrogate) mockkStatic("org.mozilla.fenix.ext.ContextKt") every { activity.components.core.engine } returns engine every { activity.components.core.sessionManager } returns sessionManager @@ -91,12 +90,6 @@ class DefaultSessionControlControllerTest { ) } - @After - fun tearDown() { - Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher - mainThreadSurrogate.close() - } - @Test fun handleCollectionAddTabTapped() { val collection: TabCollection = mockk(relaxed = true) diff --git a/app/src/test/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessorTest.kt index 4acbc4365..4e34aa574 100644 --- a/app/src/test/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessorTest.kt @@ -8,17 +8,22 @@ import android.content.Intent import androidx.core.net.toUri import androidx.navigation.NavController import io.mockk.Called +import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject import io.mockk.verify +import mozilla.appservices.places.BookmarkRoot import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.BuildConfig.DEEP_LINK_SCHEME import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.SearchWidgetCreator import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @RunWith(FenixRobolectricTestRunner::class) @@ -48,7 +53,7 @@ class DeepLinkIntentProcessorTest { @Test fun `return true if scheme is fenix`() { - assertTrue(processor.process(testIntent("fenix://test"), navController, out)) + assertTrue(processor.process(testIntent("test"), navController, out)) verify { activity wasNot Called } verify { navController wasNot Called } @@ -66,16 +71,40 @@ class DeepLinkIntentProcessorTest { @Test fun `process home deep link`() { - assertTrue(processor.process(testIntent("fenix://home"), navController, out)) + assertTrue(processor.process(testIntent("home"), navController, out)) verify { activity wasNot Called } verify { navController.navigate(NavGraphDirections.actionGlobalHome()) } verify { out wasNot Called } } + @Test + fun `process urls_bookmarks deep link`() { + assertTrue(processor.process(testIntent("urls_bookmarks"), navController, out)) + + verify { navController.navigate(NavGraphDirections.actionGlobalBookmarkFragment(BookmarkRoot.Root.id)) } + verify { out wasNot Called } + } + + @Test + fun `process urls_history deep link`() { + assertTrue(processor.process(testIntent("urls_history"), navController, out)) + + verify { navController.navigate(NavGraphDirections.actionGlobalHistoryFragment()) } + verify { out wasNot Called } + } + + @Test + fun `process home_collections deep link`() { + assertTrue(processor.process(testIntent("home_collections"), navController, out)) + + verify { navController.navigate(NavGraphDirections.actionGlobalHome()) } + verify { out wasNot Called } + } + @Test fun `process settings deep link`() { - assertTrue(processor.process(testIntent("fenix://settings"), navController, out)) + assertTrue(processor.process(testIntent("settings"), navController, out)) verify { activity wasNot Called } verify { navController.navigate(NavGraphDirections.actionGlobalSettingsFragment()) } @@ -84,7 +113,7 @@ class DeepLinkIntentProcessorTest { @Test fun `process turn_on_sync deep link`() { - assertTrue(processor.process(testIntent("fenix://turn_on_sync"), navController, out)) + assertTrue(processor.process(testIntent("turn_on_sync"), navController, out)) verify { activity wasNot Called } verify { navController.navigate(NavGraphDirections.actionGlobalTurnOnSync()) } @@ -93,7 +122,7 @@ class DeepLinkIntentProcessorTest { @Test fun `process settings_search_engine deep link`() { - assertTrue(processor.process(testIntent("fenix://settings_search_engine"), navController, out)) + assertTrue(processor.process(testIntent("settings_search_engine"), navController, out)) verify { activity wasNot Called } verify { navController.navigate(NavGraphDirections.actionGlobalSearchEngineFragment()) } @@ -102,7 +131,7 @@ class DeepLinkIntentProcessorTest { @Test fun `process settings_accessibility deep link`() { - assertTrue(processor.process(testIntent("fenix://settings_accessibility"), navController, out)) + assertTrue(processor.process(testIntent("settings_accessibility"), navController, out)) verify { activity wasNot Called } verify { navController.navigate(NavGraphDirections.actionGlobalAccessibilityFragment()) } @@ -111,16 +140,48 @@ class DeepLinkIntentProcessorTest { @Test fun `process settings_delete_browsing_data deep link`() { - assertTrue(processor.process(testIntent("fenix://settings_delete_browsing_data"), navController, out)) + assertTrue(processor.process(testIntent("settings_delete_browsing_data"), navController, out)) verify { activity wasNot Called } verify { navController.navigate(NavGraphDirections.actionGlobalDeleteBrowsingDataFragment()) } verify { out wasNot Called } } + @Test + fun `process settings_addon_manager deep link`() { + assertTrue(processor.process(testIntent("settings_addon_manager"), navController, out)) + + verify { navController.navigate(NavGraphDirections.actionGlobalSettingsAddonsManagementFragment()) } + verify { out wasNot Called } + } + + @Test + fun `process settings_logins deep link`() { + assertTrue(processor.process(testIntent("settings_logins"), navController, out)) + + verify { navController.navigate(NavGraphDirections.actionGlobalSavedLoginsAuthFragment()) } + verify { out wasNot Called } + } + + @Test + fun `process settings_tracking_protection deep link`() { + assertTrue(processor.process(testIntent("settings_tracking_protection"), navController, out)) + + verify { navController.navigate(NavGraphDirections.actionGlobalTrackingProtectionFragment()) } + verify { out wasNot Called } + } + + @Test + fun `process settings_privacy deep link`() { + assertTrue(processor.process(testIntent("settings_privacy"), navController, out)) + + verify { navController.navigate(NavGraphDirections.actionGlobalSettingsFragment()) } + verify { out wasNot Called } + } + @Test fun `process enable_private_browsing deep link`() { - assertTrue(processor.process(testIntent("fenix://enable_private_browsing"), navController, out)) + assertTrue(processor.process(testIntent("enable_private_browsing"), navController, out)) verify { activity.browsingModeManager.mode = BrowsingMode.Private } verify { navController.navigate(NavGraphDirections.actionGlobalHome()) } @@ -129,13 +190,13 @@ class DeepLinkIntentProcessorTest { @Test fun `process open deep link`() { - assertTrue(processor.process(testIntent("fenix://open"), navController, out)) + assertTrue(processor.process(testIntent("open"), navController, out)) verify { activity wasNot Called } verify { navController wasNot Called } verify { out wasNot Called } - assertTrue(processor.process(testIntent("fenix://open?url=test"), navController, out)) + assertTrue(processor.process(testIntent("open?url=test"), navController, out)) verify { activity.openToBrowserAndLoad( @@ -150,19 +211,31 @@ class DeepLinkIntentProcessorTest { @Test fun `process make_default_browser deep link`() { - assertTrue(processor.process(testIntent("fenix://make_default_browser"), navController, out)) + assertTrue(processor.process(testIntent("make_default_browser"), navController, out)) verify { navController wasNot Called } verify { out wasNot Called } } @Test - fun `process settings_addon_manager deep link`() { - assertTrue(processor.process(testIntent("fenix://settings_addon_manager"), navController, out)) + fun `process settings_notifications deep link`() { + assertTrue(processor.process(testIntent("settings_notifications"), navController, out)) - verify { navController.navigate(NavGraphDirections.actionGlobalSettingsAddonsManagementFragment()) } + verify { navController wasNot Called } verify { out wasNot Called } + verify { activity.startActivity(any()) } } - private fun testIntent(uri: String) = Intent("", uri.toUri()) + @Test + fun `process install_search_widget deep link`() { + mockkObject(SearchWidgetCreator) + every { SearchWidgetCreator.createSearchWidget(any()) } returns true + assertTrue(processor.process(testIntent("install_search_widget"), navController, out)) + + verify { navController wasNot Called } + verify { out wasNot Called } + verify { activity wasNot Called } + } + + private fun testIntent(uri: String) = Intent("", "$DEEP_LINK_SCHEME://$uri".toUri()) } diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolderTest.kt new file mode 100644 index 000000000..e2c54a205 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolderTest.kt @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.home.sessioncontrol.viewholders + +import android.view.LayoutInflater +import android.view.View +import io.mockk.mockk +import io.mockk.verify +import kotlinx.android.synthetic.main.private_browsing_description.view.* +import mozilla.components.support.test.robolectric.testContext +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor + +@RunWith(FenixRobolectricTestRunner::class) +class PrivateBrowsingDescriptionViewHolderTest { + + private lateinit var view: View + private lateinit var interactor: TabSessionInteractor + + @Before + fun setup() { + view = LayoutInflater.from(testContext) + .inflate(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID, null) + interactor = mockk(relaxed = true) + } + + @Test + fun `call interactor on click`() { + PrivateBrowsingDescriptionViewHolder(view, interactor) + + view.private_session_common_myths.performClick() + verify { interactor.onPrivateBrowsingLearnMoreClicked() } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyBrowsingViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyBrowsingViewHolderTest.kt index 524ccf614..f261640c6 100644 --- a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyBrowsingViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyBrowsingViewHolderTest.kt @@ -4,18 +4,13 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding -import android.content.res.Resources import android.view.LayoutInflater import android.view.View -import io.mockk.every +import androidx.appcompat.view.ContextThemeWrapper import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.android.synthetic.main.onboarding_private_browsing.view.* -import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.test.robolectric.testContext -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,19 +26,10 @@ class OnboardingPrivacyBrowsingViewHolderTest { @Before fun setup() { - mockkStatic("mozilla.components.support.ktx.android.content.res.ThemeKt") - view = LayoutInflater.from(testContext) + val context = ContextThemeWrapper(testContext, R.style.NormalTheme) + view = LayoutInflater.from(context) .inflate(OnboardingPrivateBrowsingViewHolder.LAYOUT_ID, null) interactor = mockk(relaxed = true) - - every { - any().resolveAttribute(R.attr.onboardingSelected) - } returns R.color.onboarding_illustration_selected_normal_theme - } - - @After - fun teardown() { - unmockkStatic("mozilla.components.support.ktx.android.content.res.ThemeKt") } @Test diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolderTest.kt index 00ab421ec..fdf7a7ded 100644 --- a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolderTest.kt @@ -4,18 +4,13 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding -import android.content.res.Resources import android.view.LayoutInflater import android.view.View -import io.mockk.every +import androidx.appcompat.view.ContextThemeWrapper import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.android.synthetic.main.onboarding_privacy_notice.view.* -import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.test.robolectric.testContext -import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,19 +26,10 @@ class OnboardingPrivacyNoticeViewHolderTest { @Before fun setup() { - mockkStatic("mozilla.components.support.ktx.android.content.res.ThemeKt") - view = LayoutInflater.from(testContext) + val context = ContextThemeWrapper(testContext, R.style.NormalTheme) + view = LayoutInflater.from(context) .inflate(OnboardingPrivacyNoticeViewHolder.LAYOUT_ID, null) interactor = mockk(relaxed = true) - - every { - any().resolveAttribute(R.attr.onboardingSelected) - } returns R.color.onboarding_illustration_selected_normal_theme - } - - @After - fun teardown() { - unmockkStatic("mozilla.components.support.ktx.android.content.res.ThemeKt") } @Test diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingTrackingProtectionViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingTrackingProtectionViewHolderTest.kt index b422e87a6..f72c8cd13 100644 --- a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingTrackingProtectionViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingTrackingProtectionViewHolderTest.kt @@ -4,16 +4,11 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding -import android.content.res.Resources import android.view.LayoutInflater import android.view.View -import io.mockk.every -import io.mockk.mockkStatic -import io.mockk.unmockkStatic +import androidx.appcompat.view.ContextThemeWrapper import kotlinx.android.synthetic.main.onboarding_tracking_protection.view.* -import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.test.robolectric.testContext -import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -28,18 +23,9 @@ class OnboardingTrackingProtectionViewHolderTest { @Before fun setup() { - mockkStatic("mozilla.components.support.ktx.android.content.res.ThemeKt") - view = LayoutInflater.from(testContext) + val context = ContextThemeWrapper(testContext, R.style.NormalTheme) + view = LayoutInflater.from(context) .inflate(OnboardingTrackingProtectionViewHolder.LAYOUT_ID, null) - - every { - any().resolveAttribute(R.attr.onboardingSelected) - } returns R.color.onboarding_illustration_selected_normal_theme - } - - @After - fun teardown() { - unmockkStatic("mozilla.components.support.ktx.android.content.res.ThemeKt") } @Test diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingWhatsNewViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingWhatsNewViewHolderTest.kt new file mode 100644 index 000000000..b1e128432 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingWhatsNewViewHolderTest.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.home.sessioncontrol.viewholders.onboarding + +import android.content.res.Resources +import android.text.Spanned +import android.view.LayoutInflater +import android.view.View +import androidx.core.text.HtmlCompat +import androidx.core.text.HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verify +import kotlinx.android.synthetic.main.onboarding_whats_new.view.* +import mozilla.components.support.ktx.android.content.res.resolveAttribute +import mozilla.components.support.test.robolectric.testContext +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.home.sessioncontrol.OnboardingInteractor + +@RunWith(FenixRobolectricTestRunner::class) +class OnboardingWhatsNewViewHolderTest { + + private lateinit var view: View + private lateinit var interactor: OnboardingInteractor + + @Before + fun setup() { + mockkStatic("mozilla.components.support.ktx.android.content.res.ThemeKt") + view = LayoutInflater.from(testContext) + .inflate(OnboardingWhatsNewViewHolder.LAYOUT_ID, null) + interactor = mockk(relaxed = true) + + every { + any().resolveAttribute(R.attr.onboardingSelected) + } returns R.color.onboarding_illustration_selected_normal_theme + } + + @After + fun teardown() { + unmockkStatic("mozilla.components.support.ktx.android.content.res.ThemeKt") + } + + @Test + fun `sets and styles strings`() { + OnboardingWhatsNewViewHolder(view, interactor) + + assertEquals( + "Have questions about the redesigned Firefox Preview? Want to know what’s changed?", + view.description_text.text + ) + + val getAnswersHtml = HtmlCompat.toHtml(view.get_answers.text as Spanned, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) + assertTrue(getAnswersHtml, "Get answers here" in getAnswersHtml) + } + + @Test + fun `call interactor on click`() { + OnboardingWhatsNewViewHolder(view, interactor) + + view.get_answers.performClick() + verify { interactor.onWhatsNewGetAnswersClicked() } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/library/history/HistoryControllerTest.kt b/app/src/test/java/org/mozilla/fenix/library/history/HistoryControllerTest.kt index 6fe7ddd29..aeb3a7738 100644 --- a/app/src/test/java/org/mozilla/fenix/library/history/HistoryControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/history/HistoryControllerTest.kt @@ -25,6 +25,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.ext.directionsEq import org.mozilla.fenix.helpers.FenixRobolectricTestRunner // Robolectric needed for `onShareItem()` @@ -189,15 +190,12 @@ class HistoryControllerTest { fun onShareItem() { controller.handleShare(historyItem) - // `verify` checks for referential equality. - // This would fail as the NavDirections are created and used in place in the tested method. - // Capture the NavDirections and `assert` for structural equality after. verify { - navController.navigate( + navController.navigate(directionsEq( HistoryFragmentDirections.actionGlobalShareFragment( data = arrayOf(ShareData(url = historyItem.url, title = historyItem.title)) ) - ) + )) } } diff --git a/app/src/test/java/org/mozilla/fenix/perf/StartupReportFullyDrawnTest.kt b/app/src/test/java/org/mozilla/fenix/perf/StartupReportFullyDrawnTest.kt new file mode 100644 index 000000000..23b67713d --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/perf/StartupReportFullyDrawnTest.kt @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.perf + +import android.view.View +import android.view.ViewTreeObserver +import android.widget.LinearLayout +import io.mockk.Called +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import kotlinx.android.synthetic.main.top_site_item.view.* +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSiteItemViewHolder +import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination +import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupState + +class StartupReportFullyDrawnTest { + + @MockK private lateinit var activity: HomeActivity + private lateinit var holder: TopSiteItemViewHolder + @MockK(relaxed = true) private lateinit var rootContainer: LinearLayout + @MockK(relaxed = true) private lateinit var holderItemView: View + @MockK(relaxed = true) private lateinit var viewTreeObserver: ViewTreeObserver + private lateinit var fullyDrawn: StartupReportFullyDrawn + + @Before + fun setup() { + MockKAnnotations.init(this) + every { activity.findViewById(R.id.rootContainer) } returns rootContainer + every { holderItemView.context } returns activity + every { holderItemView.top_site_item } returns mockk(relaxed = true) + holder = TopSiteItemViewHolder(holderItemView, mockk()) + every { rootContainer.viewTreeObserver } returns viewTreeObserver + every { holderItemView.viewTreeObserver } returns viewTreeObserver + + fullyDrawn = StartupReportFullyDrawn() + } + + @Test + fun testOnActivityCreateEndHome() { + // Only APP_LINK destination + fullyDrawn.onActivityCreateEndHome(StartupState.Cold(StartupDestination.UNKNOWN), activity) + fullyDrawn.onActivityCreateEndHome(StartupState.Cold(StartupDestination.HOMESCREEN), activity) + verify { activity wasNot Called } + + // Only run once + fullyDrawn.onActivityCreateEndHome(StartupState.Cold(StartupDestination.APP_LINK), activity) + verify(exactly = 1) { activity.findViewById(R.id.rootContainer) } + + fullyDrawn.onActivityCreateEndHome(StartupState.Cold(StartupDestination.APP_LINK), activity) + verify(exactly = 1) { activity.findViewById(R.id.rootContainer) } + + every { activity.reportFullyDrawn() } just Runs + triggerPreDraw() + verify { activity.reportFullyDrawn() } + } + + @Test + fun testOnTopSitesItemBound() { + fullyDrawn.onTopSitesItemBound(StartupState.Cold(StartupDestination.HOMESCREEN), holder) + + every { activity.reportFullyDrawn() } just Runs + triggerPreDraw() + verify { activity.reportFullyDrawn() } + } + + private fun triggerPreDraw() { + val listener = slot() + verify { viewTreeObserver.addOnPreDrawListener(capture(listener)) } + listener.captured.onPreDraw() + } +} diff --git a/app/src/test/java/org/mozilla/fenix/push/LeanplumNotificationCustomizerTest.kt b/app/src/test/java/org/mozilla/fenix/push/LeanplumNotificationCustomizerTest.kt new file mode 100644 index 000000000..709b07c16 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/push/LeanplumNotificationCustomizerTest.kt @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.push + +import android.app.Notification +import androidx.core.app.NotificationCompat +import io.mockk.Called +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.mozilla.fenix.R + +class LeanplumNotificationCustomizerTest { + + private val customizer = LeanplumNotificationCustomizer() + + @Test + fun `customize adds icon`() { + val builder = mockk(relaxed = true) + customizer.customize(builder, mockk()) + + verify { builder.setSmallIcon(R.drawable.ic_status_logo) } + } + + @Test + fun `customize for BigPictureStyle does nothing`() { + val builder = mockk() + customizer.customize(builder, mockk(), mockk()) + + verify { builder wasNot Called } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/search/toolbar/ToolbarViewTest.kt b/app/src/test/java/org/mozilla/fenix/search/toolbar/ToolbarViewTest.kt index f5fc0b5ad..7506fdb60 100644 --- a/app/src/test/java/org/mozilla/fenix/search/toolbar/ToolbarViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/search/toolbar/ToolbarViewTest.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.search.toolbar +import android.content.Context +import androidx.appcompat.view.ContextThemeWrapper import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.every @@ -21,6 +23,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mozilla.fenix.R import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @RunWith(FenixRobolectricTestRunner::class) @@ -28,12 +31,14 @@ class ToolbarViewTest { @MockK(relaxed = true) private lateinit var interactor: ToolbarInteractor @MockK private lateinit var engine: Engine + private lateinit var context: Context private lateinit var toolbar: BrowserToolbar @Before fun setup() { MockKAnnotations.init(this) - toolbar = spyk(BrowserToolbar(testContext)) + context = ContextThemeWrapper(testContext, R.style.NormalTheme) + toolbar = spyk(BrowserToolbar(context)) } @Test @@ -68,7 +73,7 @@ class ToolbarViewTest { } private fun buildToolbarView(isPrivate: Boolean) = ToolbarView( - testContext, + context, interactor, historyStorage = null, isPrivate = isPrivate, diff --git a/app/src/test/java/org/mozilla/fenix/settings/advanced/BaseLocaleViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/settings/advanced/BaseLocaleViewHolderTest.kt new file mode 100644 index 000000000..e1ff92b41 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/advanced/BaseLocaleViewHolderTest.kt @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.advanced + +import android.content.Context +import android.view.View +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import mozilla.components.support.locale.LocaleManager +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.util.Locale + +class BaseLocaleViewHolderTest { + + private val selectedLocale = Locale("en", "UK") + private val view: View = mockk() + private val context: Context = mockk() + + private val localeViewHolder = object : BaseLocaleViewHolder(view, selectedLocale) { + override fun bind(locale: Locale) = Unit + } + + @Before + fun setup() { + mockkObject(LocaleManager) + every { view.context } returns context + } + + @Test + fun `verify other locale checker returns false`() { + every { LocaleManager.getCurrentLocale(context) } returns mockk() + val otherLocale = mockk() + + assertFalse(localeViewHolder.isCurrentLocaleSelected(otherLocale, isDefault = true)) + assertFalse(localeViewHolder.isCurrentLocaleSelected(otherLocale, isDefault = false)) + } + + @Test + fun `verify selected locale checker returns true`() { + every { LocaleManager.getCurrentLocale(context) } returns mockk() + + assertFalse(localeViewHolder.isCurrentLocaleSelected(selectedLocale, isDefault = true)) + assertTrue(localeViewHolder.isCurrentLocaleSelected(selectedLocale, isDefault = false)) + } + + @Test + fun `verify default locale checker returns true`() { + every { LocaleManager.getCurrentLocale(context) } returns null + + assertTrue(localeViewHolder.isCurrentLocaleSelected(selectedLocale, isDefault = true)) + assertFalse(localeViewHolder.isCurrentLocaleSelected(selectedLocale, isDefault = false)) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleAdapterTest.kt deleted file mode 100644 index f883e9c5b..000000000 --- a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleAdapterTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package org.mozilla.fenix.settings.advanced - -import android.content.Context -import android.view.View -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import mozilla.components.support.locale.LocaleManager -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import java.util.Locale - -class LocaleAdapterTest { - - private val selectedLocale = Locale("en", "UK") - private val view: View = mockk(relaxed = true) - private val context: Context = mockk(relaxed = true) - - private val localeViewHolder: BaseLocaleViewHolder = - object : BaseLocaleViewHolder(view, selectedLocale) { - - override fun bind(locale: Locale) { - // not required - } - } - - @Before - fun setup() { - every { view.context } returns context - } - - @Test - fun `verify selected locale checker returns true`() { - mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") - every { LocaleManager.isDefaultLocaleSelected(context) } returns false - - assertTrue(localeViewHolder.isCurrentLocaleSelected(selectedLocale, isDefault = false)) - } - - @Test - fun `verify default locale checker returns true`() { - mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") - every { LocaleManager.isDefaultLocaleSelected(context) } returns true - - assertTrue(localeViewHolder.isCurrentLocaleSelected(selectedLocale, isDefault = true)) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleManagerExtensionTest.kt b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleManagerExtensionTest.kt index 780795bef..99e914cdd 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleManagerExtensionTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleManagerExtensionTest.kt @@ -8,7 +8,6 @@ import android.content.Context import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject -import io.mockk.mockkStatic import mozilla.components.support.locale.LocaleManager import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -24,9 +23,12 @@ import java.util.Locale @RunWith(FenixRobolectricTestRunner::class) class LocaleManagerExtensionTest { + private lateinit var context: Context + @Before fun setup() { - mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") + context = mockk() + mockkObject(LocaleManager) } @Test @@ -44,8 +46,6 @@ class LocaleManagerExtensionTest { @Test @Config(qualifiers = "en-rUS") fun `default locale selected`() { - val context: Context = mockk() - mockkObject(LocaleManager) every { LocaleManager.getCurrentLocale(context) } returns null assertTrue(LocaleManager.isDefaultLocaleSelected(context)) @@ -54,8 +54,6 @@ class LocaleManagerExtensionTest { @Test @Config(qualifiers = "en-rUS") fun `custom locale selected`() { - val context: Context = mockk() - mockkObject(LocaleManager) val selectedLocale = Locale("en", "UK") every { LocaleManager.getCurrentLocale(context) } returns selectedLocale @@ -65,13 +63,9 @@ class LocaleManagerExtensionTest { @Test @Config(qualifiers = "en-rUS") fun `match current stored locale string with a Locale from our list`() { - val context: Context = mockk() - mockkObject(LocaleManager) val otherLocale = Locale("fr") val selectedLocale = Locale("en", "UK") - val localeList = ArrayList() - localeList.add(otherLocale) - localeList.add(selectedLocale) + val localeList = listOf(otherLocale, selectedLocale) every { LocaleManager.getCurrentLocale(context) } returns selectedLocale @@ -81,13 +75,9 @@ class LocaleManagerExtensionTest { @Test @Config(qualifiers = "en-rUS") fun `match null stored locale with the default Locale from our list`() { - val context: Context = mockk() - mockkObject(LocaleManager) val firstLocale = Locale("fr") val secondLocale = Locale("en", "UK") - val localeList = ArrayList() - localeList.add(firstLocale) - localeList.add(secondLocale) + val localeList = listOf(firstLocale, secondLocale) every { LocaleManager.getCurrentLocale(context) } returns null diff --git a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsControllerTest.kt index 8923f5e2a..c1f4f463d 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsControllerTest.kt @@ -5,14 +5,15 @@ package org.mozilla.fenix.settings.advanced import android.app.Activity -import android.content.Context import io.mockk.Runs import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject import io.mockk.mockkStatic +import io.mockk.spyk import io.mockk.verify +import io.mockk.verifyAll import mozilla.components.support.locale.LocaleManager import org.junit.Before import org.junit.Test @@ -20,118 +21,113 @@ import java.util.Locale class LocaleSettingsControllerTest { - private val context: Context = mockk(relaxed = true) + private val activity = mockk(relaxed = true) private val localeSettingsStore: LocaleSettingsStore = mockk(relaxed = true) + private val mockState = LocaleSettingsState(mockk(), mockk(), mockk()) - private lateinit var controller: LocaleSettingsController + private lateinit var controller: DefaultLocaleSettingsController @Before fun setup() { - controller = DefaultLocaleSettingsController(context, localeSettingsStore) + controller = spyk(DefaultLocaleSettingsController(activity, localeSettingsStore)) + + mockkObject(LocaleManager) + mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") } @Test fun `don't set locale if same locale is chosen`() { val selectedLocale = Locale("en", "UK") - every { localeSettingsStore.state } returns LocaleSettingsState( - mockk(), - mockk(), - selectedLocale - ) - mockkObject(LocaleManager) - mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") - every { LocaleManager.getCurrentLocale(context) } returns mockk() - every { LocaleManager.isDefaultLocaleSelected(context) } returns false + every { localeSettingsStore.state } returns mockState.copy(selectedLocale = selectedLocale) + every { LocaleManager.getCurrentLocale(activity) } returns mockk() controller.handleLocaleSelected(selectedLocale) - verify( - inverse = true, - verifyBlock = { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) }) - verify( - inverse = true, - verifyBlock = { LocaleManager.setNewLocale(context, selectedLocale.toLanguageTag()) }) - verify( - inverse = true, - verifyBlock = { LocaleManager.updateBaseConfiguration(context, selectedLocale) }) - verify(inverse = true, verifyBlock = { (context as Activity).recreate() }) + verifyAll(inverse = true) { + localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) + LocaleManager.setNewLocale(activity, selectedLocale.toLanguageTag()) + activity.recreate() + } + with(controller) { + verify(inverse = true) { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } + } } @Test - fun `set a new locale from the list`() { + fun `set a new locale from the list if other locale is chosen`() { val selectedLocale = Locale("en", "UK") val otherLocale: Locale = mockk() - every { localeSettingsStore.state } returns LocaleSettingsState( - mockk(), - mockk(), - otherLocale - ) - mockkObject(LocaleManager) - mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") - every { LocaleManager.updateBaseConfiguration(context, selectedLocale) } just Runs - every { - LocaleManager.setNewLocale( - context, - selectedLocale.toLanguageTag() - ) - } returns context + every { localeSettingsStore.state } returns mockState.copy(selectedLocale = otherLocale) + every { LocaleManager.setNewLocale(activity, selectedLocale.toLanguageTag()) } returns activity + with(controller) { + every { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } just Runs + } controller.handleLocaleSelected(selectedLocale) verify { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) } - verify { LocaleManager.setNewLocale(context, selectedLocale.toLanguageTag()) } - verify { LocaleManager.updateBaseConfiguration(context, selectedLocale) } - verify { (context as Activity).recreate() } + verify { LocaleManager.setNewLocale(activity, selectedLocale.toLanguageTag()) } + verify { activity.recreate() } + with(controller) { + verify { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } + } + } + + @Test + fun `set a new locale from the list if default locale is not selected`() { + val selectedLocale = Locale("en", "UK") + every { localeSettingsStore.state } returns mockState.copy(selectedLocale = selectedLocale) + every { LocaleManager.getCurrentLocale(activity) } returns null + every { LocaleManager.setNewLocale(activity, selectedLocale.toLanguageTag()) } returns activity + with(controller) { + every { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } just Runs + } + + controller.handleLocaleSelected(selectedLocale) + + verify { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) } + verify { LocaleManager.setNewLocale(activity, selectedLocale.toLanguageTag()) } + verify { activity.recreate() } + with(controller) { + verify { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } + } } @Test fun `don't set default locale if default locale is already chosen`() { val selectedLocale = Locale("en", "UK") - val localeList = ArrayList() - localeList.add(selectedLocale) - every { localeSettingsStore.state } returns LocaleSettingsState( - localeList, - mockk(), - mockk() - ) - mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") - every { LocaleManager.isDefaultLocaleSelected(context) } returns true + every { localeSettingsStore.state } returns mockState.copy(localeList = listOf(selectedLocale)) + every { LocaleManager.getCurrentLocale(activity) } returns null controller.handleDefaultLocaleSelected() - verify( - inverse = true, - verifyBlock = { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) }) - verify( - inverse = true, - verifyBlock = { LocaleManager.resetToSystemDefault(context) }) - verify( - inverse = true, - verifyBlock = { LocaleManager.updateBaseConfiguration(context, selectedLocale) }) - verify(inverse = true, verifyBlock = { (context as Activity).recreate() }) + verifyAll(inverse = true) { + localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) + LocaleManager.resetToSystemDefault(activity) + activity.recreate() + with(controller) { + LocaleManager.updateBaseConfiguration(activity, selectedLocale) + } + } } @Test fun `set the default locale as the new locale`() { val selectedLocale = Locale("en", "UK") - val localeList = ArrayList() - localeList.add(selectedLocale) - every { localeSettingsStore.state } returns LocaleSettingsState( - localeList, - mockk(), - mockk() - ) - mockkObject(LocaleManager) - mockkStatic("org.mozilla.fenix.settings.advanced.LocaleManagerExtensionKt") - every { LocaleManager.resetToSystemDefault(context) } just Runs - every { LocaleManager.updateBaseConfiguration(context, selectedLocale) } just Runs + every { localeSettingsStore.state } returns mockState.copy(localeList = listOf(selectedLocale)) + every { LocaleManager.resetToSystemDefault(activity) } just Runs + with(controller) { + every { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } just Runs + } controller.handleDefaultLocaleSelected() verify { localeSettingsStore.dispatch(LocaleSettingsAction.Select(selectedLocale)) } - verify { LocaleManager.resetToSystemDefault(context) } - verify { LocaleManager.updateBaseConfiguration(context, selectedLocale) } - verify { (context as Activity).recreate() } + verify { LocaleManager.resetToSystemDefault(activity) } + verify { activity.recreate() } + with(controller) { + verify { LocaleManager.updateBaseConfiguration(activity, selectedLocale) } + } } @Test diff --git a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsStoreTest.kt b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsStoreTest.kt index 24862c5a8..71bd52014 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleSettingsStoreTest.kt @@ -18,10 +18,11 @@ class LocaleSettingsStoreTest { @Before fun setup() { - val localeList = ArrayList() - localeList.add(Locale("fr")) // default - localeList.add(otherLocale) - localeList.add(selectedLocale) + val localeList = listOf( + Locale("fr"), // default + otherLocale, + selectedLocale + ) localeSettingsStore = LocaleSettingsStore(LocaleSettingsState(localeList, localeList, selectedLocale)) diff --git a/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleViewHoldersTest.kt b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleViewHoldersTest.kt new file mode 100644 index 000000000..4fe4f34b0 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/settings/advanced/LocaleViewHoldersTest.kt @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.advanced + +import android.view.LayoutInflater +import android.view.View +import androidx.core.view.isVisible +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.verify +import kotlinx.android.synthetic.main.locale_settings_item.view.* +import mozilla.components.support.locale.LocaleManager +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import java.util.Locale + +@RunWith(FenixRobolectricTestRunner::class) +class LocaleViewHoldersTest { + + private val selectedLocale = Locale("en", "US") + private lateinit var view: View + private lateinit var interactor: LocaleSettingsViewInteractor + private lateinit var localeViewHolder: LocaleViewHolder + private lateinit var systemLocaleViewHolder: SystemLocaleViewHolder + + @Before + fun setup() { + mockkObject(LocaleManager) + every { LocaleManager.getCurrentLocale(any()) } returns null + + view = LayoutInflater.from(testContext) + .inflate(R.layout.locale_settings_item, null) + interactor = mockk() + + localeViewHolder = LocaleViewHolder(view, selectedLocale, interactor) + systemLocaleViewHolder = SystemLocaleViewHolder(view, selectedLocale, interactor) + } + + @Test + fun `bind LocaleViewHolder`() { + localeViewHolder.bind(selectedLocale) + + assertEquals("English (United States)", view.locale_title_text.text) + assertEquals("English (United States)", view.locale_subtitle_text.text) + assertFalse(view.locale_selected_icon.isVisible) + } + + @Test + fun `LocaleViewHolder calls interactor on click`() { + localeViewHolder.bind(selectedLocale) + + every { interactor.onLocaleSelected(selectedLocale) } just Runs + view.performClick() + verify { interactor.onLocaleSelected(selectedLocale) } + } + + @Test + fun `bind SystemLocaleViewHolder`() { + systemLocaleViewHolder.bind(selectedLocale) + + assertEquals("Follow device language", view.locale_title_text.text) + assertFalse(view.locale_subtitle_text.isVisible) + assertTrue(view.locale_selected_icon.isVisible) + } + + @Test + fun `SystemLocaleViewHolder calls interactor on click`() { + systemLocaleViewHolder.bind(selectedLocale) + + every { interactor.onDefaultLocaleSelected() } just Runs + view.performClick() + verify { interactor.onDefaultLocaleSelected() } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt index 44c8a4862..bf0cc524c 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt @@ -10,39 +10,30 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.verify -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.newSingleThreadContext -import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.runBlockingTest -import kotlinx.coroutines.test.setMain import mozilla.components.concept.engine.Engine -import org.junit.After +import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mozilla.fenix.ext.components -@ExperimentalCoroutinesApi +@OptIn(ExperimentalCoroutinesApi::class) class DefaultDeleteBrowsingDataControllerTest { - private val mainThreadSurrogate = newSingleThreadContext("UI thread") + @get:Rule + val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher()) private val context: Context = mockk(relaxed = true) private lateinit var controller: DefaultDeleteBrowsingDataController @Before fun setup() { - Dispatchers.setMain(mainThreadSurrogate) - every { context.components.core.engine.clearData(any()) } just Runs } - @After - fun tearDown() { - Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher - mainThreadSurrogate.close() - } - @Test fun deleteTabs() = runBlockingTest { controller = DefaultDeleteBrowsingDataController(context, coroutineContext) diff --git a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuitTest.kt b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuitTest.kt index eb975b3a3..41413b743 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuitTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuitTest.kt @@ -6,24 +6,21 @@ package org.mozilla.fenix.settings.deletebrowsingdata -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import io.mockk.every import io.mockk.mockk import io.mockk.verify import io.mockk.verifyOrder -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.newSingleThreadContext -import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.runBlockingTest -import kotlinx.coroutines.test.setMain import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.concept.engine.Engine import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.support.test.robolectric.testContext -import org.junit.After +import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.Before import org.junit.Ignore +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.HomeActivity @@ -31,13 +28,15 @@ import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.PermissionStorage import org.mozilla.fenix.ext.clearAndCommit import org.mozilla.fenix.ext.components +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.utils.Settings -@ExperimentalCoroutinesApi +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(FenixRobolectricTestRunner::class) class DeleteAndQuitTest { - private val mainThreadSurrogate = newSingleThreadContext("UI thread") + @get:Rule + val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher()) private var activity: HomeActivity = mockk(relaxed = true) lateinit var settings: Settings @@ -54,8 +53,6 @@ class DeleteAndQuitTest { clear() } - Dispatchers.setMain(mainThreadSurrogate) - every { activity.components.core.historyStorage } returns historyStorage every { activity.components.core.permissionStorage } returns permissionStorage every { activity.components.useCases.tabsUseCases } returns tabUseCases @@ -63,12 +60,6 @@ class DeleteAndQuitTest { every { activity.components.core.engine } returns engine } - @After - fun tearDown() { - Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher - mainThreadSurrogate.close() - } - private fun Settings.clear() { preferences.clearAndCommit() } diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/LoginsFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/LoginsFragmentStoreTest.kt index 236023dfd..56fc68d6c 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/LoginsFragmentStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/LoginsFragmentStoreTest.kt @@ -10,6 +10,7 @@ import mozilla.components.support.test.ext.joinBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test class LoginsFragmentStoreTest { @@ -123,4 +124,19 @@ class LoginsFragmentStoreTest { assertEquals("example", store.state.searchedForText) assertEquals(listOf(exampleLogin), store.state.filteredItems) } + + @Test + fun `LoginSelected action`() { + val store = LoginsFragmentStore(baseState.copy( + isLoading = false, + loginList = listOf(mockk()), + filteredItems = listOf(mockk()) + )) + + store.dispatch(LoginsAction.LoginSelected(mockk())).joinBlocking() + + assertTrue(store.state.isLoading) + assertTrue(store.state.loginList.isEmpty()) + assertTrue(store.state.filteredItems.isEmpty()) + } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/LoginsListViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/LoginsListViewHolderTest.kt index 7509bb10a..3f15ad9fe 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/LoginsListViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/LoginsListViewHolderTest.kt @@ -52,6 +52,6 @@ class LoginsListViewHolderTest { holder.bind(baseLogin) view.performClick() - verify { interactor.itemClicked(baseLogin) } + verify { interactor.onItemClicked(baseLogin) } } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsControllerTest.kt index 62f27e507..ea0e0f418 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsControllerTest.kt @@ -4,20 +4,30 @@ package org.mozilla.fenix.settings.logins +import androidx.navigation.NavController import io.mockk.mockk import io.mockk.verify +import io.mockk.verifyAll import mozilla.components.support.test.robolectric.testContext import org.junit.Test import org.junit.runner.RunWith +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.utils.Settings @RunWith(FenixRobolectricTestRunner::class) class SavedLoginsControllerTest { private val store: LoginsFragmentStore = mockk(relaxed = true) + private val navController: NavController = mockk(relaxed = true) + private val browserNavigator: (String, Boolean, BrowserDirection) -> Unit = mockk(relaxed = true) + private val settings: Settings = mockk(relaxed = true) + private val metrics: MetricController = mockk(relaxed = true) private val sortingStrategy: SortingStrategy = SortingStrategy.Alphabetically(testContext) - private val controller = SavedLoginsController(store, settings) + private val controller = SavedLoginsController(store, navController, browserNavigator, settings, metrics) @Test fun `GIVEN a sorting strategy, WHEN handleSort is called on the controller, THEN the correct action should be dispatched and the strategy saved in sharedPref`() { @@ -34,4 +44,32 @@ class SavedLoginsControllerTest { settings.savedLoginsSortingStrategy = sortingStrategy } } + + @Test + fun `GIVEN a SavedLogin, WHEN handleItemClicked is called for it, THEN LoginsAction$LoginSelected should be emitted`() { + val login: SavedLogin = mockk(relaxed = true) + + controller.handleItemClicked(login) + + verifyAll { + store.dispatch(LoginsAction.LoginSelected(login)) + metrics.track(Event.OpenOneLogin) + navController.navigate( + SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(login.guid) + ) + } + } + + @Test + fun `GIVEN the learn more option, WHEN handleLearnMoreClicked is called for it, then we should open the right support webpage`() { + controller.handleLearnMoreClicked() + + verify { + browserNavigator.invoke( + SupportUtils.getGenericSumoURLForTopic(SupportUtils.SumoTopic.SYNC_SETUP), + true, + BrowserDirection.FromSavedLoginsFragment + ) + } + } } diff --git a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsInteractorTest.kt index b9db6f70b..7e56dd379 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/logins/SavedLoginsInteractorTest.kt @@ -5,7 +5,7 @@ package org.mozilla.fenix.settings.logins import io.mockk.mockk -import io.mockk.verify +import io.mockk.verifyAll import mozilla.components.support.test.robolectric.testContext import org.junit.Test import org.junit.runner.RunWith @@ -15,32 +15,35 @@ import kotlin.random.Random @RunWith(FenixRobolectricTestRunner::class) class SavedLoginsInteractorTest { private val controller: SavedLoginsController = mockk(relaxed = true) - private val savedLoginClicked: (SavedLogin) -> Unit = mockk(relaxed = true) - private val learnMore: () -> Unit = mockk(relaxed = true) - private val interactor = SavedLoginsInteractor( - controller, - savedLoginClicked, - learnMore - ) + private val interactor = SavedLoginsInteractor(controller) @Test - fun itemClicked() { + fun `GIVEN a SavedLogin being clicked, WHEN the interactor is called for it, THEN it should just delegate the controller`() { val item = SavedLogin("mozilla.org", "username", "password", "id", Random.nextLong()) - interactor.itemClicked(item) + interactor.onItemClicked(item) - verify { - savedLoginClicked.invoke(item) + verifyAll { + controller.handleItemClicked(item) } } @Test - fun `GIVEN a sorting strategy, WHEN sort method is called on the interactor, THEN controller should call handleSort with the same parameter`() { + fun `GIVEN a change in sorting strategy, WHEN the interactor is called for it, THEN it should just delegate the controller`() { val sortingStrategy: SortingStrategy = SortingStrategy.Alphabetically(testContext) - interactor.sort(sortingStrategy) + interactor.onSortingStrategyChanged(sortingStrategy) - verify { + verifyAll { controller.handleSort(sortingStrategy) } } + + @Test + fun `GIVEN the learn more option is clicked, WHEN the interactor is called for it, THEN it should just delegate the controller`() { + interactor.onLearnMoreClicked() + + verifyAll { + controller.handleLearnMoreClicked() + } + } } diff --git a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsIntegrationTest.kt b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsIntegrationTest.kt new file mode 100644 index 000000000..83086d21a --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsIntegrationTest.kt @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.sync + +import android.content.Context +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import mozilla.components.concept.sync.AccountObserver +import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage +import mozilla.components.service.fxa.manager.FxaAccountManager +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.FenixApplication + +class SyncedTabsIntegrationTest { + + @MockK private lateinit var context: Context + @MockK private lateinit var syncedTabsStorage: SyncedTabsStorage + @MockK private lateinit var accountManager: FxaAccountManager + + @Before + fun setup() { + MockKAnnotations.init(this) + every { syncedTabsStorage.stop() } just Runs + every { accountManager.register(any(), owner = any(), autoPause = true) } just Runs + every { context.applicationContext } returns mockk { + every { components } returns mockk { + every { backgroundServices.syncedTabsStorage } returns syncedTabsStorage + } + } + } + + @Test + fun `starts and stops syncedTabsStorage on user authentication`() { + val observer = slot() + SyncedTabsIntegration(context, accountManager).launch() + verify { accountManager.register(capture(observer), owner = any(), autoPause = true) } + + every { syncedTabsStorage.start() } just Runs + observer.captured.onAuthenticated(mockk(), mockk()) + verify { syncedTabsStorage.start() } + + every { syncedTabsStorage.stop() } just Runs + observer.captured.onLoggedOut() + verify { syncedTabsStorage.stop() } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsViewHolderTest.kt new file mode 100644 index 000000000..b81a0fc3b --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsViewHolderTest.kt @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.sync + +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.android.synthetic.main.sync_tabs_list_item.view.* +import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* +import mozilla.components.browser.storage.sync.Tab +import mozilla.components.browser.storage.sync.TabEntry +import mozilla.components.concept.sync.Device +import mozilla.components.concept.sync.DeviceType +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class SyncedTabsViewHolderTest { + + private lateinit var tabViewHolder: SyncedTabsViewHolder.TabViewHolder + private lateinit var tabView: View + private lateinit var deviceViewHolder: SyncedTabsViewHolder.DeviceViewHolder + private lateinit var deviceView: View + private lateinit var deviceViewGroupName: TextView + + private val tab = Tab( + history = listOf( + mockk(), + TabEntry( + title = "Firefox", + url = "https://firefox.com", + iconUrl = "https://firefox.com/favicon.ico" + ), + mockk() + ), + active = 1, + lastUsed = 0L + ) + + @Before + fun setup() { + val inflater = LayoutInflater.from(testContext) + + tabView = inflater.inflate(SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID, null) + tabViewHolder = SyncedTabsViewHolder.TabViewHolder(tabView) + + deviceViewGroupName = mockk(relaxUnitFun = true) + deviceView = mockk { + every { synced_tabs_group_name } returns deviceViewGroupName + } + deviceViewHolder = SyncedTabsViewHolder.DeviceViewHolder(deviceView) + } + + @Test + fun `TabViewHolder binds active tab`() { + tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), mockk()) + + assertEquals("Firefox", tabView.synced_tab_item_title.text) + assertEquals("https://firefox.com", tabView.synced_tab_item_url.text) + } + + @Test + fun `TabViewHolder calls interactor on click`() { + val interactor = mockk<(Tab) -> Unit>(relaxed = true) + tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), interactor) + + tabView.performClick() + verify { interactor(tab) } + } + + @Test + fun `DeviceViewHolder binds desktop device`() { + val device = mockk { + every { displayName } returns "Charcoal" + every { deviceType } returns DeviceType.DESKTOP + } + deviceViewHolder.bind(SyncedTabsAdapter.AdapterItem.Device(device), mockk()) + + verify { deviceViewGroupName.text = "Charcoal" } + verify { + deviceViewGroupName.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.mozac_ic_device_desktop, 0, 0, 0 + ) + } + } + + @Test + fun `DeviceViewHolder binds mobile device`() { + val device = mockk { + every { displayName } returns "Emerald" + every { deviceType } returns DeviceType.MOBILE + } + deviceViewHolder.bind(SyncedTabsAdapter.AdapterItem.Device(device), mockk()) + + verify { deviceViewGroupName.text = "Emerald" } + verify { + deviceViewGroupName.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.mozac_ic_device_mobile, 0, 0, 0 + ) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.kt b/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.kt new file mode 100644 index 000000000..77c481bf2 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.kt @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.wifi + +import io.mockk.Called +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE +import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_INAUDIBLE +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ALL +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ON_WIFI +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL +import org.mozilla.fenix.utils.Settings + +class SitePermissionsWifiIntegrationTest { + + private lateinit var settings: Settings + private lateinit var wifiConnectionMonitor: WifiConnectionMonitor + private lateinit var wifiIntegration: SitePermissionsWifiIntegration + + @Before + fun setup() { + settings = mockk() + wifiConnectionMonitor = mockk(relaxed = true) + wifiIntegration = SitePermissionsWifiIntegration(settings, wifiConnectionMonitor) + + every { settings.setSitePermissionsPhoneFeatureAction(any(), any()) } just Runs + } + + @Test + fun `add and remove wifi connected listener`() { + wifiIntegration.addWifiConnectedListener() + verify { wifiConnectionMonitor.register(any()) } + + wifiIntegration.removeWifiConnectedListener() + verify { wifiConnectionMonitor.unregister(any()) } + } + + @Test + fun `start and stop wifi connection monitor`() { + wifiIntegration.start() + verify { wifiConnectionMonitor.start() } + + wifiIntegration.stop() + verify { wifiConnectionMonitor.stop() } + } + + @Test + fun `add only if autoplay is only allowed on wifi`() { + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL + wifiIntegration.maybeAddWifiConnectedListener() + verify { wifiConnectionMonitor wasNot Called } + + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI + wifiIntegration.maybeAddWifiConnectedListener() + verify { wifiConnectionMonitor.register(any()) } + } + + @Test + fun `listener removes itself if autoplay is not only allowed on wifi`() { + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL + wifiIntegration.onWifiConnectionChanged(connected = true) + verify { wifiConnectionMonitor.unregister(any()) } + } + + @Test + fun `listener sets audible and inaudible settings to allowed on connect`() { + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI + wifiIntegration.onWifiConnectionChanged(connected = true) + verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.ALLOWED) } + verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.ALLOWED) } + } + + @Test + fun `listener sets audible and inaudible settings to blocked on disconnected`() { + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI + wifiIntegration.onWifiConnectionChanged(connected = false) + verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.BLOCKED) } + verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.BLOCKED) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt b/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt new file mode 100644 index 000000000..94b8a7f10 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.wifi + +import android.net.ConnectivityManager +import android.net.NetworkRequest +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.slot +import io.mockk.unmockkConstructor +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Test + +class WifiConnectionMonitorTest { + + private lateinit var connectivityManager: ConnectivityManager + private lateinit var wifiConnectionMonitor: WifiConnectionMonitor + + @Before + fun setup() { + mockkConstructor(NetworkRequest.Builder::class) + connectivityManager = mockk(relaxUnitFun = true) + wifiConnectionMonitor = WifiConnectionMonitor(connectivityManager) + + every { + anyConstructed().addTransportType(any()) + } answers { self as NetworkRequest.Builder } + } + + @After + fun teardown() { + unmockkConstructor(NetworkRequest.Builder::class) + } + + @Test + fun `start runs only once`() { + wifiConnectionMonitor.start() + wifiConnectionMonitor.start() + + verify(exactly = 1) { + connectivityManager.registerNetworkCallback(any(), any()) + } + } + + @Test + fun `stop only runs after start`() { + wifiConnectionMonitor.stop() + verify(exactly = 0) { + connectivityManager.unregisterNetworkCallback(any()) + } + + wifiConnectionMonitor.start() + wifiConnectionMonitor.stop() + verify { + connectivityManager.unregisterNetworkCallback(any()) + } + } + + @Test + fun `passes results from connectivity manager to observers`() { + val slot = slot() + every { connectivityManager.registerNetworkCallback(any(), capture(slot)) } just Runs + + wifiConnectionMonitor.start() + + // Immediately notifies observer when registered + val observer = mockk(relaxed = true) + wifiConnectionMonitor.register(observer) + verify { observer.onWifiConnectionChanged(connected = false) } + + // Notifies observer when network is available or lost + slot.captured.onAvailable(mockk()) + verify { observer.onWifiConnectionChanged(connected = true) } + + slot.captured.onLost(mockk()) + verify { observer.onWifiConnectionChanged(connected = false) } + } + + private fun captureNetworkCallback(): ConnectivityManager.NetworkCallback { + val slot = slot() + verify { connectivityManager.registerNetworkCallback(any(), capture(slot)) } + return slot.captured + } +} diff --git a/automation/taskcluster/androidTest/flank-x86-screenshots-tests.yml b/automation/taskcluster/androidTest/flank-x86-screenshots-tests.yml new file mode 100644 index 000000000..61adceda5 --- /dev/null +++ b/automation/taskcluster/androidTest/flank-x86-screenshots-tests.yml @@ -0,0 +1,55 @@ +# gcloud args match the official gcloud cli +# https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run +gcloud: + results-bucket: fenix_test_artifacts + record-video: true + + # The maximum possible testing time is 30m on physical devices and 60m on virtual devices. + timeout: 20m + # will start test then close socket. no reports will be generated. + # to retrieve results later, use the "refresh" command + # reports will be generated from /results/matrix_ids.json + #async: true + # will start test then leave socket open. reports will be published + # to /results + # see: https://github.com/TestArmada/flank/issues/339 + async: false + + # results-history-name + # by default, set to app name + # declare results-history-name to create a separate dropdown menu in Firebase + # see: https://github.com/TestArmada/flank/issues/341 + #results-history-name: tmp_parallel + + # The number of times a test execution should be re-attempted if one or more failures occur. + # The maximum number of reruns allowed is 10. Default is 0, which implies no reruns. + num-flaky-test-attempts: 1 + + # test and app are the only required args + app: /app/path + test: /test/path + + auto-google-login: true + use-orchestrator: true + environment-variables: + clearPackageData: true + directories-to-pull: + - /sdcard/screenshots + performance-metrics: true + + test-targets: + - package org.mozilla.fenix.screenshots + + device: + - model: Pixel2 + version: 28 + +flank: + project: GOOGLE_PROJECT + # test shards - the amount of groups to split the test suite into + # set to -1 to use one shard per test. + max-test-shards: 1 + # num-test-runs: the amount of times to run the tests. + # 1 runs the tests once. 10 runs all the tests 10x + num-test-runs: 1 + diff --git a/automation/taskcluster/androidTest/ui-test.sh b/automation/taskcluster/androidTest/ui-test.sh index 2b1812ea2..e45e9b8cc 100755 --- a/automation/taskcluster/androidTest/ui-test.sh +++ b/automation/taskcluster/androidTest/ui-test.sh @@ -80,6 +80,8 @@ elif [[ "${device_type}" == "x86-start-test" ]]; then flank_template="${PATH_TEST}/flank-x86-start-test.yml" elif [[ "${device_type}" == "arm-start-test" ]]; then flank_template="${PATH_TEST}/flank-armeabi-v7a-start-test.yml" +elif [[ "${device_type}" == "x86-screenshots-tests" ]]; then + flank_template="${PATH_TEST}/flank-x86-screenshots-tests.yml" else echo "FAILURE: flank config file not found!" exitcode=1 diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index 5cc71f95f..2968a13d6 100644 --- a/buildSrc/src/main/java/AndroidComponents.kt +++ b/buildSrc/src/main/java/AndroidComponents.kt @@ -3,5 +3,5 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object AndroidComponents { - const val VERSION = "50.0.20200712190143" + const val VERSION = "51.0.20200716130034" } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 69f2d65e0..3c2ba184e 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object Versions { - const val kotlin = "1.3.30" + const val kotlin = "1.3.72" const val coroutines = "1.3.3" const val android_gradle_plugin = "3.5.0" const val sentry = "1.7.10" diff --git a/docs/mma.md b/docs/mma.md index 90f3d7a5c..1340ff105 100644 --- a/docs/mma.md +++ b/docs/mma.md @@ -144,6 +144,16 @@ User Attributes A boolean indicating that the user has at least one search widget placed on the home screen #4694 + + `tracking_protection_enabled` + A boolean indicating that the user has enabled tracking protection + #11965 + + + `tracking_protection_setting` + A string indicating the level at which the user has set tracking protection. Possible values are `none`, `standard`, `strict` and `custom` + #11965 + `fenix` A boolean indicating that this is a Fenix installation @@ -254,6 +264,18 @@ Here is the list of current deep links available, which can be found here in the `fenix://home` Opens to the Fenix home screen + + `fenix://urls_bookmarks` + Opens to the list of the user's bookmarks at its root + + + `fenix://urls_history` + Opens to the list of pages the user has visited + + + `fenix://home_collections` + Opens to the list of collections the user has saved. It is implemented as `fenix://home` + `fenix://settings` Opens to the top level settings screen @@ -274,6 +296,22 @@ Here is the list of current deep links available, which can be found here in the `fenix://settings_delete_browsing_data` Opens to the delete browsing data settings screen + + `fenix://settings_addon_manager` + Opens to the settings page to install and manage addons + + + `fenix://settings_logins` + Opens to the Logins and passwords settings page configure how logins are treated. This is *not* the list of actual logins + + + `fenix://settings_tracking_protection` + Opens to the Enhanced Tracking Protection settings page + + + `fenix://settings_privacy` + Opens to the settings page which contains the privacy settings. Currently, this is the same as `fenix://settings` + `fenix://enable_private_browsing` Opens to the Fenix home screen and enables private browsing @@ -286,6 +324,14 @@ Here is the list of current deep links available, which can be found here in the `fenix://make_default_browser` Opens to the Android default apps settings screen. **Only works on Android API >=24** + + `fenix://settings_notifications` + Opens to the Android notification settings screen for Fenix. **Only works on Android API >=24** + + + `fenix://install_search_widget` + Adds the search widget to the users launcher homescreen. **Only works on Android API >=26** + Messages diff --git a/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/ButtonStyleXmlDetector.kt b/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/ButtonStyleXmlDetector.kt index ceac3222d..c96f3a8e9 100644 --- a/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/ButtonStyleXmlDetector.kt +++ b/mozilla-lint-rules/src/main/java/org/mozilla/fenix/lintrules/ButtonStyleXmlDetector.kt @@ -22,7 +22,6 @@ class ButtonStyleXmlDetector : ResourceXmlDetector() { companion object { const val SCHEMA = "http://schemas.android.com/apk/res-auto" - // TODO: const val ERROR_MESSAGE = "All buttons must have a style, try using NeutralButton or similar." diff --git a/taskcluster/ci/ui-test/kind.yml b/taskcluster/ci/ui-test/kind.yml index 4426d8989..66665abb2 100644 --- a/taskcluster/ci/ui-test/kind.yml +++ b/taskcluster/ci/ui-test/kind.yml @@ -11,44 +11,56 @@ transforms: job-defaults: attributes: + build-type: debug code-review: true retrigger: true + dependencies: + signing: signing-debug + signing-android-test: signing-android-test-debug + include-pull-request-number: true + run: + commands: + - [wget, {artifact-reference: ''}, '-O', app.apk] + - [wget, {artifact-reference: ''}, '-O', android-test.apk] + secrets: + - name: project/mobile/fenix/firebase + key: firebaseToken + path: .firebase_token.json + json: true + using: run-commands + use-caches: false + treeherder: + kind: test + platform: 'ui-test/opt' + tier: 2 + worker: + docker-image: {in-tree: ui-tests} + max-run-time: 7200 + env: + GOOGLE_APPLICATION_CREDENTIALS: '.firebase_token.json' + GOOGLE_PROJECT: moz-fenix + artifacts: + - name: public + path: /builds/worker/artifacts + type: directory + worker-type: b-android jobs: x86-debug: - attributes: - build-type: debug - dependencies: - signing: signing-debug - signing-android-test: signing-android-test-debug description: Test Fenix - include-pull-request-number: true run-on-tasks-for: [github-pull-request, github-push] run: commands: - - [wget, {artifact-reference: ''}, '-O', app.apk] - - [wget, {artifact-reference: ''}, '-O', android-test.apk] - [automation/taskcluster/androidTest/ui-test.sh, x86, app.apk, android-test.apk, '50'] - secrets: - - name: project/mobile/fenix/firebase - key: firebaseToken - path: .firebase_token.json - json: true - using: run-commands - use-caches: false treeherder: - kind: test - platform: 'ui-test/opt' symbol: debug(ui-test-x86) - tier: 2 - worker: - docker-image: {in-tree: ui-tests} - max-run-time: 7200 - env: - GOOGLE_APPLICATION_CREDENTIALS: '.firebase_token.json' - GOOGLE_PROJECT: moz-fenix - artifacts: - - name: public - path: /builds/worker/artifacts - type: directory - worker-type: b-android + screenshots-x86: + attributes: + screenshots: true + description: Run UI screenshots tests to keep them up to date + run-on-tasks-for: [] + run: + commands: + - [automation/taskcluster/androidTest/ui-test.sh, x86-screenshots-tests, app.apk, android-test.apk, '-1'] + treeherder: + symbol: debug(screenshots-x86) diff --git a/taskcluster/fenix_taskgraph/target_tasks.py b/taskcluster/fenix_taskgraph/target_tasks.py index 9694d1b38..66242ebff 100644 --- a/taskcluster/fenix_taskgraph/target_tasks.py +++ b/taskcluster/fenix_taskgraph/target_tasks.py @@ -73,3 +73,13 @@ def target_tasks_bump_android_components(full_task_graph, parameters, graph_conf return task.attributes.get("bump-type", "") == "android-components" return [l for l, t in full_task_graph.tasks.iteritems() if filter(t, parameters)] + + +@_target_task("screenshots") +def target_tasks_screnshots(full_task_graph, parameters, graph_config): + """Select the set of tasks required to generate screenshots on a real device.""" + + def filter(task, parameters): + return task.attributes.get("screenshots", False) + + return [l for l, t in full_task_graph.tasks.iteritems() if filter(t, parameters)]