Implements: Viewmodel slice for ActivityLogCard

Closes #19022

## Description
Implements the viewmodel slice for Activity log card

## To test:
- Go to app
- Login with a site having activity log
- Go to me → Debug settings
- Verify that `dashboard_activity_log` flag doesn’t exist
- Click on an `ActivityCardItem`
- Verify that the event is triggered
- ✅  🔵 Tracked: my_site_dashboard_card_item_tapped, Properties: {"type":"activity_log","subtype":"activity_log"}
- Navigate Back
- Click on more menu
- ✅  Verify that the event is triggered
-  🔵 Tracked: my_site_dashboard_contextual_menu_accessed, Properties: {"card":"activity_log"}
- Click on all activity
- ✅  Verify  🔵 Tracked: my_site_dashboard_card_menu_item_tapped, Properties: {"card":"activity_log","item":"all_activity"}
- Navigate back
- Click on more menu
- Click on Hide this
- Verify event triggered 🔵 Tracked: my_site_dashboard_card_menu_item_tapped, Properties: {"card":"activity_log","item":"hide_this"}

## Regression Notes
1. Potential unintended areas of impact
- Activity log card does not work as expected
- Tracking on Activity log card is incorrect

2. What I did to test those areas of impact (or what existing automated tests I relied on)
Manual tests and Unit tests

3. What automated tests I added (or what prevented me from doing so)
Unit tests

PR submission checklist:
- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:
- [x] Portrait and landscape orientations.
- [x] Light and dark modes.
- [x] Fonts: Larger, smaller and bold text.
- [x] High contrast.
- [x] Talkback.
- [x] Languages with large words or with letters/accents not frequently used in English.
- [x] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [x] Large and small screen sizes. (Tablet and smaller phones)
- [x] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher)

Dashboard Personalization: Refactor Recent Activity Card
Refactor the recent activity card

```[tasklist]
### Tasks
- [ ] Remove activity log remote feature flag
- [ ] Create ActivityLogViewModelSlice
- [ ] Move tracking to ViewModelSlice
- [ ] Update unit tests
```

diff --git a/WordPress/build.gradle b/WordPress/build.gradle
index db53819515e6..1e56a4912799 100644
--- a/WordPress/build.gradle
+++ b/WordPress/build.gradle
@@ -146,7 +146,6 @@ android {
         buildConfigField "boolean", "JETPACK_INSTALL_FULL_PLUGIN", "false"
         buildConfigField "boolean", "ENABLE_BLAZE_FEATURE", "false"
         buildConfigField "boolean", "WP_INDIVIDUAL_PLUGIN_OVERLAY", "false"
-        buildConfigField "boolean", "DASHBOARD_CARD_ACTIVITY_LOG", "false"
         buildConfigField "boolean", "DASHBOARD_CARD_DOMAIN", "false"
         buildConfigField "boolean", "DASHBOARD_CARD_FREE_TO_PAID_PLANS", "false"
         buildConfigField "boolean", "SITE_EDITOR_MVP", "false"
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
index ceaa1d2519d4..139b2e74aa3e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
@@ -58,8 +58,6 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.SingleActionCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.JetpackBadge
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.SiteInfoHeaderCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.Type
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams.ActivityCardItemClickParams
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloggingPromptCardBuilderParams
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardDomainBuilderParams
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardPlansBuilderParams
@@ -84,6 +82,7 @@ import org.wordpress.android.ui.mysite.SiteDialogModel.ChangeSiteIconDialogModel
 import org.wordpress.android.ui.mysite.cards.CardsBuilder
 import org.wordpress.android.ui.mysite.cards.DomainRegistrationCardShownTracker
 import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker
+import org.wordpress.android.ui.mysite.cards.dashboard.activity.ActivityLogCardViewModelSlice
 import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptsCardAnalyticsTracker
 import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardCardDomainUtils
 import org.wordpress.android.ui.mysite.cards.dashboard.domaintransfer.DomainTransferCardViewModel
@@ -208,7 +207,8 @@ class MySiteViewModel @Inject constructor(
     private val domainTransferCardViewModel: DomainTransferCardViewModel,
     private val pagesCardViewModelSlice: PagesCardViewModelSlice,
     private val todaysStatsViewModelSlice: TodaysStatsViewModelSlice,
-    private val postsCardViewModelSlice: PostsCardViewModelSlice
+    private val postsCardViewModelSlice: PostsCardViewModelSlice,
+    private val activityLogCardViewModelSlice: ActivityLogCardViewModelSlice
 ) : ScopedViewModel(mainDispatcher) {
     private var isDefaultTabSet: Boolean = false
     private val _onSnackbarMessage = MutableLiveData<Event<SnackbarMessageHolder>>()
@@ -297,7 +297,8 @@ class MySiteViewModel @Inject constructor(
         pagesCardViewModelSlice.onNavigation,
         domainTransferCardViewModel.onNavigation,
         todaysStatsViewModelSlice.onNavigation,
-        postsCardViewModelSlice.onNavigation
+        postsCardViewModelSlice.onNavigation,
+        activityLogCardViewModelSlice.onNavigation
     )
     val onMediaUpload = _onMediaUpload as LiveData<Event<MediaModel>>
     val onUploadedItem = siteIconUploadHandler.onUploadedItem
@@ -316,7 +317,8 @@ class MySiteViewModel @Inject constructor(
             blazeCardViewModelSlice.refresh,
             pagesCardViewModelSlice.refresh,
             todaysStatsViewModelSlice.refresh,
-            postsCardViewModelSlice.refresh
+            postsCardViewModelSlice.refresh,
+            activityLogCardViewModelSlice.refresh
         )
     val domainTransferCardRefresh = domainTransferCardViewModel.refresh

@@ -577,7 +579,7 @@ class MySiteViewModel @Inject constructor(
                 showErrorCard = cardsUpdate?.showErrorCard == true,
                 onErrorRetryClick = this::onDashboardErrorRetry,
                 todaysStatsCardBuilderParams = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(
-                        cardsUpdate?.cards?.firstOrNull { it is TodaysStatsCardModel } as? TodaysStatsCardModel
+                    cardsUpdate?.cards?.firstOrNull { it is TodaysStatsCardModel } as? TodaysStatsCardModel
                 ),
                 postCardBuilderParams = postsCardViewModelSlice.getPostsCardBuilderParams(
                     cardsUpdate?.cards?.firstOrNull { it is PostsCardModel } as? PostsCardModel
@@ -618,14 +620,8 @@ class MySiteViewModel @Inject constructor(
                 pagesCardBuilderParams = pagesCardViewModelSlice.getPagesCardBuilderParams(
                     cardsUpdate?.cards?.firstOrNull { it is PagesCardModel } as? PagesCardModel,
                 ),
-                activityCardBuilderParams = ActivityCardBuilderParams(
-                    activityCardModel = cardsUpdate?.cards?.firstOrNull {
-                        it is ActivityCardModel
-                    } as? ActivityCardModel,
-                    onActivityItemClick = this::onActivityCardItemClick,
-                    onMoreMenuClick = this::onActivityCardMoreMenuClick,
-                    onAllActivityMenuItemClick = this::onActivityCardAllActivityItemClick,
-                    onHideMenuItemClick = this::onActivityCardHideMenuItemClick
+                activityCardBuilderParams = activityLogCardViewModelSlice.getActivityLogCardBuilderParams(
+                    cardsUpdate?.cards?.firstOrNull { it is ActivityCardModel } as? ActivityCardModel
                 ),
             ),
             QuickLinkRibbonBuilderParams(
@@ -691,32 +687,6 @@ class MySiteViewModel @Inject constructor(
         )
     }

-    private fun onActivityCardItemClick(activityCardItemClickParams: ActivityCardItemClickParams) {
-        cardsTracker.trackActivityCardItemClicked()
-        _onNavigation.value =
-            Event(
-                SiteNavigationAction.OpenActivityLogDetail(
-                    requireNotNull(selectedSiteRepository.getSelectedSite()),
-                    activityCardItemClickParams.activityId,
-                    activityCardItemClickParams.isRewindable
-                )
-            )
-    }
-    private fun onActivityCardHideMenuItemClick() {
-        cardsTracker.trackActivityCardMenuItemClicked(CardsTracker.MenuItemType.HIDE_THIS)
-        appPrefsWrapper.setShouldHideActivityDashboardCard(
-            requireNotNull(selectedSiteRepository.getSelectedSite()).siteId,true)
-        refresh()
-    }
-
-    private fun onActivityCardAllActivityItemClick() {
-        cardsTracker.trackActivityCardMenuItemClicked(CardsTracker.MenuItemType.ALL_ACTIVITY)
-        _onNavigation.value =
-            Event(SiteNavigationAction.OpenActivityLog(requireNotNull(selectedSiteRepository.getSelectedSite())))
-    }
-
-    private fun onActivityCardMoreMenuClick() = cardsTracker.trackActivityLogMoreMenuClicked()
-
     private fun buildJetpackBadgeIfEnabled(): JetpackBadge? {
         val screen = JetpackPoweredScreen.WithStaticText.HOME
         return JetpackBadge(
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTracker.kt
index 753353d435da..69c2df9cc719 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTracker.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTracker.kt
@@ -65,30 +65,11 @@ class CardsTracker @Inject constructor(
         NO_CAMPAIGNS("no_campaigns"),
         CAMPAIGNS("campaigns")
     }
-    enum class MenuItemType(val label: String) {
-        ALL_ACTIVITY("all_activity"),
-        HIDE_THIS("hide_this")
-    }
+
     fun trackQuickStartCardItemClicked(quickStartTaskType: QuickStartTaskType) {
         trackCardItemClicked(Type.QUICK_START.label, quickStartTaskType.toSubtypeValue().label)
     }

-    fun trackActivityCardItemClicked() {
-        trackCardItemClicked(Type.ACTIVITY.label, ActivityLogSubtype.ACTIVITY_LOG.label)
-    }
-
-    fun trackActivityCardFooterClicked() {
-        trackCardFooterLinkClicked(Type.ACTIVITY.label, ActivityLogSubtype.ACTIVITY_LOG.label)
-    }
-
-    fun trackActivityCardMenuItemClicked(menuItemType: MenuItemType) {
-        trackCardMoreMenuItemClicked(Type.ACTIVITY.label, menuItemType.label)
-    }
-
-    fun trackActivityLogMoreMenuClicked() {
-        trackCardMoreMenuClicked(Type.ACTIVITY.label)
-    }
-
     fun trackCardFooterLinkClicked(type: String, subtype: String) {
         analyticsTrackerWrapper.track(
             Stat.MY_SITE_DASHBOARD_CARD_FOOTER_ACTION_TAPPED,
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityLogCardViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityLogCardViewModelSlice.kt
new file mode 100644
index 000000000000..a6f8226b8c3c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityLogCardViewModelSlice.kt
@@ -0,0 +1,79 @@
+package org.wordpress.android.ui.mysite.cards.dashboard.activity
+
+import androidx.lifecycle.MutableLiveData
+import org.wordpress.android.fluxc.model.dashboard.CardModel
+import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams
+import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams.ActivityCardItemClickParams
+import org.wordpress.android.ui.mysite.SelectedSiteRepository
+import org.wordpress.android.ui.mysite.SiteNavigationAction
+import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+import org.wordpress.android.viewmodel.Event
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ActivityLogCardViewModelSlice @Inject constructor(
+    private val cardsTracker: CardsTracker,
+    private val selectedSiteRepository: SelectedSiteRepository,
+    private val appPrefsWrapper: AppPrefsWrapper
+) {
+    private val _onNavigation = MutableLiveData<Event<SiteNavigationAction>>()
+    val onNavigation = _onNavigation
+
+    private val _refresh = MutableLiveData<Event<Boolean>>()
+    val refresh = _refresh
+
+    fun getActivityLogCardBuilderParams(activityCardModel: CardModel.ActivityCardModel?) =
+        ActivityCardBuilderParams(
+            activityCardModel = activityCardModel,
+            onActivityItemClick = this::onActivityCardItemClick,
+            onMoreMenuClick = this::onActivityCardMoreMenuClick,
+            onAllActivityMenuItemClick = this::onActivityCardAllActivityItemClick,
+            onHideMenuItemClick = this::onActivityCardHideMenuItemClick
+        )
+
+    private fun onActivityCardItemClick(activityCardItemClickParams: ActivityCardItemClickParams) {
+        cardsTracker.trackCardItemClicked(
+            CardsTracker.Type.ACTIVITY.label,
+            CardsTracker.ActivityLogSubtype.ACTIVITY_LOG.label
+        )
+        _onNavigation.value =
+            Event(
+                SiteNavigationAction.OpenActivityLogDetail(
+                    requireNotNull(selectedSiteRepository.getSelectedSite()),
+                    activityCardItemClickParams.activityId,
+                    activityCardItemClickParams.isRewindable
+                )
+            )
+    }
+
+    private fun onActivityCardHideMenuItemClick() {
+        cardsTracker.trackCardMoreMenuItemClicked(
+            CardsTracker.Type.ACTIVITY.label,
+            MenuItemType.HIDE_THIS.label
+        )
+        appPrefsWrapper.setShouldHideActivityDashboardCard(
+            requireNotNull(selectedSiteRepository.getSelectedSite()).siteId, true
+        )
+        _refresh.postValue(Event(true))
+    }
+
+    private fun onActivityCardAllActivityItemClick() {
+        cardsTracker.trackCardMoreMenuItemClicked(
+            CardsTracker.Type.ACTIVITY.label,
+            MenuItemType.ALL_ACTIVITY.label
+        )
+        _onNavigation.value =
+            Event(SiteNavigationAction.OpenActivityLog(requireNotNull(selectedSiteRepository.getSelectedSite())))
+    }
+
+    private fun onActivityCardMoreMenuClick() = cardsTracker.trackCardMoreMenuClicked(CardsTracker.Type.ACTIVITY.label)
+
+    enum class MenuItemType(val label: String) {
+        ALL_ACTIVITY("all_activity"),
+        HIDE_THIS("hide_this")
+    }
+}
+
+
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/DashboardActivityLogCardFeatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/DashboardActivityLogCardFeatureUtils.kt
index edb5c583c14c..7bc10ec693e0 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/DashboardActivityLogCardFeatureUtils.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/DashboardActivityLogCardFeatureUtils.kt
@@ -3,16 +3,14 @@ package org.wordpress.android.ui.mysite.cards.dashboard.activity
 import org.wordpress.android.fluxc.model.SiteModel
 import org.wordpress.android.ui.prefs.AppPrefsWrapper
 import org.wordpress.android.util.SiteUtilsWrapper
-import org.wordpress.android.util.config.DashboardCardActivityLogConfig
 import javax.inject.Inject

 class DashboardActivityLogCardFeatureUtils @Inject constructor(
-    private val dashboardCardActivityLogConfig: DashboardCardActivityLogConfig,
     private val siteUtilsWrapper: SiteUtilsWrapper,
     private val appPrefsWrapper: AppPrefsWrapper
 ) {
     fun shouldRequestActivityCard(selectedSite: SiteModel): Boolean {
-        if (!dashboardCardActivityLogConfig.isEnabled() || isActivityCardHiddenByUser(selectedSite.siteId)) return false
+        if (isActivityCardHiddenByUser(selectedSite.siteId)) return false
         val isWpComOrJetpack = siteUtilsWrapper.isAccessedViaWPComRest(selectedSite) ||
                 selectedSite.isJetpackConnected
         return selectedSite.hasCapabilityManageOptions
diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardActvitiyLogConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardActvitiyLogConfig.kt
deleted file mode 100644
index 1c22bc7be68f..000000000000
--- a/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardActvitiyLogConfig.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.wordpress.android.util.config
-
-import org.wordpress.android.BuildConfig
-import org.wordpress.android.annotation.Feature
-import javax.inject.Inject
-
-const val DASHBOARD_CARD_ACTIVITY_LOG_REMOTE_FIELD = "dashboard_card_activity_log"
-
-@Feature(DASHBOARD_CARD_ACTIVITY_LOG_REMOTE_FIELD, false)
-class DashboardCardActivityLogConfig @Inject constructor(
-    appConfig: AppConfig
-) : FeatureConfig(
-    appConfig,
-    BuildConfig.DASHBOARD_CARD_ACTIVITY_LOG,
-    DASHBOARD_CARD_ACTIVITY_LOG_REMOTE_FIELD
-)

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt
index 3aa003be9c75..7eacc476e1b0 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt
@@ -72,7 +72,6 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.SingleActionCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.JetpackBadge
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.SiteInfoHeaderCard
 import org.wordpress.android.ui.mysite.MySiteCardAndItem.SiteInfoHeaderCard.IconState
-import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams.ActivityCardItemClickParams
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardsBuilderParams
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DomainRegistrationCardBuilderParams
 import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.InfoItemBuilderParams
@@ -100,6 +99,7 @@ import org.wordpress.android.ui.mysite.SiteDialogModel.ChangeSiteIconDialogModel
 import org.wordpress.android.ui.mysite.cards.CardsBuilder
 import org.wordpress.android.ui.mysite.cards.DomainRegistrationCardShownTracker
 import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker
+import org.wordpress.android.ui.mysite.cards.dashboard.activity.ActivityLogCardViewModelSlice
 import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution
 import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptsCardAnalyticsTracker
 import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardCardDomainUtils
@@ -326,6 +326,9 @@ class MySiteViewModelTest : BaseUnitTest() {
     @Mock
     lateinit var postsCardViewModelSlice: PostsCardViewModelSlice

+    @Mock
+    lateinit var activityLogCardViewModelSlice: ActivityLogCardViewModelSlice
+
     private lateinit var viewModel: MySiteViewModel
     private lateinit var uiModels: MutableList<UiModel>
     private lateinit var snackbars: MutableList<SnackbarMessageHolder>
@@ -349,8 +352,6 @@ class MySiteViewModelTest : BaseUnitTest() {
     private val emailAddress = "test@email.com"
     private val localHomepageId = 1
     private val bloggingPromptId = 123
-    private val activityId = "activityId"
-    private val isRewindable = false
     private lateinit var site: SiteModel
     private lateinit var siteInfoHeader: SiteInfoHeaderCard
     private lateinit var homepage: PageModel
@@ -383,10 +384,6 @@ class MySiteViewModelTest : BaseUnitTest() {
     private var onBloggingPromptViewMoreClicked: (() -> Unit)? = null
     private var onBloggingPromptViewAnswersClicked: ((promptId: Int) -> Unit)? = null
     private var onBloggingPromptRemoveClicked: (() -> Unit)? = null
-    private var onActivityCardAllActivityMenuItemClick: (() -> Unit)? = null
-    private var onActivityCardHideMenuItemClick: (() -> Unit)? = null
-    private var onActivityCardMoreMenuClick: (() -> Unit)? = null
-    private var onActivityItemClick: ((params: ActivityCardItemClickParams) -> Unit)? = null
     private val quickStartCategory: QuickStartCategory
         get() = QuickStartCategory(
             taskType = QuickStartTaskType.CUSTOMIZE,
@@ -495,6 +492,7 @@ class MySiteViewModelTest : BaseUnitTest() {
         whenever(pagesCardViewModelSlice.getPagesCardBuilderParams(anyOrNull())).thenReturn(mock())
         whenever(todaysStatsViewModelSlice.getTodaysStatsBuilderParams(anyOrNull())).thenReturn(mock())
         whenever(postsCardViewModelSlice.getPostsCardBuilderParams(anyOrNull())).thenReturn(mock())
+        whenever(activityLogCardViewModelSlice.getActivityLogCardBuilderParams(anyOrNull())).thenReturn(mock())

         viewModel = MySiteViewModel(
             networkUtilsWrapper,
@@ -553,7 +551,8 @@ class MySiteViewModelTest : BaseUnitTest() {
             domainTransferCardViewModel,
             pagesCardViewModelSlice,
             todaysStatsViewModelSlice,
-            postsCardViewModelSlice
+            postsCardViewModelSlice,
+            activityLogCardViewModelSlice
         )
         uiModels = mutableListOf()
         snackbars = mutableListOf()
@@ -1853,61 +1852,6 @@ class MySiteViewModelTest : BaseUnitTest() {
             )
     }

-    /* DASHBOARD */
-    @Test
-    fun `when dashboard cards are shown, then card shown event is tracked`() = test {
-        initSelectedSite()
-
-        verify(cardsTracker, atLeastOnce()).trackShown(any())
-    }
-
-    /* DASHBOARD ACTIVITY CARD */
-    @Test
-    fun `given activity card, when all activity menu item is clicked, then trigger navigate to activity log list`() =
-        test {
-            initSelectedSite()
-
-            requireNotNull(onActivityCardAllActivityMenuItemClick).invoke()
-
-            assertThat(navigationActions).containsOnly(SiteNavigationAction.OpenActivityLog(site))
-        }
-
-    @Test
-    fun `given activity card, when activity item is clicked, then navigate to activity detail`() =
-        test {
-            initSelectedSite()
-
-            requireNotNull(onActivityItemClick).invoke(ActivityCardItemClickParams(activityId, isRewindable))
-
-            assertThat(navigationActions).containsOnly(
-                SiteNavigationAction.OpenActivityLogDetail(
-                    site,
-                    activityId,
-                    isRewindable
-                )
-            )
-        }
-
-    @Test
-    fun `given activity card, when activity item is clicked, then event is tracked`() =
-        test {
-            initSelectedSite()
-
-            requireNotNull(onActivityItemClick).invoke(ActivityCardItemClickParams(activityId, isRewindable))
-
-            verify(cardsTracker).trackActivityCardItemClicked()
-        }
-
-    @Test
-    fun `given activity card, when hide this menu item is clicked, then the sources are refreshed`() =
-        test {
-            initSelectedSite()
-
-            requireNotNull(onActivityCardHideMenuItemClick).invoke()
-
-            verify(mySiteSourceManager).refresh()
-        }
-
     /* DASHBOARD ERROR SNACKBAR */

     @Test
@@ -3208,7 +3152,6 @@ class MySiteViewModelTest : BaseUnitTest() {
                 } else {
                 //    add(initPostCard(mockInvocation))
                     if (bloggingPromptsFeatureConfig.isEnabled()) add(initBloggingPromptCard(mockInvocation))
-                    add(initActivityCard(mockInvocation))
                 }
             }
         )
@@ -3287,36 +3230,6 @@ class MySiteViewModelTest : BaseUnitTest() {
         return items
     }

-    private fun initActivityCard(mockInvocation: InvocationOnMock): DashboardCard.ActivityCard.ActivityCardWithItems {
-        val params = (mockInvocation.arguments.filterIsInstance<DashboardCardsBuilderParams>()).first()
-        onActivityItemClick = params.activityCardBuilderParams.onActivityItemClick
-        onActivityCardMoreMenuClick = params.activityCardBuilderParams.onMoreMenuClick
-        onActivityCardAllActivityMenuItemClick = params.activityCardBuilderParams.onAllActivityMenuItemClick
-        onActivityCardHideMenuItemClick = params.activityCardBuilderParams.onHideMenuItemClick
-        return DashboardCard.ActivityCard.ActivityCardWithItems(
-            title = UiStringRes(0),
-            onAllActivityMenuItemClick = ListItemInteraction.create {
-                (onActivityCardAllActivityMenuItemClick as () -> Unit).invoke()
-            },
-            onHideMenuItemClick = ListItemInteraction.create {
-                (onActivityCardHideMenuItemClick as () -> Unit).invoke()
-            },
-            onMoreMenuClick = ListItemInteraction.create {
-                (onActivityCardMoreMenuClick as () -> Unit).invoke()
-            },
-            activityItems = listOf(
-                DashboardCard.ActivityCard.ActivityCardWithItems.ActivityItem(
-                    label = UiStringRes(0),
-                    subLabel = "",
-                    displayDate = "",
-                    icon = 0,
-                    iconBackgroundColor = 0,
-                    onClick = mock()
-                )
-            ),
-        )
-    }
-
     fun ViewModel.invokeOnCleared() {
         val viewModelStore = ViewModelStore()
         val viewModelProvider = ViewModelProvider(viewModelStore, object : ViewModelProvider.Factory {
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTrackerTest.kt
index cd7987188d0e..951b45fe44a5 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTrackerTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTrackerTest.kt
@@ -8,8 +8,6 @@ import org.mockito.junit.MockitoJUnitRunner
 import org.mockito.kotlin.verify
 import org.wordpress.android.analytics.AnalyticsTracker.Stat
 import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType
-import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.ActivityLogSubtype
-import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.MenuItemType
 import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.QuickStartSubtype
 import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.Type
 import org.wordpress.android.ui.quickstart.QuickStartTracker
@@ -17,8 +15,6 @@ import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper

 private const val TYPE = "type"
 private const val SUBTYPE = "subtype"
-private const val ITEM = "item"
-private const val CARD = "card"

 @RunWith(MockitoJUnitRunner::class)
 class CardsTrackerTest {
@@ -30,6 +26,7 @@ class CardsTrackerTest {

     @Mock
     lateinit var quickStartTracker: QuickStartTracker
+
     private lateinit var cardsTracker: CardsTracker

     @Before
@@ -53,61 +50,6 @@ class CardsTrackerTest {
         verifyQuickStartCardItemClickedTracked(QuickStartSubtype.CUSTOMIZE.label)
     }

-    @Test
-    fun `when activity log item is clicked, then activity card item event is tracked`() {
-        cardsTracker.trackActivityCardItemClicked()
-
-        verifyCardItemClickedTracked(Type.ACTIVITY, ActivityLogSubtype.ACTIVITY_LOG.label)
-    }
-
-    @Test
-    fun `when activity card footer link is clicked, then footer link clicked is tracked`() {
-        cardsTracker.trackActivityCardFooterClicked()
-
-        verifyFooterLinkClickedTracked(Type.ACTIVITY, ActivityLogSubtype.ACTIVITY_LOG.label)
-    }
-
-    @Test
-    fun `when activity card hide this menu item is clicked, then hide this event is tracked`() {
-        cardsTracker.trackActivityCardMenuItemClicked(MenuItemType.HIDE_THIS)
-
-        verifyCardMenuItemClickedTracked(Type.ACTIVITY, MenuItemType.HIDE_THIS.label)
-    }
-
-    @Test
-    fun `when activity card all activity menu item is clicked, then all activity this event is tracked`() {
-        cardsTracker.trackActivityCardMenuItemClicked(MenuItemType.ALL_ACTIVITY)
-
-        verifyCardMenuItemClickedTracked(Type.ACTIVITY, MenuItemType.ALL_ACTIVITY.label)
-    }
-
-    @Test
-    fun `when activity card more menu is clicked, then activity more menu event is tracked`() {
-        cardsTracker.trackActivityLogMoreMenuClicked()
-
-        verifyCardMoreMenuClickTracked(Type.ACTIVITY)
-    }
-
-    private fun verifyFooterLinkClickedTracked(
-        typeValue: Type,
-        subtypeValue: String
-    ) {
-        verify(analyticsTracker).track(
-            Stat.MY_SITE_DASHBOARD_CARD_FOOTER_ACTION_TAPPED,
-            mapOf(TYPE to typeValue.label, SUBTYPE to subtypeValue)
-        )
-    }
-
-    private fun verifyCardItemClickedTracked(
-        typeValue: Type,
-        subtypeValue: String
-    ) {
-        verify(analyticsTracker).track(
-            Stat.MY_SITE_DASHBOARD_CARD_ITEM_TAPPED,
-            mapOf(TYPE to typeValue.label, SUBTYPE to subtypeValue)
-        )
-    }
-
     private fun verifyQuickStartCardItemClickedTracked(
         subtypeValue: String
     ) {
@@ -116,22 +58,4 @@ class CardsTrackerTest {
             mapOf(TYPE to Type.QUICK_START.label, SUBTYPE to subtypeValue)
         )
     }
-
-    private fun verifyCardMenuItemClickedTracked(
-        typeValue: Type,
-        menuItem: String
-    ) {
-        verify(analyticsTracker).track(
-            Stat.MY_SITE_DASHBOARD_CARD_MENU_ITEM_TAPPED,
-            mapOf(CARD to typeValue.label, ITEM to menuItem)
-        )
-    }
-    private fun verifyCardMoreMenuClickTracked(
-        typeValue: Type
-    ) {
-        verify(analyticsTracker).track(
-            Stat.MY_SITE_DASHBOARD_CONTEXTUAL_MENU_ACCESSED,
-            mapOf(CARD to typeValue.label)
-        )
-    }
 }
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityLogCardViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityLogCardViewModelSliceTest.kt
new file mode 100644
index 000000000000..8eac4c8128d6
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityLogCardViewModelSliceTest.kt
@@ -0,0 +1,124 @@
+package org.wordpress.android.ui.mysite.cards.dashboard.activity
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.assertj.core.api.Assertions
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.fluxc.model.SiteModel
+import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams.ActivityCardItemClickParams
+import org.wordpress.android.ui.mysite.SelectedSiteRepository
+import org.wordpress.android.ui.mysite.SiteNavigationAction
+import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
+
+@ExperimentalCoroutinesApi
+@RunWith(MockitoJUnitRunner::class)
+class ActivityLogCardViewModelSliceTest : BaseUnitTest() {
+    @Mock
+    lateinit var cardsTracker: CardsTracker
+
+    @Mock
+    lateinit var selectedSiteRepository: SelectedSiteRepository
+
+    @Mock
+    lateinit var appPrefsWrapper: AppPrefsWrapper
+
+    private lateinit var activityLogCardViewModelSlice: ActivityLogCardViewModelSlice
+
+    private lateinit var navigationActions: MutableList<SiteNavigationAction>
+
+    private lateinit var refreshEvents: MutableList<Boolean>
+
+    private val site = mock<SiteModel>()
+
+    private val activityId = "activityId"
+    private val isRewindable = false
+
+    @Before
+    fun setUp() {
+        activityLogCardViewModelSlice = ActivityLogCardViewModelSlice(
+            cardsTracker,
+            selectedSiteRepository,
+            appPrefsWrapper
+        )
+        navigationActions = mutableListOf()
+        activityLogCardViewModelSlice.onNavigation.observeForever { event ->
+            event?.getContentIfNotHandled()?.let {
+                navigationActions.add(it)
+            }
+        }
+        refreshEvents = mutableListOf()
+        activityLogCardViewModelSlice.refresh.observeForever { event ->
+            event?.getContentIfNotHandled()?.let {
+                refreshEvents.add(it)
+            }
+        }
+        whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site)
+    }
+
+    @Test
+    fun `given activity log card, when card item is clicked, then stats page is opened`() =
+        test {
+            val params = activityLogCardViewModelSlice.getActivityLogCardBuilderParams(mock())
+            val cardItemClickParams = ActivityCardItemClickParams(activityId, isRewindable)
+
+            params.onActivityItemClick(cardItemClickParams)
+
+            Assertions.assertThat(navigationActions).containsOnly(
+                SiteNavigationAction.OpenActivityLogDetail(
+                    site,
+                    activityId,
+                    isRewindable
+                )
+            )
+        }
+
+    @Test
+    fun `given activity log card, when card more menu is clicked, then event is tracked`() =
+        test {
+            val params = activityLogCardViewModelSlice.getActivityLogCardBuilderParams(mock())
+
+            params.onMoreMenuClick()
+
+            verify(cardsTracker).trackCardMoreMenuClicked(CardsTracker.Type.ACTIVITY.label)
+        }
+
+    @Test
+    fun `given activity log card, when all activity menu item is clicked, all activity page is opened`() =
+        test {
+            val params = activityLogCardViewModelSlice.getActivityLogCardBuilderParams(mock())
+
+            params.onAllActivityMenuItemClick()
+
+            Assertions.assertThat(navigationActions).containsOnly(
+                SiteNavigationAction.OpenActivityLog(site)
+            )
+            verify(cardsTracker).trackCardMoreMenuItemClicked(
+                CardsTracker.Type.ACTIVITY.label,
+                ActivityLogCardViewModelSlice.MenuItemType.ALL_ACTIVITY.label
+            )
+        }
+
+    @Test
+    fun `given activity log card, when hide menu item is clicked, all activity page is opened`() =
+        test {
+            val params = activityLogCardViewModelSlice.getActivityLogCardBuilderParams(mock())
+
+            params.onHideMenuItemClick()
+
+            Assertions.assertThat(refreshEvents).containsOnly(true)
+            verify(cardsTracker).trackCardMoreMenuItemClicked(
+                CardsTracker.Type.ACTIVITY.label,
+                ActivityLogCardViewModelSlice.MenuItemType.HIDE_THIS.label
+            )
+            verify(appPrefsWrapper).setShouldHideActivityDashboardCard(any(), any())
+        }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/DashboardActivityLogCardFeatureUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/DashboardActivityLogCardFeatureUtilsTest.kt
index 9788418728b6..27b01d4edbcb 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/DashboardActivityLogCardFeatureUtilsTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/DashboardActivityLogCardFeatureUtilsTest.kt
@@ -11,13 +11,9 @@ import org.wordpress.android.BaseUnitTest
 import org.wordpress.android.fluxc.model.SiteModel
 import org.wordpress.android.ui.prefs.AppPrefsWrapper
 import org.wordpress.android.util.SiteUtilsWrapper
-import org.wordpress.android.util.config.DashboardCardActivityLogConfig

 @ExperimentalCoroutinesApi
 class DashboardActivityLogCardFeatureUtilsTest : BaseUnitTest() {
-    @Mock
-    private lateinit var dashboardCardActivityLogConfig: DashboardCardActivityLogConfig
-
     @Mock
     private lateinit var siteUtilsWrapper: SiteUtilsWrapper

@@ -32,7 +28,6 @@ class DashboardActivityLogCardFeatureUtilsTest : BaseUnitTest() {
     @Before
     fun setUp() {
         dashboardActivityLogCardFeatureUtils = DashboardActivityLogCardFeatureUtils(
-            dashboardCardActivityLogConfig,
             siteUtilsWrapper,
             appPrefsWrapper
         )
@@ -40,7 +35,6 @@ class DashboardActivityLogCardFeatureUtilsTest : BaseUnitTest() {
     }

     private fun setUpMocks() {
-        whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(true)
         whenever(siteUtilsWrapper.isAccessedViaWPComRest(siteModel)).thenReturn(true)
         whenever(siteModel.hasCapabilityManageOptions).thenReturn(true)
         whenever(siteModel.isJetpackConnected).thenReturn(true)
@@ -48,16 +42,6 @@ class DashboardActivityLogCardFeatureUtilsTest : BaseUnitTest() {
         whenever(appPrefsWrapper.getShouldHideActivityDashboardCard(any())).thenReturn(false)
     }

-    @Test
-    fun `given feature flag is disabled, when isEnaled is called, then false is returned`() {
-        whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(false)
-
-        val result = dashboardActivityLogCardFeatureUtils.shouldRequestActivityCard(siteModel)
-
-        Assertions.assertThat(result).isFalse
-    }
-
-
     @Test
     fun `given site accessed is not via wpComOrJetpack, when build is called, then null is returned`() {
         whenever(siteUtilsWrapper.isAccessedViaWPComRest(siteModel)).thenReturn(false)


Dashboard Personalization: Todays Stats Implement Hide This

Closes #18944

This PR implements the logic for hiding the `todays_stats` dashboard card when the "hide this" more menu item is tapped.

**Merge Instructions**
- Ensure PR #19058 has been merged
- Remove Not Ready for Merge
- Ensure base branch is feature/dashboard-personalization
- Merge as normal

**To test:**
- Install the app
- Login to the app and select a site that has stats access
- ✅ Verify the todays stats card is shown
- Tap on todays stats card > more menu
- Tap on the Hide this more menu item
- ✅ Verify the dashboard refresh indicator is showing
- ✅ Verify the todays stats card is hidden
- Switch to another site that has stats access
- ✅ Verify the todays stats card is shown.

## Regression Notes
1. Potential unintended areas of impact
Todays stats card is not hidden

2. What I did to test those areas of impact (or what existing automated tests I relied on)
Manual and unit tests

3. What automated tests I added (or what prevented me from doing so)
`CardsSourceTest` and `TodaysStatsViewModelSliceTest`

PR submission checklist:
- [X] I have completed the Regression Notes.
- [X] I have considered adding accessibility improvements for my changes.
- [X] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist: N/A

Dashboard Personalization: Add more menu to Today's Stats card
Today's Stats card

```[tasklist]
### Tasks
- [x] Add more menu with options **View Stats** and **Hide This**
- [x] Implement action for Hide This -> Turn the today's stats preference to hide
- [x] Implement action for View Stats -> navigate to stats
- [x] Add track events (see below)
- [x] Remove footer links
- [x] Remove tracking events for footer CTA
- [x] Remove `my_site_dashboard_todays_stats_card` remote feature flag from code base
- [x] Create stats view model slice
```

**Tracking Events**
<div>

Action | Event | Property
-- | -- | --
Tap More | my_site_dashboard_contextual_menu_accessed | card: stats
Tap More > View Stats | my_site_dashboard_card_menu_item_tapped | card: stats item:view_stats
Tap > Hide This | my_site_dashboard_card_menu_item_tapped | card: stats item:hide_this

</div>

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
index 905d06f1532a..ecf046c70f95 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
@@ -312,7 +312,8 @@ class MySiteViewModel @Inject constructor(
     val onShowJetpackIndividualPluginOverlay = _onShowJetpackIndividualPluginOverlay as LiveData<Event<Unit>>
     val onTrackWithTabSource = _onTrackWithTabSource as LiveData<Event<MySiteTrackWithTabSource>>
     val selectTab: LiveData<Event<TabNavigation>> = _selectTab
-    val refresh = merge(blazeCardViewModelSlice.refresh, pagesCardViewModelSlice.refresh)
+    val refresh =
+        merge(blazeCardViewModelSlice.refresh, pagesCardViewModelSlice.refresh, todaysStatsViewModelSlice.refresh)
     val domainTransferCardRefresh = domainTransferCardViewModel.refresh

     private var shouldMarkUpdateSiteTitleTaskComplete = false
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt
index 386c1d8be23c..182673578d15 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt
@@ -99,7 +99,7 @@ class CardsSource @Inject constructor(
     }

     private fun getCardTypes(selectedSite: SiteModel) = mutableListOf<Type>().apply {
-        add(Type.TODAYS_STATS)
+        if (shouldRequestStatsCard(selectedSite)) add(Type.TODAYS_STATS)
         if (shouldRequestPagesCard(selectedSite)) add(Type.PAGES)
         if (dashboardActivityLogCardFeatureUtils.shouldRequestActivityCard(selectedSite)) add(Type.ACTIVITY)
         add(Type.POSTS)
@@ -110,6 +110,10 @@ class CardsSource @Inject constructor(
                 !appPrefsWrapper.getShouldHidePagesDashboardCard(selectedSite.siteId)
     }

+    private fun shouldRequestStatsCard(selectedSite: SiteModel): Boolean {
+        return !appPrefsWrapper.getShouldHideTodaysStatsDashboardCard(selectedSite.siteId)
+    }
+
     private fun MediatorLiveData<CardsUpdate>.postErrorState() {
         val lastStateCards = this.value?.cards
         val showErrorCard = lastStateCards.isNullOrEmpty()
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt
index de3a06c142cc..0a6b9f64a56d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt
@@ -7,17 +7,22 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.TodaysStat
 import org.wordpress.android.ui.mysite.SelectedSiteRepository
 import org.wordpress.android.ui.mysite.SiteNavigationAction
 import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker
+import org.wordpress.android.ui.prefs.AppPrefsWrapper
 import org.wordpress.android.viewmodel.Event
 import javax.inject.Inject

 class TodaysStatsViewModelSlice @Inject constructor(
     private val cardsTracker: CardsTracker,
     private val selectedSiteRepository: SelectedSiteRepository,
-    private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper
+    private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper,
+    private val appPrefsWrapper: AppPrefsWrapper
 ) {
     private val _onNavigation = MutableLiveData<Event<SiteNavigationAction>>()
     val onNavigation = _onNavigation

+    private val _refresh = MutableLiveData<Event<Boolean>>()
+    val refresh = _refresh
+
     fun getTodaysStatsBuilderParams(todaysStatsCardModel: TodaysStatsCardModel?): TodaysStatsCardBuilderParams {
         return TodaysStatsCardBuilderParams(
             todaysStatsCard = todaysStatsCardModel,
@@ -61,7 +66,8 @@ class TodaysStatsViewModelSlice @Inject constructor(
             CardsTracker.Type.STATS.label,
             TodaysStatsMenuItemType.HIDE_THIS.label
         )
-        // todo implement the logic to hide the card
+        appPrefsWrapper.setShouldHideTodaysStatsDashboardCard(selectedSiteRepository.getSelectedSite()!!.siteId, true)
+        _refresh.postValue(Event(true))
     }

     private fun onViewStatsMenuItemClick() {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
index 9d1c38c3f221..84ead9e91dc3 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
@@ -200,7 +200,8 @@ public enum DeletablePrefKey implements PrefKey {
         // Should show Jetpack Social no connections UI
         SHOULD_SHOW_JETPACK_SOCIAL_NO_CONNECTIONS,
         SHOULD_HIDE_ACTIVITY_DASHBOARD_CARD,
-        SHOULD_HIDE_PAGES_DASHBOARD_CARD
+        SHOULD_HIDE_PAGES_DASHBOARD_CARD,
+        SHOULD_HIDE_TODAY_STATS_DASHBOARD_CARD
     }

     /**
@@ -1743,4 +1744,16 @@ public static void setShouldHidePagesDashboardCard(final long siteId, final bool
     public static Boolean getShouldHidePagesDashboardCard(final long siteId) {
         return prefs().getBoolean(getSiteIdHidePagesDashboardCardKey(siteId), false);
     }
+
+    public static void setShouldHideTodaysStatsDashboardCard(final long siteId, final boolean isHidden) {
+        prefs().edit().putBoolean(getSiteIdHideTodaysStatsDashboardCardKey(siteId), isHidden).apply();
+    }
+
+    @NonNull private static String getSiteIdHideTodaysStatsDashboardCardKey(long siteId) {
+        return DeletablePrefKey.SHOULD_HIDE_TODAY_STATS_DASHBOARD_CARD.name() + siteId;
+    }
+
+    public static Boolean getShouldHideTodaysStatsDashboardCard(final long siteId) {
+        return prefs().getBoolean(getSiteIdHideTodaysStatsDashboardCardKey(siteId), false);
+    }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
index b2d4b78952bc..6f2ca0de5d1b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt
@@ -386,6 +386,14 @@ class AppPrefsWrapper @Inject constructor() {
     fun getShouldHidePagesDashboardCard(siteId: Long): Boolean =
         AppPrefs.getShouldHidePagesDashboardCard(siteId)

+    fun setShouldHideTodaysStatsDashboardCard(
+        siteId: Long,
+        isHidden: Boolean
+    ) = AppPrefs.setShouldHideTodaysStatsDashboardCard(siteId, isHidden)
+
+    fun getShouldHideTodaysStatsDashboardCard(siteId: Long): Boolean =
+        AppPrefs.getShouldHideTodaysStatsDashboardCard(siteId)
+
     fun getAllPrefs(): Map<String, Any?> = AppPrefs.getAllPrefs()

     fun setString(prefKey: PrefKey, value: String) {

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSourceTest.kt
index bb1d6d601da8..01014309a99e 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSourceTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSourceTest.kt
@@ -188,7 +188,8 @@ class CardsSourceTest : BaseUnitTest() {
     private fun setUpMocks(
         isDashboardCardActivityLogEnabled: Boolean = false,
         isRequestPages: Boolean = false,
-        isPagesCardHidden: Boolean = false
+        isPagesCardHidden: Boolean = false,
+        isTodaysStatsCardHidden: Boolean = false
     ) {
         whenever(siteModel.id).thenReturn(SITE_LOCAL_ID)
         whenever(selectedSiteRepository.getSelectedSite()).thenReturn(siteModel)
@@ -197,6 +198,8 @@ class CardsSourceTest : BaseUnitTest() {
         whenever(siteModel.hasCapabilityEditPages).thenReturn(isRequestPages)
         whenever(appPrefsWrapper.getShouldHidePagesDashboardCard(any()))
             .thenReturn(isPagesCardHidden)
+        whenever(appPrefsWrapper.getShouldHideTodaysStatsDashboardCard(any()))
+            .thenReturn(isTodaysStatsCardHidden)
     }

     /* GET DATA */
@@ -544,4 +547,37 @@ class CardsSourceTest : BaseUnitTest() {

             verify(cardsStore).fetchCards(siteModel, DEFAULT_CARD_TYPE)
         }
+
+    @Test
+    fun `given stats card not hidden, when refresh is invoked, then stats requested from network`() =
+        test {
+            setUpMocks(isTodaysStatsCardHidden = false)
+            val result = mutableListOf<CardsUpdate>()
+            whenever(cardsStore.getCards(siteModel)).thenReturn(flowOf(data))
+            whenever(cardsStore.fetchCards(siteModel, DEFAULT_CARD_TYPE)).thenReturn(success)
+            cardSource.refresh.observeForever { }
+
+            cardSource.build(testScope(), SITE_LOCAL_ID).observeForever {
+                it?.let { result.add(it) }
+            }
+            advanceUntilIdle()
+
+            verify(cardsStore).fetchCards(siteModel, DEFAULT_CARD_TYPE)
+        }
+
+    @Test
+    fun `given stats card is hidden, when refresh is invoked, then stats not requested from network`() =
+        test {
+            setUpMocks(isTodaysStatsCardHidden = true)
+            val result = mutableListOf<CardsUpdate>()
+            whenever(cardsStore.getCards(siteModel)).thenReturn(flowOf(data))
+            cardSource.refresh.observeForever { }
+
+            cardSource.build(testScope(), SITE_LOCAL_ID).observeForever {
+                it?.let { result.add(it) }
+            }
+            advanceUntilIdle()
+
+            verify(cardsStore).fetchCards(siteModel, listOf(CardModel.Type.POSTS))
+        }
 }
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt
index 37d325dc3408..dc057216dcdd 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt
@@ -16,6 +16,7 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
 import org.wordpress.android.ui.mysite.SelectedSiteRepository
 import org.wordpress.android.ui.mysite.SiteNavigationAction
 import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker
+import org.wordpress.android.ui.prefs.AppPrefsWrapper

 @ExperimentalCoroutinesApi
 @RunWith(MockitoJUnitRunner::class)
@@ -29,10 +30,15 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() {
     @Mock
     lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper

+    @Mock
+    lateinit var appPrefsWrapper: AppPrefsWrapper
+
     private lateinit var todaysStatsViewModelSlice: TodaysStatsViewModelSlice

     private lateinit var navigationActions: MutableList<SiteNavigationAction>

+    private lateinit var refreshEvents: MutableList<Boolean>
+
     private val site = mock<SiteModel>()

     @Before
@@ -40,7 +46,8 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() {
         todaysStatsViewModelSlice = TodaysStatsViewModelSlice(
             cardsTracker,
             selectedSiteRepository,
-            jetpackFeatureRemovalPhaseHelper
+            jetpackFeatureRemovalPhaseHelper,
+            appPrefsWrapper
         )
         navigationActions = mutableListOf()
         todaysStatsViewModelSlice.onNavigation.observeForever { event ->
@@ -48,6 +55,12 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() {
                 navigationActions.add(it)
             }
         }
+        refreshEvents = mutableListOf()
+        todaysStatsViewModelSlice.refresh.observeForever { event ->
+            event?.getContentIfNotHandled()?.let {
+                refreshEvents.add(it)
+            }
+        }
         whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site)
     }

@@ -110,13 +123,18 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() {

     @Test
     fun `given todays stats card, when more menu item hide this is accessed, then event is tracked`() = test {
+        val siteId = 1L
+        whenever(selectedSiteRepository.getSelectedSite()?.siteId).thenReturn(siteId)
+
         val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(mock())

         params.moreMenuClickParams.onHideThisMenuItemClick.invoke()

+        verify(appPrefsWrapper).setShouldHideTodaysStatsDashboardCard(siteId,true)
         verify(cardsTracker).trackCardMoreMenuItemClicked(
             CardsTracker.Type.STATS.label,
             TodaysStatsMenuItemType.HIDE_THIS.label
         )
+        assertThat(refreshEvents).containsOnly(true)
     }
 }


[Post Upload] Crash fix: remove call to mutex#unlock from UploadStarter

Fixes #18958

The `mutex.unlock` call is redundant (because we already use `withLock`, which unlocks after the block runs) and can be the source of crashes since the mutex cannot be unlocked if it's not locked and the internal `Mutex` source code tries to unlock it in certain Cancellation situations.

More info in the issue comment: https://github.com/wordpress-mobile/WordPress-Android/issues/18958#issuecomment-1675239908

To test:
We could not find a way of reproducing this issue as it might be related to some race conditions, multiple threads, and likely, how the device / system handles them.

## Regression Notes
1. Potential unintended areas of impact
N/A

2. What I did to test those areas of impact (or what existing automated tests I relied on)
N/A

3. What automated tests I added (or what prevented me from doing so)
WIP

PR submission checklist:

- [x] I have completed the Regression Notes.
- [x] I have considered adding accessibility improvements for my changes.
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

UI Changes testing checklist:
None

CompletionHandlerException: Exception in resume onCancellation handler for CancellableContinuation(DispatchedContinuation[Dis...
Sentry Issue: [JETPACK-ANDROID-92S](https://a8c.sentry.io/issues/4295575735/?referrer=github_integration)

```
IllegalStateException: This mutex is not locked
    at kotlinx.coroutines.sync.MutexImpl.unlock(Mutex.kt:207)
    at kotlinx.coroutines.sync.MutexImpl$CancellableContinuationWithOwner$tryResume$token$1.invoke(Mutex.kt:255)
    at kotlinx.coroutines.sync.MutexImpl$CancellableContinuationWithOwner$tryResume$token$1.invoke(Mutex.kt:252)
    at kotlinx.coroutines.CancellableContinuationImpl.callOnCancellation(CancellableContinuationImpl.kt:259)
    at kotlinx.coroutines.CompletedContinuation.invokeHandlers(CancellableContinuationImpl.kt:659)
...
(8 additional frame(s) were not displayed)

CompletionHandlerException: Exception in resume onCancellation handler for CancellableContinuation(DispatchedContinuation[Dispatchers.IO, Continuation at org.wordpress.android.ui.uploads.UploadStarter$upload$2.invokeSuspend(UploadStarter.kt:211)@df58903]){Completed}@c9c7080
    at kotlinx.coroutines.CancellableContinuationImpl.callOnCancellation(CancellableContinuationImpl.kt:264)
    at kotlinx.coroutines.CompletedContinuation.invokeHandlers(CancellableContinuationImpl.kt:659)
    at kotlinx.coroutines.CancellableContinuationImpl.cancelCompletedResult$kotlinx_coroutines_core(CancellableContinuationImpl.kt:180)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
    at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
...
(5 additional frame(s) were not displayed)
```

This is not a duplicate of #17463 since this is now happening on coroutines 1.7.1 which fixed the underlying root cause of that other ticket. This has a similar stacktrace, but it looks like we might be misusing the Mutex in our `UploadStarter` implementation.

**TLDR:** I think our code is silently making the crash look like it’s an internal Mutex problem but in reality, we might be calling `unlock` unnecessarily when the Mutex internals are not expecting it. We should remove [this line](https://github.com/wordpress-mobile/WordPress-Android/blob/1b5e1f3fc97e13d0068ea803d5df7777f421180b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt#L186) and I think it might solve our issues on coroutines 1.7.1.

Based on the [breadcrumbs](https://a8c.sentry.io/issues/4295575735/?project=5731682&referrer=issue-stream&sort=freq&statsPeriod=14d&stream_index=1#breadcrumbs) I believe the actual issue is that we tried to [start the post upload service](https://github.com/wordpress-mobile/WordPress-Android/blob/1b5e1f3fc97e13d0068ea803d5df7777f421180b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt#L175-L179) while the app was in the background and the system didn’t let us, which caused all `upload` jobs (created [here](https://github.com/wordpress-mobile/WordPress-Android/blob/1b5e1f3fc97e13d0068ea803d5df7777f421180b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt#L138-L142)) to be canceled since the [Parent Job](https://github.com/wordpress-mobile/WordPress-Android/blob/1b5e1f3fc97e13d0068ea803d5df7777f421180b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt#L68) is not a `SupervisorJob`.

This made all jobs (except the first one which failed because of an `IllegalStateException`) [fall here](https://github.com/wordpress-mobile/WordPress-Android/blob/1b5e1f3fc97e13d0068ea803d5df7777f421180b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt#L183-L187) (as can be seen in the breadcrumb logs).

**What I first thought could be happening is:**

We use the `bgDispatcher` ([Dispatchers.Default](https://github.com/wordpress-mobile/WordPress-Android/blob/e03d208247c7487a7497f873d91f6b0e52a3622e/WordPress/src/main/java/org/wordpress/android/modules/ThreadModule.kt#L40-L42)) which has several threads in the pool and we use the same `mutex` for all upload jobs. Because of that, multiple coroutines (running in different threads) are suspended awaiting the to [acquire](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Semaphore.kt#L196-L201C6) the semaphore (which [creates a queue of segments](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Semaphore.kt#L285-L315)).

Since multiple jobs are being canceled at once, I think that the cancellation was not able to be properly propagated and the `segment` state for one of them was not marked as `CANCELLED` making the `Semaphore#tryResumeNextFromQueue` ([which is called on](https://github.com/Kotlin/kotlinx.coroutines/blob/1.7.1/kotlinx-coroutines-core/common/src/sync/Semaphore.kt#L265) `Semaphore#release` [via the](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Mutex.kt#L216) `Mutex#unlock`) [call the](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Semaphore.kt#L329-L341) `tryResumeAcquire`. Which in turn [calls](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Semaphore.kt#L347C23-L347C23) `Mutex#tryResume` but since that is canceled it runs the `onCancellation` [callback](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Mutex.kt#L253-L255) when trying to resume the underlying continuation, which is [where the crash is happening](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Mutex.kt#L250-L262).

But I am not 100% sure about that, as I feel like this scenario would be pretty common for a synchronization mechanism using coroutines (like `Mutex` and `Semaphore`), which is why I believe that `onCancellation` callback also unlocks the `Mutex`, in case of that late propagation of the cancellation to the mutex `Segment`.

**So, considering that the code makes sense and works (hopefully) what I think might be the issue is:**

We also have a [call to](https://github.com/wordpress-mobile/WordPress-Android/blob/1b5e1f3fc97e13d0068ea803d5df7777f421180b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt#L186) `unlock` on our side and from the breadcrumbs we know that the code is reaching that line. So it might be that this `unlock` is actually unlocking the mutex **at the same time as** the whole cancellation flow I mentioned above is happening (_which would be possible because of the multiple threads in the Dispatcher pool_), so when the cancelation reaches the `onCancellation` [callback here](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Mutex.kt#L253-L255) our code already unlocked it, causing the crash to happen in the `Mutex` internals.

This is my best guess, but the Mutex/Semaphore code is a bit complex and I didn’t fully understand it yet (and probably never will 😆).

### Suggestion
Remove that `unlock` [call ASAP](https://github.com/wordpress-mobile/WordPress-Android/blob/1b5e1f3fc97e13d0068ea803d5df7777f421180b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt#L186) as we really don’t need it since we use `withLock` which is better in 2 ways:
1. It makes sure to `unlock` when exceptions happen ([try/finally](https://github.com/Kotlin/kotlinx.coroutines/blob/02382479398518ea1c4507170aa92692595b6839/kotlinx-coroutines-core/common/src/sync/Mutex.kt#L120-L131))
2. It only calls `unlock` if it was successfully locked before running the `action`, so if my hypothesis above is correct, the internal `lockSuspend` code will cause a cancellation before reaching that `try/finally` block

./gradlew :WordPress:testWordpressVanillaDebugUnitTest --tests "org.wordpress.android.ui.uploads.UploadStarterMutexTest"

diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt
index 66bff7046d62..ceca1428fc62 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadStarter.kt
@@ -136,7 +136,7 @@ class UploadStarter @Inject constructor(
         }

         sites.forEach {
-            launch(ioDispatcher) {
+            launch {
                 upload(site = it)
             }
         }
@@ -148,8 +148,8 @@ class UploadStarter @Inject constructor(
     private suspend fun upload(site: SiteModel) = coroutineScope {
         try {
             mutex.withLock {
-                val posts = async { postStore.getPostsWithLocalChanges(site) }
-                val pages = async { pageStore.getPagesWithLocalChanges(site) }
+                val posts = async(ioDispatcher) { postStore.getPostsWithLocalChanges(site) }
+                val pages = async(ioDispatcher) { pageStore.getPagesWithLocalChanges(site) }
                 val list = posts.await() + pages.await()

                 list.asSequence()
@@ -181,9 +181,7 @@ class UploadStarter @Inject constructor(
             }
         } catch (e: CancellationException) {
             AppLog.e(T.MEDIA, e)
-            // Do any needed actions while we are still holding the mutex lock, then release it and rethrow the
-            // exception so it can be handled upstream
-            mutex.unlock()
+            // rethrow the exception so it can be handled upstream
             throw e
         }
     }

diff --git a/WordPress/src/test/java/org/wordpress/android/ui/uploads/UploadStarterMutexTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/uploads/UploadStarterMutexTest.kt
new file mode 100644
index 000000000000..611e018567a2
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/uploads/UploadStarterMutexTest.kt
@@ -0,0 +1,142 @@
+package org.wordpress.android.ui.uploads
+
+import android.content.Context
+import androidx.lifecycle.MutableLiveData
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.delay
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doSuspendableAnswer
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+import org.wordpress.android.fluxc.Dispatcher
+import org.wordpress.android.fluxc.model.PostModel
+import org.wordpress.android.fluxc.store.PageStore
+import org.wordpress.android.fluxc.store.PostStore
+import org.wordpress.android.fluxc.store.SiteStore
+import org.wordpress.android.util.NetworkUtilsWrapper
+import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
+import org.wordpress.android.viewmodel.helpers.ConnectionStatus
+import java.util.concurrent.Executors
+
+/**
+ * This class tries to reproduce the situation of trying to start the upload service when the device is in the
+ * background. Such situation causes the system to throw a IllegalStateException, as it's not allowed to create
+ * services from the background, which in turns causes a failure in the coroutine jobs, which should be cancelled
+ * without crashing the application.
+ *
+ * This was created because the situation above was causing a crash in our app:
+ * https://a8c.sentry.io/issues/4295575735/?project=573168
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class UploadStarterMutexTest : BaseUnitTest() {
+    private val bgDispatcher = Executors.newFixedThreadPool(DEFAULT_THREADS).asCoroutineDispatcher()
+    private val ioDispatcher = Executors.newFixedThreadPool(IO_THREADS).asCoroutineDispatcher()
+
+    @Mock
+    lateinit var context: Context
+
+    @Mock
+    lateinit var dispatcher: Dispatcher
+
+    @Mock
+    lateinit var postStore: PostStore
+
+    @Mock
+    lateinit var pageStore: PageStore
+
+    @Mock
+    lateinit var siteStore: SiteStore
+
+    @Mock
+    lateinit var uploadActionUseCase: UploadActionUseCase
+
+    @Mock
+    lateinit var tracker: AnalyticsTrackerWrapper
+
+    @Mock
+    lateinit var uploadServiceFacade: UploadServiceFacade
+
+    @Mock
+    lateinit var networkUtilsWrapper: NetworkUtilsWrapper
+
+    private var connectionStatus: MutableLiveData<ConnectionStatus> = MutableLiveData()
+
+    private lateinit var uploadStarter: UploadStarter
+
+    @Before
+    fun setUp() {
+        uploadStarter = UploadStarter(
+            context = context,
+            dispatcher = dispatcher,
+            postStore = postStore,
+            pageStore = pageStore,
+            siteStore = siteStore,
+            uploadActionUseCase = uploadActionUseCase,
+            tracker = tracker,
+            bgDispatcher = bgDispatcher,
+            ioDispatcher = ioDispatcher,
+            uploadServiceFacade = uploadServiceFacade,
+            networkUtilsWrapper = networkUtilsWrapper,
+            connectionStatus = connectionStatus
+        )
+    }
+
+    @Test
+    fun `should not crash if uploadPost throws an exception`() = test {
+        // network is available
+        whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true)
+        // return 10 sites for queuing
+        whenever(siteStore.sites).thenReturn(List(10) { mock() })
+        // each site has 10 posts with local changes
+        whenever(postStore.getPostsWithLocalChanges(any())).doAnswer {
+            Thread.sleep(10)
+            List(10) { fakePostModel(it, false) }
+        }
+        // each site has 5 pages with local changes
+        whenever(pageStore.getPagesWithLocalChanges(any())).doSuspendableAnswer {
+            delay(10)
+            List(5) {
+                fakePostModel(
+                    100 + it,
+                    true
+                )
+            }
+        }
+
+        whenever(uploadActionUseCase.getAutoUploadAction(any(), any()))
+            .thenReturn(UploadActionUseCase.UploadAction.UPLOAD)
+
+        // throw IllegalStateException when uploading post (emulate trying to start background service)
+        whenever(uploadServiceFacade.uploadPost(any(), any<PostModel>(), any()))
+            .thenThrow(IllegalStateException("FAKE: Not allowed to start service intent"))
+
+        // ACT
+        uploadStarter.queueUploadFromAllSites().join()
+    }
+
+    companion object {
+        // # of threads in Dispatchers.Default is equal to the # of CPU cores, 8 is a common # of cores in modern phones
+        // https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
+        private const val DEFAULT_THREADS = 8
+
+        // default # of threads in Dispatchers.IO is 64 but it can be even larger, let's use 64, which is enough
+        // https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-i-o.html
+        private const val IO_THREADS = 64
+
+        private fun fakePostModel(
+            id: Int,
+            isPage: Boolean,
+        ) = PostModel().apply {
+            setId(id)
+            setIsPage(isPage)
+            setTitle("Title $id")
+            setStatus("draft")
+        }
+    }
+}


### Details
We are now consistently show the `recents` and `contacts` headers on the participant selector pages across all four of these flows: `Request Money`, `Split Bill`, `New Chat` and New` Group`.

### Fixed Issues
Fixed #2760

### Tests

1. Click the `+` icon on the home screen to access the CreateMenu
2. Select `Request Money
`3. Enter an amount and click `next` to access the single-user picker on the IOUParticipants page

OR

1. Click the `+` icon on the home screen to access the CreateMenu
2. Select `New Chat` to access the single-user picker on the Participants page

### QA Steps
See above

### Tested On

- [x] Web
- [x] Mobile Web
- [x] Desktop
- [x] iOS
- [x] Android

### Screenshots

#### Web
![Screen Shot 2021-05-13 at 4 59 02 PM](https://user-images.githubusercontent.com/11542415/118122669-9b1e6600-b40c-11eb-9678-03d678c0808e.png)

#### Mobile Web
![Screen Shot 2021-05-13 at 5 11 34 PM](https://user-images.githubusercontent.com/11542415/118123874-47ad1780-b40e-11eb-96af-1cbf577386dc.png)

#### Desktop
![Screen Shot 2021-05-13 at 5 11 49 PM](https://user-images.githubusercontent.com/11542415/118123908-50055280-b40e-11eb-90d5-01dfd88b4e09.png)

#### iOS
![Screen Shot 2021-05-13 at 5 05 00 PM](https://user-images.githubusercontent.com/11542415/118123214-5e9f3a00-b40d-11eb-9199-56a2f53cffc4.png)

#### Android
![Screen Shot 2021-05-13 at 5 10 30 PM](https://user-images.githubusercontent.com/11542415/118123748-23513b00-b40e-11eb-8c8f-349bda947d6c.png)

Add the Recents and Contacts headers to the single-user participants page used in the Request Money and New Chat flows
If you haven’t already, check out our [contributing guidelines](https://github.com/Expensify/ReactNativeChat/blob/main/CONTRIBUTING.md) for onboarding and email contributors@expensify.com to request to join our Slack channel!
___

**How to enable the `Request Money` or `Spilt Bills` flow**
As the feature is currently behind a beta, you must perform the following change to reproduce the issue. Either navigate to Permissions.js,[ comment out this line ](https://github.com/Expensify/Expensify.cash/blob/e28b50e139c492442ea984c3d48e23be744ceb4e/src/libs/Permissions.js#L31)and return true -- or apply this [diff.txt](https://github.com/Expensify/Expensify/files/6427621/diff.txt) with ` git apply path/to/diff.txt`

## Expected Result:
- We should consistently show the `recents` and `contacts` headers on the participant selector pages across all four of these flows: `Request Money`, `Split Bill`, `New Chat` and `New Group`.

## Actual Result:
- We don't display the headers on the single-user picker on the participants page within the  `Request Money` or `New Chat` flows today, but we do for the multi-user picker in the `Split Bill` & `New Group` flows.

## Action Performed:
1. Click the `+` icon on the home screen to access the CreateMenu
2. Select `Request Money`
3. Enter an amount and click `next` to access the single-user picker on the IOUParticipants page

OR

1. Click the `+` icon on the home screen to access the CreateMenu
2. Select `New Chat` to access the single-user picker on the Participants page

## Workaround:
Users can still search for users, this is an optimization in convenience and recent activity.

## Platform:
- All platforms

**Version Number:** `v1.0.37-0`

**Notes/Photos/Videos:** Here's an example of how `Request Money` should look:
![image](https://user-images.githubusercontent.com/16232057/117209193-f12c5180-aded-11eb-9ee6-f37806664654.png)

**Expensify/Expensify Issue URL:** https://github.com/Expensify/Expensify/issues/162993

[View all open jobs on Upwork](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2)

Triggered auto assignment to @mallenexpensify (`External`), see https://stackoverflow.com/c/expensify/questions/8582 for more details.
Triggered auto assignment to @chiragsalian (`Engineering`), see https://stackoverflow.com/c/expensify/questions/4319 for more details.
Sorry about that. Disregard @chiragsalian and @mallenexpensify
Posting to Upwork now.
Thanks for checking us out on Upwork!

If you are interested in working on this issue, please apply to [this Upwork posting](https://www.upwork.com/jobs/~015652383fbbeae713), AND then post directly on this issue with a proposal with the technical explanation of the changes you will make. Somebody from Expensify will review your proposal, and if your proposal is accepted, you will be hired in Upwork for the job.

Thank you!

NODE_OPTIONS='--experimental-vm-modules --max-old-space-size=4096' npx --no-install jest tests/unit/OptionsListUtilsTest.js --json --outputFile=/tmp/test-results.json --passWithNoTests 2>&1

diff --git a/src/components/OptionsList.js b/src/components/OptionsList.js
index 5629831e7f49..96c06425fbb9 100644
--- a/src/components/OptionsList.js
+++ b/src/components/OptionsList.js
@@ -188,7 +188,7 @@ class OptionsList extends Component {
         if (title && shouldShow && !this.props.hideSectionHeaders) {
             return (
                 <View>
-                    <Text style={[styles.p5, styles.textMicroBold, styles.colorHeading]}>
+                    <Text style={[styles.p5, styles.textMicroBold, styles.colorHeading, styles.textUppercase]}>
                         {title}
                     </Text>
                 </View>
diff --git a/src/languages/en.js b/src/languages/en.js
index ea9b5061e42f..6b1eff6d2840 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -27,6 +27,8 @@ export default {
         email: 'Email',
         and: 'and',
         details: 'Details',
+        contacts: 'Contacts',
+        recents: 'Recents',
     },
     attachmentPicker: {
         cameraPermissionRequired: 'Camera Permission Required',
@@ -93,8 +95,6 @@ export default {
         newGroup: 'New Group',
     },
     iou: {
-        contacts: 'CONTACTS',
-        recents: 'RECENTS',
         amount: 'Amount',
         participants: 'Participants',
         confirm: 'Confirm',
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index f53835f9a116..c02ceb802278 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -364,6 +364,8 @@ function getNewChatOptions(
     return getOptions(reports, personalDetails, {}, 0, {
         searchValue,
         includePersonalDetails: true,
+        includeRecentReports: true,
+        maxRecentReportsToShow: 5,
         excludeConcierge,
     });
 }
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js
index b87a755f288f..6922f9d81cce 100755
--- a/src/pages/NewChatPage.js
+++ b/src/pages/NewChatPage.js
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
 import {View} from 'react-native';
 import PropTypes from 'prop-types';
 import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
 import OptionsSelector from '../components/OptionsSelector';
 import {getNewChatOptions, getHeaderMessage} from '../libs/OptionsListUtils';
 import ONYXKEYS from '../ONYXKEYS';
@@ -54,6 +55,7 @@ class NewChatPage extends Component {
         this.createNewChat = this.createNewChat.bind(this);

         const {
+            recentReports,
             personalDetails,
             userToInvite,
         } = getNewChatOptions(
@@ -64,6 +66,7 @@ class NewChatPage extends Component {

         this.state = {
             searchValue: '',
+            recentReports,
             personalDetails,
             userToInvite,
         };
@@ -78,9 +81,16 @@ class NewChatPage extends Component {
         const sections = [];

         sections.push({
-            title: this.props.translate('iou.contacts'),
+            title: this.props.translate('common.recents'),
+            data: this.state.recentReports,
+            shouldShow: !_.isEmpty(this.state.recentReports),
+            indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
+        });
+
+        sections.push({
+            title: this.props.translate('common.contacts'),
             data: this.state.personalDetails,
-            shouldShow: this.state.personalDetails.length > 0,
+            shouldShow: !_.isEmpty(this.state.personalDetails),
             indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
         });

@@ -128,6 +138,7 @@ class NewChatPage extends Component {
                         onSelectRow={this.createNewChat}
                         onChangeText={(searchValue = '') => {
                             const {
+                                recentReports,
                                 personalDetails,
                                 userToInvite,
                             } = getNewChatOptions(
@@ -137,12 +148,12 @@ class NewChatPage extends Component {
                             );
                             this.setState({
                                 searchValue,
+                                recentReports,
                                 userToInvite,
                                 personalDetails,
                             });
                         }}
                         headerMessage={headerMessage}
-                        hideSectionHeaders
                         disableArrowKeysActions
                         hideAdditionalOptionStates
                         forceTextUnreadStyle
diff --git a/src/pages/NewGroupPage.js b/src/pages/NewGroupPage.js
index 3a5bd94ec924..525f286863a2 100755
--- a/src/pages/NewGroupPage.js
+++ b/src/pages/NewGroupPage.js
@@ -97,16 +97,16 @@ class NewGroupPage extends Component {
         }

         sections.push({
-            title: this.props.translate('iou.recents'),
+            title: this.props.translate('common.recents'),
             data: this.state.recentReports,
-            shouldShow: this.state.recentReports.length > 0,
+            shouldShow: !_.isEmpty(this.state.recentReports),
             indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
         });

         sections.push({
-            title: this.props.translate('iou.contacts'),
+            title: this.props.translate('common.contacts'),
             data: this.state.personalDetails,
-            shouldShow: this.state.personalDetails.length > 0,
+            shouldShow: !_.isEmpty(this.state.personalDetails),
             indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
         });

diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js
index dff19ddea251..9d8a95e326e2 100755
--- a/src/pages/SearchPage.js
+++ b/src/pages/SearchPage.js
@@ -90,7 +90,7 @@ class SearchPage extends Component {
      */
     getSections() {
         const sections = [{
-            title: this.props.translate('iou.recents'),
+            title: this.props.translate('common.recents'),
             data: this.state.recentReports.concat(this.state.personalDetails),
             shouldShow: true,
             indexOffset: 0,
diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
index 56d1769a5c4a..19bdb905ccdf 100755
--- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
+++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
@@ -1,6 +1,7 @@
 import React, {Component} from 'react';
 import PropTypes from 'prop-types';
 import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
 import {getNewChatOptions} from '../../../../libs/OptionsListUtils';
 import OptionsSelector from '../../../../components/OptionsSelector';
 import ONYXKEYS from '../../../../ONYXKEYS';
@@ -44,7 +45,11 @@ class IOUParticipantsRequest extends Component {

         this.addSingleParticipant = this.addSingleParticipant.bind(this);

-        const {personalDetails, userToInvite} = getNewChatOptions(
+        const {
+            recentReports,
+            personalDetails,
+            userToInvite,
+        } = getNewChatOptions(
             props.reports,
             props.personalDetails,
             '',
@@ -52,6 +57,7 @@ class IOUParticipantsRequest extends Component {
         );

         this.state = {
+            recentReports,
             personalDetails,
             userToInvite,
             searchValue: '',
@@ -65,10 +71,18 @@ class IOUParticipantsRequest extends Component {
      */
     getSections() {
         const sections = [];
+
+        sections.push({
+            title: this.props.translate('common.recents'),
+            data: this.state.recentReports,
+            shouldShow: !_.isEmpty(this.state.recentReports),
+            indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
+        });
+
         sections.push({
-            title: this.props.translate('iou.contacts'),
+            title: this.props.translate('common.contacts'),
             data: this.state.personalDetails,
-            shouldShow: this.state.personalDetails.length > 0,
+            shouldShow: !_.isEmpty(this.state.personalDetails),
             indexOffset: 0,
         });

@@ -102,7 +116,11 @@ class IOUParticipantsRequest extends Component {
                 value={this.state.searchValue}
                 onSelectRow={this.addSingleParticipant}
                 onChangeText={(searchValue = '') => {
-                    const {personalDetails, userToInvite} = getNewChatOptions(
+                    const {
+                        recentReports,
+                        personalDetails,
+                        userToInvite,
+                    } = getNewChatOptions(
                         this.props.reports,
                         this.props.personalDetails,
                         searchValue,
@@ -110,11 +128,11 @@ class IOUParticipantsRequest extends Component {
                     );
                     this.setState({
                         searchValue,
+                        recentReports,
                         userToInvite,
                         personalDetails,
                     });
                 }}
-                hideSectionHeaders
                 disableArrowKeysActions
                 hideAdditionalOptionStates
                 forceTextUnreadStyle
diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
index f1c1b0f7baeb..df8b40695799 100755
--- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
+++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
@@ -108,9 +108,9 @@ class IOUParticipantsSplit extends Component {
         }

         sections.push({
-            title: this.props.translate('iou.recents'),
+            title: this.props.translate('common.recents'),
             data: this.state.recentReports,
-            shouldShow: this.state.recentReports.length > 0,
+            shouldShow: !_.isEmpty(this.state.recentReports),

             // takes the sum off the length of all data
             // (this.state.selectedOptions) in previous sections
@@ -118,9 +118,9 @@ class IOUParticipantsSplit extends Component {
         });

         sections.push({
-            title: this.props.translate('iou.contacts'),
+            title: this.props.translate('common.contacts'),
             data: this.state.personalDetails,
-            shouldShow: this.state.personalDetails.length > 0,
+            shouldShow: !_.isEmpty(this.state.personalDetails),

             // takes the sum off the length of all data
             // (this.state.selectedOptions, this.state.recentReports) in previous sections
diff --git a/src/styles/styles.js b/src/styles/styles.js
index b6cbb169ef01..940649298cb4 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -89,6 +89,10 @@ const styles = {
         textDecorationLine: 'none',
     },

+    textUppercase: {
+        textTransform: 'uppercase',
+    },
+
     colorReversed: {
         color: themeColors.textReversed,
     },

diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js
index d24b3b5a2cdf..9ce34322cdfb 100644
--- a/tests/unit/OptionsListUtilsTest.js
+++ b/tests/unit/OptionsListUtilsTest.js
@@ -193,20 +193,25 @@ describe('OptionsListUtils', () => {
     });

     it('getNewChatOptions()', () => {
+        // maxRecentReportsToShow in src/libs/OptionsListUtils.js
+        const MAX_RECENT_REPORTS = 5;
+
         // When we call getNewChatOptions() with no search value
         let results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, '');

-        // Then no reports should be returned, only personalDetails and all the personalDetails should be returned
-        // minus the currently logged in user
-        expect(results.recentReports.length).toBe(0);
-        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 1);
+        // We should expect maximimum of 5 recent reports to be returned
+        expect(results.recentReports.length).toBe(MAX_RECENT_REPORTS);
+
+        // We should expect all personalDetails to be returned,
+        // minus the currently logged in user and recent reports count
+        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS) - 1 - MAX_RECENT_REPORTS);

         // Then the result which has an existing report should also have the reportID attached
         const personalDetailWithExistingReport = _.find(
             results.personalDetails,
-            personalDetail => personalDetail.login === 'reedrichards@expensify.com',
+            personalDetail => personalDetail.login === 'peterparker@expensify.com',
         );
-        expect(personalDetailWithExistingReport.reportID).toBe(3);
+        expect(personalDetailWithExistingReport.reportID).toBe(2);

         // When we provide a search value that does not match any personal details
         results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, 'magneto');
@@ -217,27 +222,30 @@ describe('OptionsListUtils', () => {
         // When we provide a search value that matches an email
         results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, 'peterparker@expensify.com');

-        // Then one option will be returned and it will be the correct option
-        expect(results.personalDetails.length).toBe(1);
-        expect(results.personalDetails[0].text).toBe('Spider-Man');
+        // Then one recentReports will be returned and it will be the correct option
+        // personalDetails should be empty array
+        expect(results.recentReports.length).toBe(1);
+        expect(results.recentReports[0].text).toBe('Spider-Man');
+        expect(results.personalDetails.length).toBe(0);

         // When we provide a search value that matches a partial display name or email
         results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, 'man');

         // Then several options will be returned and they will be each have the search string in their email or name
         // even though the currently logged in user matches they should not show.
-        expect(results.personalDetails.length).toBe(3);
-        expect(results.personalDetails[0].text).toBe('Spider-Man');
-        expect(results.personalDetails[1].text).toBe('Invisible Woman');
-        expect(results.personalDetails[2].login).toBe('natasharomanoff@expensify.com');
+        expect(results.personalDetails.length).toBe(1);
+        expect(results.recentReports.length).toBe(2);
+        expect(results.personalDetails[0].login).toBe('natasharomanoff@expensify.com');
+        expect(results.recentReports[0].text).toBe('Invisible Woman');
+        expect(results.recentReports[1].text).toBe('Spider-Man');

         // Test for Concierge's existence in chat options
         results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE);

-        // Concierge is included in the results by default and all the personalDetails should be returned
-        // minus the currently logged in user
-        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 1);
-        expect(results.personalDetails).toEqual(
+        // Concierge is included in the results by default. We should expect all the personalDetails to show
+        // (minus the 5 that are already showing and the currently logged in user)
+        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 1 - MAX_RECENT_REPORTS);
+        expect(results.recentReports).toEqual(
             expect.arrayContaining([
                 expect.objectContaining({login: 'concierge@expensify.com'}),
             ]),
@@ -247,7 +255,7 @@ describe('OptionsListUtils', () => {
         results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, '', true);

         // All the personalDetails should be returned minus the currently logged in user and Concierge
-        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2);
+        expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2 - MAX_RECENT_REPORTS);
         expect(results.personalDetails).not.toEqual(
             expect.arrayContaining([
                 expect.objectContaining({login: 'concierge@expensify.com'}),