diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/GetActivityLogHiddenGroupsUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/GetActivityLogHiddenGroupsUseCase.kt new file mode 100644 index 000000000000..3fb96f166b36 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/GetActivityLogHiddenGroupsUseCase.kt @@ -0,0 +1,39 @@ +package org.wordpress.android.ui.activitylog + +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.jetpack.JetpackCapabilitiesUseCase +import javax.inject.Inject + +/** + * Determines which activity log groups should be hidden from the user for a given site. + * + * WordPress.com backs up Simple sites internally and writes those events to the activity log regardless of plan, + * so the activity endpoint can return backup ("rewind") and scan activities for sites whose plan does not include + * those products. The web Activity Log hides these groups (see getActivityLogHiddenGroups in wp-calypso), and the + * apps need to do the same to stay consistent with it. + * + * The groups stay visible when the site has the "backups-self-serve" plan feature (the same check the web makes). + * The plan feature list is not guaranteed to reflect standalone product purchases on self-hosted Jetpack sites, + * so each group also stays visible when the matching Jetpack product was purchased. + */ +class GetActivityLogHiddenGroupsUseCase @Inject constructor( + private val jetpackCapabilitiesUseCase: JetpackCapabilitiesUseCase +) { + fun getHiddenGroups(site: SiteModel): List { + if (hasBackupsSelfServeFeature(site)) { + return emptyList() + } + val purchasedProducts = jetpackCapabilitiesUseCase.getCachedJetpackPurchasedProducts(site.siteId) + return listOfNotNull( + REWIND_GROUP.takeUnless { purchasedProducts.backup }, + SCAN_GROUP.takeUnless { purchasedProducts.scan } + ) + } + + private fun hasBackupsSelfServeFeature(site: SiteModel) = + site.planActiveFeatures?.split(",")?.contains(BACKUPS_SELF_SERVE_FEATURE) == true +} + +private const val BACKUPS_SELF_SERVE_FEATURE = "backups-self-serve" +private const val REWIND_GROUP = "rewind" +private const val SCAN_GROUP = "scan" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt index 98c50ec81a8e..9e160c36dac8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt @@ -9,8 +9,10 @@ import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId import org.wordpress.android.fluxc.model.activity.ActivityTypeModel import org.wordpress.android.fluxc.store.ActivityLogStore import org.wordpress.android.fluxc.store.ActivityLogStore.FetchActivityTypesPayload +import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.activitylog.GetActivityLogHiddenGroupsUseCase import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.UiState.Content import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.UiState.Error import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.UiState.FullscreenLoading @@ -27,6 +29,8 @@ import javax.inject.Named class ActivityLogTypeFilterViewModel @Inject constructor( private val activityLogStore: ActivityLogStore, + private val siteStore: SiteStore, + private val getActivityLogHiddenGroupsUseCase: GetActivityLogHiddenGroupsUseCase, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher ) : ScopedViewModel(mainDispatcher) { @@ -70,11 +74,21 @@ class ActivityLogTypeFilterViewModel @Inject constructor( if (response.isError) { _uiState.value = buildErrorUiState() } else { - _uiState.value = buildContentUiState(response.activityTypeModels) + _uiState.value = buildContentUiState(filterOutHiddenGroups(response.activityTypeModels)) } } } + /** + * Groups hidden from the activity log list are removed from the filter too, so the user cannot select a + * filter that would always produce an empty list. + */ + private fun filterOutHiddenGroups(activityTypes: List): List { + val site = siteStore.getSiteBySiteId(remoteSiteId.value) ?: return activityTypes + val hiddenGroups = getActivityLogHiddenGroupsUseCase.getHiddenGroups(site) + return activityTypes.filterNot { hiddenGroups.contains(it.key) } + } + private fun buildErrorUiState() = Error.ConnectionError(Action(UiStringRes(R.string.retry)).apply { action = ::onRetryClicked }) diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt index e689441992ae..60c9a60d1945 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt @@ -17,6 +17,7 @@ import org.wordpress.android.fluxc.model.activity.ActivityTypeModel import org.wordpress.android.fluxc.store.ActivityLogStore import org.wordpress.android.fluxc.store.ActivityLogStore.OnActivityLogFetched import org.wordpress.android.ui.activitylog.ActivityLogNavigationEvents +import org.wordpress.android.ui.activitylog.GetActivityLogHiddenGroupsUseCase import org.wordpress.android.ui.activitylog.list.ActivityLogListItem import org.wordpress.android.ui.jetpack.JetpackCapabilitiesUseCase import org.wordpress.android.ui.jetpack.backup.download.BackupDownloadRequestState @@ -69,7 +70,8 @@ class ActivityLogViewModel @Inject constructor( private val resourceProvider: ResourceProvider, private val statsDateUtils: StatsDateUtils, private val activityLogTracker: ActivityLogTracker, - private val jetpackCapabilitiesUseCase: JetpackCapabilitiesUseCase + private val jetpackCapabilitiesUseCase: JetpackCapabilitiesUseCase, + private val getActivityLogHiddenGroupsUseCase: GetActivityLogHiddenGroupsUseCase ) : ViewModel() { enum class ActivityLogListStatus { CAN_LOAD_MORE, @@ -336,7 +338,9 @@ class ActivityLogViewModel @Inject constructor( loadMore, currentDateRangeFilter?.first?.let { Date(it) }, currentDateRangeFilter?.second?.let { Date(it) }, - currentActivityTypeFilter.map { it.key } + currentActivityTypeFilter.map { it.key }, + // The Backup screen exists to show the rewind group, so never hide groups there + if (rewindableOnly) emptyList() else getActivityLogHiddenGroupsUseCase.getHiddenGroups(site) ) fetchActivitiesJob = viewModelScope.launch { val result = activityLogStore.fetchActivities(payload) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/activitylog/GetActivityLogHiddenGroupsUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/activitylog/GetActivityLogHiddenGroupsUseCaseTest.kt new file mode 100644 index 000000000000..0a7efca6abce --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/activitylog/GetActivityLogHiddenGroupsUseCaseTest.kt @@ -0,0 +1,81 @@ +package org.wordpress.android.ui.activitylog + +import org.assertj.core.api.Assertions.assertThat +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.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.jetpack.JetpackCapabilitiesUseCase +import org.wordpress.android.ui.jetpack.JetpackCapabilitiesUseCase.JetpackPurchasedProducts + +private const val SITE_ID = 123L + +@RunWith(MockitoJUnitRunner::class) +class GetActivityLogHiddenGroupsUseCaseTest { + @Mock + private lateinit var jetpackCapabilitiesUseCase: JetpackCapabilitiesUseCase + + private lateinit var useCase: GetActivityLogHiddenGroupsUseCase + private lateinit var site: SiteModel + + @Before + fun setUp() { + useCase = GetActivityLogHiddenGroupsUseCase(jetpackCapabilitiesUseCase) + site = SiteModel().apply { siteId = SITE_ID } + } + + @Test + fun `no groups hidden, when site has the backups-self-serve feature`() { + site.planActiveFeatures = "subscriber-unlimited-imports,backups-self-serve,support" + + assertThat(useCase.getHiddenGroups(site)).isEmpty() + } + + @Test + fun `rewind and scan hidden, when site has no backup feature or purchases`() { + site.planActiveFeatures = "subscriber-unlimited-imports,support" + stubPurchasedProducts(scan = false, backup = false) + + assertThat(useCase.getHiddenGroups(site)).containsExactly("rewind", "scan") + } + + @Test + fun `rewind and scan hidden, when site has no plan features and no purchases`() { + site.planActiveFeatures = null + stubPurchasedProducts(scan = false, backup = false) + + assertThat(useCase.getHiddenGroups(site)).containsExactly("rewind", "scan") + } + + @Test + fun `only scan hidden, when site purchased a backup product`() { + site.planActiveFeatures = null + stubPurchasedProducts(scan = false, backup = true) + + assertThat(useCase.getHiddenGroups(site)).containsExactly("scan") + } + + @Test + fun `only rewind hidden, when site purchased a scan product`() { + site.planActiveFeatures = null + stubPurchasedProducts(scan = true, backup = false) + + assertThat(useCase.getHiddenGroups(site)).containsExactly("rewind") + } + + @Test + fun `no groups hidden, when site purchased backup and scan products`() { + site.planActiveFeatures = null + stubPurchasedProducts(scan = true, backup = true) + + assertThat(useCase.getHiddenGroups(site)).isEmpty() + } + + private fun stubPurchasedProducts(scan: Boolean, backup: Boolean) { + whenever(jetpackCapabilitiesUseCase.getCachedJetpackPurchasedProducts(SITE_ID)) + .thenReturn(JetpackPurchasedProducts(scan = scan, backup = backup)) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModelTest.kt index 3dc6f87b0bcd..c781fb6099c5 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModelTest.kt @@ -15,12 +15,15 @@ import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.action.ActivityLogAction.FETCH_ACTIVITY_TYPES import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.activity.ActivityTypeModel import org.wordpress.android.fluxc.store.ActivityLogStore import org.wordpress.android.fluxc.store.ActivityLogStore.ActivityTypesError import org.wordpress.android.fluxc.store.ActivityLogStore.ActivityTypesErrorType.GENERIC_ERROR import org.wordpress.android.fluxc.store.ActivityLogStore.FetchActivityTypesPayload import org.wordpress.android.fluxc.store.ActivityLogStore.OnActivityTypesFetched +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.ui.activitylog.GetActivityLogHiddenGroupsUseCase import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.ListItemUiState import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.ListItemUiState.ActivityType import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.UiState @@ -40,10 +43,18 @@ class ActivityLogTypeFilterViewModelTest : BaseUnitTest() { @Mock private lateinit var activityLogStore: ActivityLogStore + @Mock + private lateinit var siteStore: SiteStore + + @Mock + private lateinit var getActivityLogHiddenGroupsUseCase: GetActivityLogHiddenGroupsUseCase + @Before fun setUp() { viewModel = ActivityLogTypeFilterViewModel( activityLogStore, + siteStore, + getActivityLogHiddenGroupsUseCase, testDispatcher(), testDispatcher() ) @@ -207,6 +218,20 @@ class ActivityLogTypeFilterViewModelTest : BaseUnitTest() { ).containsExactlyElementsOf(initialSelection) } + @Test + fun `hidden groups are not shown in the list, when site hides activity log groups`() = test { + init(activityTypeCount = 3) + val site = SiteModel() + whenever(siteStore.getSiteBySiteId(REMOTE_SITE_ID)).thenReturn(site) + whenever(getActivityLogHiddenGroupsUseCase.getHiddenGroups(site)).thenReturn(listOf("1")) + + startVM() + + assertThat( + (viewModel.uiState.value as Content).items.filterIsInstance(ActivityType::class.java).map { it.id } + ).containsExactly("2", "3") + } + @Test fun `date range passed to the api, when provided on view model start`() = test { val after = 1L diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModelTest.kt index 19e270141b3b..03070db93ae4 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModelTest.kt @@ -36,6 +36,7 @@ import org.wordpress.android.fluxc.store.ActivityLogStore.FetchActivityLogPayloa import org.wordpress.android.fluxc.store.ActivityLogStore.OnActivityLogFetched import org.wordpress.android.ui.activitylog.ActivityLogNavigationEvents import org.wordpress.android.ui.activitylog.ActivityLogNavigationEvents.DownloadBackupFile +import org.wordpress.android.ui.activitylog.GetActivityLogHiddenGroupsUseCase import org.wordpress.android.ui.activitylog.list.ActivityLogListItem import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Notice import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Progress.Type.BACKUP_DOWNLOAD @@ -127,6 +128,9 @@ class ActivityLogViewModelTest : BaseUnitTest() { @Mock private lateinit var postDismissBackupDownloadUseCase: PostDismissBackupDownloadUseCase + @Mock + private lateinit var getActivityLogHiddenGroupsUseCase: GetActivityLogHiddenGroupsUseCase + private lateinit var fetchActivityLogCaptor: KArgumentCaptor private lateinit var formatDateRangeTimezoneCaptor: KArgumentCaptor private lateinit var viewModel: ActivityLogViewModel @@ -152,7 +156,8 @@ class ActivityLogViewModelTest : BaseUnitTest() { resourceProvider, mStatsDateUtils, activityLogTracker, - jetpackCapabilitiesUseCase + jetpackCapabilitiesUseCase, + getActivityLogHiddenGroupsUseCase ) viewModel.site = site viewModel.rewindableOnly = rewindableOnly @@ -195,6 +200,25 @@ class ActivityLogViewModelTest : BaseUnitTest() { assertFetchEvents() } + @Test + fun fetchesEventsWithHiddenGroupsExcluded() = test { + whenever(getActivityLogHiddenGroupsUseCase.getHiddenGroups(site)).thenReturn(listOf("rewind", "scan")) + + viewModel.start(site, rewindableOnly) + + verify(store).fetchActivities(fetchActivityLogCaptor.capture()) + assertEquals(listOf("rewind", "scan"), fetchActivityLogCaptor.lastValue.notGroups) + } + + @Test + fun fetchesEventsWithoutHiddenGroupsWhenRewindableOnly() = test { + viewModel.start(site, rewindableOnly = true) + + verify(store).fetchActivities(fetchActivityLogCaptor.capture()) + assertTrue(fetchActivityLogCaptor.lastValue.notGroups.isEmpty()) + verify(getActivityLogHiddenGroupsUseCase, never()).getHiddenGroups(anyOrNull()) + } + @Test fun fetchesEventsOnPullToRefresh() = test { viewModel.onPullToRefresh() diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt index 64f6a1a8a302..c661d78e6c45 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt @@ -278,6 +278,9 @@ class ActivityLogRestClient @Inject constructor( payload.groups.forEachIndexed { index, value -> params["group[$index]"] = value } + payload.notGroups.forEachIndexed { index, value -> + params["not_group[$index]"] = value + } return params } diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/ActivityLogStore.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/ActivityLogStore.kt index ba9c892bdde4..68cb2ee84d18 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/ActivityLogStore.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/ActivityLogStore.kt @@ -360,7 +360,8 @@ class ActivityLogStore val loadMore: Boolean = false, val after: Date? = null, val before: Date? = null, - val groups: List = listOf() + val groups: List = listOf(), + val notGroups: List = listOf() ) : Payload() class FetchRewindStatePayload(val site: SiteModel) : Payload() diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClientTest.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClientTest.kt index 46523934cfb9..d1db2b3612a7 100644 --- a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClientTest.kt +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClientTest.kt @@ -60,6 +60,7 @@ import java.util.TimeZone private const val DATE_1_IN_MILLIS = 1578614400000L // 2020-01-10T00:00:00+00:00 private const val DATE_2_IN_MILLIS = 1578787200000L // 2020-01-12T00:00:00+00:00 +@Suppress("LargeClass") @RunWith(MockitoJUnitRunner::class) class ActivityLogRestClientTest { @Mock private lateinit var dispatcher: Dispatcher @@ -117,6 +118,23 @@ class ActivityLogRestClientTest { } } + @Test + fun fetchActivity_passesNotGroupParamsToBuildRequest() = test { + initFetchActivity() + val payload = FetchActivityLogPayload( + site, + false, + notGroups = listOf("rewind", "scan") + ) + + activityRestClient.fetchActivity(payload, number, offset) + + with(paramsCaptor.firstValue) { + assertEquals("rewind", this["not_group[0]"]) + assertEquals("scan", this["not_group[1]"]) + } + } + @Test fun fetchActivity_passesOnlyNonEmptyParamsToBuildRequest() = test { initFetchActivity()