From 1dde44034480b74d9153807b42f5d2fe81e70e20 Mon Sep 17 00:00:00 2001 From: Gabriel Luong Date: Mon, 23 Aug 2021 12:25:30 -0400 Subject: [PATCH] For #20893 - Search term groups in history --- .../java/org/mozilla/fenix/ui/HistoryTest.kt | 86 +-------- .../java/org/mozilla/fenix/ui/SmokeTest.kt | 119 +----------- .../mozilla/fenix/ui/robots/HistoryRobot.kt | 29 +-- .../ui/robots/RecentlyClosedTabsRobot.kt | 129 +------------ .../org/mozilla/fenix/BrowserDirection.kt | 1 + .../java/org/mozilla/fenix/FeatureFlags.kt | 5 + .../java/org/mozilla/fenix/HomeActivity.kt | 11 +- .../history/PagedHistoryProvider.kt | 169 +++++++++++++++--- .../fenix/library/history/HistoryAdapter.kt | 14 +- .../library/history/HistoryController.kt | 78 ++++---- .../library/history/HistoryDataSource.kt | 30 +--- .../history/HistoryDataSourceFactory.kt | 4 +- .../fenix/library/history/HistoryFragment.kt | 46 ++--- .../library/history/HistoryFragmentStore.kt | 80 +++++++-- .../library/history/HistoryInteractor.kt | 57 +----- .../fenix/library/history/HistoryItemMenu.kt | 68 ------- .../fenix/library/history/HistoryViewModel.kt | 4 +- .../viewholders/HistoryListItemViewHolder.kt | 61 ++++--- .../HistoryMetadataGroupFragment.kt | 168 +++++++++++++++++ .../HistoryMetadataGroupFragmentStore.kt | 86 +++++++++ .../HistoryMetadataGroupController.kt | 99 ++++++++++ .../HistoryMetadataGroupInteractor.kt | 80 +++++++++ .../view/HistoryMetadataGroupAdapter.kt | 54 ++++++ .../HistoryMetadataGroupItemViewHolder.kt | 55 ++++++ .../view/HistoryMetadataGroupView.kt | 59 ++++++ .../RecentlyClosedController.kt | 20 --- .../recentlyclosed/RecentlyClosedFragment.kt | 10 -- .../RecentlyClosedFragmentInteractor.kt | 20 --- .../RecentlyClosedFragmentView.kt | 35 ---- .../RecentlyClosedItemViewHolder.kt | 39 ++-- .../component_history_metadata_group.xml | 33 ++++ .../fragment_history_metadata_group.xml | 9 + .../history_metadata_group_list_item.xml | 16 ++ app/src/main/res/layout/library_site_item.xml | 4 +- app/src/main/res/navigation/nav_graph.xml | 15 ++ app/src/main/res/values/strings.xml | 17 +- .../history/PagedHistoryProviderTest.kt | 90 +++++++++- .../library/history/HistoryControllerTest.kt | 94 +--------- .../history/HistoryFragmentStoreTest.kt | 4 +- .../library/history/HistoryInteractorTest.kt | 41 +---- .../library/history/HistoryItemMenuTest.kt | 76 -------- .../HistoryMetadataGroupFragmentStoreTest.kt | 82 +++++++++ .../HistoryMetadataGroupControllerTest.kt | 132 ++++++++++++++ .../HistoryMetadataGroupItemViewHolderTest.kt | 50 ++++++ .../DefaultRecentlyClosedControllerTest.kt | 55 +----- .../RecentlyClosedFragmentInteractorTest.kt | 51 ------ 46 files changed, 1399 insertions(+), 1086 deletions(-) delete mode 100644 app/src/main/java/org/mozilla/fenix/library/history/HistoryItemMenu.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragmentStore.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/historymetadata/controller/HistoryMetadataGroupController.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/historymetadata/interactor/HistoryMetadataGroupInteractor.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupAdapter.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupItemViewHolder.kt create mode 100644 app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupView.kt create mode 100644 app/src/main/res/layout/component_history_metadata_group.xml create mode 100644 app/src/main/res/layout/fragment_history_metadata_group.xml create mode 100644 app/src/main/res/layout/history_metadata_group_list_item.xml delete mode 100644 app/src/test/java/org/mozilla/fenix/library/history/HistoryItemMenuTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragmentStoreTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/library/historymetadata/controller/HistoryMetadataGroupControllerTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupItemViewHolderTest.kt 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 5c5fcbeb974c..6d853f4f68e7 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt @@ -95,87 +95,6 @@ class HistoryTest { } } - @Test - fun copyHistoryItemURLTest() { - val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - navigationToolbar { - }.enterURLAndEnterToBrowser(firstWebPage.url) { - mDevice.waitForIdle() - }.openThreeDotMenu { - }.openHistory { - verifyHistoryListExists() - historyListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1) - IdlingRegistry.getInstance().register(historyListIdlingResource!!) - }.openThreeDotMenu { - }.clickCopy { - verifyCopySnackBarText() - } - } - - @Test - fun shareHistoryItemTest() { - val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - navigationToolbar { - }.enterURLAndEnterToBrowser(firstWebPage.url) { - mDevice.waitForIdle() - }.openThreeDotMenu { - }.openHistory { - verifyHistoryListExists() - historyListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1) - IdlingRegistry.getInstance().register(historyListIdlingResource!!) - }.openThreeDotMenu { - }.clickShare { - verifyShareOverlay() - verifyShareTabFavicon() - verifyShareTabTitle() - verifyShareTabUrl() - } - } - - @Test - fun openHistoryItemInNewTabTest() { - val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - navigationToolbar { - }.enterURLAndEnterToBrowser(firstWebPage.url) { - mDevice.waitForIdle() - }.openThreeDotMenu { - }.openHistory { - verifyHistoryListExists() - historyListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1) - IdlingRegistry.getInstance().register(historyListIdlingResource!!) - }.openThreeDotMenu { - }.clickOpenInNormalTab { - verifyTabTrayIsOpened() - verifyNormalModeSelected() - } - } - - @Test - fun openHistoryItemInNewPrivateTabTest() { - val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - navigationToolbar { - }.enterURLAndEnterToBrowser(firstWebPage.url) { - mDevice.waitForIdle() - }.openThreeDotMenu { - }.openHistory { - verifyHistoryListExists() - historyListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1) - IdlingRegistry.getInstance().register(historyListIdlingResource!!) - }.openThreeDotMenu { - }.clickOpenInPrivateTab { - verifyTabTrayIsOpened() - verifyPrivateModeSelected() - } - } - @Test fun deleteHistoryItemTest() { val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -189,9 +108,8 @@ class HistoryTest { historyListIdlingResource = RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1) IdlingRegistry.getInstance().register(historyListIdlingResource!!) - }.openThreeDotMenu { + clickDeleteHistoryButton() IdlingRegistry.getInstance().unregister(historyListIdlingResource!!) - }.clickDelete { verifyDeleteSnackbarText("Deleted") verifyEmptyHistoryView() } @@ -210,7 +128,7 @@ class HistoryTest { historyListIdlingResource = RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1) IdlingRegistry.getInstance().register(historyListIdlingResource!!) - clickDeleteHistoryButton() + clickDeleteAllHistoryButton() IdlingRegistry.getInstance().unregister(historyListIdlingResource!!) verifyDeleteConfirmationMessage() confirmDeleteAllHistory() 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 f1c8777f50cf..9bfaaa549fa5 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -708,89 +708,6 @@ class SmokeTest { } } - @Test - // Verifies the items from the overflow menu of Recently Closed Tabs - fun recentlyClosedTabsMenuItemsTest() { - val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - homeScreen { - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(website.url) { - mDevice.waitForIdle() - }.openTabDrawer { - closeTab() - }.openTabDrawer { - }.openRecentlyClosedTabs { - waitForListToExist() - recentlyClosedTabsListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1) - IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!) - verifyRecentlyClosedTabsMenuView() - IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!) - openRecentlyClosedTabsThreeDotMenu() - verifyRecentlyClosedTabsMenuCopy() - verifyRecentlyClosedTabsMenuShare() - verifyRecentlyClosedTabsMenuNewTab() - verifyRecentlyClosedTabsMenuPrivateTab() - verifyRecentlyClosedTabsMenuDelete() - } - } - - @Test - // Verifies the Copy option from the Recently Closed Tabs overflow menu - fun copyRecentlyClosedTabsItemTest() { - val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - homeScreen { - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(website.url) { - mDevice.waitForIdle() - }.openTabDrawer { - closeTab() - }.openTabDrawer { - }.openRecentlyClosedTabs { - waitForListToExist() - recentlyClosedTabsListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1) - IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!) - verifyRecentlyClosedTabsMenuView() - IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!) - openRecentlyClosedTabsThreeDotMenu() - verifyRecentlyClosedTabsMenuCopy() - clickCopyRecentlyClosedTabs() - verifyCopyRecentlyClosedTabsSnackBarText() - } - } - - @Test - // Verifies the Share option from the Recently Closed Tabs overflow menu - fun shareRecentlyClosedTabsItemTest() { - val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - homeScreen { - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(website.url) { - mDevice.waitForIdle() - }.openTabDrawer { - closeTab() - }.openTabDrawer { - }.openRecentlyClosedTabs { - waitForListToExist() - recentlyClosedTabsListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1) - IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!) - verifyRecentlyClosedTabsMenuView() - IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!) - openRecentlyClosedTabsThreeDotMenu() - verifyRecentlyClosedTabsMenuShare() - clickShareRecentlyClosedTabs() - verifyShareOverlay() - verifyShareTabTitle("Test_Page_1") - verifyShareTabUrl(website.url) - verifyShareTabFavicon() - } - } - @Test // Verifies the Open in a new tab option from the Recently Closed Tabs overflow menu fun openRecentlyClosedTabsInNewTabTest() { @@ -810,8 +727,6 @@ class SmokeTest { IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!) verifyRecentlyClosedTabsMenuView() IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!) - openRecentlyClosedTabsThreeDotMenu() - verifyRecentlyClosedTabsMenuNewTab() }.clickOpenInNewTab { verifyUrl(website.url.toString()) }.openTabDrawer { @@ -820,35 +735,7 @@ class SmokeTest { } @Test - // Verifies the Open in a private tab option from the Recently Closed Tabs overflow menu - fun openRecentlyClosedTabsInNewPrivateTabTest() { - val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) - - homeScreen { - }.openNavigationToolbar { - }.enterURLAndEnterToBrowser(website.url) { - mDevice.waitForIdle() - }.openTabDrawer { - closeTab() - }.openTabDrawer { - }.openRecentlyClosedTabs { - waitForListToExist() - recentlyClosedTabsListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1) - IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!) - verifyRecentlyClosedTabsMenuView() - IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!) - openRecentlyClosedTabsThreeDotMenu() - verifyRecentlyClosedTabsMenuPrivateTab() - }.clickOpenInPrivateTab { - verifyUrl(website.url.toString()) - }.openTabDrawer { - verifyPrivateModeSelected() - } - } - - @Test - // Verifies the delete option from the Recently Closed Tabs overflow menu + // Verifies the delete button from the Recently Closed Tabs fun deleteRecentlyClosedTabsItemTest() { val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -866,9 +753,7 @@ class SmokeTest { IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!) verifyRecentlyClosedTabsMenuView() IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!) - openRecentlyClosedTabsThreeDotMenu() - verifyRecentlyClosedTabsMenuDelete() - clickDeleteCopyRecentlyClosedTabs() + clickDeleteRecentlyClosedTabs() verifyEmptyRecentlyClosedTabsList() } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt index b1fd19dc9178..45e1e3e9907a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt @@ -61,20 +61,12 @@ class HistoryRobot { fun verifyHomeScreen() = HomeScreenRobot().verifyHomeScreen() - fun openOverflowMenu() { - mDevice.waitNotNull( - Until.findObject( - By.res("org.mozilla.fenix.debug:id/overflow_menu") - ), - waitingTime - ) - threeDotMenu().click() - } - fun clickDeleteHistoryButton() { - deleteAllHistoryButton().click() + deleteButton().click() } + fun clickDeleteAllHistoryButton() = deleteAllButton().click() + fun confirmDeleteAllHistory() { onView(withText("Delete")) .inRoot(isDialog()) @@ -91,15 +83,6 @@ class HistoryRobot { BrowserRobot().interact() return BrowserRobot.Transition() } - - fun openThreeDotMenu(interact: ThreeDotMenuHistoryItemRobot.() -> Unit): - ThreeDotMenuHistoryItemRobot.Transition { - - threeDotMenu().click() - - ThreeDotMenuHistoryItemRobot().interact() - return ThreeDotMenuHistoryItemRobot.Transition() - } } } @@ -112,11 +95,11 @@ private fun testPageTitle() = onView(allOf(withId(R.id.title), withText("Test_Pa private fun pageUrl() = onView(withId(R.id.url)) -private fun threeDotMenu() = onView(withId(R.id.overflow_menu)) +private fun deleteButton() = onView(withId(R.id.overflow_menu)) -private fun snackBarText() = onView(withId(R.id.snackbar_text)) +private fun deleteAllButton() = onView(withId(R.id.history_delete_all)) -private fun deleteAllHistoryButton() = onView(withId(R.id.history_delete_all)) +private fun snackBarText() = onView(withId(R.id.snackbar_text)) private fun assertHistoryMenuView() { onView( diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt index b112db7e2eb6..d3984227c5ce 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt @@ -41,44 +41,11 @@ class RecentlyClosedTabsRobot { fun verifyRecentlyClosedTabsUrl(expectedUrl: Uri) = assertPageUrl(expectedUrl) - fun openRecentlyClosedTabsThreeDotMenu() = recentlyClosedTabsThreeDotButton().click() - - fun verifyRecentlyClosedTabsMenuCopy() = assertRecentlyClosedTabsMenuCopy() - - fun verifyRecentlyClosedTabsMenuShare() = assertRecentlyClosedTabsMenuShare() - - fun verifyRecentlyClosedTabsMenuNewTab() = assertRecentlyClosedTabsOverlayNewTab() - - fun verifyRecentlyClosedTabsMenuPrivateTab() = assertRecentlyClosedTabsMenuPrivateTab() - - fun verifyRecentlyClosedTabsMenuDelete() = assertRecentlyClosedTabsMenuDelete() - - fun clickCopyRecentlyClosedTabs() = recentlyClosedTabsCopyButton().click() - - fun clickShareRecentlyClosedTabs() = recentlyClosedTabsShareButton().click() - - fun clickDeleteCopyRecentlyClosedTabs() = recentlyClosedTabsDeleteButton().click() - - fun verifyCopyRecentlyClosedTabsSnackBarText() = assertCopySnackBarText() - - fun verifyShareOverlay() = assertRecentlyClosedShareOverlay() - - fun verifyShareTabFavicon() = assertRecentlyClosedShareFavicon() - - fun verifyShareTabTitle(title: String) = assetRecentlyClosedShareTitle(title) - - fun verifyShareTabUrl(expectedUrl: Uri) = assertRecentlyClosedShareUrl(expectedUrl) + fun clickDeleteRecentlyClosedTabs() = recentlyClosedTabsDeleteButton().click() class Transition { fun clickOpenInNewTab(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - recentlyClosedTabsNewTabButton().click() - - BrowserRobot().interact() - return BrowserRobot.Transition() - } - - fun clickOpenInPrivateTab(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - recentlyClosedTabsNewPrivateTabButton().click() + recentlyClosedTabsPageTitle().click() BrowserRobot().interact() return BrowserRobot.Transition() @@ -138,7 +105,7 @@ private fun assertRecentlyClosedTabsPageTitle(title: String) { ) } -private fun recentlyClosedTabsThreeDotButton() = +private fun recentlyClosedTabsDeleteButton() = onView( allOf( withId(R.id.overflow_menu), @@ -147,93 +114,3 @@ private fun recentlyClosedTabsThreeDotButton() = ) ) ) - -private fun assertRecentlyClosedTabsMenuCopy() = - onView(withText("Copy")) - .check( - matches( - withEffectiveVisibility(Visibility.VISIBLE) - ) - ) - -private fun assertRecentlyClosedTabsMenuShare() = - onView(withText("Share")) - .check( - matches( - withEffectiveVisibility(Visibility.VISIBLE) - ) - ) - -private fun assertRecentlyClosedTabsOverlayNewTab() = - onView(withText("Open in new tab")) - .check( - matches( - withEffectiveVisibility(Visibility.VISIBLE) - ) - ) - -private fun assertRecentlyClosedTabsMenuPrivateTab() = - onView(withText("Open in private tab")) - .check( - matches( - withEffectiveVisibility(Visibility.VISIBLE) - ) - ) - -private fun assertRecentlyClosedTabsMenuDelete() = - onView(withText("Delete")) - .check( - matches( - withEffectiveVisibility(Visibility.VISIBLE) - ) - ) - -private fun recentlyClosedTabsCopyButton() = onView(withText("Copy")) - -private fun copySnackBarText() = onView(withId(R.id.snackbar_text)) - -private fun assertCopySnackBarText() = copySnackBarText() - .check( - matches - (withText("URL copied")) - ) - -private fun recentlyClosedTabsShareButton() = onView(withText("Share")) - -private fun assertRecentlyClosedShareOverlay() = - onView(withId(R.id.shareWrapper)) - .check( - matches(ViewMatchers.isDisplayed()) - ) - -private fun assetRecentlyClosedShareTitle(title: String) = - onView(withId(R.id.share_tab_title)) - .check( - matches(ViewMatchers.isDisplayed()) - ) - .check( - matches(withText(title)) - ) - -private fun assertRecentlyClosedShareFavicon() = - onView(withId(R.id.share_tab_favicon)) - .check( - matches(ViewMatchers.isDisplayed()) - ) - -private fun assertRecentlyClosedShareUrl(expectedUrl: Uri) = - onView( - allOf( - withId(R.id.share_tab_url), - withEffectiveVisibility(Visibility.VISIBLE) - ) - ) - .check( - matches(withText(Matchers.containsString(expectedUrl.toString()))) - ) - -private fun recentlyClosedTabsNewTabButton() = onView(withText("Open in new tab")) - -private fun recentlyClosedTabsNewPrivateTabButton() = onView(withText("Open in private tab")) - -private fun recentlyClosedTabsDeleteButton() = onView(withText("Delete")) diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index 23cafc54b3ba..bad10a707bfb 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -20,6 +20,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) { FromSettings(R.id.settingsFragment), FromBookmarks(R.id.bookmarkFragment), FromHistory(R.id.historyFragment), + FromHistoryMetadataGroup(R.id.historyMetadataGroupFragment), FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment), FromAbout(R.id.aboutFragment), FromTrackingProtection(R.id.trackingProtectionFragment), diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index de4a63296bc9..e6e6f1f8abd3 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -68,4 +68,9 @@ object FeatureFlags { * Identifies and separates the tabs list with a group containing search term tabs. */ val tabGroupFeature = Config.channel.isNightlyOrDebug + + /** + * Enables showing search groupings in the History. + */ + val showHistorySearchGroups = Config.channel.isNightlyOrDebug } diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 04c5ad4f3ac6..2e139ea57b9a 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -15,11 +15,11 @@ import android.os.StrictMode import android.os.SystemClock import android.text.format.DateUtils import android.util.AttributeSet +import android.view.ActionMode import android.view.KeyEvent import android.view.LayoutInflater -import android.view.View -import android.view.ActionMode import android.view.MotionEvent +import android.view.View import android.view.ViewConfiguration import android.view.WindowManager.LayoutParams.FLAG_SECURE import androidx.annotation.CallSuper @@ -35,12 +35,12 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.state.action.ContentAction @@ -96,6 +96,7 @@ import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections import org.mozilla.fenix.library.bookmarks.DesktopFolders import org.mozilla.fenix.library.history.HistoryFragmentDirections +import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker import org.mozilla.fenix.perf.MarkersLifecycleCallbacks @@ -779,6 +780,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromHistory -> HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId) + BrowserDirection.FromHistoryMetadataGroup -> + HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromTrackingProtectionExceptions -> TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromAbout -> diff --git a/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt b/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt index 32f943936ca6..1d795805991d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt @@ -4,51 +4,164 @@ package org.mozilla.fenix.components.history -import mozilla.components.concept.storage.HistoryStorage +import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.concept.storage.VisitInfo import mozilla.components.concept.storage.VisitType +import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl +import org.mozilla.fenix.FeatureFlags +import org.mozilla.fenix.library.history.History import org.mozilla.fenix.perf.runBlockingIncrement /** - * An Interface for providing a paginated list of [VisitInfo] + * An Interface for providing a paginated list of [History]. */ interface PagedHistoryProvider { /** - * Gets a list of [VisitInfo] + * Gets a list of [History]. + * * @param offset How much to offset the list by * @param numberOfItems How many items to fetch - * @param onComplete A callback that returns the list of [VisitInfo] + * @param onComplete A callback that returns the list of [History] */ - fun getHistory(offset: Long, numberOfItems: Long, onComplete: (List) -> Unit) + fun getHistory(offset: Long, numberOfItems: Long, onComplete: (List) -> Unit) } -// A PagedList DataSource runs on a background thread automatically. -// If we run this in our own coroutineScope it breaks the PagedList -fun HistoryStorage.createSynchronousPagedHistoryProvider(): PagedHistoryProvider { - return object : PagedHistoryProvider { - - override fun getHistory( - offset: Long, - numberOfItems: Long, - onComplete: (List) -> Unit - ) { - runBlockingIncrement { - val history = getVisitsPaginated( - offset, - numberOfItems, - excludeTypes = listOf( - VisitType.NOT_A_VISIT, - VisitType.DOWNLOAD, - VisitType.REDIRECT_TEMPORARY, - VisitType.RELOAD, - VisitType.EMBED, - VisitType.FRAMED_LINK, - VisitType.REDIRECT_PERMANENT +/** + * @param historyStorage + */ +class DefaultPagedHistoryProvider( + private val historyStorage: PlacesHistoryStorage, + private val showHistorySearchGroups: Boolean = FeatureFlags.showHistorySearchGroups, +) : PagedHistoryProvider { + + private var historyGroups: List? = null + + @Suppress("LongMethod") + override fun getHistory( + offset: Long, + numberOfItems: Long, + onComplete: (List) -> Unit, + ) { + // A PagedList DataSource runs on a background thread automatically. + // If we run this in our own coroutineScope it breaks the PagedList + runBlockingIncrement { + val history: List + + if (showHistorySearchGroups) { + // We need to refetch all the history metadata if the offset resets back at 0 + // in the case of a pull to refresh. + if (historyGroups == null || offset == 0L) { + + historyGroups = historyStorage.getHistoryMetadataSince(Long.MIN_VALUE) + .filter { it.key.searchTerm != null } + .groupBy { it.key.searchTerm!! } + .map { (searchTerm, items) -> + History.Group( + id = items.first().createdAt.toInt(), + title = searchTerm, + visitedAt = items.first().updatedAt, + items = items.map { + History.Metadata( + id = it.createdAt.toInt(), + title = it.title?.takeIf(String::isNotEmpty) + ?: it.key.url.tryGetHostFromUrl(), + url = it.key.url, + visitedAt = it.createdAt, + totalViewTime = it.totalViewTime + ) + } + ) + } + } + + history = getHistoryAndSearchGroups(offset, numberOfItems) + } else { + history = historyStorage + .getVisitsPaginated( + offset, + numberOfItems, + excludeTypes = listOf( + VisitType.NOT_A_VISIT, + VisitType.DOWNLOAD, + VisitType.REDIRECT_TEMPORARY, + VisitType.RELOAD, + VisitType.EMBED, + VisitType.FRAMED_LINK, + VisitType.REDIRECT_PERMANENT + ) ) + .mapIndexed(transformVisitInfoToHistoryItem(offset.toInt())) + } + + onComplete(history) + } + } + + @Suppress("MagicNumber") + private suspend fun getHistoryAndSearchGroups( + offset: Long, + numberOfItems: Long, + ): List { + val result = mutableListOf() + val history: List = historyStorage + .getVisitsPaginated( + offset, + numberOfItems, + excludeTypes = listOf( + VisitType.NOT_A_VISIT, + VisitType.DOWNLOAD, + VisitType.REDIRECT_TEMPORARY, + VisitType.RELOAD, + VisitType.EMBED, + VisitType.FRAMED_LINK, + VisitType.REDIRECT_PERMANENT ) + ) + .mapIndexed(transformVisitInfoToHistoryItem(offset.toInt())) + + // History metadata items are recorded after their associated visited info, we add an + // additional buffer time to the most recent visit to account for a history group + // appearing as the most recent item. + val visitedAtBuffer = if (offset == 0L) 15000 else 0 /* 15 seconds in ms */ + + // Get the history groups that fit within the range of visited times in the current history + // items. + val historyGroupsInOffset = if (history.isNotEmpty()) { + historyGroups?.filter { + history.last().visitedAt <= it.visitedAt && + it.visitedAt <= (history.first().visitedAt + visitedAtBuffer) + } ?: emptyList() + } else { + emptyList() + } + val historyMetadata = historyGroupsInOffset.flatMap { it.items } - onComplete(history) + // Add all items that are not in a group filtering out any matches with a history metadata + // item. + result.addAll(history.filter { item -> historyMetadata.find { it.url == item.url } == null }) + + // Filter history metadata items with no view time. + result.addAll( + historyGroupsInOffset.map { group -> + group.copy(items = group.items.filter { it.totalViewTime > 0 }) } + ) + + return result.sortedByDescending { it.visitedAt } + } + + private fun transformVisitInfoToHistoryItem(offset: Int): (id: Int, visit: VisitInfo) -> History.Regular { + return { id, visit -> + val title = visit.title + ?.takeIf(String::isNotEmpty) + ?: visit.url.tryGetHostFromUrl() + + History.Regular( + id = offset + id, + title = title, + url = visit.url, + visitedAt = visit.visitTime + ) } } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt index 7b111cda5acf..5e535de4b0bc 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt @@ -30,8 +30,8 @@ enum class HistoryItemTimeGroup { class HistoryAdapter( private val historyInteractor: HistoryInteractor, -) : PagedListAdapter(historyDiffCallback), - SelectionHolder { +) : PagedListAdapter(historyDiffCallback), + SelectionHolder { private var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal override val selectedItems get() = mode.selectedItems @@ -102,7 +102,7 @@ class HistoryAdapter( return calendar.time } - private fun timeGroupForHistoryItem(item: HistoryItem): HistoryItemTimeGroup { + private fun timeGroupForHistoryItem(item: History): HistoryItemTimeGroup { return when { DateUtils.isToday(item.visitedAt) -> HistoryItemTimeGroup.Today yesterdayRange.contains(item.visitedAt) -> HistoryItemTimeGroup.Yesterday @@ -112,16 +112,16 @@ class HistoryAdapter( } } - private val historyDiffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { + private val historyDiffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: History, newItem: History): Boolean { return oldItem == newItem } - override fun areContentsTheSame(oldItem: HistoryItem, newItem: HistoryItem): Boolean { + override fun areContentsTheSame(oldItem: History, newItem: History): Boolean { return oldItem == newItem } - override fun getChangePayload(oldItem: HistoryItem, newItem: HistoryItem): Any? { + override fun getChangePayload(oldItem: History, newItem: History): Any? { return newItem } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt index 1f72c14eb99c..7e033e69e789 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt @@ -4,32 +4,23 @@ package org.mozilla.fenix.library.history -import android.content.ClipData -import android.content.ClipboardManager -import android.content.res.Resources import androidx.navigation.NavController import androidx.navigation.NavOptions import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import mozilla.components.concept.engine.prompt.ShareData import org.mozilla.fenix.R -import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController @Suppress("TooManyFunctions") interface HistoryController { - fun handleOpen(item: HistoryItem) - fun handleOpenInNewTab(item: HistoryItem, mode: BrowsingMode) - fun handleSelect(item: HistoryItem) - fun handleDeselect(item: HistoryItem) + fun handleOpen(item: History) + fun handleSelect(item: History) + fun handleDeselect(item: History) fun handleBackPressed(): Boolean fun handleModeSwitched() fun handleDeleteAll() - fun handleDeleteSome(items: Set) - fun handleCopyUrl(item: HistoryItem) - fun handleShare(item: HistoryItem) + fun handleDeleteSome(items: Set) fun handleRequestSync() fun handleEnterRecentlyClosed() } @@ -38,35 +29,45 @@ interface HistoryController { class DefaultHistoryController( private val store: HistoryFragmentStore, private val navController: NavController, - private val resources: Resources, - private val snackbar: FenixSnackbar, - private val clipboardManager: ClipboardManager, private val scope: CoroutineScope, - private val openToBrowser: (item: HistoryItem) -> Unit, - private val openInNewTab: (item: HistoryItem, mode: BrowsingMode) -> Unit, + private val openToBrowser: (item: History.Regular) -> Unit, private val displayDeleteAll: () -> Unit, private val invalidateOptionsMenu: () -> Unit, - private val deleteHistoryItems: (Set) -> Unit, + private val deleteHistoryItems: (Set) -> Unit, private val syncHistory: suspend () -> Unit, private val metrics: MetricController ) : HistoryController { - override fun handleOpen(item: HistoryItem) { - openToBrowser(item) - } - override fun handleOpenInNewTab(item: HistoryItem, mode: BrowsingMode) { - openInNewTab(item, mode) + override fun handleOpen(item: History) { + when (item) { + is History.Regular -> openToBrowser(item) + is History.Group -> { + navController.navigate( + HistoryFragmentDirections.actionGlobalHistoryMetadataGroup( + title = item.title, + historyMetadataItems = item.items.toTypedArray() + ), + NavOptions.Builder().setPopUpTo(R.id.historyMetadataGroupFragment, true).build() + ) + } + else -> { /* noop */ } + } } - override fun handleSelect(item: HistoryItem) { + override fun handleSelect(item: History) { if (store.state.mode === HistoryFragmentState.Mode.Syncing) { return } - store.dispatch(HistoryFragmentAction.AddItemForRemoval(item)) + + if (item is History.Regular) { + store.dispatch(HistoryFragmentAction.AddItemForRemoval(item)) + } } - override fun handleDeselect(item: HistoryItem) { - store.dispatch(HistoryFragmentAction.RemoveItemForRemoval(item)) + override fun handleDeselect(item: History) { + if (item is History.Regular) { + store.dispatch(HistoryFragmentAction.RemoveItemForRemoval(item)) + } } override fun handleBackPressed(): Boolean { @@ -86,27 +87,12 @@ class DefaultHistoryController( displayDeleteAll.invoke() } - override fun handleDeleteSome(items: Set) { - deleteHistoryItems.invoke(items) - } - - override fun handleCopyUrl(item: HistoryItem) { - val urlClipData = ClipData.newPlainText(item.url, item.url) - clipboardManager.setPrimaryClip(urlClipData) - with(snackbar) { - setText(resources.getString(R.string.url_copied)) - show() + override fun handleDeleteSome(items: Set) { + items.filterIsInstance().let { + deleteHistoryItems.invoke(it.toSet()) } } - override fun handleShare(item: HistoryItem) { - navController.navigate( - HistoryFragmentDirections.actionGlobalShareFragment( - data = arrayOf(ShareData(url = item.url, title = item.title)) - ) - ) - } - override fun handleRequestSync() { scope.launch { store.dispatch(HistoryFragmentAction.StartSync) diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryDataSource.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryDataSource.kt index 2c5d52176efe..a9b7c6008955 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryDataSource.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryDataSource.kt @@ -5,49 +5,35 @@ package org.mozilla.fenix.library.history import androidx.paging.ItemKeyedDataSource -import mozilla.components.concept.storage.VisitInfo -import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import org.mozilla.fenix.components.history.PagedHistoryProvider class HistoryDataSource( private val historyProvider: PagedHistoryProvider -) : ItemKeyedDataSource() { +) : ItemKeyedDataSource() { - // Because the pagination is not based off of they key + // Because the pagination is not based off of the key // we want to start at 1, not 0 to be able to send the correct offset // to the `historyProvider.getHistory` call. - override fun getKey(item: HistoryItem): Int = item.id + 1 + override fun getKey(item: History): Int = item.id + 1 override fun loadInitial( params: LoadInitialParams, - callback: LoadInitialCallback + callback: LoadInitialCallback ) { historyProvider.getHistory(INITIAL_OFFSET, params.requestedLoadSize.toLong()) { history -> - val items = history.mapIndexed(transformVisitInfoToHistoryItem(INITIAL_OFFSET.toInt())) - callback.onResult(items) + callback.onResult(history) } } - override fun loadAfter(params: LoadParams, callback: LoadCallback) { + override fun loadAfter(params: LoadParams, callback: LoadCallback) { historyProvider.getHistory(params.key.toLong(), params.requestedLoadSize.toLong()) { history -> - val items = history.mapIndexed(transformVisitInfoToHistoryItem(params.key)) - callback.onResult(items) + callback.onResult(history) } } - override fun loadBefore(params: LoadParams, callback: LoadCallback) { /* noop */ } + override fun loadBefore(params: LoadParams, callback: LoadCallback) { /* noop */ } companion object { private const val INITIAL_OFFSET = 0L - - fun transformVisitInfoToHistoryItem(offset: Int): (id: Int, visit: VisitInfo) -> HistoryItem { - return { id, visit -> - val title = visit.title - ?.takeIf(String::isNotEmpty) - ?: visit.url.tryGetHostFromUrl() - - HistoryItem(offset + id, title, visit.url, visit.visitTime) - } - } } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryDataSourceFactory.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryDataSourceFactory.kt index 78eb2fc5e857..64aeba2299d7 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryDataSourceFactory.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryDataSourceFactory.kt @@ -10,10 +10,10 @@ import org.mozilla.fenix.components.history.PagedHistoryProvider class HistoryDataSourceFactory( private val historyProvider: PagedHistoryProvider -) : DataSource.Factory() { +) : DataSource.Factory() { val datasource = MutableLiveData() - override fun create(): DataSource { + override fun create(): DataSource { val datasource = HistoryDataSource(historyProvider) this.datasource.postValue(datasource) return datasource diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index e308c1e83b8d..a79cbdbd0b0f 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -4,8 +4,6 @@ package org.mozilla.fenix.library.history -import android.content.ClipboardManager -import android.content.Context.CLIPBOARD_SERVICE import android.content.DialogInterface import android.os.Bundle import android.text.SpannableString @@ -38,9 +36,8 @@ import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.R import org.mozilla.fenix.addons.showSnackBar import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.StoreProvider -import org.mozilla.fenix.components.history.createSynchronousPagedHistoryProvider +import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.databinding.FragmentHistoryBinding import org.mozilla.fenix.ext.components @@ -52,7 +49,7 @@ import org.mozilla.fenix.library.LibraryPageFragment import org.mozilla.fenix.utils.allowUndo @SuppressWarnings("TooManyFunctions", "LargeClass") -class HistoryFragment : LibraryPageFragment(), UserInteractionHandler { +class HistoryFragment : LibraryPageFragment(), UserInteractionHandler { private lateinit var historyStore: HistoryFragmentStore private lateinit var historyInteractor: HistoryInteractor private lateinit var viewModel: HistoryViewModel @@ -85,16 +82,8 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl val historyController: HistoryController = DefaultHistoryController( store = historyStore, navController = findNavController(), - resources = resources, - snackbar = FenixSnackbar.make( - view = view, - duration = FenixSnackbar.LENGTH_LONG, - isDisplayedWithBrowserToolbar = false - ), - clipboardManager = activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager, scope = lifecycleScope, openToBrowser = ::openItem, - openInNewTab = ::openItemAndShowTray, displayDeleteAll = ::displayDeleteAllDialog, invalidateOptionsMenu = ::invalidateOptionsMenu, deleteHistoryItems = ::deleteHistoryItems, @@ -122,7 +111,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl super.onCreate(savedInstanceState) viewModel = HistoryViewModel( - requireComponents.core.historyStorage.createSynchronousPagedHistoryProvider() + historyProvider = DefaultPagedHistoryProvider(requireComponents.core.historyStorage) ) viewModel.userHasHistory.observe( @@ -137,7 +126,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl setHasOptionsMenu(true) } - private fun deleteHistoryItems(items: Set) { + private fun deleteHistoryItems(items: Set) { updatePendingHistoryToDelete(items) undoScope = CoroutineScope(IO) undoScope?.allowUndo( @@ -200,6 +189,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl } R.id.open_history_in_new_tabs_multi_select -> { openItemsInNewTab { selectedItem -> + selectedItem as History.Regular requireComponents.analytics.metrics.track(Event.HistoryOpenedInNewTabs) selectedItem.url } @@ -209,6 +199,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl } R.id.open_history_in_private_tabs_multi_select -> { openItemsInNewTab(private = true) { selectedItem -> + selectedItem as History.Regular requireComponents.analytics.metrics.track(Event.HistoryOpenedInPrivateTabs) selectedItem.url } @@ -236,7 +227,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl ) } - private fun getMultiSelectSnackBarMessage(historyItems: Set): String { + private fun getMultiSelectSnackBarMessage(historyItems: Set): String { return if (historyItems.size > 1) { getString(R.string.history_delete_multiple_items_snackbar) } else { @@ -265,7 +256,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl _binding = null } - private fun openItem(item: HistoryItem) { + private fun openItem(item: History.Regular) { requireComponents.analytics.metrics.track(Event.HistoryItemOpened) (activity as HomeActivity).openToBrowserAndLoad( @@ -275,21 +266,6 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl ) } - private fun openItemAndShowTray(item: HistoryItem, mode: BrowsingMode) { - when (mode.isPrivate) { - true -> requireComponents.analytics.metrics.track(Event.HistoryOpenedInPrivateTab) - false -> requireComponents.analytics.metrics.track(Event.HistoryOpenedInNewTab) - } - - val homeActivity = activity as HomeActivity - homeActivity.browsingModeManager.mode = mode - homeActivity.components.useCases.tabsUseCases.addTab.invoke( - item.url, private = (mode == BrowsingMode.Private) - ) - - showTabTray() - } - private fun displayDeleteAllDialog() { activity?.let { activity -> AlertDialog.Builder(activity).apply { @@ -342,7 +318,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl ) } - private fun getDeleteHistoryItemsOperation(items: Set): (suspend () -> Unit) { + private fun getDeleteHistoryItemsOperation(items: Set): (suspend () -> Unit) { return { CoroutineScope(IO).launch { historyStore.dispatch(HistoryFragmentAction.EnterDeletionMode) @@ -358,13 +334,13 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl } } - private fun updatePendingHistoryToDelete(items: Set) { + private fun updatePendingHistoryToDelete(items: Set) { pendingHistoryDeletionJob = getDeleteHistoryItemsOperation(items) val ids = items.map { item -> item.visitedAt }.toSet() historyStore.dispatch(HistoryFragmentAction.AddPendingDeletionSet(ids)) } - private fun undoPendingDeletion(items: Set) { + private fun undoPendingDeletion(items: Set) { pendingHistoryDeletionJob = null val ids = items.map { item -> item.visitedAt }.toSet() historyStore.dispatch(HistoryFragmentAction.UndoPendingDeletionSet(ids)) diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragmentStore.kt index 08d6f3682008..db82e640f21c 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragmentStore.kt @@ -4,18 +4,74 @@ package org.mozilla.fenix.library.history +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import mozilla.components.lib.state.Action import mozilla.components.lib.state.State import mozilla.components.lib.state.Store /** - * Class representing a history entry - * @property id Unique id of the history item - * @property title Title of the history item - * @property url URL of the history item - * @property visitedAt Timestamp of when this history item was visited + * Class representing a history entry. */ -data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long) +sealed class History : Parcelable { + abstract val id: Int + abstract val title: String + abstract val visitedAt: Long + abstract val selected: Boolean + + /** + * A regular history item. + * + * @property id Unique id of the history item. + * @property title Title of the history item. + * @property url URL of the history item. + * @property visitedAt Timestamp of when this history item was visited. + * @property selected Whether or not the history item is selected. + */ + @Parcelize data class Regular( + override val id: Int, + override val title: String, + val url: String, + override val visitedAt: Long, + override val selected: Boolean = false + ) : History() + + /** + * A history metadata item. + * + * @property id Unique id of the history metadata item. + * @property title Title of the history metadata item. + * @property url URL of the history metadata item. + * @property visitedAt Timestamp of when this history metadata item was visited. + * @property totalViewTime Total time the user viewed the page associated with this record. + * @property selected Whether or not the history metadata item is selected. + */ + @Parcelize data class Metadata( + override val id: Int, + override val title: String, + val url: String, + override val visitedAt: Long, + val totalViewTime: Int, + override val selected: Boolean = false + ) : History() + + /** + * A history metadata group. + * + * @property id Unique id of the history metadata group. + * @property title Title of the history metadata group. + * @property visitedAt Timestamp of when this history metadata group was visited. + * @property items List of history metadata items associated with the group. + * @property selected Whether or not the history group is selected. + */ + @Parcelize data class Group( + override val id: Int, + override val title: String, + override val visitedAt: Long, + val items: List, + override val selected: Boolean = false + ) : History() +} /** * The [Store] for holding the [HistoryFragmentState] and applying [HistoryFragmentAction]s. @@ -28,8 +84,8 @@ class HistoryFragmentStore(initialState: HistoryFragmentState) : */ sealed class HistoryFragmentAction : Action { object ExitEditMode : HistoryFragmentAction() - data class AddItemForRemoval(val item: HistoryItem) : HistoryFragmentAction() - data class RemoveItemForRemoval(val item: HistoryItem) : HistoryFragmentAction() + data class AddItemForRemoval(val item: History.Regular) : HistoryFragmentAction() + data class RemoveItemForRemoval(val item: History.Regular) : HistoryFragmentAction() data class AddPendingDeletionSet(val itemIds: Set) : HistoryFragmentAction() data class UndoPendingDeletionSet(val itemIds: Set) : HistoryFragmentAction() object EnterDeletionMode : HistoryFragmentAction() @@ -40,21 +96,21 @@ sealed class HistoryFragmentAction : Action { /** * The state for the History Screen - * @property items List of HistoryItem to display + * @property items List of History to display * @property mode Current Mode of History */ data class HistoryFragmentState( - val items: List, + val items: List, val mode: Mode, val pendingDeletionIds: Set, val isDeletingItems: Boolean ) : State { sealed class Mode { - open val selectedItems = emptySet() + open val selectedItems = emptySet() object Normal : Mode() object Syncing : Mode() - data class Editing(override val selectedItems: Set) : Mode() + data class Editing(override val selectedItems: Set) : Mode() } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt index e72acf29c923..21066f49af9a 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt @@ -4,14 +4,13 @@ package org.mozilla.fenix.library.history -import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.selection.SelectionInteractor /** * Interface for the HistoryInteractor. This interface is implemented by objects that want * to respond to user interaction on the HistoryView */ -interface HistoryInteractor : SelectionInteractor { +interface HistoryInteractor : SelectionInteractor { /** * Called on backpressed to exit edit mode @@ -23,34 +22,6 @@ interface HistoryInteractor : SelectionInteractor { */ fun onModeSwitched() - /** - * Copies the URL of a history item to the copy-paste buffer. - * - * @param item the history item to copy the URL from - */ - fun onCopyPressed(item: HistoryItem) - - /** - * Opens the share sheet for a history item. - * - * @param item the history item to share - */ - fun onSharePressed(item: HistoryItem) - - /** - * Opens a history item in a new tab. - * - * @param item the history item to open in a new tab - */ - fun onOpenInNormalTab(item: HistoryItem) - - /** - * Opens a history item in a private tab. - * - * @param item the history item to open in a private tab - */ - fun onOpenInPrivateTab(item: HistoryItem) - /** * Called when delete all is tapped */ @@ -60,7 +31,7 @@ interface HistoryInteractor : SelectionInteractor { * Called when multiple history items are deleted * @param items the history items to delete */ - fun onDeleteSome(items: Set) + fun onDeleteSome(items: Set) /** * Called when the user requests a sync of the history @@ -81,15 +52,15 @@ interface HistoryInteractor : SelectionInteractor { class DefaultHistoryInteractor( private val historyController: HistoryController ) : HistoryInteractor { - override fun open(item: HistoryItem) { + override fun open(item: History) { historyController.handleOpen(item) } - override fun select(item: HistoryItem) { + override fun select(item: History) { historyController.handleSelect(item) } - override fun deselect(item: HistoryItem) { + override fun deselect(item: History) { historyController.handleDeselect(item) } @@ -101,27 +72,11 @@ class DefaultHistoryInteractor( historyController.handleModeSwitched() } - override fun onCopyPressed(item: HistoryItem) { - historyController.handleCopyUrl(item) - } - - override fun onSharePressed(item: HistoryItem) { - historyController.handleShare(item) - } - - override fun onOpenInNormalTab(item: HistoryItem) { - historyController.handleOpenInNewTab(item, BrowsingMode.Normal) - } - - override fun onOpenInPrivateTab(item: HistoryItem) { - historyController.handleOpenInNewTab(item, BrowsingMode.Private) - } - override fun onDeleteAll() { historyController.handleDeleteAll() } - override fun onDeleteSome(items: Set) { + override fun onDeleteSome(items: Set) { historyController.handleDeleteSome(items) } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryItemMenu.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryItemMenu.kt deleted file mode 100644 index d4e99d5f992d..000000000000 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryItemMenu.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.library.history - -import android.content.Context -import androidx.annotation.VisibleForTesting -import mozilla.components.browser.menu2.BrowserMenuController -import mozilla.components.concept.menu.MenuController -import mozilla.components.concept.menu.candidate.TextMenuCandidate -import mozilla.components.concept.menu.candidate.TextStyle -import mozilla.components.support.ktx.android.content.getColorFromAttr -import org.mozilla.fenix.R - -class HistoryItemMenu( - private val context: Context, - private val onItemTapped: (Item) -> Unit -) { - - enum class Item { - Copy, - Share, - OpenInNewTab, - OpenInPrivateTab, - Delete; - } - - val menuController: MenuController by lazy { - BrowserMenuController().apply { - submitList(menuItems()) - } - } - - @VisibleForTesting - internal fun menuItems(): List { - return listOf( - TextMenuCandidate( - text = context.getString(R.string.history_menu_copy_button) - ) { - onItemTapped.invoke(Item.Copy) - }, - TextMenuCandidate( - text = context.getString(R.string.history_menu_share_button) - ) { - onItemTapped.invoke(Item.Share) - }, - TextMenuCandidate( - text = context.getString(R.string.history_menu_open_in_new_tab_button) - ) { - onItemTapped.invoke(Item.OpenInNewTab) - }, - TextMenuCandidate( - text = context.getString(R.string.history_menu_open_in_private_tab_button) - ) { - onItemTapped.invoke(Item.OpenInPrivateTab) - }, - TextMenuCandidate( - text = context.getString(R.string.history_delete_item), - textStyle = TextStyle( - color = context.getColorFromAttr(R.attr.destructive) - ) - ) { - onItemTapped.invoke(Item.Delete) - } - ) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryViewModel.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryViewModel.kt index 8c758d78085f..f4aa380e4931 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryViewModel.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryViewModel.kt @@ -12,7 +12,7 @@ import androidx.paging.PagedList import org.mozilla.fenix.components.history.PagedHistoryProvider class HistoryViewModel(historyProvider: PagedHistoryProvider) : ViewModel() { - var history: LiveData> + var history: LiveData> var userHasHistory = MutableLiveData(true) private val datasource: LiveData @@ -21,7 +21,7 @@ class HistoryViewModel(historyProvider: PagedHistoryProvider) : ViewModel() { datasource = historyDataSourceFactory.datasource history = LivePagedListBuilder(historyDataSourceFactory, PAGE_SIZE) - .setBoundaryCallback(object : PagedList.BoundaryCallback() { + .setBoundaryCallback(object : PagedList.BoundaryCallback() { override fun onZeroItemsLoaded() { userHasHistory.value = false } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt index d9ee8c1b8fa0..e81bb1d1026c 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt @@ -12,37 +12,40 @@ import org.mozilla.fenix.databinding.HistoryListItemBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.hideAndDisable import org.mozilla.fenix.ext.showAndEnable -import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.library.history.History import org.mozilla.fenix.library.history.HistoryFragmentState import org.mozilla.fenix.library.history.HistoryInteractor -import org.mozilla.fenix.library.history.HistoryItem -import org.mozilla.fenix.library.history.HistoryItemMenu import org.mozilla.fenix.library.history.HistoryItemTimeGroup +import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.utils.Do class HistoryListItemViewHolder( view: View, private val historyInteractor: HistoryInteractor, - private val selectionHolder: SelectionHolder + private val selectionHolder: SelectionHolder, ) : RecyclerView.ViewHolder(view) { - private var item: HistoryItem? = null + private var item: History? = null private val binding = HistoryListItemBinding.bind(view) init { - setupMenu() - binding.recentlyClosedNavEmpty.recentlyClosedNav.setOnClickListener { historyInteractor.onRecentlyClosedClicked() } + + binding.historyLayout.overflowView.setImageResource(R.drawable.ic_close) + binding.historyLayout.overflowView.setOnClickListener { + val item = this.item ?: return@setOnClickListener + historyInteractor.onDeleteSome(setOf(item)) + } } fun bind( - item: HistoryItem, + item: History, timeGroup: HistoryItemTimeGroup?, showTopContent: Boolean, mode: HistoryFragmentState.Mode, - isPendingDeletion: Boolean = false + isPendingDeletion: Boolean = false, ) { if (isPendingDeletion) { binding.historyLayout.visibility = View.GONE @@ -50,8 +53,23 @@ class HistoryListItemViewHolder( binding.historyLayout.visibility = View.VISIBLE } + binding.historyLayout.overflowView.isVisible = item !is History.Group + binding.historyLayout.titleView.text = item.title - binding.historyLayout.urlView.text = item.url + + binding.historyLayout.urlView.text = Do exhaustive when (item) { + is History.Regular -> item.url + is History.Metadata -> item.url + is History.Group -> { + val numChildren = item.items.size + val stringId = if (numChildren == 1) { + R.string.history_search_group_site + } else { + R.string.history_search_group_sites + } + String.format(itemView.context.getString(stringId), numChildren) + } + } toggleTopContent(showTopContent, mode === HistoryFragmentState.Mode.Normal) @@ -61,8 +79,12 @@ class HistoryListItemViewHolder( binding.historyLayout.setSelectionInteractor(item, selectionHolder, historyInteractor) binding.historyLayout.changeSelected(item in selectionHolder.selectedItems) - if (this.item?.url != item.url) { + if (item is History.Regular && + (this.item as? History.Regular)?.url != item.url + ) { binding.historyLayout.loadFavicon(item.url) + } else if (item is History.Group) { + binding.historyLayout.iconView.setImageResource(R.drawable.ic_multiple_tabs) } if (mode is HistoryFragmentState.Mode.Editing) { @@ -85,7 +107,7 @@ class HistoryListItemViewHolder( private fun toggleTopContent( showTopContent: Boolean, - isNormalMode: Boolean + isNormalMode: Boolean, ) { binding.recentlyClosedNavEmpty.recentlyClosedNav.isVisible = showTopContent @@ -110,21 +132,6 @@ class HistoryListItemViewHolder( } } - private fun setupMenu() { - val historyMenu = HistoryItemMenu(itemView.context) { - val item = this.item ?: return@HistoryItemMenu - Do exhaustive when (it) { - HistoryItemMenu.Item.Copy -> historyInteractor.onCopyPressed(item) - HistoryItemMenu.Item.Share -> historyInteractor.onSharePressed(item) - HistoryItemMenu.Item.OpenInNewTab -> historyInteractor.onOpenInNormalTab(item) - HistoryItemMenu.Item.OpenInPrivateTab -> historyInteractor.onOpenInPrivateTab(item) - HistoryItemMenu.Item.Delete -> historyInteractor.onDeleteSome(setOf(item)) - } - } - - binding.historyLayout.attachMenu(historyMenu.menuController) - } - companion object { const val DISABLED_BUTTON_ALPHA = 0.7f const val LAYOUT_ID = R.layout.history_list_item diff --git a/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt b/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt new file mode 100644 index 000000000000..d103364d2575 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. 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.library.historymetadata + +import android.os.Bundle +import android.text.SpannableString +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.support.base.feature.UserInteractionHandler +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.databinding.FragmentHistoryMetadataGroupBinding +import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.ext.setTextColor +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.library.LibraryPageFragment +import org.mozilla.fenix.library.history.History +import org.mozilla.fenix.library.historymetadata.controller.DefaultHistoryMetadataGroupController +import org.mozilla.fenix.library.historymetadata.interactor.DefaultHistoryMetadataGroupInteractor +import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor +import org.mozilla.fenix.library.historymetadata.view.HistoryMetadataGroupView + +/** + * Displays a list of history metadata items for a history metadata search group. + */ +class HistoryMetadataGroupFragment : LibraryPageFragment(), UserInteractionHandler { + + private lateinit var historyMetadataGroupStore: HistoryMetadataGroupFragmentStore + private lateinit var interactor: HistoryMetadataGroupInteractor + + private var _historyMetadataGroupView: HistoryMetadataGroupView? = null + private val historyMetadataGroupView: HistoryMetadataGroupView + get() = _historyMetadataGroupView!! + + private val args by navArgs() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val binding = FragmentHistoryMetadataGroupBinding.inflate(inflater, container, false) + + historyMetadataGroupStore = StoreProvider.get(this) { + HistoryMetadataGroupFragmentStore( + HistoryMetadataGroupFragmentState( + items = args.historyMetadataItems.filterIsInstance() + ) + ) + } + + interactor = DefaultHistoryMetadataGroupInteractor( + controller = DefaultHistoryMetadataGroupController( + activity = activity as HomeActivity, + store = historyMetadataGroupStore, + navController = findNavController() + ) + ) + + _historyMetadataGroupView = HistoryMetadataGroupView( + container = binding.historyMetadataGroupLayout, + interactor = interactor, + title = args.title + ) + + return binding.root + } + + @ExperimentalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + consumeFrom(historyMetadataGroupStore) { state -> + historyMetadataGroupView.update(state) + activity?.invalidateOptionsMenu() + } + } + + override fun onResume() { + super.onResume() + showToolbar(args.title) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + if (selectedItems.isNotEmpty()) { + inflater.inflate(R.menu.history_select_multi, menu) + + menu.findItem(R.id.delete_history_multi_select)?.let { deleteItem -> + deleteItem.title = SpannableString(deleteItem.title).apply { + setTextColor(requireContext(), R.attr.destructive) + } + } + } else { + inflater.inflate(R.menu.history_menu, menu) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.share_history_multi_select -> { + interactor.onShareMenuItem(selectedItems) + true + } + R.id.delete_history_multi_select -> { + interactor.onDeleteMenuItem(selectedItems) + true + } + R.id.open_history_in_new_tabs_multi_select -> { + openItemsInNewTab { selectedItem -> + selectedItem.url + } + + showTabTray() + true + } + R.id.open_history_in_private_tabs_multi_select -> { + openItemsInNewTab(private = true) { selectedItem -> + selectedItem.url + } + + (activity as HomeActivity).apply { + browsingModeManager.mode = BrowsingMode.Private + supportActionBar?.hide() + } + + showTabTray() + true + } + R.id.history_delete_all -> { + interactor.onDeleteAllMenuItem() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _historyMetadataGroupView = null + } + + override val selectedItems: Set get() = + historyMetadataGroupStore.state.items.filter { it.selected }.toSet() + + override fun onBackPressed(): Boolean = interactor.onBackPressed(selectedItems) + + private fun showTabTray() { + findNavController().nav( + R.id.historyMetadataGroupFragment, + HistoryMetadataGroupFragmentDirections.actionGlobalTabsTrayFragment() + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragmentStore.kt new file mode 100644 index 000000000000..b3825e6c9cca --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragmentStore.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.library.historymetadata + +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store +import org.mozilla.fenix.library.history.History + +/** + * The [Store] for holding the [HistoryMetadataGroupFragmentState] and applying + * [HistoryMetadataGroupFragmentAction]s. + */ +class HistoryMetadataGroupFragmentStore(initialState: HistoryMetadataGroupFragmentState) : + Store( + initialState, + ::historyStateReducer + ) + +/** + * Actions to dispatch through the [HistoryMetadataGroupFragmentStore to modify the + * [HistoryMetadataGroupFragmentState] through the [historyStateReducer]. + */ +sealed class HistoryMetadataGroupFragmentAction : Action { + data class UpdateHistoryItems(val items: List) : + HistoryMetadataGroupFragmentAction() + data class Select(val item: History.Metadata) : HistoryMetadataGroupFragmentAction() + data class Deselect(val item: History.Metadata) : HistoryMetadataGroupFragmentAction() + object DeselectAll : HistoryMetadataGroupFragmentAction() +} + +/** + * The state for [HistoryMetadataGroupFragment]. + * + * @property items The list of [History.Metadata] to display. + */ +data class HistoryMetadataGroupFragmentState( + val items: List = emptyList() +) : State + +/** + * Reduces the history metadata state from the current state with the provided [action] to be + * performed. + * + * @param state The current history metadata state. + * @param action The action to be performed on the state. + * @return the new [HistoryMetadataGroupFragmentState] with the [action] executed. + */ +private fun historyStateReducer( + state: HistoryMetadataGroupFragmentState, + action: HistoryMetadataGroupFragmentAction +): HistoryMetadataGroupFragmentState { + return when (action) { + is HistoryMetadataGroupFragmentAction.UpdateHistoryItems -> + state.copy(items = action.items) + is HistoryMetadataGroupFragmentAction.Select -> + state.copy( + items = state.items.toMutableList() + .map { + if (it == action.item) { + it.copy(selected = true) + } else { + it + } + } + ) + is HistoryMetadataGroupFragmentAction.Deselect -> + state.copy( + items = state.items.toMutableList() + .map { + if (it == action.item) { + it.copy(selected = false) + } else { + it + } + } + ) + is HistoryMetadataGroupFragmentAction.DeselectAll -> + state.copy( + items = state.items.toMutableList() + .map { it.copy(selected = false) } + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/historymetadata/controller/HistoryMetadataGroupController.kt b/app/src/main/java/org/mozilla/fenix/library/historymetadata/controller/HistoryMetadataGroupController.kt new file mode 100644 index 000000000000..fc37da602b11 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/historymetadata/controller/HistoryMetadataGroupController.kt @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.historymetadata.controller + +import androidx.navigation.NavController +import mozilla.components.concept.engine.prompt.ShareData +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.library.history.History +import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentAction +import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections +import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentStore + +/** + * An interface that handles the view manipulation of the history metadata group in the History + * metadata group screen. + */ +interface HistoryMetadataGroupController { + + /** + * Opens the given history [item] in a new tab. + * + * @param item The [History] to open in a new tab. + */ + fun handleOpen(item: History.Metadata) + + /** + * Toggles the given history [item] to be selected in multi-select mode. + * + * @param item The [History] to select. + */ + fun handleSelect(item: History.Metadata) + + /** + * Toggles the given history [item] to be deselected in multi-select mode. + * + * @param item The [History] to deselect. + */ + fun handleDeselect(item: History.Metadata) + + /** + * Called on backpressed to deselect all the given [items]. + * + * @param items The set of [History]s to deselect. + */ + fun handleBackPressed(items: Set): Boolean + + /** + * Opens the share sheet for a set of history [items]. + * + * @param items The set of [History]s to share. + */ + fun handleShare(items: Set) +} + +/** + * The default implementation of [HistoryMetadataGroupController]. + */ +class DefaultHistoryMetadataGroupController( + private val activity: HomeActivity, + private val store: HistoryMetadataGroupFragmentStore, + private val navController: NavController, +) : HistoryMetadataGroupController { + + override fun handleOpen(item: History.Metadata) { + activity.openToBrowserAndLoad( + searchTermOrURL = item.url, + newTab = true, + from = BrowserDirection.FromHistoryMetadataGroup + ) + } + + override fun handleSelect(item: History.Metadata) { + store.dispatch(HistoryMetadataGroupFragmentAction.Select(item)) + } + + override fun handleDeselect(item: History.Metadata) { + store.dispatch(HistoryMetadataGroupFragmentAction.Deselect(item)) + } + + override fun handleBackPressed(items: Set): Boolean { + return if (items.isNotEmpty()) { + store.dispatch(HistoryMetadataGroupFragmentAction.DeselectAll) + true + } else { + false + } + } + + override fun handleShare(items: Set) { + navController.navigate( + HistoryMetadataGroupFragmentDirections.actionGlobalShareFragment( + data = items.map { ShareData(url = it.url, title = it.title) }.toTypedArray() + ) + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/historymetadata/interactor/HistoryMetadataGroupInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/historymetadata/interactor/HistoryMetadataGroupInteractor.kt new file mode 100644 index 000000000000..4af92e3abe7b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/historymetadata/interactor/HistoryMetadataGroupInteractor.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.library.historymetadata.interactor + +import org.mozilla.fenix.library.history.History +import org.mozilla.fenix.library.historymetadata.controller.HistoryMetadataGroupController +import org.mozilla.fenix.selection.SelectionInteractor + +/** + * Interface for history metadata group related actions in the History view. + */ +interface HistoryMetadataGroupInteractor : SelectionInteractor { + + /** + * Called on backpressed to deselect all the given [items]. + * + * @param items The set of [History]s to deselect. + */ + fun onBackPressed(items: Set): Boolean + + /** + * Deletes the given set of history [items] that are selected. Called when a user clicks on the + * "Delete" menu item. + * + * @param items The set of [History]s to delete. + */ + fun onDeleteMenuItem(items: Set) + + /** + * Deletes the all the history items in the history metadata group. Called when a user clicks + * on the "Delete history" menu item. + */ + fun onDeleteAllMenuItem() + + /** + * Opens the share sheet for a set of history [items]. Called when a user clicks on the + * "Share" menu item. + * + * @param items The set of [History]s to share. + */ + fun onShareMenuItem(items: Set) +} + +/** + * The default implementation of [HistoryMetadataGroupInteractor]. + */ +class DefaultHistoryMetadataGroupInteractor( + private val controller: HistoryMetadataGroupController +) : HistoryMetadataGroupInteractor { + + override fun open(item: History.Metadata) { + controller.handleOpen(item) + } + + override fun select(item: History.Metadata) { + controller.handleSelect(item) + } + + override fun deselect(item: History.Metadata) { + controller.handleDeselect(item) + } + + override fun onBackPressed(items: Set): Boolean { + return controller.handleBackPressed(items) + } + + override fun onDeleteMenuItem(items: Set) { + // no-op + } + + override fun onDeleteAllMenuItem() { + // no-op + } + + override fun onShareMenuItem(items: Set) { + controller.handleShare(items) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupAdapter.kt new file mode 100644 index 000000000000..e0a5d6bbbe77 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupAdapter.kt @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.historymetadata.view + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import org.mozilla.fenix.library.history.History +import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor +import org.mozilla.fenix.selection.SelectionHolder + +/** + * Adapter for a list of history metadata items to be displayed. + */ +class HistoryMetadataGroupAdapter( + private val interactor: HistoryMetadataGroupInteractor +) : ListAdapter(DiffCallback), + SelectionHolder { + + private var selectedHistoryItems: Set = emptySet() + + override val selectedItems: Set + get() = selectedHistoryItems + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): HistoryMetadataGroupItemViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(HistoryMetadataGroupItemViewHolder.LAYOUT_ID, parent, false) + return HistoryMetadataGroupItemViewHolder(view, interactor, this) + } + + override fun onBindViewHolder(holder: HistoryMetadataGroupItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + fun updateData(items: List) { + this.selectedHistoryItems = items.filter { it.selected }.toSet() + notifyItemRangeChanged(0, items.size) + submitList(items) + } + + internal object DiffCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: History.Metadata, newItem: History.Metadata): Boolean = + oldItem.id == newItem.id + + override fun areItemsTheSame(oldItem: History.Metadata, newItem: History.Metadata): Boolean = + oldItem == newItem + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupItemViewHolder.kt new file mode 100644 index 000000000000..cb536f32e33d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupItemViewHolder.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.library.historymetadata.view + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.HistoryMetadataGroupListItemBinding +import org.mozilla.fenix.ext.hideAndDisable +import org.mozilla.fenix.ext.showAndEnable +import org.mozilla.fenix.library.history.History +import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor +import org.mozilla.fenix.selection.SelectionHolder + +/** + * View holder for a history metadata list item. + */ +class HistoryMetadataGroupItemViewHolder( + view: View, + private val interactor: HistoryMetadataGroupInteractor, + private val selectionHolder: SelectionHolder +) : RecyclerView.ViewHolder(view) { + + private val binding = HistoryMetadataGroupListItemBinding.bind(view) + + private var item: History.Metadata? = null + + fun bind(item: History.Metadata) { + binding.historyLayout.titleView.text = item.title + binding.historyLayout.urlView.text = item.url + + binding.historyLayout.setSelectionInteractor(item, selectionHolder, interactor) + binding.historyLayout.changeSelected(item in selectionHolder.selectedItems) + + if (this.item?.url != item.url) { + binding.historyLayout.loadFavicon(item.url) + } + + binding.historyLayout.overflowView.setImageResource(R.drawable.ic_close) + + if (selectionHolder.selectedItems.isEmpty()) { + binding.historyLayout.overflowView.showAndEnable() + } else { + binding.historyLayout.overflowView.hideAndDisable() + } + + this.item = item + } + + companion object { + const val LAYOUT_ID = R.layout.history_metadata_group_list_item + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupView.kt b/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupView.kt new file mode 100644 index 000000000000..896b1ca3062f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupView.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.library.historymetadata.view + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.ComponentHistoryMetadataGroupBinding +import org.mozilla.fenix.library.LibraryPageView +import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentState +import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor + +/** + * Shows a list of history metadata items. + */ +class HistoryMetadataGroupView( + container: ViewGroup, + val interactor: HistoryMetadataGroupInteractor, + val title: String +) : LibraryPageView(container) { + + private val binding = ComponentHistoryMetadataGroupBinding.inflate( + LayoutInflater.from(container.context), container, true + ) + + private val historyMetadataGroupAdapter = HistoryMetadataGroupAdapter(interactor) + + init { + binding.historyMetadataGroupList.apply { + layoutManager = LinearLayoutManager(containerView.context) + adapter = historyMetadataGroupAdapter + } + } + + /** + * Updates the display of the history metadata items based on the given + * [HistoryMetadataGroupFragmentState]. + */ + fun update(state: HistoryMetadataGroupFragmentState) { + binding.historyMetadataGroupList.isVisible = state.items.isNotEmpty() + binding.historyMetadataGroupEmptyView.isVisible = state.items.isEmpty() + + historyMetadataGroupAdapter.updateData(state.items) + + val selectedItems = state.items.filter { it.selected } + + if (selectedItems.isEmpty()) { + setUiForNormalMode(title) + } else { + setUiForSelectingMode( + context.getString(R.string.history_multi_select_title, selectedItems.size) + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt index 264bc8901300..468350a8b8f9 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt @@ -4,9 +4,6 @@ package org.mozilla.fenix.library.recentlyclosed -import android.content.ClipData -import android.content.ClipboardManager -import android.content.res.Resources import androidx.navigation.NavController import androidx.navigation.NavOptions import mozilla.components.browser.state.action.RecentlyClosedAction @@ -18,7 +15,6 @@ import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.components.FenixSnackbar @Suppress("TooManyFunctions") interface RecentlyClosedController { @@ -26,8 +22,6 @@ interface RecentlyClosedController { fun handleOpen(tabs: Set, mode: BrowsingMode? = null) fun handleDelete(tab: RecoverableTab) fun handleDelete(tabs: Set) - fun handleCopyUrl(item: RecoverableTab) - fun handleShare(tab: RecoverableTab) fun handleShare(tabs: Set) fun handleNavigateToHistory() fun handleRestore(item: RecoverableTab) @@ -42,9 +36,6 @@ class DefaultRecentlyClosedController( private val browserStore: BrowserStore, private val recentlyClosedStore: RecentlyClosedFragmentStore, private val tabsUseCases: TabsUseCases, - private val resources: Resources, - private val snackbar: FenixSnackbar, - private val clipboardManager: ClipboardManager, private val activity: HomeActivity, private val openToBrowser: (item: RecoverableTab, mode: BrowsingMode?) -> Unit ) : RecentlyClosedController { @@ -81,17 +72,6 @@ class DefaultRecentlyClosedController( ) } - override fun handleCopyUrl(item: RecoverableTab) { - val urlClipData = ClipData.newPlainText(item.url, item.url) - clipboardManager.setPrimaryClip(urlClipData) - with(snackbar) { - setText(resources.getString(R.string.url_copied)) - show() - } - } - - override fun handleShare(tab: RecoverableTab) = handleShare(setOf(tab)) - override fun handleShare(tabs: Set) { val shareData = tabs.map { ShareData(url = it.url, title = it.title) } navController.navigate( diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt index 3b9c72a34c82..e825e05bad2b 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt @@ -4,8 +4,6 @@ package org.mozilla.fenix.library.recentlyclosed -import android.content.ClipboardManager -import android.content.Context import android.os.Bundle import android.text.SpannableString import android.view.LayoutInflater @@ -27,10 +25,8 @@ import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentRecentlyClosedTabsBinding -import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.setTextColor import org.mozilla.fenix.ext.showToolbar @@ -116,12 +112,6 @@ class RecentlyClosedFragment : LibraryPageFragment(), UserIntera recentlyClosedStore = recentlyClosedFragmentStore, activity = activity as HomeActivity, tabsUseCases = requireComponents.useCases.tabsUseCases, - resources = requireContext().resources, - snackbar = FenixSnackbar.make( - view = requireActivity().getRootView()!!, - isDisplayedWithBrowserToolbar = true - ), - clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager, openToBrowser = ::openItem ) recentlyClosedInteractor = RecentlyClosedFragmentInteractor(recentlyClosedController) diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt index 8c1469adea0c..15e3edb8600c 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.library.recentlyclosed import mozilla.components.browser.state.state.recover.RecoverableTab -import org.mozilla.fenix.browser.browsingmode.BrowsingMode /** * Interactor for the recently closed screen @@ -14,25 +13,6 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode class RecentlyClosedFragmentInteractor( private val recentlyClosedController: RecentlyClosedController ) : RecentlyClosedInteractor { - override fun restore(item: RecoverableTab) { - recentlyClosedController.handleRestore(item) - } - - override fun onCopyPressed(item: RecoverableTab) { - recentlyClosedController.handleCopyUrl(item) - } - - override fun onSharePressed(item: RecoverableTab) { - recentlyClosedController.handleShare(item) - } - - override fun onOpenInNormalTab(item: RecoverableTab) { - recentlyClosedController.handleOpen(item, BrowsingMode.Normal) - } - - override fun onOpenInPrivateTab(item: RecoverableTab) { - recentlyClosedController.handleOpen(item, BrowsingMode.Private) - } override fun onDelete(tab: RecoverableTab) { recentlyClosedController.handleDelete(tab) diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt index 16bfa738e41a..c20b08a2a2c6 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt @@ -17,46 +17,11 @@ import org.mozilla.fenix.library.LibraryPageView import org.mozilla.fenix.selection.SelectionInteractor interface RecentlyClosedInteractor : SelectionInteractor { - /** - * Called when an item is tapped to restore it. - * - * @param item the tapped item to restore. - */ - fun restore(item: RecoverableTab) - /** * Called when the view more history option is tapped. */ fun onNavigateToHistory() - /** - * Copies the URL of a recently closed tab item to the copy-paste buffer. - * - * @param item the recently closed tab item to copy the URL from - */ - fun onCopyPressed(item: RecoverableTab) - - /** - * Opens the share sheet for a recently closed tab item. - * - * @param item the recently closed tab item to share - */ - fun onSharePressed(item: RecoverableTab) - - /** - * Opens a recently closed tab item in a new tab. - * - * @param item the recently closed tab item to open in a new tab - */ - fun onOpenInNormalTab(item: RecoverableTab) - - /** - * Opens a recently closed tab item in a private tab. - * - * @param item the recently closed tab item to open in a private tab - */ - fun onOpenInPrivateTab(item: RecoverableTab) - /** * Called when recently closed tab is selected for deletion. * diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt index eca26d56d036..50dbb394729e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt @@ -12,13 +12,11 @@ import org.mozilla.fenix.databinding.HistoryListItemBinding import org.mozilla.fenix.ext.hideAndDisable import org.mozilla.fenix.ext.showAndEnable import org.mozilla.fenix.selection.SelectionHolder -import org.mozilla.fenix.library.history.HistoryItemMenu -import org.mozilla.fenix.utils.Do class RecentlyClosedItemViewHolder( view: View, private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor, - private val selectionHolder: SelectionHolder + private val selectionHolder: SelectionHolder, ) : RecyclerView.ViewHolder(view) { private val binding = HistoryListItemBinding.bind(view) @@ -26,17 +24,23 @@ class RecentlyClosedItemViewHolder( private var item: RecoverableTab? = null init { - setupMenu() + binding.historyLayout.overflowView.setImageResource(R.drawable.ic_close) + binding.historyLayout.overflowView.setOnClickListener { + val item = this.item ?: return@setOnClickListener + recentlyClosedFragmentInteractor.onDelete(item) + } } - fun bind( - item: RecoverableTab - ) { + fun bind(item: RecoverableTab) { binding.historyLayout.titleView.text = if (item.title.isNotEmpty()) item.title else item.url binding.historyLayout.urlView.text = item.url - binding.historyLayout.setSelectionInteractor(item, selectionHolder, recentlyClosedFragmentInteractor) + binding.historyLayout.setSelectionInteractor( + item, + selectionHolder, + recentlyClosedFragmentInteractor + ) binding.historyLayout.changeSelected(item in selectionHolder.selectedItems) if (this.item?.url != item.url) { @@ -52,25 +56,6 @@ class RecentlyClosedItemViewHolder( this.item = item } - private fun setupMenu() { - val historyMenu = HistoryItemMenu(itemView.context) { - val item = this.item ?: return@HistoryItemMenu - Do exhaustive when (it) { - HistoryItemMenu.Item.Copy -> recentlyClosedFragmentInteractor.onCopyPressed(item) - HistoryItemMenu.Item.Share -> recentlyClosedFragmentInteractor.onSharePressed(item) - HistoryItemMenu.Item.OpenInNewTab -> recentlyClosedFragmentInteractor.onOpenInNormalTab( - item - ) - HistoryItemMenu.Item.OpenInPrivateTab -> recentlyClosedFragmentInteractor.onOpenInPrivateTab( - item - ) - HistoryItemMenu.Item.Delete -> recentlyClosedFragmentInteractor.onDelete(item) - } - } - - binding.historyLayout.attachMenu(historyMenu.menuController) - } - companion object { const val LAYOUT_ID = R.layout.history_list_item } diff --git a/app/src/main/res/layout/component_history_metadata_group.xml b/app/src/main/res/layout/component_history_metadata_group.xml new file mode 100644 index 000000000000..25b0f348ee7c --- /dev/null +++ b/app/src/main/res/layout/component_history_metadata_group.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_history_metadata_group.xml b/app/src/main/res/layout/fragment_history_metadata_group.xml new file mode 100644 index 000000000000..cc8af567be84 --- /dev/null +++ b/app/src/main/res/layout/fragment_history_metadata_group.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/history_metadata_group_list_item.xml b/app/src/main/res/layout/history_metadata_group_list_item.xml new file mode 100644 index 000000000000..3fa370519eeb --- /dev/null +++ b/app/src/main/res/layout/history_metadata_group_list_item.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/layout/library_site_item.xml b/app/src/main/res/layout/library_site_item.xml index ca050dafe5ad..530759fed501 100644 --- a/app/src/main/res/layout/library_site_item.xml +++ b/app/src/main/res/layout/library_site_item.xml @@ -23,9 +23,7 @@ android:id="@+id/favicon" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="10dp" - android:background="@drawable/favicon_background" - android:backgroundTint="?neutral" + android:padding="8dp" android:importantForAccessibility="no" tools:src="@drawable/ic_folder_icon" /> + + @@ -236,6 +240,17 @@ android:label="@string/library_history" tools:layout="@layout/fragment_history" /> + + + + + Close + + %d site + + %d sites + Recently closed tabs @@ -759,15 +766,15 @@ Clear - Copy + Copy - Share + Share - Open in new tab + Open in new tab - Open in private tab + Open in private tab - Delete + Delete %1$d selected diff --git a/app/src/test/java/org/mozilla/fenix/components/history/PagedHistoryProviderTest.kt b/app/src/test/java/org/mozilla/fenix/components/history/PagedHistoryProviderTest.kt index af02f54b9432..1abcea3334b0 100644 --- a/app/src/test/java/org/mozilla/fenix/components/history/PagedHistoryProviderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/history/PagedHistoryProviderTest.kt @@ -7,16 +7,20 @@ package org.mozilla.fenix.components.history import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk -import mozilla.components.concept.storage.HistoryStorage +import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.concept.storage.DocumentType +import mozilla.components.concept.storage.HistoryMetadata +import mozilla.components.concept.storage.HistoryMetadataKey import mozilla.components.concept.storage.VisitInfo import mozilla.components.concept.storage.VisitType -import org.junit.Assert.assertSame +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import org.mozilla.fenix.library.history.History class PagedHistoryProviderTest { - private lateinit var storage: HistoryStorage + private lateinit var storage: PlacesHistoryStorage @Before fun setup() { @@ -25,11 +29,52 @@ class PagedHistoryProviderTest { @Test fun `getHistory uses getVisitsPaginated`() { - val provider = storage.createSynchronousPagedHistoryProvider() - val results = listOf(mockk(), mockk()) - coEvery { storage.getVisitsPaginated(any(), any(), any()) } returns results + val provider = DefaultPagedHistoryProvider( + historyStorage = storage, + showHistorySearchGroups = true + ) - var actualResults: List? = null + val visitInfo1 = VisitInfo( + url = "http://www.mozilla.com", + title = "mozilla", + visitTime = 5, + visitType = VisitType.LINK + ) + val visitInfo2 = VisitInfo( + url = "http://www.firefox.com", + title = "firefox", + visitTime = 2, + visitType = VisitType.LINK + ) + val visitInfo3 = VisitInfo( + url = "http://www.wikipedia.com", + title = "wikipedia", + visitTime = 1, + visitType = VisitType.LINK + ) + val historyEntry1 = HistoryMetadata( + key = HistoryMetadataKey("http://www.mozilla.com", "mozilla", null), + title = "mozilla", + createdAt = 5, + updatedAt = 5, + totalViewTime = 10, + documentType = DocumentType.Regular, + previewImageUrl = null + ) + val historyEntry2 = HistoryMetadata( + key = HistoryMetadataKey("http://www.firefox.com", "mozilla", null), + title = "firefox", + createdAt = 2, + updatedAt = 2, + totalViewTime = 20, + documentType = DocumentType.Regular, + previewImageUrl = null + ) + + coEvery { storage.getVisitsPaginated(any(), any(), any()) } returns listOf(visitInfo1, visitInfo2, visitInfo3) + coEvery { storage.getHistoryMetadataSince(any()) } returns listOf(historyEntry1, historyEntry2) + + var actualResults: List? = null provider.getHistory(10L, 5) { actualResults = it } @@ -50,6 +95,35 @@ class PagedHistoryProviderTest { ) } - assertSame(results, actualResults) + val results = listOf( + History.Group( + id = historyEntry1.createdAt.toInt(), + title = historyEntry1.key.searchTerm!!, + visitedAt = historyEntry1.createdAt, + items = listOf( + History.Metadata( + id = historyEntry1.createdAt.toInt(), + title = historyEntry1.title!!, + url = historyEntry1.key.url, + visitedAt = historyEntry1.createdAt, + totalViewTime = historyEntry1.totalViewTime + ), + History.Metadata( + id = historyEntry2.createdAt.toInt(), + title = historyEntry2.title!!, + url = historyEntry2.key.url, + visitedAt = historyEntry2.createdAt, + totalViewTime = historyEntry2.totalViewTime + ) + ) + ), + History.Regular( + id = 12, + title = visitInfo3.title!!, + url = visitInfo3.url, + visitedAt = visitInfo3.visitTime + ) + ) + assertEquals(results, actualResults) } } 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 dd192fcfa09b..a17cabe1cec9 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 @@ -4,18 +4,13 @@ package org.mozilla.fenix.library.history -import android.content.ClipData -import android.content.ClipboardManager -import android.content.res.Resources import androidx.navigation.NavController import io.mockk.coVerifyOrder import io.mockk.every import io.mockk.mockk -import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScope -import mozilla.components.concept.engine.prompt.ShareData import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -23,24 +18,17 @@ import org.junit.Assert.assertTrue import org.junit.Before 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.components.metrics.MetricController -import org.mozilla.fenix.ext.directionsEq import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -// Robolectric needed for `onShareItem()` @ExperimentalCoroutinesApi @RunWith(FenixRobolectricTestRunner::class) class HistoryControllerTest { - private val historyItem = HistoryItem(0, "title", "url", 0.toLong()) + private val historyItem = History.Regular(0, "title", "url", 0.toLong()) private val scope = TestCoroutineScope() private val store: HistoryFragmentStore = mockk(relaxed = true) private val state: HistoryFragmentState = mockk(relaxed = true) private val navController: NavController = mockk(relaxed = true) - private val resources: Resources = mockk(relaxed = true) - private val snackbar: FenixSnackbar = mockk(relaxed = true) - private val clipboardManager: ClipboardManager = mockk(relaxed = true) private val metrics: MetricController = mockk(relaxed = true) @Before @@ -55,7 +43,7 @@ class HistoryControllerTest { @Test fun onPressHistoryItemInNormalMode() { - var actualHistoryItem: HistoryItem? = null + var actualHistoryItem: History? = null val controller = createController( openInBrowser = { actualHistoryItem = it @@ -65,36 +53,6 @@ class HistoryControllerTest { assertEquals(historyItem, actualHistoryItem) } - @Test - fun onOpenItemInNormalMode() { - var actualHistoryItem: HistoryItem? = null - var actualBrowsingMode: BrowsingMode? = null - val controller = createController( - openAndShowTray = { historyItem, browsingMode -> - actualHistoryItem = historyItem - actualBrowsingMode = browsingMode - } - ) - controller.handleOpenInNewTab(historyItem, BrowsingMode.Normal) - assertEquals(historyItem, actualHistoryItem) - assertEquals(BrowsingMode.Normal, actualBrowsingMode) - } - - @Test - fun onOpenItemInPrivateMode() { - var actualHistoryItem: HistoryItem? = null - var actualBrowsingMode: BrowsingMode? = null - val controller = createController( - openAndShowTray = { historyItem, browsingMode -> - actualHistoryItem = historyItem - actualBrowsingMode = browsingMode - } - ) - controller.handleOpenInNewTab(historyItem, BrowsingMode.Private) - assertEquals(historyItem, actualHistoryItem) - assertEquals(BrowsingMode.Private, actualBrowsingMode) - } - @Test fun onPressHistoryItemInEditMode() { every { state.mode } returns HistoryFragmentState.Mode.Editing(setOf()) @@ -174,7 +132,7 @@ class HistoryControllerTest { @Test fun onDeleteSome() { val itemsToDelete = setOf(historyItem) - var actualItems: Set? = null + var actualItems: Set? = null val controller = createController( deleteHistoryItems = { items -> actualItems = items @@ -185,37 +143,6 @@ class HistoryControllerTest { assertEquals(itemsToDelete, actualItems) } - @Test - fun onCopyItem() { - val clipdata = slot() - - createController().handleCopyUrl(historyItem) - - verify { - clipboardManager.setPrimaryClip(capture(clipdata)) - snackbar.show() - } - assertEquals(1, clipdata.captured.itemCount) - assertEquals(historyItem.url, clipdata.captured.description.label) - assertEquals(historyItem.url, clipdata.captured.getItemAt(0).text) - } - - @Test - @Suppress("UNCHECKED_CAST") - fun onShareItem() { - createController().handleShare(historyItem) - - verify { - navController.navigate( - directionsEq( - HistoryFragmentDirections.actionGlobalShareFragment( - data = arrayOf(ShareData(url = historyItem.url, title = historyItem.title)) - ) - ) - ) - } - } - @Test fun onRequestSync() { var syncHistoryInvoked = false @@ -235,22 +162,17 @@ class HistoryControllerTest { @Suppress("LongParameterList") private fun createController( - openInBrowser: (HistoryItem) -> Unit = { _ -> }, - openAndShowTray: (HistoryItem, BrowsingMode) -> Unit = { _, _ -> }, - displayDeleteAll: () -> Unit = { }, - invalidateOptionsMenu: () -> Unit = { }, - deleteHistoryItems: (Set) -> Unit = { _ -> }, - syncHistory: suspend () -> Unit = { } + openInBrowser: (History) -> Unit = { _ -> }, + displayDeleteAll: () -> Unit = {}, + invalidateOptionsMenu: () -> Unit = {}, + deleteHistoryItems: (Set) -> Unit = { _ -> }, + syncHistory: suspend () -> Unit = {} ): HistoryController { return DefaultHistoryController( store, navController, - resources, - snackbar, - clipboardManager, scope, openInBrowser, - openAndShowTray, displayDeleteAll, invalidateOptionsMenu, deleteHistoryItems, diff --git a/app/src/test/java/org/mozilla/fenix/library/history/HistoryFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/library/history/HistoryFragmentStoreTest.kt index a7b9470d50fa..135636a2cc64 100644 --- a/app/src/test/java/org/mozilla/fenix/library/history/HistoryFragmentStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/history/HistoryFragmentStoreTest.kt @@ -10,8 +10,8 @@ import org.junit.Assert.assertNotSame import org.junit.Test class HistoryFragmentStoreTest { - private val historyItem = HistoryItem(0, "title", "url", 0.toLong()) - private val newHistoryItem = HistoryItem(1, "title", "url", 0.toLong()) + private val historyItem = History.Regular(0, "title", "url", 0.toLong()) + private val newHistoryItem = History.Regular(1, "title", "url", 0.toLong()) @Test fun exitEditMode() = runBlocking { diff --git a/app/src/test/java/org/mozilla/fenix/library/history/HistoryInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/library/history/HistoryInteractorTest.kt index 557ce5f2f46d..9a86488f426b 100644 --- a/app/src/test/java/org/mozilla/fenix/library/history/HistoryInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/history/HistoryInteractorTest.kt @@ -9,10 +9,9 @@ import io.mockk.mockk import io.mockk.verifyAll import org.junit.Assert.assertTrue import org.junit.Test -import org.mozilla.fenix.browser.browsingmode.BrowsingMode class HistoryInteractorTest { - private val historyItem = HistoryItem(0, "title", "url", 0.toLong()) + private val historyItem = History.Regular(0, "title", "url", 0.toLong()) val controller: HistoryController = mockk(relaxed = true) val interactor = DefaultHistoryInteractor(controller) @@ -66,42 +65,6 @@ class HistoryInteractorTest { } } - @Test - fun onCopyPressed() { - interactor.onCopyPressed(historyItem) - - verifyAll { - controller.handleCopyUrl(historyItem) - } - } - - @Test - fun onSharePressed() { - interactor.onSharePressed(historyItem) - - verifyAll { - controller.handleShare(historyItem) - } - } - - @Test - fun onOpenInNormalTab() { - interactor.onOpenInNormalTab(historyItem) - - verifyAll { - controller.handleOpenInNewTab(historyItem, BrowsingMode.Normal) - } - } - - @Test - fun onOpenInPrivateTab() { - interactor.onOpenInPrivateTab(historyItem) - - verifyAll { - controller.handleOpenInNewTab(historyItem, BrowsingMode.Private) - } - } - @Test fun onDeleteAll() { interactor.onDeleteAll() @@ -116,6 +79,7 @@ class HistoryInteractorTest { val items = setOf(historyItem) interactor.onDeleteSome(items) + verifyAll { controller.handleDeleteSome(items) } @@ -124,6 +88,7 @@ class HistoryInteractorTest { @Test fun onRequestSync() { interactor.onRequestSync() + verifyAll { controller.handleRequestSync() } diff --git a/app/src/test/java/org/mozilla/fenix/library/history/HistoryItemMenuTest.kt b/app/src/test/java/org/mozilla/fenix/library/history/HistoryItemMenuTest.kt deleted file mode 100644 index 1590c0f39956..000000000000 --- a/app/src/test/java/org/mozilla/fenix/library/history/HistoryItemMenuTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.library.history - -import android.content.Context -import androidx.appcompat.view.ContextThemeWrapper -import mozilla.components.concept.menu.candidate.TextStyle -import mozilla.components.support.ktx.android.content.getColorFromAttr -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 -import org.mozilla.fenix.library.history.HistoryItemMenu.Item - -@RunWith(FenixRobolectricTestRunner::class) -class HistoryItemMenuTest { - - private lateinit var context: Context - private lateinit var menu: HistoryItemMenu - private var onItemTappedCaptured: Item? = null - - @Before - fun setup() { - context = ContextThemeWrapper(testContext, R.style.NormalTheme) - onItemTappedCaptured = null - menu = HistoryItemMenu(context) { - onItemTappedCaptured = it - } - } - - @Test - fun `delete item has special styling`() { - val deleteItem = menu.menuItems().last() - assertEquals("Delete", deleteItem.text) - assertEquals( - TextStyle(color = context.getColorFromAttr(R.attr.destructive)), - deleteItem.textStyle - ) - - deleteItem.onClick() - assertEquals(Item.Delete, onItemTappedCaptured) - } - - @Test - fun `builds menu items`() { - val items = menu.menuItems() - assertEquals(5, items.size) - val (copy, share, openInNewTab, openInPrivateTab, delete) = items - - assertEquals("Copy", copy.text) - assertEquals("Share", share.text) - assertEquals("Open in new tab", openInNewTab.text) - assertEquals("Open in private tab", openInPrivateTab.text) - assertEquals("Delete", delete.text) - - copy.onClick() - assertEquals(Item.Copy, onItemTappedCaptured) - - share.onClick() - assertEquals(Item.Share, onItemTappedCaptured) - - openInNewTab.onClick() - assertEquals(Item.OpenInNewTab, onItemTappedCaptured) - - openInPrivateTab.onClick() - assertEquals(Item.OpenInPrivateTab, onItemTappedCaptured) - - delete.onClick() - assertEquals(Item.Delete, onItemTappedCaptured) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragmentStoreTest.kt new file mode 100644 index 000000000000..0caabb377ef1 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragmentStoreTest.kt @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.historymetadata + +import kotlinx.coroutines.runBlocking +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.mozilla.fenix.library.history.History + +class HistoryMetadataGroupFragmentStoreTest { + + private lateinit var state: HistoryMetadataGroupFragmentState + private lateinit var store: HistoryMetadataGroupFragmentStore + + private val mozillaHistoryMetadataItem = History.Metadata( + id = 0, + title = "Mozilla", + url = "mozilla.org", + visitedAt = 0, + totalViewTime = 0 + ) + private val firefoxHistoryMetadataItem = History.Metadata( + id = 0, + title = "Firefox", + url = "firefox.com", + visitedAt = 0, + totalViewTime = 0 + ) + + @Before + fun setup() { + state = HistoryMetadataGroupFragmentState() + store = HistoryMetadataGroupFragmentStore(state) + } + + @Test + fun `Test updating the items in HistoryMetadataGroupFragmentStore`() = runBlocking { + assertEquals(0, store.state.items.size) + + val items = listOf(mozillaHistoryMetadataItem, firefoxHistoryMetadataItem) + store.dispatch(HistoryMetadataGroupFragmentAction.UpdateHistoryItems(items)).join() + + assertEquals(items, store.state.items) + } + + @Test + fun `Test selecting and deselecting an item in HistoryMetadataGroupFragmentStore`() = runBlocking { + val items = listOf(mozillaHistoryMetadataItem, firefoxHistoryMetadataItem) + + store.dispatch(HistoryMetadataGroupFragmentAction.UpdateHistoryItems(items)).join() + + assertFalse(store.state.items[0].selected) + assertFalse(store.state.items[1].selected) + + store.dispatch(HistoryMetadataGroupFragmentAction.Select(mozillaHistoryMetadataItem)).join() + + assertTrue(store.state.items[0].selected) + assertFalse(store.state.items[1].selected) + + store.dispatch(HistoryMetadataGroupFragmentAction.Deselect(store.state.items[0])).join() + + assertFalse(store.state.items[0].selected) + assertFalse(store.state.items[1].selected) + } + + @Test + fun `Test deselecting all items in HistoryMetadataGroupFragmentStore`() = runBlocking { + val items = listOf(mozillaHistoryMetadataItem, firefoxHistoryMetadataItem) + + store.dispatch(HistoryMetadataGroupFragmentAction.UpdateHistoryItems(items)).join() + store.dispatch(HistoryMetadataGroupFragmentAction.Select(mozillaHistoryMetadataItem)).join() + store.dispatch(HistoryMetadataGroupFragmentAction.DeselectAll).join() + + assertFalse(store.state.items[0].selected) + assertFalse(store.state.items[1].selected) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/library/historymetadata/controller/HistoryMetadataGroupControllerTest.kt b/app/src/test/java/org/mozilla/fenix/library/historymetadata/controller/HistoryMetadataGroupControllerTest.kt new file mode 100644 index 000000000000..8f8aef73eba1 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/historymetadata/controller/HistoryMetadataGroupControllerTest.kt @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.historymetadata.controller + +import androidx.navigation.NavController +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.ext.directionsEq +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.library.history.History +import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentAction +import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections +import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentStore + +@ExperimentalCoroutinesApi +@RunWith(FenixRobolectricTestRunner::class) +class HistoryMetadataGroupControllerTest { + + private val testDispatcher = TestCoroutineDispatcher() + + @get:Rule + val coroutinesTestRule = MainCoroutineRule(testDispatcher) + + private val activity: HomeActivity = mockk(relaxed = true) + private val store: HistoryMetadataGroupFragmentStore = mockk(relaxed = true) + private val navController: NavController = mockk(relaxed = true) + + private val mozillaHistoryMetadataItem = History.Metadata( + id = 0, + title = "Mozilla", + url = "mozilla.org", + visitedAt = 0, + totalViewTime = 1 + ) + private val firefoxHistoryMetadataItem = History.Metadata( + id = 0, + title = "Firefox", + url = "firefox.com", + visitedAt = 0, + totalViewTime = 1 + ) + + private lateinit var controller: DefaultHistoryMetadataGroupController + + @Before + fun setUp() { + controller = DefaultHistoryMetadataGroupController( + activity = activity, + store = store, + navController = navController + ) + } + + @Test + fun handleOpen() { + controller.handleOpen(mozillaHistoryMetadataItem) + + verify { + activity.openToBrowserAndLoad( + searchTermOrURL = mozillaHistoryMetadataItem.url, + newTab = true, + from = BrowserDirection.FromHistoryMetadataGroup, + historyMetadata = mozillaHistoryMetadataItem.historyMetadataKey + ) + } + } + + @Test + fun handleSelect() { + controller.handleSelect(mozillaHistoryMetadataItem) + + verify { + store.dispatch(HistoryMetadataGroupFragmentAction.Select(mozillaHistoryMetadataItem)) + } + } + + @Test + fun handleDeselect() { + controller.handleDeselect(mozillaHistoryMetadataItem) + + verify { + store.dispatch(HistoryMetadataGroupFragmentAction.Deselect(mozillaHistoryMetadataItem)) + } + } + + @Test + fun handleBackPressed() { + assertTrue(controller.handleBackPressed(setOf(mozillaHistoryMetadataItem))) + + verify { + store.dispatch(HistoryMetadataGroupFragmentAction.DeselectAll) + } + + assertFalse(controller.handleBackPressed(emptySet())) + } + + @Test + fun handleShare() { + controller.handleShare(setOf(mozillaHistoryMetadataItem, firefoxHistoryMetadataItem)) + + val data = arrayOf( + ShareData( + title = mozillaHistoryMetadataItem.title, + url = mozillaHistoryMetadataItem.url + ), + ShareData( + title = firefoxHistoryMetadataItem.title, + url = firefoxHistoryMetadataItem.url + ), + ) + + verify { + navController.navigate( + directionsEq(HistoryMetadataGroupFragmentDirections.actionGlobalShareFragment(data)) + ) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupItemViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupItemViewHolderTest.kt new file mode 100644 index 000000000000..465117c5baae --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/library/historymetadata/view/HistoryMetadataGroupItemViewHolderTest.kt @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.fenix.library.historymetadata.view + +import android.view.LayoutInflater +import androidx.navigation.Navigation +import io.mockk.mockk +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.databinding.HistoryMetadataGroupListItemBinding +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.library.history.History +import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor +import org.mozilla.fenix.selection.SelectionHolder + +@RunWith(FenixRobolectricTestRunner::class) +class HistoryMetadataGroupItemViewHolderTest { + + private lateinit var binding: HistoryMetadataGroupListItemBinding + private lateinit var interactor: HistoryMetadataGroupInteractor + private lateinit var selectionHolder: SelectionHolder + + private val item = History.Metadata( + id = 0, + title = "Mozilla", + url = "mozilla.org", + visitedAt = 0, + totalViewTime = 0 + ) + + @Before + fun setup() { + binding = HistoryMetadataGroupListItemBinding.inflate(LayoutInflater.from(testContext)) + Navigation.setViewNavController(binding.root, mockk(relaxed = true)) + interactor = mockk(relaxed = true) + selectionHolder = mockk(relaxed = true) + } + + @Test + fun `GIVEN a history metadata item on bind THEN set the title and url text`() { + HistoryMetadataGroupItemViewHolder(binding.root, interactor, selectionHolder).bind(item) + + assertEquals(item.title, binding.historyLayout.titleView.text) + assertEquals(item.url, binding.historyLayout.urlView.text) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt index feb2bc8eb687..710c2a38b0b3 100644 --- a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt @@ -4,16 +4,12 @@ package org.mozilla.fenix.library.recentlyclosed -import android.content.ClipData -import android.content.ClipboardManager -import android.content.res.Resources import androidx.navigation.NavController import androidx.navigation.NavOptions import io.mockk.Runs import io.mockk.every import io.mockk.just import io.mockk.mockk -import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineDispatcher @@ -30,25 +26,19 @@ import org.junit.runner.RunWith import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.ext.directionsEq import org.mozilla.fenix.ext.optionsEq import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -// Robolectric needed for `onShareItem()` @ExperimentalCoroutinesApi @RunWith(FenixRobolectricTestRunner::class) class DefaultRecentlyClosedControllerTest { private val dispatcher = TestCoroutineDispatcher() private val navController: NavController = mockk(relaxed = true) - private val resources: Resources = mockk(relaxed = true) - private val snackbar: FenixSnackbar = mockk(relaxed = true) - private val clipboardManager: ClipboardManager = mockk(relaxed = true) private val activity: HomeActivity = mockk(relaxed = true) private val browserStore: BrowserStore = mockk(relaxed = true) private val recentlyClosedStore: RecentlyClosedFragmentStore = mockk(relaxed = true) private val tabsUseCases: TabsUseCases = mockk(relaxed = true) - val mockedTab: RecoverableTab = mockk(relaxed = true) @Before fun setUp() { @@ -177,42 +167,6 @@ class DefaultRecentlyClosedControllerTest { } } - @Test - fun handleCopyUrl() { - val item = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L) - - val clipdata = slot() - - createController().handleCopyUrl(item) - - verify { - clipboardManager.setPrimaryClip(capture(clipdata)) - snackbar.show() - } - - assertEquals(1, clipdata.captured.itemCount) - assertEquals("mozilla.org", clipdata.captured.description.label) - assertEquals("mozilla.org", clipdata.captured.getItemAt(0).text) - } - - @Test - @Suppress("UNCHECKED_CAST") - fun handleShare() { - val item = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L) - - createController().handleShare(item) - - verify { - navController.navigate( - directionsEq( - RecentlyClosedFragmentDirections.actionGlobalShareFragment( - data = arrayOf(ShareData(url = item.url, title = item.title)) - ) - ) - ) - } - } - @Test fun `share multiple tabs`() { val tabs = createFakeTabList(2) @@ -232,11 +186,13 @@ class DefaultRecentlyClosedControllerTest { @Test fun handleRestore() { - createController().handleRestore(mockedTab) + val item: RecoverableTab = mockk(relaxed = true) + + createController().handleRestore(item) dispatcher.advanceUntilIdle() - verify { tabsUseCases.restore.invoke(mockedTab, true) } + verify { tabsUseCases.restore.invoke(item, true) } } @Test @@ -256,9 +212,6 @@ class DefaultRecentlyClosedControllerTest { browserStore, recentlyClosedStore, tabsUseCases, - resources, - snackbar, - clipboardManager, activity, openToBrowser ) diff --git a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt index 9ba2a9ee1e68..f8f85db52824 100644 --- a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt @@ -9,7 +9,6 @@ import io.mockk.verify import mozilla.components.browser.state.state.recover.RecoverableTab import org.junit.Before import org.junit.Test -import org.mozilla.fenix.browser.browsingmode.BrowsingMode class RecentlyClosedFragmentInteractorTest { @@ -25,56 +24,6 @@ class RecentlyClosedFragmentInteractorTest { ) } - @Test - fun open() { - val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L) - interactor.restore(tab) - - verify { - defaultRecentlyClosedController.handleRestore(tab) - } - } - - @Test - fun onCopyPressed() { - val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L) - interactor.onCopyPressed(tab) - - verify { - defaultRecentlyClosedController.handleCopyUrl(tab) - } - } - - @Test - fun onSharePressed() { - val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L) - interactor.onSharePressed(tab) - - verify { - defaultRecentlyClosedController.handleShare(tab) - } - } - - @Test - fun onOpenInNormalTab() { - val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L) - interactor.onOpenInNormalTab(tab) - - verify { - defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Normal) - } - } - - @Test - fun onOpenInPrivateTab() { - val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L) - interactor.onOpenInPrivateTab(tab) - - verify { - defaultRecentlyClosedController.handleOpen(tab, mode = BrowsingMode.Private) - } - } - @Test fun onDelete() { val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)