From 62297cdf2ec3cc1fe5c944f2763b41affe72d0bb Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:42:39 +1100 Subject: [PATCH 01/56] Create TrafficOverviewMapper.kt Add Mapper for Traffic Charts --- .../sections/traffic/TrafficOverviewMapper.kt | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewMapper.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewMapper.kt new file mode 100644 index 000000000000..bc1bf50cd4cb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewMapper.kt @@ -0,0 +1,194 @@ +package org.wordpress.android.ui.stats.refresh.lists.sections.traffic + +import org.wordpress.android.R +import org.wordpress.android.fluxc.model.stats.time.VisitsAndViewsModel +import org.wordpress.android.fluxc.network.utils.StatsGranularity +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewMapper.SelectedType.Comments +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewMapper.SelectedType.Likes +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewMapper.SelectedType.Views +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewMapper.SelectedType.Visitors +import org.wordpress.android.ui.stats.refresh.utils.ContentDescriptionHelper +import org.wordpress.android.ui.stats.refresh.utils.MILLION +import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter +import org.wordpress.android.ui.stats.refresh.utils.StatsUtils +import org.wordpress.android.viewmodel.ResourceProvider +import javax.inject.Inject + +class TrafficOverviewMapper @Inject constructor( + private val statsDateFormatter: StatsDateFormatter, + private val resourceProvider: ResourceProvider, + private val statsUtils: StatsUtils, + private val contentDescriptionHelper: ContentDescriptionHelper +) { + private val units = listOf( + R.string.stats_views, + R.string.stats_visitors, + R.string.stats_likes, + R.string.stats_comments + ) + + enum class SelectedType(val value: Int) { + Views(0), + Visitors(1), + Likes(2), + Comments(3); + + companion object { + fun valueOf(value: Int): SelectedType? = entries.find { it.value == value } + } + } + + fun buildTitle( + selectedItem: VisitsAndViewsModel.PeriodData, + previousItem: VisitsAndViewsModel.PeriodData?, + selectedPosition: Int, + isLast: Boolean, + startValue: Int = MILLION, + statsGranularity: StatsGranularity = StatsGranularity.DAYS + ): BlockListItem.ValueItem { + val value = selectedItem.getValue(selectedPosition) ?: 0 + val previousValue = previousItem?.getValue(selectedPosition) + val positive = value >= (previousValue ?: 0) + val change = statsUtils.buildChange(previousValue, value, positive, isFormattedNumber = true) + val period = when (statsGranularity) { + StatsGranularity.WEEKS -> R.string.stats_traffic_change_weeks + StatsGranularity.MONTHS -> R.string.stats_traffic_change_months + StatsGranularity.YEARS -> R.string.stats_traffic_change_years + else -> R.string.stats_traffic_change_weeks + } + val unformattedChange = statsUtils.buildChange(previousValue, value, positive, isFormattedNumber = false) + val state = when { + isLast -> BlockListItem.ValueItem.State.NEUTRAL + positive -> BlockListItem.ValueItem.State.POSITIVE + else -> BlockListItem.ValueItem.State.NEGATIVE + } + return BlockListItem.ValueItem( + value = statsUtils.toFormattedString(value, startValue), + unit = units[selectedPosition], + isFirst = true, + change = change, + period = period, + state = state, + contentDescription = resourceProvider.getString( + R.string.stats_overview_content_description, + value, + resourceProvider.getString(units[selectedPosition]), + "", + statsDateFormatter.printGranularDate(selectedItem.period, statsGranularity), + unformattedChange ?: "" + ) + ) + } + + private fun VisitsAndViewsModel.PeriodData.getValue( + selectedPosition: Int + ): Long? { + return when (SelectedType.valueOf(selectedPosition)) { + Views -> this.views + Visitors -> this.visitors + Likes -> this.likes + Comments -> this.comments + else -> null + } + } + + fun buildColumns( + selectedItem: VisitsAndViewsModel.PeriodData?, + onColumnSelected: (position: Int) -> Unit, + selectedPosition: Int + ): BlockListItem.Columns { + val views = selectedItem?.views ?: 0 + val visitors = selectedItem?.visitors ?: 0 + val likes = selectedItem?.likes ?: 0 + val comments = selectedItem?.comments ?: 0 + return BlockListItem.Columns( + listOf( + BlockListItem.Columns.Column( + R.string.stats_views, + statsUtils.toFormattedString(views), + contentDescriptionHelper.buildContentDescription( + R.string.stats_views, + views + ) + ), + BlockListItem.Columns.Column( + R.string.stats_visitors, + statsUtils.toFormattedString(visitors), + contentDescriptionHelper.buildContentDescription( + R.string.stats_visitors, + visitors + ) + ), + BlockListItem.Columns.Column( + R.string.stats_likes, + statsUtils.toFormattedString(likes), + contentDescriptionHelper.buildContentDescription( + R.string.stats_likes, + likes + ) + ), + BlockListItem.Columns.Column( + R.string.stats_comments, + statsUtils.toFormattedString(comments), + contentDescriptionHelper.buildContentDescription( + R.string.stats_comments, + comments + ) + ) + ), + selectedPosition, + onColumnSelected + ) + } + + @Suppress("LongParameterList", "LongMethod") + fun buildChart( + dates: List, + statsGranularity: StatsGranularity, + onBarSelected: (String?) -> Unit, + onBarChartDrawn: (visibleBarCount: Int) -> Unit, + selectedType: Int, + selectedItemPeriod: String + ): List { + val chartItems = dates.map { + val value = when (SelectedType.valueOf(selectedType)) { + Views -> it.views + Visitors -> it.visitors + Likes -> it.likes + Comments -> it.comments + else -> 0L + } + BlockListItem.BarChartItem.Bar( + statsDateFormatter.printGranularDate(it.period, statsGranularity), + it.period, + value.toInt() + ) + } + + val result = mutableListOf() + + val entryType = when (SelectedType.valueOf(selectedType)) { + Visitors -> R.string.stats_visitors + Likes -> R.string.stats_likes + Comments -> R.string.stats_comments + else -> R.string.stats_views + } + + val contentDescriptions = statsUtils.getBarChartEntryContentDescriptions( + entryType, + chartItems + ) + + result.add( + BlockListItem.BarChartItem( + chartItems, + selectedItem = selectedItemPeriod, + onBarSelected = onBarSelected, + onBarChartDrawn = onBarChartDrawn, + entryContentDescriptions = contentDescriptions + ) + ) + return result + } +} From d79fcab4490d3bfa78bbc75d587cb6d6f7acd9cc Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:44:35 +1100 Subject: [PATCH 02/56] Update strings.xml --- WordPress/src/main/res/values/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index f7531852e2c6..552022d68972 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1340,6 +1340,13 @@ Unknown Search Terms + + higher than the previous %1$s + lower than the previous %1$s + 7-days + month + year + Traffic Insights From 0b54e056fb848bb2b2f60af70aa6fd764f4909dd Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:44:57 +1100 Subject: [PATCH 03/56] Create stats_block_traffic_value_item.xml --- .../layout/stats_block_traffic_value_item.xml | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 WordPress/src/main/res/layout/stats_block_traffic_value_item.xml diff --git a/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml b/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml new file mode 100644 index 000000000000..166f4ecbf43a --- /dev/null +++ b/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + From 7cbdaad24608ea05f55a7d7afc9f58e63b5fb0a8 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:45:16 +1100 Subject: [PATCH 04/56] Create TrafficValueViewHolder.kt --- .../traffic/TrafficValueViewHolder.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt new file mode 100644 index 000000000000..93c7c3c2ae0b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt @@ -0,0 +1,53 @@ +package org.wordpress.android.ui.stats.refresh.lists.sections.traffic + +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.RecyclerView +import org.wordpress.android.R +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.NEGATIVE +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.NEUTRAL +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.POSITIVE +import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder +import org.wordpress.android.util.extensions.getColorResIdFromAttribute +import org.wordpress.android.util.extensions.getString + +class TrafficValueViewHolder(parent: ViewGroup) : BlockListItemViewHolder( + parent, + R.layout.stats_block_traffic_value_item +) { + private val container = itemView.findViewById(R.id.value_container) + private val value = itemView.findViewById(R.id.value) + private val unit = itemView.findViewById(R.id.unit) + private val change = itemView.findViewById(R.id.change) + private val period = itemView.findViewById(R.id.period) + fun bind(item: BlockListItem.ValueItem) { + value.text = item.value + unit.setText(item.unit) + val hasChange = item.change != null + val color = when (item.state) { + POSITIVE -> change.context.getColorResIdFromAttribute(R.attr.wpColorSuccess) + NEGATIVE -> change.context.getColorResIdFromAttribute(R.attr.wpColorError) + NEUTRAL -> change.context.getColorResIdFromAttribute(R.attr.wpColorOnSurfaceMedium) + } + val granularity = if (item.period != 0) itemView.getString(item.period) else "" + val periodText = when (item.state) { + POSITIVE -> String.format(itemView.getString(R.string.stats_traffic_change_higher), granularity) + NEGATIVE -> String.format(itemView.getString(R.string.stats_traffic_change_lower), granularity) + NEUTRAL -> "" + } + + change.setTextColor(AppCompatResources.getColorStateList(change.context, color)) + change.visibility = if (hasChange) View.VISIBLE else View.GONE + change.text = item.change + period.text = periodText + val params = container.layoutParams as RecyclerView.LayoutParams + val topMargin = if (item.isFirst) container.resources.getDimensionPixelSize(R.dimen.margin_medium) else 0 + params.setMargins(0, topMargin, 0, 0) + container.layoutParams = params + container.contentDescription = item.contentDescription + } +} From ab25d31c8f8ce575cb67b0023a2c6f8dcebb374c Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:45:37 +1100 Subject: [PATCH 05/56] Create stats_block_traffic_selectable_column.xml --- .../stats_block_traffic_selectable_column.xml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 WordPress/src/main/res/layout/stats_block_traffic_selectable_column.xml diff --git a/WordPress/src/main/res/layout/stats_block_traffic_selectable_column.xml b/WordPress/src/main/res/layout/stats_block_traffic_selectable_column.xml new file mode 100644 index 000000000000..cbc916e6d56f --- /dev/null +++ b/WordPress/src/main/res/layout/stats_block_traffic_selectable_column.xml @@ -0,0 +1,29 @@ + + + + + + + + From 27079e18f6407bc882e4fbe558a40366b0089333 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:45:56 +1100 Subject: [PATCH 06/56] Create stats_block_traffic_four_columns_item.xml --- .../stats_block_traffic_four_columns_item.xml | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml diff --git a/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml b/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml new file mode 100644 index 000000000000..364bb368fd64 --- /dev/null +++ b/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + From f3737e75ad5044627cd54c214083aadaf200bbde Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:46:17 +1100 Subject: [PATCH 07/56] Create TrafficFourColumnsViewHolder.kt --- .../traffic/TrafficFourColumnsViewHolder.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt new file mode 100644 index 000000000000..8e650e541f97 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt @@ -0,0 +1,56 @@ +package org.wordpress.android.ui.stats.refresh.lists.sections.traffic + +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import org.wordpress.android.R +import org.wordpress.android.ui.stats.refresh.BlockDiffCallback +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem +import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder + +class TrafficFourColumnsViewHolder(parent: ViewGroup) : BlockListItemViewHolder( + parent, + R.layout.stats_block_traffic_four_columns_item +) { + private val columnLayouts = listOf( + itemView.findViewById(R.id.column1), + itemView.findViewById(R.id.column2), + itemView.findViewById(R.id.column3), + itemView.findViewById(R.id.column4) + ) + + fun bind( + item: BlockListItem.Columns, + payloads: List + ) { + val tabSelected = payloads.contains(BlockDiffCallback.BlockListPayload.SELECTED_COLUMN_CHANGED) + when { + tabSelected -> { + columnLayouts.forEachIndexed { index, layout -> + layout.setSelection(item.selectedColumn == index) + } + } + else -> { + columnLayouts.forEachIndexed { index, layout -> + layout.setOnClickListener { + it.announceForAccessibility(it.resources.getString(R.string.stats_graph_updated)) + item.onColumnSelected?.invoke(index) + } + val currentColumn = item.columns[index] + layout.key().setText(currentColumn.header) + layout.setSelection(item.selectedColumn == null || item.selectedColumn == index) + layout.contentDescription = currentColumn.contentDescription + } + } + } + } + + private fun LinearLayout.setSelection(isSelected: Boolean) { + key().isSelected = isSelected + selector().visibility = if (isSelected) View.VISIBLE else View.GONE + } + + private fun LinearLayout.key(): TextView = this.findViewById(R.id.key) + private fun LinearLayout.selector(): View = this.findViewById(R.id.selector) +} From ffc1c55f8358d7baba0c69e97b27a59dc0e2938e Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:46:45 +1100 Subject: [PATCH 08/56] Update StatsViewAllFragment.kt --- .../android/ui/stats/refresh/StatsViewAllFragment.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt index 80d8688c73a7..056f78e59d12 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt @@ -35,6 +35,7 @@ import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider import org.wordpress.android.ui.stats.refresh.utils.drawDateSelector import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.WPSwipeToRefreshHelper +import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.extensions.getParcelableExtraCompat import org.wordpress.android.util.extensions.getSerializableCompat @@ -61,6 +62,10 @@ class StatsViewAllFragment : Fragment(R.layout.stats_view_all_fragment) { @Inject lateinit var uiHelpers: UiHelpers + + @Inject + lateinit var trafficTabFeatureConfig: StatsTrafficTabFeatureConfig + private lateinit var viewModel: StatsViewAllViewModel private lateinit var swipeToRefreshHelper: SwipeToRefreshHelper private var binding: StatsViewAllFragmentBinding? = null @@ -283,7 +288,7 @@ class StatsViewAllFragment : Fragment(R.layout.stats_view_all_fragment) { private fun loadData(recyclerView: RecyclerView, data: List) { val adapter: BlockListAdapter if (recyclerView.adapter == null) { - adapter = BlockListAdapter(imageManager) + adapter = BlockListAdapter(imageManager, trafficTabFeatureConfig.isEnabled()) recyclerView.adapter = adapter } else { adapter = recyclerView.adapter as BlockListAdapter From 14fd126e67c5406c5351c55d77ead73c3e3c10f2 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:46:54 +1100 Subject: [PATCH 09/56] Update LoadingViewHolder.kt --- .../stats/refresh/lists/viewholders/LoadingViewHolder.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/viewholders/LoadingViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/viewholders/LoadingViewHolder.kt index 1988eef49867..c10ed195e09a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/viewholders/LoadingViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/viewholders/LoadingViewHolder.kt @@ -9,7 +9,11 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListAdapter import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem import org.wordpress.android.util.image.ImageManager -class LoadingViewHolder(parent: ViewGroup, val imageManager: ImageManager) : BaseStatsViewHolder( +class LoadingViewHolder( + parent: ViewGroup, + val imageManager: ImageManager, + val trafficTabEnabled: Boolean +) : BaseStatsViewHolder( parent, R.layout.stats_loading_view ) { @@ -19,7 +23,7 @@ class LoadingViewHolder(parent: ViewGroup, val imageManager: ImageManager) : Bas list.isNestedScrollingEnabled = false if (list.adapter == null) { list.layoutManager = LinearLayoutManager(list.context, RecyclerView.VERTICAL, false) - list.adapter = BlockListAdapter(imageManager) + list.adapter = BlockListAdapter(imageManager, trafficTabEnabled) } (list.adapter as BlockListAdapter).update(items) } From 989329f2c21d99cd45e5f918e52d51354baac13e Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:47:00 +1100 Subject: [PATCH 10/56] Update BlockListViewHolder.kt --- .../refresh/lists/viewholders/BlockListViewHolder.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/viewholders/BlockListViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/viewholders/BlockListViewHolder.kt index b10eb34171cd..7887de448a9e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/viewholders/BlockListViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/viewholders/BlockListViewHolder.kt @@ -10,7 +10,11 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem import org.wordpress.android.ui.stats.refresh.utils.WrappingLinearLayoutManager import org.wordpress.android.util.image.ImageManager -open class BlockListViewHolder(parent: ViewGroup, val imageManager: ImageManager) : BaseStatsViewHolder( +open class BlockListViewHolder( + parent: ViewGroup, + val imageManager: ImageManager, + private val trafficTabEnabled: Boolean +) : BaseStatsViewHolder( parent, R.layout.stats_list_block ) { @@ -19,7 +23,7 @@ open class BlockListViewHolder(parent: ViewGroup, val imageManager: ImageManager super.bind(statsType, items) list.isNestedScrollingEnabled = false if (list.adapter == null) { - val blockListAdapter = BlockListAdapter(imageManager) + val blockListAdapter = BlockListAdapter(imageManager, trafficTabEnabled) val layoutManager = WrappingLinearLayoutManager( list.context, LinearLayoutManager.VERTICAL, From 8913dc9e07d572a68e1cb29d6c3e8406a3b3c8ac Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:47:11 +1100 Subject: [PATCH 11/56] Update StatsListFragment.kt --- .../android/ui/stats/refresh/lists/StatsListFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt index c8d09a0b3643..913fde3dc9c6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt @@ -337,7 +337,7 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { val adapter: StatsBlockAdapter if (recyclerView.adapter == null) { - adapter = StatsBlockAdapter(imageManager) + adapter = StatsBlockAdapter(imageManager, statsTrafficTabFeatureConfig.isEnabled()) recyclerView.adapter = adapter } else { adapter = recyclerView.adapter as StatsBlockAdapter From f834abdef567614e5274df7d1052ff4d45fda09f Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:47:22 +1100 Subject: [PATCH 12/56] Update StatsBlockAdapter.kt --- .../android/ui/stats/refresh/lists/StatsBlockAdapter.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsBlockAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsBlockAdapter.kt index b6625a949b04..af149e7e48f7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsBlockAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsBlockAdapter.kt @@ -14,7 +14,10 @@ import org.wordpress.android.ui.stats.refresh.lists.viewholders.BlockListViewHol import org.wordpress.android.ui.stats.refresh.lists.viewholders.LoadingViewHolder import org.wordpress.android.util.image.ImageManager -class StatsBlockAdapter(val imageManager: ImageManager) : Adapter() { +class StatsBlockAdapter( + val imageManager: ImageManager, + private val trafficTabEnabled: Boolean +) : Adapter() { private var items: List = listOf() fun update(newItems: List) { @@ -30,8 +33,8 @@ class StatsBlockAdapter(val imageManager: ImageManager) : Adapter BlockListViewHolder(parent, imageManager) - LOADING -> LoadingViewHolder(parent, imageManager) + SUCCESS, ERROR, EMPTY -> BlockListViewHolder(parent, imageManager, trafficTabEnabled) + LOADING -> LoadingViewHolder(parent, imageManager, trafficTabEnabled) } } From 9303cf1ab772591652b1e3d057f990bfd788e4e3 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:47:57 +1100 Subject: [PATCH 13/56] Update BlockListItem.kt --- .../android/ui/stats/refresh/lists/sections/BlockListItem.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListItem.kt index df5b1fe1e03d..222fc9b6bb50 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListItem.kt @@ -126,6 +126,7 @@ sealed class BlockListItem(val type: Type) { @StringRes val unit: Int, val isFirst: Boolean = false, val change: String? = null, + val period: Int = 0, val state: State = POSITIVE, val contentDescription: String ) : BlockListItem(VALUE_ITEM) { From 612aefe63a63a951e4631f68ff9978ff9defe3f6 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:48:05 +1100 Subject: [PATCH 14/56] Create TrafficBarChartViewHolder.kt --- .../traffic/TrafficBarChartViewHolder.kt | 349 ++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt new file mode 100644 index 000000000000..b60a0e3c47c4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt @@ -0,0 +1,349 @@ +package org.wordpress.android.ui.stats.refresh.lists.sections.traffic + +import android.content.Context +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import com.github.mikephil.charting.charts.BarChart +import com.github.mikephil.charting.components.Description +import com.github.mikephil.charting.data.BarData +import com.github.mikephil.charting.data.BarDataSet +import com.github.mikephil.charting.data.BarEntry +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.interfaces.datasets.IBarDataSet +import com.github.mikephil.charting.listener.OnChartValueSelectedListener +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.wordpress.android.R +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem +import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder +import org.wordpress.android.ui.stats.refresh.utils.BarChartAccessibilityHelper +import org.wordpress.android.ui.stats.refresh.utils.LargeValueFormatter +import org.wordpress.android.util.DisplayUtils + +private const val MIN_COLUMN_COUNT = 5 +private const val MIN_VALUE = 4f + +private typealias BarCount = Int + +class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( + parent, + R.layout.stats_block_bar_chart_item +) { + private val coroutineScope = CoroutineScope(Dispatchers.Main) + private val chart = itemView.findViewById(R.id.chart) + private val labelStart = itemView.findViewById(R.id.label_start) + private val labelEnd = itemView.findViewById(R.id.label_end) + private lateinit var accessibilityHelper: BarChartAccessibilityHelper + + @Suppress("MagicNumber") + fun bind(item: BlockListItem.BarChartItem) { + chart.setNoDataText("") + coroutineScope.launch { + delay(50) + val barCount = chart.draw(item, labelStart, labelEnd) + chart.post { + val accessibilityEvent = object : BarChartAccessibilityHelper.BarChartAccessibilityEvent { + override fun onHighlight( + entry: BarEntry, + index: Int + ) { + chart.highlightColumn(index, item.overlappingEntries != null) + val value = entry.data as? String + value?.let { + item.onBarSelected?.invoke(it) + } + } + } + + val cutContentDescriptions = takeEntriesWithinGraphWidth(barCount, item.entryContentDescriptions) + accessibilityHelper = BarChartAccessibilityHelper( + chart, + contentDescriptions = cutContentDescriptions, + accessibilityEvent = accessibilityEvent + ) + + ViewCompat.setAccessibilityDelegate(chart, accessibilityHelper) + } + } + } + + @Suppress("CyclomaticComplexMethod", "LongMethod", "MagicNumber") + private fun BarChart.draw( + item: BlockListItem.BarChartItem, + labelStart: TextView, + labelEnd: TextView + ): BarCount { + resetChart() + val graphWidth = DisplayUtils.pxToDp(context, width) + val columnNumber = (graphWidth / 24) - 1 + val count = if (columnNumber > MIN_COLUMN_COUNT) columnNumber else MIN_COLUMN_COUNT + val cutEntries = takeEntriesWithinGraphWidth(count, item.entries) + val mappedEntries = cutEntries.mapIndexed { index, pair -> toBarEntry(pair, index) } + val maxYValue = cutEntries.maxByOrNull { it.value }!!.value + val hasData = item.entries.isNotEmpty() && item.entries.any { it.value > 0 } + val dataSet = if (hasData) { + buildDataSet(context, mappedEntries) + } else { + buildEmptyDataSet(context, cutEntries.size) + } + item.onBarChartDrawn?.invoke(dataSet.entryCount) + val dataSets = mutableListOf() + dataSets.add(dataSet) + val hasOverlappingEntries = hasData && item.overlappingEntries != null + if (hasData && item.overlappingEntries != null) { + val overlappingCut = takeEntriesWithinGraphWidth(count, item.overlappingEntries) + val mappedOverlappingEntries = overlappingCut.mapIndexed { index, pair -> toBarEntry(pair, index) } + val overlappingDataSet = buildOverlappingDataSet(context, mappedOverlappingEntries) + dataSets.add(overlappingDataSet) + } + if (hasData && item.onBarSelected != null) { + getHighlightDataSet(context, mappedEntries)?.let { dataSets.add(it) } + } + data = BarData(dataSets) + val greyColor = ContextCompat.getColor( + context, + R.color.neutral_30 + ) + val lightGreyColor = ContextCompat.getColor( + context, + R.color.stats_bar_chart_gridline + ) + axisLeft.apply { + setDrawGridLines(false) + setDrawZeroLine(false) + setDrawLabels(false) + setDrawAxisLine(false) + } + extraRightOffset = 8f + axisRight.apply { + valueFormatter = LargeValueFormatter() + setDrawGridLines(true) + setDrawTopYLabelEntry(true) + setDrawZeroLine(false) + setDrawAxisLine(true) + granularity = 1f + axisMinimum = 0f + axisMaximum = if (maxYValue < MIN_VALUE) { + MIN_VALUE + } else { + roundUp(maxYValue.toFloat()) + } + setLabelCount(5, true) + textColor = greyColor + gridColor = lightGreyColor + textSize = 10f + gridLineWidth = 1f + } + xAxis.apply { + granularity = 1f + setDrawAxisLine(false) + setDrawGridLines(false) + setDrawLabels(false) + } + labelStart.text = cutEntries.first().label + labelEnd.text = cutEntries.last().label + setPinchZoom(false) + setScaleEnabled(false) + legend.isEnabled = false + setDrawBorders(false) + + val isClickable = item.onBarSelected != null + if (isClickable) { + setOnChartValueSelectedListener(object : OnChartValueSelectedListener { + override fun onNothingSelected() { + item.selectedItem + highlightColumn(cutEntries.indexOfFirst { it.id == item.selectedItem }, hasOverlappingEntries) + item.onBarSelected?.invoke(item.selectedItem) + } + + override fun onValueSelected(e: Entry, h: Highlight) { + val value = (e as? BarEntry)?.data as? String + highlightColumn(e.x.toInt(), hasOverlappingEntries) + item.onBarSelected?.invoke(value) + } + }) + } else { + setOnChartValueSelectedListener(null) + } + isHighlightFullBarEnabled = isClickable + isHighlightPerDragEnabled = false + isHighlightPerTapEnabled = isClickable + val description = Description() + description.text = "" + this.description = description + + if (item.selectedItem != null) { + val index = cutEntries.indexOfFirst { it.id == item.selectedItem } + if (index >= 0) { + highlightColumn(index, hasOverlappingEntries) + } else { + highlightValue(null, false) + } + } + invalidate() + return cutEntries.size + } + + private fun BarChart.highlightColumn(index: Int, hasOverlappingColumns: Boolean) { + if (hasOverlappingColumns) { + val high = Highlight(index.toFloat(), 0, 0) + val high2 = Highlight(index.toFloat(), 1, 1) + val high3 = Highlight(index.toFloat(), 2, 2) + high.dataIndex = index + high2.dataIndex = index + high3.dataIndex = index + highlightValues(arrayOf(high3, high, high2)) + } else { + val high = Highlight(index.toFloat(), 0, 0) + val high2 = Highlight(index.toFloat(), 1, 1) + high.dataIndex = index + high2.dataIndex = index + highlightValues(arrayOf(high2, high)) + } + } + + @Suppress("MagicNumber") + private fun buildEmptyDataSet(context: Context, count: Int): BarDataSet { + val emptyValues = (0 until count).map { index -> BarEntry(index.toFloat(), 1f, "empty") } + val dataSet = BarDataSet(emptyValues, "Empty") + dataSet.setGradientColor( + ContextCompat.getColor( + context, + R.color.primary_5 + ), ContextCompat.getColor( + context, + android.R.color.transparent + ) + ) + dataSet.formLineWidth = 0f + dataSet.setDrawValues(false) + dataSet.isHighlightEnabled = false + dataSet.highLightAlpha = 255 + return dataSet + } + + @Suppress("MagicNumber") + private fun buildDataSet(context: Context, cut: List): BarDataSet { + val dataSet = BarDataSet(cut, "Data") + dataSet.color = ContextCompat.getColor(context, R.color.wordpress_blue_40) + dataSet.setGradientColor( + ContextCompat.getColor( + context, + R.color.wordpress_blue_40 + ), ContextCompat.getColor( + context, + R.color.wordpress_blue_40 + ) + ) + dataSet.formLineWidth = 0f + dataSet.setDrawValues(false) + dataSet.isHighlightEnabled = true + dataSet.highLightColor = ContextCompat.getColor( + context, + R.color.stats_bar_chart_accent_top + ) + dataSet.highLightAlpha = 255 + return dataSet + } + + @Suppress("MagicNumber") + private fun buildOverlappingDataSet(context: Context, cut: List): BarDataSet { + val dataSet = BarDataSet(cut, "Overlapping data") + dataSet.color = ContextCompat.getColor(context, R.color.primary_60) + dataSet.setGradientColor( + ContextCompat.getColor( + context, + R.color.stats_bar_chart_bottom + ), ContextCompat.getColor( + context, + R.color.stats_bar_chart_bottom + ) + ) + dataSet.formLineWidth = 0f + dataSet.setDrawValues(false) + dataSet.isHighlightEnabled = true + dataSet.highLightColor = ContextCompat.getColor( + context, + R.color.stats_bar_chart_accent_bottom + ) + dataSet.highLightAlpha = 255 + return dataSet + } + + @Suppress("MagicNumber") + private fun getHighlightDataSet(context: Context, cut: List): BarDataSet? { + val maxEntry = cut.maxByOrNull { it.y } ?: return null + val highlightedDataSet = cut.map { + BarEntry(it.x, maxEntry.y, it.data) + } + val dataSet = BarDataSet(highlightedDataSet, "Highlight") + dataSet.color = ContextCompat.getColor(context, android.R.color.transparent) + dataSet.setGradientColor( + ContextCompat.getColor( + context, + android.R.color.transparent + ), ContextCompat.getColor( + context, + android.R.color.transparent + ) + ) + dataSet.formLineWidth = 0f + dataSet.isHighlightEnabled = true + dataSet.highLightColor = ContextCompat.getColor( + context, + R.color.stats_bar_chart_accent_top + ) + dataSet.setDrawValues(false) + dataSet.highLightAlpha = 51 + return dataSet + } + + private fun takeEntriesWithinGraphWidth( + count: Int, + entries: List + ): List { + return if (count < entries.size) entries.subList( + entries.size - count, + entries.size + ) else { + entries + } + } + + private fun BarChart.resetChart() { + fitScreen() + data?.clearValues() + xAxis.valueFormatter = null + notifyDataSetChanged() + clear() + invalidate() + } + + private fun toBarEntry(bar: BlockListItem.BarChartItem.Bar, index: Int): BarEntry { + return BarEntry( + index.toFloat(), + bar.value.toFloat(), + bar.id + ) + } + + @Suppress("ReturnCount", "MagicNumber") + private fun roundUp(input: Float): Float { + return if (input > 100) { + roundUp(input / 10) * 10 + } else { + for (i in 1..25) { + val limit = 4 * i + if (input < limit) { + return limit.toFloat() + } + } + return 100F + } + } +} From 07a62f0897d2e3d17a9afd822b8cabb1df99e0f4 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:48:32 +1100 Subject: [PATCH 15/56] Update BlockListAdapter.kt --- .../refresh/lists/sections/BlockListAdapter.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListAdapter.kt index 3acdb7c217e4..f186cc2381f2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListAdapter.kt @@ -78,6 +78,9 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type. import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueWithChartItem import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValuesItem +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficBarChartViewHolder +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficFourColumnsViewHolder +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficValueViewHolder import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.ActionCardViewHolder import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.ActivityViewHolder import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BarChartViewHolder @@ -117,7 +120,10 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.ValueWi import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.ValuesViewHolder import org.wordpress.android.util.image.ImageManager -class BlockListAdapter(val imageManager: ImageManager) : Adapter() { +class BlockListAdapter( + val imageManager: ImageManager, + private val trafficTabEnabled: Boolean +) : Adapter() { private var items: List = listOf() fun update(newItems: List) { val diffResult = DiffUtil.calculateDiff( @@ -142,10 +148,10 @@ class BlockListAdapter(val imageManager: ImageManager) : Adapter ListItemViewHolder(parent) EMPTY -> EmptyViewHolder(parent) TEXT -> TextViewHolder(parent) - COLUMNS -> FourColumnsViewHolder(parent) + COLUMNS -> if (trafficTabEnabled) TrafficFourColumnsViewHolder(parent) else FourColumnsViewHolder(parent) CHIPS -> ChipsViewHolder(parent) LINK -> LinkViewHolder(parent) - BAR_CHART -> BarChartViewHolder(parent) + BAR_CHART -> if (trafficTabEnabled) TrafficBarChartViewHolder(parent) else BarChartViewHolder(parent) PIE_CHART -> PieChartViewHolder(parent) LINE_CHART -> LineChartViewHolder(parent) CHART_LEGEND -> ChartLegendViewHolder(parent) @@ -159,7 +165,7 @@ class BlockListAdapter(val imageManager: ImageManager) : Adapter LoadingItemViewHolder(parent) MAP -> MapViewHolder(parent) MAP_LEGEND -> MapLegendViewHolder(parent) - VALUE_ITEM -> ValueViewHolder(parent) + VALUE_ITEM -> if (trafficTabEnabled) TrafficValueViewHolder(parent) else ValueViewHolder(parent) VALUE_WITH_CHART_ITEM -> ValueWithChartViewHolder(parent) VALUES_ITEM -> ValuesViewHolder(parent) ACTIVITY_ITEM -> ActivityViewHolder(parent) @@ -188,14 +194,17 @@ class BlockListAdapter(val imageManager: ImageManager) : Adapter holder.bind(item as ValueItem) is ValueWithChartViewHolder -> holder.bind(item as ValueWithChartItem) is ValuesViewHolder -> holder.bind(item as ValuesItem) + is TrafficValueViewHolder -> holder.bind(item as ValueItem) is ListItemWithImageViewHolder -> holder.bind(item as ListItemWithImage) is ListItemWithIconViewHolder -> holder.bind(item as ListItemWithIcon) is ListItemViewHolder -> holder.bind(item as ListItem) is TextViewHolder -> holder.bind(item as Text) is FourColumnsViewHolder -> holder.bind(item as Columns, payloads) + is TrafficFourColumnsViewHolder -> holder.bind(item as Columns, payloads) is ChipsViewHolder -> holder.bind(item as Chips) is LinkViewHolder -> holder.bind(item as Link) is BarChartViewHolder -> holder.bind(item as BarChartItem) + is TrafficBarChartViewHolder -> holder.bind(item as BarChartItem) is PieChartViewHolder -> holder.bind(item as PieChartItem) is LineChartViewHolder -> holder.bind(item as LineChartItem) is ChartLegendViewHolder -> holder.bind(item as ChartLegend) From c01ab07cc234a7d991d07250590a9082a3c77f7c Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:48:41 +1100 Subject: [PATCH 16/56] Update OverviewUseCase.kt --- .../granular/usecases/OverviewUseCase.kt | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt index 104ebebb120c..3a8e102978f9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt @@ -20,6 +20,7 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Value import org.wordpress.android.ui.stats.refresh.lists.sections.granular.GranularUseCaseFactory import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider import org.wordpress.android.ui.stats.refresh.lists.sections.granular.usecases.OverviewUseCase.UiState +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewMapper import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetUpdater.StatsWidgetUpdaters import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider @@ -53,7 +54,8 @@ class OverviewUseCase constructor( private val localeManagerWrapper: LocaleManagerWrapper, private val resourceProvider: ResourceProvider, private val statsUtils: StatsUtils, - private val trafficTabFeatureConfig: StatsTrafficTabFeatureConfig + private val trafficTabFeatureConfig: StatsTrafficTabFeatureConfig, + private val trafficOverviewMapper: TrafficOverviewMapper ) : BaseStatsUseCase( OVERVIEW, mainDispatcher, @@ -61,6 +63,27 @@ class OverviewUseCase constructor( UiState(), uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { + // The granularity of the chart is one level lower than the current one, probably there's a better way to do this + val trafficTabGranularity = when (statsGranularity) { + StatsGranularity.WEEKS -> StatsGranularity.DAYS + StatsGranularity.MONTHS -> StatsGranularity.WEEKS + StatsGranularity.YEARS -> StatsGranularity.MONTHS + else -> statsGranularity + } + + val granularity = if (trafficTabFeatureConfig.isEnabled()) { + trafficTabGranularity + } else { + statsGranularity + } + + val itemsToLoad = when (statsGranularity) { + StatsGranularity.WEEKS -> 7 + StatsGranularity.MONTHS -> 5 + StatsGranularity.YEARS -> 12 + else -> OVERVIEW_ITEMS_TO_LOAD + } + override fun buildLoadingItem(): List = listOf( ValueItem( @@ -76,12 +99,12 @@ class OverviewUseCase constructor( statsWidgetUpdaters.updateWeekViewsWidget(statsSiteProvider.siteModel.siteId) val cachedData = visitsAndViewsStore.getVisits( statsSiteProvider.siteModel, - statsGranularity, - LimitMode.All + granularity, + if (trafficTabFeatureConfig.isEnabled()) LimitMode.Top(itemsToLoad) else LimitMode.All ) if (cachedData != null) { - logIfIncorrectData(cachedData, statsGranularity, statsSiteProvider.siteModel, false) - selectedDateProvider.onDateLoadingSucceeded(statsGranularity) + logIfIncorrectData(cachedData, granularity, statsSiteProvider.siteModel, false) + selectedDateProvider.onDateLoadingSucceeded(granularity) } return cachedData } @@ -89,8 +112,8 @@ class OverviewUseCase constructor( override suspend fun fetchRemoteData(forced: Boolean): State { val response = visitsAndViewsStore.fetchVisits( statsSiteProvider.siteModel, - statsGranularity, - LimitMode.Top(OVERVIEW_ITEMS_TO_LOAD), + granularity, + LimitMode.Top(if (trafficTabFeatureConfig.isEnabled()) itemsToLoad else OVERVIEW_ITEMS_TO_LOAD), forced ) val model = response.model @@ -98,16 +121,16 @@ class OverviewUseCase constructor( return when { error != null -> { - selectedDateProvider.onDateLoadingFailed(statsGranularity) + selectedDateProvider.onDateLoadingFailed(granularity) State.Error(error.message ?: error.type.name) } model != null && model.dates.isNotEmpty() -> { - logIfIncorrectData(model, statsGranularity, statsSiteProvider.siteModel, true) - selectedDateProvider.onDateLoadingSucceeded(statsGranularity) + logIfIncorrectData(model, granularity, statsSiteProvider.siteModel, true) + selectedDateProvider.onDateLoadingSucceeded(granularity) State.Data(model) } else -> { - selectedDateProvider.onDateLoadingSucceeded(statsGranularity) + selectedDateProvider.onDateLoadingSucceeded(granularity) State.Empty() } } @@ -159,11 +182,11 @@ class OverviewUseCase constructor( private fun buildTrafficOverview(domainModel: VisitsAndViewsModel, uiState: UiState): List { val items = mutableListOf() if (domainModel.dates.isNotEmpty()) { - val dateFromProvider = selectedDateProvider.getSelectedDate(statsGranularity) + val dateFromProvider = selectedDateProvider.getSelectedDate(granularity) val visibleBarCount = uiState.visibleBarCount ?: domainModel.dates.size val availableDates = domainModel.dates.map { statsDateFormatter.parseStatsDate( - statsGranularity, + granularity, it.period ) } @@ -173,7 +196,7 @@ class OverviewUseCase constructor( selectedDateProvider.selectDate( selectedDate, availableDates.takeLast(visibleBarCount), - statsGranularity + granularity ) val selectedItem = domainModel.dates.getOrNull(index) ?: domainModel.dates.last() val previousItem = domainModel.dates.getOrNull(domainModel.dates.indexOf(selectedItem) - 1) @@ -184,7 +207,7 @@ class OverviewUseCase constructor( buildGranularChart(domainModel, uiState, items, selectedItem, previousItem) } } else { - selectedDateProvider.onDateLoadingFailed(statsGranularity) + selectedDateProvider.onDateLoadingFailed(granularity) AppLog.e(T.STATS, "There is no data to be shown in the overview block") } return items @@ -235,18 +258,18 @@ class OverviewUseCase constructor( previousItem: VisitsAndViewsModel.PeriodData? ) { items.add( - overviewMapper.buildTitle( + trafficOverviewMapper.buildTitle( selectedItem, previousItem, uiState.selectedPosition, isLast = selectedItem == domainModel.dates.last(), - statsGranularity = statsGranularity + statsGranularity = granularity ) ) items.addAll( - overviewMapper.buildChart( + trafficOverviewMapper.buildChart( domainModel.dates, - statsGranularity, + granularity, this::onBarSelected, this::onBarChartDrawn, uiState.selectedPosition, @@ -254,7 +277,7 @@ class OverviewUseCase constructor( ) ) items.add( - overviewMapper.buildColumns( + trafficOverviewMapper.buildColumns( selectedItem, this::onColumnSelected, uiState.selectedPosition @@ -358,7 +381,8 @@ class OverviewUseCase constructor( private val localeManagerWrapper: LocaleManagerWrapper, private val resourceProvider: ResourceProvider, private val statsUtils: StatsUtils, - private val trafficTabFeatureConfig: StatsTrafficTabFeatureConfig + private val trafficTabFeatureConfig: StatsTrafficTabFeatureConfig, + private val trafficOverviewMapper: TrafficOverviewMapper ) : GranularUseCaseFactory { override fun build(granularity: StatsGranularity, useCaseMode: UseCaseMode) = OverviewUseCase( @@ -375,7 +399,8 @@ class OverviewUseCase constructor( localeManagerWrapper, resourceProvider, statsUtils, - trafficTabFeatureConfig + trafficTabFeatureConfig, + trafficOverviewMapper ) } } From a68e891ccf74d20c9f88bbd8b60653d6a7b93366 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:48:50 +1100 Subject: [PATCH 17/56] Update OverviewUseCaseTest.kt --- .../sections/granular/usecases/OverviewUseCaseTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCaseTest.kt index d0206175cee8..64f49a4a6428 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCaseTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCaseTest.kt @@ -32,6 +32,7 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.BarCh import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Columns import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewMapper import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetUpdater.StatsWidgetUpdaters import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider @@ -86,6 +87,9 @@ class OverviewUseCaseTest : BaseUnitTest() { @Mock lateinit var trafficTabFeatureConfig: StatsTrafficTabFeatureConfig + @Mock + lateinit var trafficOverviewMapper: TrafficOverviewMapper + private lateinit var useCase: OverviewUseCase private val site = SiteModel() private val siteId = 1L @@ -111,7 +115,8 @@ class OverviewUseCaseTest : BaseUnitTest() { localeManagerWrapper, resourceProvider, statsUtils, - trafficTabFeatureConfig + trafficTabFeatureConfig, + trafficOverviewMapper ) site.siteId = siteId whenever(statsSiteProvider.siteModel).thenReturn(site) From 419dd93ed8ee1f8ece75711c72d4ef8b34946785 Mon Sep 17 00:00:00 2001 From: Ravi Date: Thu, 15 Feb 2024 18:48:53 +1100 Subject: [PATCH 18/56] Update ViewsWidgetListViewModelTest.kt --- .../lists/widget/views/ViewsWidgetListViewModelTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/ViewsWidgetListViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/ViewsWidgetListViewModelTest.kt index 336ad06cc0a5..975f01188d73 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/ViewsWidgetListViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/ViewsWidgetListViewModelTest.kt @@ -98,7 +98,7 @@ class ViewsWidgetListViewModelTest { any(), any() ) - ).thenReturn(ValueItem(firstViews.toString(), 0, false, change, POSITIVE, change)) + ).thenReturn(ValueItem(firstViews.toString(), 0, false, change, state = POSITIVE, contentDescription = change)) whenever( overviewMapper.buildTitle( eq(dates[1]), @@ -108,7 +108,7 @@ class ViewsWidgetListViewModelTest { any(), any() ) - ).thenReturn(ValueItem(todayViews.toString(), 0, true, change, NEGATIVE, change)) + ).thenReturn(ValueItem(todayViews.toString(), 0, true, change, state = NEGATIVE, contentDescription = change)) whenever( overviewMapper.buildTitle( eq(dates[2]), @@ -118,7 +118,7 @@ class ViewsWidgetListViewModelTest { any(), any() ) - ).thenReturn(ValueItem(todayViews.toString(), 0, true, change, NEUTRAL, change)) + ).thenReturn(ValueItem(todayViews.toString(), 0, true, change, state = NEUTRAL, contentDescription = change)) viewModel.start(siteId, color, showChangeColumn, appWidgetId) From ea9872dce96d73554641e6c8673731d74d0314a8 Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 16 Feb 2024 16:59:30 +1100 Subject: [PATCH 19/56] Create stats_block_traffic_bar_chart_item.xml --- .../stats_block_traffic_bar_chart_item.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 WordPress/src/main/res/layout/stats_block_traffic_bar_chart_item.xml diff --git a/WordPress/src/main/res/layout/stats_block_traffic_bar_chart_item.xml b/WordPress/src/main/res/layout/stats_block_traffic_bar_chart_item.xml new file mode 100644 index 000000000000..b7d17898c4b0 --- /dev/null +++ b/WordPress/src/main/res/layout/stats_block_traffic_bar_chart_item.xml @@ -0,0 +1,19 @@ + + + + + + From d2ca76ddcb3f540eb9d533116312c2d6a30abd47 Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 16 Feb 2024 16:59:42 +1100 Subject: [PATCH 20/56] Update TrafficBarChartViewHolder.kt --- .../traffic/TrafficBarChartViewHolder.kt | 287 ++++++------------ 1 file changed, 95 insertions(+), 192 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt index b60a0e3c47c4..c72167a719c8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt @@ -2,125 +2,136 @@ package org.wordpress.android.ui.stats.refresh.lists.sections.traffic import android.content.Context import android.view.ViewGroup -import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat +import com.github.mikephil.charting.animation.Easing import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.components.Description import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarEntry -import com.github.mikephil.charting.data.Entry -import com.github.mikephil.charting.highlight.Highlight import com.github.mikephil.charting.interfaces.datasets.IBarDataSet -import com.github.mikephil.charting.listener.OnChartValueSelectedListener import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.wordpress.android.R import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.BarChartItem.Bar import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder import org.wordpress.android.ui.stats.refresh.utils.BarChartAccessibilityHelper import org.wordpress.android.ui.stats.refresh.utils.LargeValueFormatter import org.wordpress.android.util.DisplayUtils -private const val MIN_COLUMN_COUNT = 5 -private const val MIN_VALUE = 4f - -private typealias BarCount = Int - +@Suppress("MagicNumber") class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( parent, - R.layout.stats_block_bar_chart_item + R.layout.stats_block_traffic_bar_chart_item ) { private val coroutineScope = CoroutineScope(Dispatchers.Main) - private val chart = itemView.findViewById(R.id.chart) - private val labelStart = itemView.findViewById(R.id.label_start) - private val labelEnd = itemView.findViewById(R.id.label_end) + private val chart = itemView.findViewById(R.id.bar_chart) + private lateinit var accessibilityHelper: BarChartAccessibilityHelper - @Suppress("MagicNumber") fun bind(item: BlockListItem.BarChartItem) { chart.setNoDataText("") coroutineScope.launch { delay(50) - val barCount = chart.draw(item, labelStart, labelEnd) - chart.post { - val accessibilityEvent = object : BarChartAccessibilityHelper.BarChartAccessibilityEvent { - override fun onHighlight( - entry: BarEntry, - index: Int - ) { - chart.highlightColumn(index, item.overlappingEntries != null) - val value = entry.data as? String - value?.let { - item.onBarSelected?.invoke(it) + val barCount = chart.draw(item) + if (hasData(item.entries)) { + chart.post { + val accessibilityEvent = object : BarChartAccessibilityHelper.BarChartAccessibilityEvent { + override fun onHighlight( + entry: BarEntry, + index: Int + ) { + val value = entry.data as? String + value?.let { + item.onBarSelected?.invoke(it) + } } } - } - val cutContentDescriptions = takeEntriesWithinGraphWidth(barCount, item.entryContentDescriptions) - accessibilityHelper = BarChartAccessibilityHelper( - chart, - contentDescriptions = cutContentDescriptions, - accessibilityEvent = accessibilityEvent - ) + val cutContentDescriptions = takeEntriesWithinGraphWidth(barCount, item.entryContentDescriptions) + accessibilityHelper = BarChartAccessibilityHelper( + chart, + contentDescriptions = cutContentDescriptions, + accessibilityEvent = accessibilityEvent + ) - ViewCompat.setAccessibilityDelegate(chart, accessibilityHelper) + ViewCompat.setAccessibilityDelegate(chart, accessibilityHelper) + } } } } - @Suppress("CyclomaticComplexMethod", "LongMethod", "MagicNumber") - private fun BarChart.draw( - item: BlockListItem.BarChartItem, - labelStart: TextView, - labelEnd: TextView - ): BarCount { + private fun BarChart.draw(item: BlockListItem.BarChartItem): Int { resetChart() - val graphWidth = DisplayUtils.pxToDp(context, width) + + data = BarData(getData(item)) + + configureChartView() + configureYAxis(item) + configureXAxis() + + invalidate() + return data.dataSets.size + } + + private fun hasData(entries: List) = entries.isNotEmpty() && entries.any { it.value > 0 } + + private fun getData(item: BlockListItem.BarChartItem): List { + val minColumnCount = 5 + + val graphWidth = DisplayUtils.pxToDp(chart.context, chart.width) val columnNumber = (graphWidth / 24) - 1 - val count = if (columnNumber > MIN_COLUMN_COUNT) columnNumber else MIN_COLUMN_COUNT + val count = if (columnNumber > minColumnCount) columnNumber else minColumnCount val cutEntries = takeEntriesWithinGraphWidth(count, item.entries) val mappedEntries = cutEntries.mapIndexed { index, pair -> toBarEntry(pair, index) } - val maxYValue = cutEntries.maxByOrNull { it.value }!!.value - val hasData = item.entries.isNotEmpty() && item.entries.any { it.value > 0 } - val dataSet = if (hasData) { - buildDataSet(context, mappedEntries) + + val dataSet = if (hasData(item.entries)) { + buildDataSet(chart.context, mappedEntries) } else { - buildEmptyDataSet(context, cutEntries.size) + buildEmptyDataSet(chart.context, cutEntries.size) } item.onBarChartDrawn?.invoke(dataSet.entryCount) val dataSets = mutableListOf() dataSets.add(dataSet) - val hasOverlappingEntries = hasData && item.overlappingEntries != null - if (hasData && item.overlappingEntries != null) { - val overlappingCut = takeEntriesWithinGraphWidth(count, item.overlappingEntries) - val mappedOverlappingEntries = overlappingCut.mapIndexed { index, pair -> toBarEntry(pair, index) } - val overlappingDataSet = buildOverlappingDataSet(context, mappedOverlappingEntries) - dataSets.add(overlappingDataSet) - } - if (hasData && item.onBarSelected != null) { - getHighlightDataSet(context, mappedEntries)?.let { dataSets.add(it) } + + return dataSets + } + + private fun configureChartView() { + chart.apply { + setPinchZoom(false) + setScaleEnabled(false) + legend.isEnabled = false + setDrawBorders(false) + + isHighlightPerDragEnabled = false + isHighlightPerTapEnabled = isClickable + val description = Description() + description.text = "" + this.description = description + + extraRightOffset = 8f + + animateY(1000, Easing.EaseInSine) } - data = BarData(dataSets) - val greyColor = ContextCompat.getColor( - context, - R.color.neutral_30 - ) - val lightGreyColor = ContextCompat.getColor( - context, - R.color.stats_bar_chart_gridline - ) - axisLeft.apply { + } + + private fun configureYAxis(item: BlockListItem.BarChartItem) { + val minYValue = 4f + val maxYValue = item.entries.maxByOrNull { it.value }!!.value + + chart.axisLeft.apply { setDrawGridLines(false) setDrawZeroLine(false) setDrawLabels(false) setDrawAxisLine(false) } - extraRightOffset = 8f - axisRight.apply { + + chart.axisRight.apply { valueFormatter = LargeValueFormatter() setDrawGridLines(true) setDrawTopYLabelEntry(true) @@ -128,86 +139,36 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( setDrawAxisLine(true) granularity = 1f axisMinimum = 0f - axisMaximum = if (maxYValue < MIN_VALUE) { - MIN_VALUE + axisMaximum = if (maxYValue < minYValue) { + minYValue } else { roundUp(maxYValue.toFloat()) } setLabelCount(5, true) - textColor = greyColor - gridColor = lightGreyColor + textColor = ContextCompat.getColor(chart.context, R.color.neutral_30) + gridColor = ContextCompat.getColor(chart.context, R.color.stats_bar_chart_gridline) textSize = 10f gridLineWidth = 1f } - xAxis.apply { + } + + private fun configureXAxis() { + chart.xAxis.apply { granularity = 1f setDrawAxisLine(false) setDrawGridLines(false) setDrawLabels(false) - } - labelStart.text = cutEntries.first().label - labelEnd.text = cutEntries.last().label - setPinchZoom(false) - setScaleEnabled(false) - legend.isEnabled = false - setDrawBorders(false) - - val isClickable = item.onBarSelected != null - if (isClickable) { - setOnChartValueSelectedListener(object : OnChartValueSelectedListener { - override fun onNothingSelected() { - item.selectedItem - highlightColumn(cutEntries.indexOfFirst { it.id == item.selectedItem }, hasOverlappingEntries) - item.onBarSelected?.invoke(item.selectedItem) - } - - override fun onValueSelected(e: Entry, h: Highlight) { - val value = (e as? BarEntry)?.data as? String - highlightColumn(e.x.toInt(), hasOverlappingEntries) - item.onBarSelected?.invoke(value) - } - }) - } else { - setOnChartValueSelectedListener(null) - } - isHighlightFullBarEnabled = isClickable - isHighlightPerDragEnabled = false - isHighlightPerTapEnabled = isClickable - val description = Description() - description.text = "" - this.description = description - - if (item.selectedItem != null) { - val index = cutEntries.indexOfFirst { it.id == item.selectedItem } - if (index >= 0) { - highlightColumn(index, hasOverlappingEntries) - } else { - highlightValue(null, false) - } - } - invalidate() - return cutEntries.size - } - private fun BarChart.highlightColumn(index: Int, hasOverlappingColumns: Boolean) { - if (hasOverlappingColumns) { - val high = Highlight(index.toFloat(), 0, 0) - val high2 = Highlight(index.toFloat(), 1, 1) - val high3 = Highlight(index.toFloat(), 2, 2) - high.dataIndex = index - high2.dataIndex = index - high3.dataIndex = index - highlightValues(arrayOf(high3, high, high2)) - } else { - val high = Highlight(index.toFloat(), 0, 0) - val high2 = Highlight(index.toFloat(), 1, 1) - high.dataIndex = index - high2.dataIndex = index - highlightValues(arrayOf(high2, high)) +// setLabelCount(3, true) +// setAvoidFirstLastClipping(true) +// setLabelCount(3, true) +// setAvoidFirstLastClipping(true) +// position = XAxis.XAxisPosition.BOTTOM +// valueFormatter = LineChartLabelFormatter(thisWeekData) +// textColor = ContextCompat.getColor(chart.context, R.color.neutral_30) } } - @Suppress("MagicNumber") private fun buildEmptyDataSet(context: Context, count: Int): BarDataSet { val emptyValues = (0 until count).map { index -> BarEntry(index.toFloat(), 1f, "empty") } val dataSet = BarDataSet(emptyValues, "Empty") @@ -227,7 +188,6 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( return dataSet } - @Suppress("MagicNumber") private fun buildDataSet(context: Context, cut: List): BarDataSet { val dataSet = BarDataSet(cut, "Data") dataSet.color = ContextCompat.getColor(context, R.color.wordpress_blue_40) @@ -242,64 +202,7 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( ) dataSet.formLineWidth = 0f dataSet.setDrawValues(false) - dataSet.isHighlightEnabled = true - dataSet.highLightColor = ContextCompat.getColor( - context, - R.color.stats_bar_chart_accent_top - ) - dataSet.highLightAlpha = 255 - return dataSet - } - @Suppress("MagicNumber") - private fun buildOverlappingDataSet(context: Context, cut: List): BarDataSet { - val dataSet = BarDataSet(cut, "Overlapping data") - dataSet.color = ContextCompat.getColor(context, R.color.primary_60) - dataSet.setGradientColor( - ContextCompat.getColor( - context, - R.color.stats_bar_chart_bottom - ), ContextCompat.getColor( - context, - R.color.stats_bar_chart_bottom - ) - ) - dataSet.formLineWidth = 0f - dataSet.setDrawValues(false) - dataSet.isHighlightEnabled = true - dataSet.highLightColor = ContextCompat.getColor( - context, - R.color.stats_bar_chart_accent_bottom - ) - dataSet.highLightAlpha = 255 - return dataSet - } - - @Suppress("MagicNumber") - private fun getHighlightDataSet(context: Context, cut: List): BarDataSet? { - val maxEntry = cut.maxByOrNull { it.y } ?: return null - val highlightedDataSet = cut.map { - BarEntry(it.x, maxEntry.y, it.data) - } - val dataSet = BarDataSet(highlightedDataSet, "Highlight") - dataSet.color = ContextCompat.getColor(context, android.R.color.transparent) - dataSet.setGradientColor( - ContextCompat.getColor( - context, - android.R.color.transparent - ), ContextCompat.getColor( - context, - android.R.color.transparent - ) - ) - dataSet.formLineWidth = 0f - dataSet.isHighlightEnabled = true - dataSet.highLightColor = ContextCompat.getColor( - context, - R.color.stats_bar_chart_accent_top - ) - dataSet.setDrawValues(false) - dataSet.highLightAlpha = 51 return dataSet } @@ -324,7 +227,7 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( invalidate() } - private fun toBarEntry(bar: BlockListItem.BarChartItem.Bar, index: Int): BarEntry { + private fun toBarEntry(bar: Bar, index: Int): BarEntry { return BarEntry( index.toFloat(), bar.value.toFloat(), @@ -332,7 +235,7 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( ) } - @Suppress("ReturnCount", "MagicNumber") + @Suppress("ReturnCount") private fun roundUp(input: Float): Float { return if (input > 100) { roundUp(input / 10) * 10 From 430058099800f3fdb0dd83ddb1766c1a3bda3ddd Mon Sep 17 00:00:00 2001 From: Ravi Date: Mon, 19 Feb 2024 19:08:48 +1100 Subject: [PATCH 21/56] Create BarChartLabelFormatter.kt --- .../refresh/utils/BarChartLabelFormatter.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt new file mode 100644 index 000000000000..64363463be33 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt @@ -0,0 +1,19 @@ +package org.wordpress.android.ui.stats.refresh.utils + +import com.github.mikephil.charting.components.AxisBase +import com.github.mikephil.charting.formatter.ValueFormatter +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.BarChartItem.Bar +import javax.inject.Inject + +class BarChartLabelFormatter @Inject constructor( + val entries: List +) : ValueFormatter() { + override fun getAxisLabel(value: Float, axis: AxisBase?): String { + val index = value.toInt() + return if (index < entries.size) { + entries[index].label + } else { + "" + } + } +} From 2a84c682850664305c88daa97c6e2b17c5ea5a5c Mon Sep 17 00:00:00 2001 From: Ravi Date: Mon, 19 Feb 2024 19:09:05 +1100 Subject: [PATCH 22/56] Update TrafficBarChartViewHolder.kt --- .../traffic/TrafficBarChartViewHolder.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt index c72167a719c8..2d151ca5f0f2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt @@ -7,6 +7,7 @@ import androidx.core.view.ViewCompat import com.github.mikephil.charting.animation.Easing import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.components.Description +import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarEntry @@ -20,6 +21,7 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.BarChartItem.Bar import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder import org.wordpress.android.ui.stats.refresh.utils.BarChartAccessibilityHelper +import org.wordpress.android.ui.stats.refresh.utils.BarChartLabelFormatter import org.wordpress.android.ui.stats.refresh.utils.LargeValueFormatter import org.wordpress.android.util.DisplayUtils @@ -72,7 +74,7 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( configureChartView() configureYAxis(item) - configureXAxis() + configureXAxis(item) invalidate() return data.dataSets.size @@ -115,6 +117,7 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( this.description = description extraRightOffset = 8f + extraBottomOffset = 8f animateY(1000, Easing.EaseInSine) } @@ -152,20 +155,17 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( } } - private fun configureXAxis() { + private fun configureXAxis(item: BlockListItem.BarChartItem) { chart.xAxis.apply { granularity = 1f setDrawAxisLine(false) setDrawGridLines(false) - setDrawLabels(false) -// setLabelCount(3, true) -// setAvoidFirstLastClipping(true) -// setLabelCount(3, true) -// setAvoidFirstLastClipping(true) -// position = XAxis.XAxisPosition.BOTTOM -// valueFormatter = LineChartLabelFormatter(thisWeekData) -// textColor = ContextCompat.getColor(chart.context, R.color.neutral_30) + setDrawLabels(true) + setAvoidFirstLastClipping(true) + position = XAxis.XAxisPosition.BOTTOM + valueFormatter = BarChartLabelFormatter(item.entries) + textColor = ContextCompat.getColor(chart.context, R.color.neutral_30) } } From 74d825f0238e134b3adad13af6bf306157968378 Mon Sep 17 00:00:00 2001 From: Ravi Date: Mon, 19 Feb 2024 19:09:12 +1100 Subject: [PATCH 23/56] Update TrafficOverviewMapper.kt --- .../refresh/lists/sections/traffic/TrafficOverviewMapper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewMapper.kt index bc1bf50cd4cb..e720fa7d60a7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewMapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewMapper.kt @@ -160,7 +160,7 @@ class TrafficOverviewMapper @Inject constructor( else -> 0L } BlockListItem.BarChartItem.Bar( - statsDateFormatter.printGranularDate(it.period, statsGranularity), + statsDateFormatter.printTrafficGranularDate(it.period, statsGranularity), it.period, value.toInt() ) From 86ea1bfe24bd54f9360aaaf9ed779ab43c0c8364 Mon Sep 17 00:00:00 2001 From: Ravi Date: Mon, 19 Feb 2024 19:09:19 +1100 Subject: [PATCH 24/56] Update StatsDateFormatter.kt --- .../stats/refresh/utils/StatsDateFormatter.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatter.kt index 620e3223f596..b262f70dfc04 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/StatsDateFormatter.kt @@ -23,6 +23,8 @@ import kotlin.math.abs private const val STATS_INPUT_FORMAT = "yyyy-MM-dd" private const val MONTH_FORMAT = "MMM, yyyy" private const val YEAR_FORMAT = "yyyy" +private const val DAYS_FORMAT = "d" +private const val YEARS_FORMAT = "MMM" @Suppress("CheckStyle") private const val REMOVE_YEAR = "([^\\p{Alpha}']|('[\\p{Alpha}]+'))*y+([^\\p{Alpha}']|('[\\p{Alpha}]+'))*" @@ -52,6 +54,16 @@ class StatsDateFormatter return sdf } + private val outputFormatTrafficDays: SimpleDateFormat + get() { + return SimpleDateFormat(DAYS_FORMAT, localeManagerWrapper.getLocale()) + } + + private val outputFormatTrafficYears: SimpleDateFormat + get() { + return SimpleDateFormat(YEARS_FORMAT, localeManagerWrapper.getLocale()) + } + /** * Parses the stats date and prints it in localizes readable format. * @param period in this format yyyy-MM-dd @@ -106,6 +118,46 @@ class StatsDateFormatter } } + /** + * Prints the given date in a localized format according to the StatsGranularity: + * DAYS - returns Jan 1, 2019 + * WEEKS - returns day sequence as 1, 2, 3... + * MONTHS - returns week ranges 18-24, 25-31... + * YEARS - returns months J, F, M... + * @param date to be printed + * @param granularity defines the output format + * @return printed date + */ + private fun printTrafficGranularDate(date: Date, granularity: StatsGranularity): String { + return when (granularity) { + DAYS -> outputFormatTrafficDays.format(date) + WEEKS -> { + val endCalendar = Calendar.getInstance() + endCalendar.time = date + if (endCalendar.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) { + endCalendar.time = dateToWeekDate(date) + } + val startCalendar = Calendar.getInstance() + startCalendar.time = endCalendar.time + startCalendar.add(Calendar.DAY_OF_WEEK, -6) + return printTrafficWeek(startCalendar, endCalendar) + } + MONTHS -> outputFormatTrafficYears.format(date).first().toString() + YEARS -> outputYearFormat.format(date) + } + } + + private fun printTrafficWeek( + startCalendar: Calendar, + endCalendar: Calendar + ): String { + return resourceProvider.getString( + R.string.stats_from_to_dates_in_week_label, + outputFormatTrafficDays.format(startCalendar.time), + outputFormatTrafficDays.format(endCalendar.time) + ) + } + /** * Prints a date in the Medium format but strips the year. For example prints only Jan 1 instead of Jan 1, 2019 * @param date @@ -164,6 +216,11 @@ class StatsDateFormatter return printGranularDate(parsedDate, granularity) } + fun printTrafficGranularDate(date: String, granularity: StatsGranularity): String { + val parsedDate = parseStatsDate(granularity, date) + return printTrafficGranularDate(parsedDate, granularity) + } + /** * Parses date coming from the endpoint in format specific for the stats granularity * DAYS -> the input format is yyyy-MM-dd, output is the selected date From a1e8c0beea5240fcef55e8d1c23cff2e4f0eee8f Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Mon, 19 Feb 2024 23:55:37 +0300 Subject: [PATCH 25/56] Update granularity usage in OverviewUseCase --- .../granular/usecases/OverviewUseCase.kt | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt index 3a8e102978f9..9565f9701a32 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt @@ -64,20 +64,20 @@ class OverviewUseCase constructor( uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { // The granularity of the chart is one level lower than the current one, probably there's a better way to do this - val trafficTabGranularity = when (statsGranularity) { + private val trafficTabGranularity = when (statsGranularity) { StatsGranularity.WEEKS -> StatsGranularity.DAYS StatsGranularity.MONTHS -> StatsGranularity.WEEKS StatsGranularity.YEARS -> StatsGranularity.MONTHS else -> statsGranularity } - val granularity = if (trafficTabFeatureConfig.isEnabled()) { + private val granularityByConfig = if (trafficTabFeatureConfig.isEnabled()) { trafficTabGranularity } else { statsGranularity } - val itemsToLoad = when (statsGranularity) { + private val itemsToLoad = when (statsGranularity) { StatsGranularity.WEEKS -> 7 StatsGranularity.MONTHS -> 5 StatsGranularity.YEARS -> 12 @@ -99,12 +99,12 @@ class OverviewUseCase constructor( statsWidgetUpdaters.updateWeekViewsWidget(statsSiteProvider.siteModel.siteId) val cachedData = visitsAndViewsStore.getVisits( statsSiteProvider.siteModel, - granularity, + granularityByConfig, if (trafficTabFeatureConfig.isEnabled()) LimitMode.Top(itemsToLoad) else LimitMode.All ) if (cachedData != null) { - logIfIncorrectData(cachedData, granularity, statsSiteProvider.siteModel, false) - selectedDateProvider.onDateLoadingSucceeded(granularity) + logIfIncorrectData(cachedData, granularityByConfig, statsSiteProvider.siteModel, false) + selectedDateProvider.onDateLoadingSucceeded(statsGranularity) } return cachedData } @@ -112,7 +112,7 @@ class OverviewUseCase constructor( override suspend fun fetchRemoteData(forced: Boolean): State { val response = visitsAndViewsStore.fetchVisits( statsSiteProvider.siteModel, - granularity, + granularityByConfig, LimitMode.Top(if (trafficTabFeatureConfig.isEnabled()) itemsToLoad else OVERVIEW_ITEMS_TO_LOAD), forced ) @@ -121,16 +121,16 @@ class OverviewUseCase constructor( return when { error != null -> { - selectedDateProvider.onDateLoadingFailed(granularity) + selectedDateProvider.onDateLoadingFailed(statsGranularity) State.Error(error.message ?: error.type.name) } model != null && model.dates.isNotEmpty() -> { - logIfIncorrectData(model, granularity, statsSiteProvider.siteModel, true) - selectedDateProvider.onDateLoadingSucceeded(granularity) + logIfIncorrectData(model, granularityByConfig, statsSiteProvider.siteModel, true) + selectedDateProvider.onDateLoadingSucceeded(statsGranularity) State.Data(model) } else -> { - selectedDateProvider.onDateLoadingSucceeded(granularity) + selectedDateProvider.onDateLoadingSucceeded(statsGranularity) State.Empty() } } @@ -182,11 +182,11 @@ class OverviewUseCase constructor( private fun buildTrafficOverview(domainModel: VisitsAndViewsModel, uiState: UiState): List { val items = mutableListOf() if (domainModel.dates.isNotEmpty()) { - val dateFromProvider = selectedDateProvider.getSelectedDate(granularity) + val dateFromProvider = selectedDateProvider.getSelectedDate(statsGranularity) val visibleBarCount = uiState.visibleBarCount ?: domainModel.dates.size val availableDates = domainModel.dates.map { statsDateFormatter.parseStatsDate( - granularity, + trafficTabGranularity, it.period ) } @@ -196,7 +196,7 @@ class OverviewUseCase constructor( selectedDateProvider.selectDate( selectedDate, availableDates.takeLast(visibleBarCount), - granularity + statsGranularity ) val selectedItem = domainModel.dates.getOrNull(index) ?: domainModel.dates.last() val previousItem = domainModel.dates.getOrNull(domainModel.dates.indexOf(selectedItem) - 1) @@ -207,7 +207,7 @@ class OverviewUseCase constructor( buildGranularChart(domainModel, uiState, items, selectedItem, previousItem) } } else { - selectedDateProvider.onDateLoadingFailed(granularity) + selectedDateProvider.onDateLoadingFailed(statsGranularity) AppLog.e(T.STATS, "There is no data to be shown in the overview block") } return items @@ -263,13 +263,13 @@ class OverviewUseCase constructor( previousItem, uiState.selectedPosition, isLast = selectedItem == domainModel.dates.last(), - statsGranularity = granularity + statsGranularity = trafficTabGranularity ) ) items.addAll( trafficOverviewMapper.buildChart( domainModel.dates, - granularity, + trafficTabGranularity, this::onBarSelected, this::onBarChartDrawn, uiState.selectedPosition, From d1715520510a21781619756b5a98a6ff00216eb9 Mon Sep 17 00:00:00 2001 From: Ravi Date: Tue, 20 Feb 2024 15:22:33 +1100 Subject: [PATCH 26/56] Create TrafficOverviewUseCase.kt Separate TrafficUseCase from previous OverviewUseCase --- .../traffic/TrafficOverviewUseCase.kt | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt new file mode 100644 index 000000000000..7a1ba5dfa696 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt @@ -0,0 +1,325 @@ +package org.wordpress.android.ui.stats.refresh.lists.sections.traffic + +import kotlinx.coroutines.CoroutineDispatcher +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.stats.LimitMode +import org.wordpress.android.fluxc.model.stats.time.VisitsAndViewsModel +import org.wordpress.android.fluxc.network.utils.StatsGranularity +import org.wordpress.android.fluxc.store.StatsStore +import org.wordpress.android.fluxc.store.stats.time.VisitsAndViewsStore +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem +import org.wordpress.android.ui.stats.refresh.lists.sections.granular.GranularUseCaseFactory +import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewUseCase.UiState +import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetUpdater +import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter +import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider +import org.wordpress.android.ui.stats.refresh.utils.StatsUtils +import org.wordpress.android.ui.stats.refresh.utils.trackGranular +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.LocaleManagerWrapper +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.viewmodel.ResourceProvider +import java.util.Calendar +import javax.inject.Inject +import javax.inject.Named +import kotlin.math.ceil + +const val OVERVIEW_ITEMS_TO_LOAD = 15 + +@Suppress("LongParameterList", "MagicNumber") +class TrafficOverviewUseCase constructor( + private val statsGranularity: StatsGranularity, + private val visitsAndViewsStore: VisitsAndViewsStore, + private val selectedDateProvider: SelectedDateProvider, + private val statsSiteProvider: StatsSiteProvider, + private val statsDateFormatter: StatsDateFormatter, + private val trafficOverviewMapper: TrafficOverviewMapper, + @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, + @Named(BG_THREAD) private val backgroundDispatcher: CoroutineDispatcher, + private val analyticsTracker: AnalyticsTrackerWrapper, + private val statsWidgetUpdaters: WidgetUpdater.StatsWidgetUpdaters, + private val localeManagerWrapper: LocaleManagerWrapper, + private val resourceProvider: ResourceProvider, + private val statsUtils: StatsUtils +) : BaseStatsUseCase( + StatsStore.TimeStatsType.OVERVIEW, + mainDispatcher, + backgroundDispatcher, + UiState(), + uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) +) { + // The granularity of the chart is one level lower than the current one, probably there's a better way to do this + private val trafficTabGranularity = when (statsGranularity) { + StatsGranularity.WEEKS -> StatsGranularity.DAYS + StatsGranularity.MONTHS -> StatsGranularity.WEEKS + StatsGranularity.YEARS -> StatsGranularity.MONTHS + else -> statsGranularity + } + + private val itemsToLoad = when (trafficTabGranularity) { + StatsGranularity.DAYS -> 7 + StatsGranularity.WEEKS -> 5 + StatsGranularity.MONTHS -> 12 + else -> OVERVIEW_ITEMS_TO_LOAD + } + + override fun buildLoadingItem(): List = + listOf( + BlockListItem.ValueItem( + value = "0", + unit = R.string.stats_views, + isFirst = true, + contentDescription = resourceProvider.getString(R.string.stats_loading_card) + ) + ) + + override suspend fun loadCachedData(): VisitsAndViewsModel? { + statsWidgetUpdaters.updateViewsWidget(statsSiteProvider.siteModel.siteId) + statsWidgetUpdaters.updateWeekViewsWidget(statsSiteProvider.siteModel.siteId) + val cachedData = visitsAndViewsStore.getVisits( + statsSiteProvider.siteModel, + trafficTabGranularity, + LimitMode.Top(itemsToLoad) + ) + if (cachedData != null) { + logIfIncorrectData(cachedData, trafficTabGranularity, statsSiteProvider.siteModel, false) + selectedDateProvider.onDateLoadingSucceeded(statsGranularity) + } + return cachedData + } + + override suspend fun fetchRemoteData(forced: Boolean): State { + val response = visitsAndViewsStore.fetchVisits( + statsSiteProvider.siteModel, + trafficTabGranularity, + LimitMode.Top(itemsToLoad), + forced + ) + val model = response.model + val error = response.error + + return when { + error != null -> { + selectedDateProvider.onDateLoadingFailed(statsGranularity) + State.Error(error.message ?: error.type.name) + } + model != null && model.dates.isNotEmpty() -> { + logIfIncorrectData(model, trafficTabGranularity, statsSiteProvider.siteModel, true) + selectedDateProvider.onDateLoadingSucceeded(statsGranularity) + State.Data(model) + } + else -> { + selectedDateProvider.onDateLoadingSucceeded(statsGranularity) + State.Empty() + } + } + } + + /** + * Track the incorrect data shown for some users + * see https://github.com/wordpress-mobile/WordPress-Android/issues/11412 + */ + private fun logIfIncorrectData( + model: VisitsAndViewsModel, + granularity: StatsGranularity, + site: SiteModel, + fetched: Boolean + ) { + model.dates.lastOrNull()?.let { lastDayData -> + val yesterday = localeManagerWrapper.getCurrentCalendar() + yesterday.add(Calendar.DAY_OF_YEAR, -1) + val lastDayDate = statsDateFormatter.parseStatsDate(granularity, lastDayData.period) + if (lastDayDate.before(yesterday.time)) { + val currentCalendar = localeManagerWrapper.getCurrentCalendar() + val lastItemAge = ceil((currentCalendar.timeInMillis - lastDayDate.time) / 86400000.0) + analyticsTracker.track( + AnalyticsTracker.Stat.STATS_OVERVIEW_ERROR, + mapOf( + "stats_last_date" to statsDateFormatter.printStatsDate(lastDayDate), + "stats_current_date" to statsDateFormatter.printStatsDate(currentCalendar.time), + "stats_age_in_days" to lastItemAge.toInt(), + "is_jetpack_connected" to site.isJetpackConnected, + "is_atomic" to site.isWPComAtomic, + "action_source" to if (fetched) "remote" else "cached" + ) + ) + } + } + } + + override fun buildUiModel( + domainModel: VisitsAndViewsModel, + uiState: UiState + ): List { + val items = mutableListOf() + if (domainModel.dates.isNotEmpty()) { + val dateFromProvider = selectedDateProvider.getSelectedDate(trafficTabGranularity) + val visibleBarCount = uiState.visibleBarCount ?: domainModel.dates.size + val availableDates = domainModel.dates.map { + statsDateFormatter.parseStatsDate( + trafficTabGranularity, + it.period + ) + } + val selectedDate = dateFromProvider ?: availableDates.last() + val index = availableDates.indexOf(selectedDate) + + selectedDateProvider.selectDate( + selectedDate, + availableDates.takeLast(visibleBarCount), + trafficTabGranularity + ) + val selectedItem = domainModel.dates.getOrNull(index) ?: domainModel.dates.last() + val previousItem = domainModel.dates.getOrNull(domainModel.dates.indexOf(selectedItem) - 1) + + if (statsGranularity == StatsGranularity.DAYS) { + buildTodayCard(selectedItem,items) + } else { + buildGranularChart(domainModel, uiState, items, selectedItem, previousItem) + } + } else { + selectedDateProvider.onDateLoadingFailed(statsGranularity) + AppLog.e(AppLog.T.STATS, "There is no data to be shown in the overview block") + } + return items + } + + private fun buildTodayCard( + selectedItem: VisitsAndViewsModel.PeriodData?, + items: MutableList + ) { + val views = selectedItem?.views ?: 0 + val visitors = selectedItem?.visitors ?: 0 + val likes = selectedItem?.likes ?: 0 + val comments = selectedItem?.comments ?: 0 + + items.add(BlockListItem.Title(R.string.stats_timeframe_today)) + items.add( + BlockListItem.QuickScanItem( + BlockListItem.QuickScanItem.Column( + R.string.stats_views, + statsUtils.toFormattedString(views) + ), + BlockListItem.QuickScanItem.Column( + R.string.stats_visitors, + statsUtils.toFormattedString(visitors) + ) + ) + ) + + items.add( + BlockListItem.QuickScanItem( + BlockListItem.QuickScanItem.Column( + R.string.stats_likes, + statsUtils.toFormattedString(likes) + ), + BlockListItem.QuickScanItem.Column( + R.string.stats_comments, + statsUtils.toFormattedString(comments) + ) + ) + ) + } + + private fun buildGranularChart( + domainModel: VisitsAndViewsModel, + uiState: UiState, + items: MutableList, + selectedItem: VisitsAndViewsModel.PeriodData, + previousItem: VisitsAndViewsModel.PeriodData? + ) { + items.add( + trafficOverviewMapper.buildTitle( + selectedItem, + previousItem, + uiState.selectedPosition, + isLast = selectedItem == domainModel.dates.last(), + statsGranularity = trafficTabGranularity + ) + ) + items.addAll( + trafficOverviewMapper.buildChart( + domainModel.dates, + trafficTabGranularity, + this::onBarSelected, + this::onBarChartDrawn, + uiState.selectedPosition, + selectedItem.period + ) + ) + items.add( + trafficOverviewMapper.buildColumns( + selectedItem, + this::onColumnSelected, + uiState.selectedPosition + ) + ) + } + + private fun onBarSelected(period: String?) { + analyticsTracker.trackGranular( + AnalyticsTracker.Stat.STATS_OVERVIEW_BAR_CHART_TAPPED, + trafficTabGranularity + ) + if (period != null && period != "empty") { + val selectedDate = statsDateFormatter.parseStatsDate(statsGranularity, period) + selectedDateProvider.selectDate( + selectedDate, + trafficTabGranularity + ) + } + } + + private fun onColumnSelected(position: Int) { + analyticsTracker.trackGranular( + AnalyticsTracker.Stat.STATS_OVERVIEW_TYPE_TAPPED, + trafficTabGranularity + ) + updateUiState { it.copy(selectedPosition = position) } + } + + private fun onBarChartDrawn(visibleBarCount: Int) { + updateUiState { it.copy(visibleBarCount = visibleBarCount) } + } + + data class UiState(val selectedPosition: Int = 0, val visibleBarCount: Int? = null) + + class TrafficOverviewUseCaseFactory + @Inject constructor( + @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, + @Named(BG_THREAD) private val backgroundDispatcher: CoroutineDispatcher, + private val statsSiteProvider: StatsSiteProvider, + private val selectedDateProvider: SelectedDateProvider, + private val statsDateFormatter: StatsDateFormatter, + private val trafficOverviewMapper: TrafficOverviewMapper, + private val visitsAndViewsStore: VisitsAndViewsStore, + private val analyticsTracker: AnalyticsTrackerWrapper, + private val statsWidgetUpdaters: WidgetUpdater.StatsWidgetUpdaters, + private val localeManagerWrapper: LocaleManagerWrapper, + private val resourceProvider: ResourceProvider, + private val statsUtils: StatsUtils + ) : GranularUseCaseFactory { + override fun build(granularity: StatsGranularity, useCaseMode: UseCaseMode) = + TrafficOverviewUseCase( + granularity, + visitsAndViewsStore, + selectedDateProvider, + statsSiteProvider, + statsDateFormatter, + trafficOverviewMapper, + mainDispatcher, + backgroundDispatcher, + analyticsTracker, + statsWidgetUpdaters, + localeManagerWrapper, + resourceProvider, + statsUtils + ) + } +} From 34d005850410ed1f4c8c7395d41faa803959e00b Mon Sep 17 00:00:00 2001 From: Ravi Date: Tue, 20 Feb 2024 15:31:33 +1100 Subject: [PATCH 27/56] Update OverviewUseCase.kt Remove traffic tab related conditions as a new traffic overview use case is created --- .../granular/usecases/OverviewUseCase.kt | 167 +----------------- 1 file changed, 9 insertions(+), 158 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt index 9565f9701a32..c70fd14e97fa 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt @@ -14,23 +14,18 @@ import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.stats.refresh.lists.sections.BaseStatsUseCase import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem -import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.QuickScanItem -import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.QuickScanItem.Column import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem import org.wordpress.android.ui.stats.refresh.lists.sections.granular.GranularUseCaseFactory import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider import org.wordpress.android.ui.stats.refresh.lists.sections.granular.usecases.OverviewUseCase.UiState -import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewMapper import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetUpdater.StatsWidgetUpdaters import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider -import org.wordpress.android.ui.stats.refresh.utils.StatsUtils import org.wordpress.android.ui.stats.refresh.utils.trackGranular import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.LocaleManagerWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper -import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig import org.wordpress.android.viewmodel.ResourceProvider import java.util.Calendar import javax.inject.Inject @@ -52,10 +47,7 @@ class OverviewUseCase constructor( private val analyticsTracker: AnalyticsTrackerWrapper, private val statsWidgetUpdaters: StatsWidgetUpdaters, private val localeManagerWrapper: LocaleManagerWrapper, - private val resourceProvider: ResourceProvider, - private val statsUtils: StatsUtils, - private val trafficTabFeatureConfig: StatsTrafficTabFeatureConfig, - private val trafficOverviewMapper: TrafficOverviewMapper + private val resourceProvider: ResourceProvider ) : BaseStatsUseCase( OVERVIEW, mainDispatcher, @@ -63,27 +55,6 @@ class OverviewUseCase constructor( UiState(), uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { - // The granularity of the chart is one level lower than the current one, probably there's a better way to do this - private val trafficTabGranularity = when (statsGranularity) { - StatsGranularity.WEEKS -> StatsGranularity.DAYS - StatsGranularity.MONTHS -> StatsGranularity.WEEKS - StatsGranularity.YEARS -> StatsGranularity.MONTHS - else -> statsGranularity - } - - private val granularityByConfig = if (trafficTabFeatureConfig.isEnabled()) { - trafficTabGranularity - } else { - statsGranularity - } - - private val itemsToLoad = when (statsGranularity) { - StatsGranularity.WEEKS -> 7 - StatsGranularity.MONTHS -> 5 - StatsGranularity.YEARS -> 12 - else -> OVERVIEW_ITEMS_TO_LOAD - } - override fun buildLoadingItem(): List = listOf( ValueItem( @@ -99,11 +70,11 @@ class OverviewUseCase constructor( statsWidgetUpdaters.updateWeekViewsWidget(statsSiteProvider.siteModel.siteId) val cachedData = visitsAndViewsStore.getVisits( statsSiteProvider.siteModel, - granularityByConfig, - if (trafficTabFeatureConfig.isEnabled()) LimitMode.Top(itemsToLoad) else LimitMode.All + statsGranularity, + LimitMode.All ) if (cachedData != null) { - logIfIncorrectData(cachedData, granularityByConfig, statsSiteProvider.siteModel, false) + logIfIncorrectData(cachedData, statsGranularity, statsSiteProvider.siteModel, false) selectedDateProvider.onDateLoadingSucceeded(statsGranularity) } return cachedData @@ -112,8 +83,8 @@ class OverviewUseCase constructor( override suspend fun fetchRemoteData(forced: Boolean): State { val response = visitsAndViewsStore.fetchVisits( statsSiteProvider.siteModel, - granularityByConfig, - LimitMode.Top(if (trafficTabFeatureConfig.isEnabled()) itemsToLoad else OVERVIEW_ITEMS_TO_LOAD), + statsGranularity, + LimitMode.Top(OVERVIEW_ITEMS_TO_LOAD), forced ) val model = response.model @@ -125,7 +96,7 @@ class OverviewUseCase constructor( State.Error(error.message ?: error.type.name) } model != null && model.dates.isNotEmpty() -> { - logIfIncorrectData(model, granularityByConfig, statsSiteProvider.siteModel, true) + logIfIncorrectData(model, statsGranularity, statsSiteProvider.siteModel, true) selectedDateProvider.onDateLoadingSucceeded(statsGranularity) State.Data(model) } @@ -172,120 +143,6 @@ class OverviewUseCase constructor( domainModel: VisitsAndViewsModel, uiState: UiState ): List { - return if (trafficTabFeatureConfig.isEnabled()) { - buildTrafficOverview(domainModel, uiState) - } else { - buildGranularOverview(domainModel, uiState) - } - } - - private fun buildTrafficOverview(domainModel: VisitsAndViewsModel, uiState: UiState): List { - val items = mutableListOf() - if (domainModel.dates.isNotEmpty()) { - val dateFromProvider = selectedDateProvider.getSelectedDate(statsGranularity) - val visibleBarCount = uiState.visibleBarCount ?: domainModel.dates.size - val availableDates = domainModel.dates.map { - statsDateFormatter.parseStatsDate( - trafficTabGranularity, - it.period - ) - } - val selectedDate = dateFromProvider ?: availableDates.last() - val index = availableDates.indexOf(selectedDate) - - selectedDateProvider.selectDate( - selectedDate, - availableDates.takeLast(visibleBarCount), - statsGranularity - ) - val selectedItem = domainModel.dates.getOrNull(index) ?: domainModel.dates.last() - val previousItem = domainModel.dates.getOrNull(domainModel.dates.indexOf(selectedItem) - 1) - - if (statsGranularity == StatsGranularity.DAYS) { - buildTodayCard(selectedItem,items) - } else { - buildGranularChart(domainModel, uiState, items, selectedItem, previousItem) - } - } else { - selectedDateProvider.onDateLoadingFailed(statsGranularity) - AppLog.e(T.STATS, "There is no data to be shown in the overview block") - } - return items - } - - private fun buildTodayCard( - selectedItem: VisitsAndViewsModel.PeriodData?, - items: MutableList - ) { - val views = selectedItem?.views ?: 0 - val visitors = selectedItem?.visitors ?: 0 - val likes = selectedItem?.likes ?: 0 - val comments = selectedItem?.comments ?: 0 - - items.add(BlockListItem.Title(R.string.stats_timeframe_today)) - items.add( - QuickScanItem( - Column( - R.string.stats_views, - statsUtils.toFormattedString(views) - ), - Column( - R.string.stats_visitors, - statsUtils.toFormattedString(visitors) - ) - ) - ) - - items.add( - QuickScanItem( - Column( - R.string.stats_likes, - statsUtils.toFormattedString(likes) - ), - Column( - R.string.stats_comments, - statsUtils.toFormattedString(comments) - ) - ) - ) - } - - private fun buildGranularChart( - domainModel: VisitsAndViewsModel, - uiState: UiState, - items: MutableList, - selectedItem: VisitsAndViewsModel.PeriodData, - previousItem: VisitsAndViewsModel.PeriodData? - ) { - items.add( - trafficOverviewMapper.buildTitle( - selectedItem, - previousItem, - uiState.selectedPosition, - isLast = selectedItem == domainModel.dates.last(), - statsGranularity = trafficTabGranularity - ) - ) - items.addAll( - trafficOverviewMapper.buildChart( - domainModel.dates, - trafficTabGranularity, - this::onBarSelected, - this::onBarChartDrawn, - uiState.selectedPosition, - selectedItem.period - ) - ) - items.add( - trafficOverviewMapper.buildColumns( - selectedItem, - this::onColumnSelected, - uiState.selectedPosition - ) - ) - } - - private fun buildGranularOverview(domainModel: VisitsAndViewsModel, uiState: UiState): List { val items = mutableListOf() if (domainModel.dates.isNotEmpty()) { val dateFromProvider = selectedDateProvider.getSelectedDate(statsGranularity) @@ -379,10 +236,7 @@ class OverviewUseCase constructor( private val analyticsTracker: AnalyticsTrackerWrapper, private val statsWidgetUpdaters: StatsWidgetUpdaters, private val localeManagerWrapper: LocaleManagerWrapper, - private val resourceProvider: ResourceProvider, - private val statsUtils: StatsUtils, - private val trafficTabFeatureConfig: StatsTrafficTabFeatureConfig, - private val trafficOverviewMapper: TrafficOverviewMapper + private val resourceProvider: ResourceProvider ) : GranularUseCaseFactory { override fun build(granularity: StatsGranularity, useCaseMode: UseCaseMode) = OverviewUseCase( @@ -397,10 +251,7 @@ class OverviewUseCase constructor( analyticsTracker, statsWidgetUpdaters, localeManagerWrapper, - resourceProvider, - statsUtils, - trafficTabFeatureConfig, - trafficOverviewMapper + resourceProvider ) } } From 5970f3a81b1942d1ab5bc0f2f041366e9349c008 Mon Sep 17 00:00:00 2001 From: Ravi Date: Tue, 20 Feb 2024 15:32:37 +1100 Subject: [PATCH 28/56] Update StatsModule.kt Add TrafficOverviewUseCase --- .../android/ui/stats/refresh/StatsModule.kt | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt index 08881ec68a44..59f9686b2298 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsModule.kt @@ -62,6 +62,7 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.T import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.TotalFollowersUseCase.TotalFollowersUseCaseFactory import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.TotalLikesUseCase.TotalLikesUseCaseFactory import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.ViewsAndVisitorsUseCase.ViewsAndVisitorsUseCaseFactory +import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficOverviewUseCase.TrafficOverviewUseCaseFactory import org.wordpress.android.ui.stats.refresh.utils.SelectedTrafficGranularityManager import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider import org.wordpress.android.util.config.StatsTrafficTabFeatureConfig @@ -84,6 +85,7 @@ const val LIST_STATS_USE_CASES = "ListStatsUseCases" const val BLOCK_INSIGHTS_USE_CASES = "BlockInsightsUseCases" const val VIEW_ALL_INSIGHTS_USE_CASES = "ViewAllInsightsUseCases" const val GRANULAR_USE_CASE_FACTORIES = "GranularUseCaseFactories" +const val TRAFFIC_USE_CASE_FACTORIES = "TrafficUseCaseFactories" // These are injected only internally private const val BLOCK_DETAIL_USE_CASES = "BlockDetailUseCases" @@ -204,7 +206,7 @@ class StatsModule { searchTermsUseCaseFactory: SearchTermsUseCaseFactory, authorsUseCaseFactory: AuthorsUseCaseFactory, overviewUseCaseFactory: OverviewUseCaseFactory, - fileDownloadsUseCaseFactory: FileDownloadsUseCaseFactory + fileDownloadsUseCaseFactory: FileDownloadsUseCaseFactory, ): List<@JvmSuppressWildcards GranularUseCaseFactory> { return listOf( postsAndPagesUseCaseFactory, @@ -215,7 +217,7 @@ class StatsModule { searchTermsUseCaseFactory, authorsUseCaseFactory, overviewUseCaseFactory, - fileDownloadsUseCaseFactory + fileDownloadsUseCaseFactory, ) } @@ -268,6 +270,38 @@ class StatsModule { ) } + /** + * Provides a list of use case factories that build use cases for the Traffic stats screen based on the given + * granularity (Day, Week, Month, Year). + */ + @Provides + @Singleton + @Named(TRAFFIC_USE_CASE_FACTORIES) + @Suppress("LongParameterList") + fun provideTrafficUseCaseFactories( + postsAndPagesUseCaseFactory: PostsAndPagesUseCaseFactory, + referrersUseCaseFactory: ReferrersUseCaseFactory, + clicksUseCaseFactory: ClicksUseCaseFactory, + countryViewsUseCaseFactory: CountryViewsUseCaseFactory, + videoPlaysUseCaseFactory: VideoPlaysUseCaseFactory, + searchTermsUseCaseFactory: SearchTermsUseCaseFactory, + authorsUseCaseFactory: AuthorsUseCaseFactory, + trafficOverviewUseCaseFactory: TrafficOverviewUseCaseFactory, + fileDownloadsUseCaseFactory: FileDownloadsUseCaseFactory + ): List<@JvmSuppressWildcards GranularUseCaseFactory> { + return listOf( + postsAndPagesUseCaseFactory, + referrersUseCaseFactory, + clicksUseCaseFactory, + countryViewsUseCaseFactory, + videoPlaysUseCaseFactory, + searchTermsUseCaseFactory, + authorsUseCaseFactory, + trafficOverviewUseCaseFactory, + fileDownloadsUseCaseFactory + ) + } + /** * Provides a singleton usecase that represents the TRAFFIC stats screen. * @param useCasesFactories build the use cases for the DAYS granularity @@ -280,7 +314,7 @@ class StatsModule { @Named(BG_THREAD) bgDispatcher: CoroutineDispatcher, @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, statsSiteProvider: StatsSiteProvider, - @Named(GRANULAR_USE_CASE_FACTORIES) useCasesFactories: List<@JvmSuppressWildcards GranularUseCaseFactory>, + @Named(TRAFFIC_USE_CASE_FACTORIES) useCasesFactories: List<@JvmSuppressWildcards GranularUseCaseFactory>, selectedTrafficGranularityManager: SelectedTrafficGranularityManager, uiModelMapper: UiModelMapper ): BaseListUseCase { @@ -416,7 +450,10 @@ class StatsModule { trafficTabFeatureConfig: StatsTrafficTabFeatureConfig ): Map { return if (trafficTabFeatureConfig.isEnabled()) { - mapOf(StatsSection.TRAFFIC to trafficUseCase, StatsSection.INSIGHTS to insightsUseCase) + mapOf( + StatsSection.TRAFFIC to trafficUseCase, + StatsSection.INSIGHTS to insightsUseCase + ) } else { mapOf( StatsSection.INSIGHTS to insightsUseCase, From e5abf2477168abe728308e76b732c08255bbf7be Mon Sep 17 00:00:00 2001 From: Ravi Date: Tue, 20 Feb 2024 15:32:52 +1100 Subject: [PATCH 29/56] Update StatsListViewModel.kt --- .../android/ui/stats/refresh/lists/StatsListViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt index 565aeb8d5d3b..ff5abea0eebb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListViewModel.kt @@ -14,7 +14,6 @@ import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.fluxc.store.StatsStore import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.stats.refresh.DAY_STATS_USE_CASE -import org.wordpress.android.ui.stats.refresh.GRANULAR_USE_CASE_FACTORIES import org.wordpress.android.ui.stats.refresh.INSIGHTS_USE_CASE import org.wordpress.android.ui.stats.refresh.MONTH_STATS_USE_CASE import org.wordpress.android.ui.stats.refresh.NavigationTarget @@ -24,6 +23,7 @@ import org.wordpress.android.ui.stats.refresh.TOTAL_COMMENTS_DETAIL_USE_CASE import org.wordpress.android.ui.stats.refresh.TOTAL_FOLLOWERS_DETAIL_USE_CASE import org.wordpress.android.ui.stats.refresh.TOTAL_LIKES_DETAIL_USE_CASE import org.wordpress.android.ui.stats.refresh.TRAFFIC_USE_CASE +import org.wordpress.android.ui.stats.refresh.TRAFFIC_USE_CASE_FACTORIES import org.wordpress.android.ui.stats.refresh.VIEWS_AND_VISITORS_USE_CASE import org.wordpress.android.ui.stats.refresh.WEEK_STATS_USE_CASE import org.wordpress.android.ui.stats.refresh.YEAR_STATS_USE_CASE @@ -217,7 +217,7 @@ class TrafficListViewModel @Inject constructor( @Named(TRAFFIC_USE_CASE) private val trafficStatsUseCase: BaseListUseCase, private val analyticsTracker: AnalyticsTrackerWrapper, dateSelectorFactory: StatsDateSelector.Factory, - @Named(GRANULAR_USE_CASE_FACTORIES) + @Named(TRAFFIC_USE_CASE_FACTORIES) private val useCasesFactories: List<@JvmSuppressWildcards GranularUseCaseFactory>, private val selectedTrafficGranularityManager: SelectedTrafficGranularityManager, ) : StatsListViewModel( From d6a7bff893794b00e3a0ef1c3e9ea7b3ad7ede06 Mon Sep 17 00:00:00 2001 From: Ravi Date: Wed, 21 Feb 2024 15:52:45 +1100 Subject: [PATCH 30/56] Remove title on traffic bar chart --- .../sections/traffic/TrafficOverviewUseCase.kt | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt index 7a1ba5dfa696..a027362bfeff 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt @@ -176,12 +176,11 @@ class TrafficOverviewUseCase constructor( trafficTabGranularity ) val selectedItem = domainModel.dates.getOrNull(index) ?: domainModel.dates.last() - val previousItem = domainModel.dates.getOrNull(domainModel.dates.indexOf(selectedItem) - 1) if (statsGranularity == StatsGranularity.DAYS) { buildTodayCard(selectedItem,items) } else { - buildGranularChart(domainModel, uiState, items, selectedItem, previousItem) + buildGranularChart(domainModel, uiState, items, selectedItem) } } else { selectedDateProvider.onDateLoadingFailed(statsGranularity) @@ -231,18 +230,8 @@ class TrafficOverviewUseCase constructor( domainModel: VisitsAndViewsModel, uiState: UiState, items: MutableList, - selectedItem: VisitsAndViewsModel.PeriodData, - previousItem: VisitsAndViewsModel.PeriodData? + selectedItem: VisitsAndViewsModel.PeriodData ) { - items.add( - trafficOverviewMapper.buildTitle( - selectedItem, - previousItem, - uiState.selectedPosition, - isLast = selectedItem == domainModel.dates.last(), - statsGranularity = trafficTabGranularity - ) - ) items.addAll( trafficOverviewMapper.buildChart( domainModel.dates, From 5ccbecc99fc58d7c94c44de74f67ae7c3c14a6aa Mon Sep 17 00:00:00 2001 From: Ravi Date: Wed, 21 Feb 2024 15:53:22 +1100 Subject: [PATCH 31/56] Update BlockListAdapter.kt remove title, and update to four columns with values --- .../ui/stats/refresh/lists/sections/BlockListAdapter.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListAdapter.kt index f186cc2381f2..af0a11f123a5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/BlockListAdapter.kt @@ -79,8 +79,6 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Value import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueWithChartItem import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValuesItem import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficBarChartViewHolder -import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficFourColumnsViewHolder -import org.wordpress.android.ui.stats.refresh.lists.sections.traffic.TrafficValueViewHolder import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.ActionCardViewHolder import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.ActivityViewHolder import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BarChartViewHolder @@ -148,7 +146,7 @@ class BlockListAdapter( LIST_ITEM -> ListItemViewHolder(parent) EMPTY -> EmptyViewHolder(parent) TEXT -> TextViewHolder(parent) - COLUMNS -> if (trafficTabEnabled) TrafficFourColumnsViewHolder(parent) else FourColumnsViewHolder(parent) + COLUMNS -> FourColumnsViewHolder(parent) CHIPS -> ChipsViewHolder(parent) LINK -> LinkViewHolder(parent) BAR_CHART -> if (trafficTabEnabled) TrafficBarChartViewHolder(parent) else BarChartViewHolder(parent) @@ -165,7 +163,7 @@ class BlockListAdapter( LOADING_ITEM -> LoadingItemViewHolder(parent) MAP -> MapViewHolder(parent) MAP_LEGEND -> MapLegendViewHolder(parent) - VALUE_ITEM -> if (trafficTabEnabled) TrafficValueViewHolder(parent) else ValueViewHolder(parent) + VALUE_ITEM -> ValueViewHolder(parent) VALUE_WITH_CHART_ITEM -> ValueWithChartViewHolder(parent) VALUES_ITEM -> ValuesViewHolder(parent) ACTIVITY_ITEM -> ActivityViewHolder(parent) @@ -194,13 +192,11 @@ class BlockListAdapter( is ValueViewHolder -> holder.bind(item as ValueItem) is ValueWithChartViewHolder -> holder.bind(item as ValueWithChartItem) is ValuesViewHolder -> holder.bind(item as ValuesItem) - is TrafficValueViewHolder -> holder.bind(item as ValueItem) is ListItemWithImageViewHolder -> holder.bind(item as ListItemWithImage) is ListItemWithIconViewHolder -> holder.bind(item as ListItemWithIcon) is ListItemViewHolder -> holder.bind(item as ListItem) is TextViewHolder -> holder.bind(item as Text) is FourColumnsViewHolder -> holder.bind(item as Columns, payloads) - is TrafficFourColumnsViewHolder -> holder.bind(item as Columns, payloads) is ChipsViewHolder -> holder.bind(item as Chips) is LinkViewHolder -> holder.bind(item as Link) is BarChartViewHolder -> holder.bind(item as BarChartItem) From f0b05c29d75844df8b02e887a304ccdce9583864 Mon Sep 17 00:00:00 2001 From: Ravi Date: Wed, 21 Feb 2024 15:54:38 +1100 Subject: [PATCH 32/56] Delete unused Delete unused ViewHolders, and layout files meant for title, and columns on traffic bar chart --- .../traffic/TrafficFourColumnsViewHolder.kt | 56 ---------------- .../traffic/TrafficValueViewHolder.kt | 53 --------------- .../stats_block_traffic_four_columns_item.xml | 49 -------------- .../layout/stats_block_traffic_value_item.xml | 64 ------------------- 4 files changed, 222 deletions(-) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt delete mode 100644 WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml delete mode 100644 WordPress/src/main/res/layout/stats_block_traffic_value_item.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt deleted file mode 100644 index 8e650e541f97..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt +++ /dev/null @@ -1,56 +0,0 @@ -package org.wordpress.android.ui.stats.refresh.lists.sections.traffic - -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import org.wordpress.android.R -import org.wordpress.android.ui.stats.refresh.BlockDiffCallback -import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem -import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder - -class TrafficFourColumnsViewHolder(parent: ViewGroup) : BlockListItemViewHolder( - parent, - R.layout.stats_block_traffic_four_columns_item -) { - private val columnLayouts = listOf( - itemView.findViewById(R.id.column1), - itemView.findViewById(R.id.column2), - itemView.findViewById(R.id.column3), - itemView.findViewById(R.id.column4) - ) - - fun bind( - item: BlockListItem.Columns, - payloads: List - ) { - val tabSelected = payloads.contains(BlockDiffCallback.BlockListPayload.SELECTED_COLUMN_CHANGED) - when { - tabSelected -> { - columnLayouts.forEachIndexed { index, layout -> - layout.setSelection(item.selectedColumn == index) - } - } - else -> { - columnLayouts.forEachIndexed { index, layout -> - layout.setOnClickListener { - it.announceForAccessibility(it.resources.getString(R.string.stats_graph_updated)) - item.onColumnSelected?.invoke(index) - } - val currentColumn = item.columns[index] - layout.key().setText(currentColumn.header) - layout.setSelection(item.selectedColumn == null || item.selectedColumn == index) - layout.contentDescription = currentColumn.contentDescription - } - } - } - } - - private fun LinearLayout.setSelection(isSelected: Boolean) { - key().isSelected = isSelected - selector().visibility = if (isSelected) View.VISIBLE else View.GONE - } - - private fun LinearLayout.key(): TextView = this.findViewById(R.id.key) - private fun LinearLayout.selector(): View = this.findViewById(R.id.selector) -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt deleted file mode 100644 index 93c7c3c2ae0b..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.wordpress.android.ui.stats.refresh.lists.sections.traffic - -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import androidx.appcompat.content.res.AppCompatResources -import androidx.recyclerview.widget.RecyclerView -import org.wordpress.android.R -import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem -import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.NEGATIVE -import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.NEUTRAL -import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.POSITIVE -import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder -import org.wordpress.android.util.extensions.getColorResIdFromAttribute -import org.wordpress.android.util.extensions.getString - -class TrafficValueViewHolder(parent: ViewGroup) : BlockListItemViewHolder( - parent, - R.layout.stats_block_traffic_value_item -) { - private val container = itemView.findViewById(R.id.value_container) - private val value = itemView.findViewById(R.id.value) - private val unit = itemView.findViewById(R.id.unit) - private val change = itemView.findViewById(R.id.change) - private val period = itemView.findViewById(R.id.period) - fun bind(item: BlockListItem.ValueItem) { - value.text = item.value - unit.setText(item.unit) - val hasChange = item.change != null - val color = when (item.state) { - POSITIVE -> change.context.getColorResIdFromAttribute(R.attr.wpColorSuccess) - NEGATIVE -> change.context.getColorResIdFromAttribute(R.attr.wpColorError) - NEUTRAL -> change.context.getColorResIdFromAttribute(R.attr.wpColorOnSurfaceMedium) - } - val granularity = if (item.period != 0) itemView.getString(item.period) else "" - val periodText = when (item.state) { - POSITIVE -> String.format(itemView.getString(R.string.stats_traffic_change_higher), granularity) - NEGATIVE -> String.format(itemView.getString(R.string.stats_traffic_change_lower), granularity) - NEUTRAL -> "" - } - - change.setTextColor(AppCompatResources.getColorStateList(change.context, color)) - change.visibility = if (hasChange) View.VISIBLE else View.GONE - change.text = item.change - period.text = periodText - val params = container.layoutParams as RecyclerView.LayoutParams - val topMargin = if (item.isFirst) container.resources.getDimensionPixelSize(R.dimen.margin_medium) else 0 - params.setMargins(0, topMargin, 0, 0) - container.layoutParams = params - container.contentDescription = item.contentDescription - } -} diff --git a/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml b/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml deleted file mode 100644 index 364bb368fd64..000000000000 --- a/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml b/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml deleted file mode 100644 index 166f4ecbf43a..000000000000 --- a/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - From 37879b9efa56c0718747075ad48837355d8e9364 Mon Sep 17 00:00:00 2001 From: Ravi Date: Wed, 21 Feb 2024 16:11:44 +1100 Subject: [PATCH 33/56] Update TrafficBarChartViewHolder.kt adjust offset above columns --- .../refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt index 2d151ca5f0f2..8c10962f724b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt @@ -117,7 +117,6 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( this.description = description extraRightOffset = 8f - extraBottomOffset = 8f animateY(1000, Easing.EaseInSine) } @@ -162,7 +161,6 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( setDrawGridLines(false) setDrawLabels(true) - setAvoidFirstLastClipping(true) position = XAxis.XAxisPosition.BOTTOM valueFormatter = BarChartLabelFormatter(item.entries) textColor = ContextCompat.getColor(chart.context, R.color.neutral_30) From 23d55dfd8cad817837a70e81d551b51328a639db Mon Sep 17 00:00:00 2001 From: Ravi Date: Wed, 21 Feb 2024 16:24:27 +1100 Subject: [PATCH 34/56] Update TrafficBarChartViewHolder.kt fix labels on Year bar chart to appear on every bar, not alternate bars --- .../refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt index 8c10962f724b..3d70ec1003b9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt @@ -161,6 +161,7 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( setDrawGridLines(false) setDrawLabels(true) + labelCount = item.entries.size position = XAxis.XAxisPosition.BOTTOM valueFormatter = BarChartLabelFormatter(item.entries) textColor = ContextCompat.getColor(chart.context, R.color.neutral_30) From 82c142dd8c3b08d39300e4c8040be72b8aa9a2de Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Thu, 22 Feb 2024 01:11:27 +0300 Subject: [PATCH 35/56] Fix a crash in `LineChartLabelFormatter` --- .../android/ui/stats/refresh/utils/LineChartLabelFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt index 59d531c21c81..8989cdbf99f1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt @@ -10,7 +10,7 @@ class LineChartLabelFormatter @Inject constructor( ) : ValueFormatter() { override fun getAxisLabel(value: Float, axis: AxisBase?): String { val index = value.toInt() - return if (index < entries.size) { + return if (entries.isNotEmpty() && index < entries.size) { entries[index].label } else { "" From b158c4f137cf91bf3878921f97f141cd875137c7 Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Fri, 23 Feb 2024 02:58:15 +0300 Subject: [PATCH 36/56] Handle nullability in TrafficBarChartViewHolder --- .../refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt index 3d70ec1003b9..fa59d17f383a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt @@ -124,7 +124,7 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( private fun configureYAxis(item: BlockListItem.BarChartItem) { val minYValue = 4f - val maxYValue = item.entries.maxByOrNull { it.value }!!.value + val maxYValue = item.entries.maxByOrNull { it.value }?.value ?: 0 chart.axisLeft.apply { setDrawGridLines(false) From e0fcd204bbde00bcd36323264e67030dcef6ddeb Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Fri, 23 Feb 2024 03:28:32 +0300 Subject: [PATCH 37/56] Allow TrafficOverviewUseCase to fetch when the date is changed --- .../refresh/lists/sections/traffic/TrafficOverviewUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt index a027362bfeff..0db6eef04b72 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt @@ -52,7 +52,7 @@ class TrafficOverviewUseCase constructor( mainDispatcher, backgroundDispatcher, UiState(), - uiUpdateParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) + fetchParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { // The granularity of the chart is one level lower than the current one, probably there's a better way to do this private val trafficTabGranularity = when (statsGranularity) { From 0c1b64f0b2c2ddd3e56dcca53beefa12993997ac Mon Sep 17 00:00:00 2001 From: Irfan Omur Date: Fri, 23 Feb 2024 03:38:07 +0300 Subject: [PATCH 38/56] Update endpoint usage within `TrafficOverviewUseCase` to fill the view --- .../traffic/TrafficOverviewUseCase.kt | 166 +++++++++++++----- 1 file changed, 124 insertions(+), 42 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt index 0db6eef04b72..993315c8d8d4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt @@ -26,6 +26,7 @@ import org.wordpress.android.util.LocaleManagerWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.ResourceProvider import java.util.Calendar +import java.util.Date import javax.inject.Inject import javax.inject.Named import kotlin.math.ceil @@ -47,22 +48,22 @@ class TrafficOverviewUseCase constructor( private val localeManagerWrapper: LocaleManagerWrapper, private val resourceProvider: ResourceProvider, private val statsUtils: StatsUtils -) : BaseStatsUseCase( +) : BaseStatsUseCase( StatsStore.TimeStatsType.OVERVIEW, mainDispatcher, backgroundDispatcher, UiState(), fetchParams = listOf(UseCaseParam.SelectedDateParam(statsGranularity)) ) { - // The granularity of the chart is one level lower than the current one, probably there's a better way to do this - private val trafficTabGranularity = when (statsGranularity) { + // The granularity of the chart is one level lower than the current one + private val lowerGranularity = when (statsGranularity) { StatsGranularity.WEEKS -> StatsGranularity.DAYS StatsGranularity.MONTHS -> StatsGranularity.WEEKS StatsGranularity.YEARS -> StatsGranularity.MONTHS else -> statsGranularity } - private val itemsToLoad = when (trafficTabGranularity) { + private val barCount = when (lowerGranularity) { StatsGranularity.DAYS -> 7 StatsGranularity.WEEKS -> 5 StatsGranularity.MONTHS -> 12 @@ -79,41 +80,83 @@ class TrafficOverviewUseCase constructor( ) ) - override suspend fun loadCachedData(): VisitsAndViewsModel? { + override suspend fun loadCachedData(): TrafficOverviewUiModel? { statsWidgetUpdaters.updateViewsWidget(statsSiteProvider.siteModel.siteId) statsWidgetUpdaters.updateWeekViewsWidget(statsSiteProvider.siteModel.siteId) val cachedData = visitsAndViewsStore.getVisits( statsSiteProvider.siteModel, - trafficTabGranularity, - LimitMode.Top(itemsToLoad) + statsGranularity, + LimitMode.All ) - if (cachedData != null) { - logIfIncorrectData(cachedData, trafficTabGranularity, statsSiteProvider.siteModel, false) + cachedData?.let { + logIfIncorrectData(it, statsGranularity, statsSiteProvider.siteModel, false) selectedDateProvider.onDateLoadingSucceeded(statsGranularity) } - return cachedData + + // Get lower granularity model for chart values + val lowerGranularityCachedData = if (statsGranularity != StatsGranularity.DAYS) { + val selectedDate = getLastDate(cachedData) + selectedDate?.let { + visitsAndViewsStore.getVisits( + statsSiteProvider.siteModel, + lowerGranularity, + LimitMode.All, + it + ) + } + } else { + null + } + + return if (cachedData != null && + (statsGranularity == StatsGranularity.DAYS || lowerGranularityCachedData != null) + ) { + TrafficOverviewUiModel(cachedData, lowerGranularityCachedData) + } else { + null + } } - override suspend fun fetchRemoteData(forced: Boolean): State { - val response = visitsAndViewsStore.fetchVisits( - statsSiteProvider.siteModel, - trafficTabGranularity, - LimitMode.Top(itemsToLoad), - forced - ) + private fun getLastDate(model: VisitsAndViewsModel?): Date? { + selectedDateProvider.getSelectedDate(statsGranularity)?.let { return it } + val lastDateString = model?.dates?.lastOrNull()?.period + return lastDateString?.let { statsDateFormatter.parseStatsDate(statsGranularity, it) } + } + + override suspend fun fetchRemoteData(forced: Boolean): State { + val response = fetchVisit(statsGranularity, OVERVIEW_ITEMS_TO_LOAD, forced) val model = response.model - val error = response.error + + // Fetch lower granularity model for chart values + val lowerGranularityResponse = if (statsGranularity != StatsGranularity.DAYS) { + val selectedDate = getLastDate(model) + selectedDate?.let { fetchVisit(lowerGranularity, OVERVIEW_ITEMS_TO_LOAD, forced, it) } + } else { + null + } + val lowerGranularityModel = lowerGranularityResponse?.model + + val error = getErrorMessage(response) ?: getErrorMessage(lowerGranularityResponse) return when { error != null -> { selectedDateProvider.onDateLoadingFailed(statsGranularity) - State.Error(error.message ?: error.type.name) + State.Error(error) + } + + statsGranularity == StatsGranularity.DAYS && model != null && model.dates.isNotEmpty() -> { + selectedDateProvider.onDateLoadingSucceeded(statsGranularity) + State.Data(TrafficOverviewUiModel(model)) } - model != null && model.dates.isNotEmpty() -> { - logIfIncorrectData(model, trafficTabGranularity, statsSiteProvider.siteModel, true) + + model != null && + model.dates.isNotEmpty() && + lowerGranularityModel != null && + lowerGranularityModel.dates.isNotEmpty() -> { selectedDateProvider.onDateLoadingSucceeded(statsGranularity) - State.Data(model) + State.Data(TrafficOverviewUiModel(model, lowerGranularityModel)) } + else -> { selectedDateProvider.onDateLoadingSucceeded(statsGranularity) State.Empty() @@ -121,6 +164,36 @@ class TrafficOverviewUseCase constructor( } } + private fun getErrorMessage(response: StatsStore.OnStatsFetched?) = + response?.error?.message ?: response?.error?.type?.name + + private suspend fun fetchVisit( + granularity: StatsGranularity, + quantity: Int, + forced: Boolean, + date: Date? = null + ) = date?.let { + visitsAndViewsStore.fetchVisits( + statsSiteProvider.siteModel, + granularity, + LimitMode.Top(quantity), + date, + forced + ) + } ?: visitsAndViewsStore.fetchVisits( + statsSiteProvider.siteModel, + granularity, + LimitMode.Top(quantity), + forced + ).apply { + error?.let { return@apply } + model?.let { + if (it.dates.isNotEmpty()) { + logIfIncorrectData(it, granularity, statsSiteProvider.siteModel, true) + } + } + } + /** * Track the incorrect data shown for some users * see https://github.com/wordpress-mobile/WordPress-Android/issues/11412 @@ -154,33 +227,30 @@ class TrafficOverviewUseCase constructor( } override fun buildUiModel( - domainModel: VisitsAndViewsModel, + domainModel: TrafficOverviewUiModel, uiState: UiState ): List { val items = mutableListOf() if (domainModel.dates.isNotEmpty()) { - val dateFromProvider = selectedDateProvider.getSelectedDate(trafficTabGranularity) - val visibleBarCount = uiState.visibleBarCount ?: domainModel.dates.size + val dateFromProvider = selectedDateProvider.getSelectedDate(statsGranularity) val availableDates = domainModel.dates.map { - statsDateFormatter.parseStatsDate( - trafficTabGranularity, - it.period - ) + statsDateFormatter.parseStatsDate(statsGranularity, it.period) } val selectedDate = dateFromProvider ?: availableDates.last() val index = availableDates.indexOf(selectedDate) - selectedDateProvider.selectDate( - selectedDate, - availableDates.takeLast(visibleBarCount), - trafficTabGranularity - ) + selectedDateProvider.selectDate(selectedDate, availableDates, statsGranularity) val selectedItem = domainModel.dates.getOrNull(index) ?: domainModel.dates.last() if (statsGranularity == StatsGranularity.DAYS) { - buildTodayCard(selectedItem,items) + buildTodayCard(selectedItem, items) } else { - buildGranularChart(domainModel, uiState, items, selectedItem) + buildGranularChart( + domainModel.lowerGranularityDates.takeLast(barCount), + uiState, + items, + selectedItem + ) } } else { selectedDateProvider.onDateLoadingFailed(statsGranularity) @@ -227,15 +297,15 @@ class TrafficOverviewUseCase constructor( } private fun buildGranularChart( - domainModel: VisitsAndViewsModel, + dates: List, uiState: UiState, items: MutableList, selectedItem: VisitsAndViewsModel.PeriodData ) { items.addAll( trafficOverviewMapper.buildChart( - domainModel.dates, - trafficTabGranularity, + dates, + lowerGranularity, this::onBarSelected, this::onBarChartDrawn, uiState.selectedPosition, @@ -254,13 +324,13 @@ class TrafficOverviewUseCase constructor( private fun onBarSelected(period: String?) { analyticsTracker.trackGranular( AnalyticsTracker.Stat.STATS_OVERVIEW_BAR_CHART_TAPPED, - trafficTabGranularity + lowerGranularity ) if (period != null && period != "empty") { val selectedDate = statsDateFormatter.parseStatsDate(statsGranularity, period) selectedDateProvider.selectDate( selectedDate, - trafficTabGranularity + lowerGranularity ) } } @@ -268,7 +338,7 @@ class TrafficOverviewUseCase constructor( private fun onColumnSelected(position: Int) { analyticsTracker.trackGranular( AnalyticsTracker.Stat.STATS_OVERVIEW_TYPE_TAPPED, - trafficTabGranularity + lowerGranularity ) updateUiState { it.copy(selectedPosition = position) } } @@ -279,6 +349,18 @@ class TrafficOverviewUseCase constructor( data class UiState(val selectedPosition: Int = 0, val visibleBarCount: Int? = null) + data class TrafficOverviewUiModel( + val period: String, + val dates: List, + val lowerGranularityDates: List + ) { + constructor(model: VisitsAndViewsModel, lowerGranularityModel: VisitsAndViewsModel? = null) : this( + model.period, + model.dates, + lowerGranularityModel?.dates ?: listOf() + ) + } + class TrafficOverviewUseCaseFactory @Inject constructor( @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, From a6636bac858bac1f586410b83abc14b0e2f5dfa4 Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 23 Feb 2024 13:21:49 +1100 Subject: [PATCH 39/56] Update LineChartLabelFormatter.kt refactor to use range --- .../android/ui/stats/refresh/utils/LineChartLabelFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt index 8989cdbf99f1..15952634d724 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt @@ -10,7 +10,7 @@ class LineChartLabelFormatter @Inject constructor( ) : ValueFormatter() { override fun getAxisLabel(value: Float, axis: AxisBase?): String { val index = value.toInt() - return if (entries.isNotEmpty() && index < entries.size) { + return if (index in 1..entries.size) { entries[index].label } else { "" From 206044184f73e82a40c04fff2aa93a98315ae8dc Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 23 Feb 2024 13:21:58 +1100 Subject: [PATCH 40/56] Update BarChartLabelFormatter.kt refactor to use range --- .../android/ui/stats/refresh/utils/BarChartLabelFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt index 64363463be33..3b5f8b5a2806 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt @@ -10,7 +10,7 @@ class BarChartLabelFormatter @Inject constructor( ) : ValueFormatter() { override fun getAxisLabel(value: Float, axis: AxisBase?): String { val index = value.toInt() - return if (index < entries.size) { + return if (index in 1..entries.size) { entries[index].label } else { "" From df3d7b243fd65370b34fd42a1ada336e386ec91b Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 23 Feb 2024 13:29:10 +1100 Subject: [PATCH 41/56] Update TrafficBarChartViewHolder.kt refactor function block to expression body --- .../traffic/TrafficBarChartViewHolder.kt | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt index fa59d17f383a..668f8c50a23a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficBarChartViewHolder.kt @@ -208,13 +208,11 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( private fun takeEntriesWithinGraphWidth( count: Int, entries: List - ): List { - return if (count < entries.size) entries.subList( - entries.size - count, - entries.size - ) else { - entries - } + ): List = if (count < entries.size) entries.subList( + entries.size - count, + entries.size + ) else { + entries } private fun BarChart.resetChart() { @@ -226,13 +224,11 @@ class TrafficBarChartViewHolder(parent: ViewGroup) : BlockListItemViewHolder( invalidate() } - private fun toBarEntry(bar: Bar, index: Int): BarEntry { - return BarEntry( - index.toFloat(), - bar.value.toFloat(), - bar.id - ) - } + private fun toBarEntry(bar: Bar, index: Int): BarEntry = BarEntry( + index.toFloat(), + bar.value.toFloat(), + bar.id + ) @Suppress("ReturnCount") private fun roundUp(input: Float): Float { From cd4a73663f27d0668df3955c4a1544871c7a8b7f Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 23 Feb 2024 13:46:17 +1100 Subject: [PATCH 42/56] Update BarChartLabelFormatter.kt update range to start at 0 --- .../android/ui/stats/refresh/utils/BarChartLabelFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt index 3b5f8b5a2806..66cca26e0e1a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/BarChartLabelFormatter.kt @@ -10,7 +10,7 @@ class BarChartLabelFormatter @Inject constructor( ) : ValueFormatter() { override fun getAxisLabel(value: Float, axis: AxisBase?): String { val index = value.toInt() - return if (index in 1..entries.size) { + return if (index in 0..entries.size) { entries[index].label } else { "" From a97f0723ca626ae07f22e101351f54ec628a91f1 Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 23 Feb 2024 13:46:31 +1100 Subject: [PATCH 43/56] Update LineChartLabelFormatter.kt update range to start at 0 --- .../android/ui/stats/refresh/utils/LineChartLabelFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt index 15952634d724..64381603e44c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/utils/LineChartLabelFormatter.kt @@ -10,7 +10,7 @@ class LineChartLabelFormatter @Inject constructor( ) : ValueFormatter() { override fun getAxisLabel(value: Float, axis: AxisBase?): String { val index = value.toInt() - return if (index in 1..entries.size) { + return if (index in 0..entries.size) { entries[index].label } else { "" From 4e7d7c7cc56b435dec6dd0239192086ff69a9ff7 Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 23 Feb 2024 13:47:03 +1100 Subject: [PATCH 44/56] Update TrafficOverviewUseCase.kt remove redundant logging --- .../traffic/TrafficOverviewUseCase.kt | 54 ++----------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt index 993315c8d8d4..11a965ba8c33 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficOverviewUseCase.kt @@ -3,7 +3,6 @@ package org.wordpress.android.ui.stats.refresh.lists.sections.traffic import kotlinx.coroutines.CoroutineDispatcher import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker -import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.stats.LimitMode import org.wordpress.android.fluxc.model.stats.time.VisitsAndViewsModel import org.wordpress.android.fluxc.network.utils.StatsGranularity @@ -22,19 +21,16 @@ import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider import org.wordpress.android.ui.stats.refresh.utils.StatsUtils import org.wordpress.android.ui.stats.refresh.utils.trackGranular import org.wordpress.android.util.AppLog -import org.wordpress.android.util.LocaleManagerWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.ResourceProvider -import java.util.Calendar import java.util.Date import javax.inject.Inject import javax.inject.Named -import kotlin.math.ceil const val OVERVIEW_ITEMS_TO_LOAD = 15 -@Suppress("LongParameterList", "MagicNumber") -class TrafficOverviewUseCase constructor( +@Suppress("LongParameterList") +class TrafficOverviewUseCase( private val statsGranularity: StatsGranularity, private val visitsAndViewsStore: VisitsAndViewsStore, private val selectedDateProvider: SelectedDateProvider, @@ -45,7 +41,6 @@ class TrafficOverviewUseCase constructor( @Named(BG_THREAD) private val backgroundDispatcher: CoroutineDispatcher, private val analyticsTracker: AnalyticsTrackerWrapper, private val statsWidgetUpdaters: WidgetUpdater.StatsWidgetUpdaters, - private val localeManagerWrapper: LocaleManagerWrapper, private val resourceProvider: ResourceProvider, private val statsUtils: StatsUtils ) : BaseStatsUseCase( @@ -88,10 +83,6 @@ class TrafficOverviewUseCase constructor( statsGranularity, LimitMode.All ) - cachedData?.let { - logIfIncorrectData(it, statsGranularity, statsSiteProvider.siteModel, false) - selectedDateProvider.onDateLoadingSucceeded(statsGranularity) - } // Get lower granularity model for chart values val lowerGranularityCachedData = if (statsGranularity != StatsGranularity.DAYS) { @@ -100,7 +91,7 @@ class TrafficOverviewUseCase constructor( visitsAndViewsStore.getVisits( statsSiteProvider.siteModel, lowerGranularity, - LimitMode.All, + LimitMode.Top(OVERVIEW_ITEMS_TO_LOAD), it ) } @@ -187,43 +178,6 @@ class TrafficOverviewUseCase constructor( forced ).apply { error?.let { return@apply } - model?.let { - if (it.dates.isNotEmpty()) { - logIfIncorrectData(it, granularity, statsSiteProvider.siteModel, true) - } - } - } - - /** - * Track the incorrect data shown for some users - * see https://github.com/wordpress-mobile/WordPress-Android/issues/11412 - */ - private fun logIfIncorrectData( - model: VisitsAndViewsModel, - granularity: StatsGranularity, - site: SiteModel, - fetched: Boolean - ) { - model.dates.lastOrNull()?.let { lastDayData -> - val yesterday = localeManagerWrapper.getCurrentCalendar() - yesterday.add(Calendar.DAY_OF_YEAR, -1) - val lastDayDate = statsDateFormatter.parseStatsDate(granularity, lastDayData.period) - if (lastDayDate.before(yesterday.time)) { - val currentCalendar = localeManagerWrapper.getCurrentCalendar() - val lastItemAge = ceil((currentCalendar.timeInMillis - lastDayDate.time) / 86400000.0) - analyticsTracker.track( - AnalyticsTracker.Stat.STATS_OVERVIEW_ERROR, - mapOf( - "stats_last_date" to statsDateFormatter.printStatsDate(lastDayDate), - "stats_current_date" to statsDateFormatter.printStatsDate(currentCalendar.time), - "stats_age_in_days" to lastItemAge.toInt(), - "is_jetpack_connected" to site.isJetpackConnected, - "is_atomic" to site.isWPComAtomic, - "action_source" to if (fetched) "remote" else "cached" - ) - ) - } - } } override fun buildUiModel( @@ -372,7 +326,6 @@ class TrafficOverviewUseCase constructor( private val visitsAndViewsStore: VisitsAndViewsStore, private val analyticsTracker: AnalyticsTrackerWrapper, private val statsWidgetUpdaters: WidgetUpdater.StatsWidgetUpdaters, - private val localeManagerWrapper: LocaleManagerWrapper, private val resourceProvider: ResourceProvider, private val statsUtils: StatsUtils ) : GranularUseCaseFactory { @@ -388,7 +341,6 @@ class TrafficOverviewUseCase constructor( backgroundDispatcher, analyticsTracker, statsWidgetUpdaters, - localeManagerWrapper, resourceProvider, statsUtils ) From bbc81c28d9f034123f79784f12dac816daff1c3e Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 23 Feb 2024 15:02:53 +1100 Subject: [PATCH 45/56] Revert "Delete unused" This reverts commit f0b05c29d75844df8b02e887a304ccdce9583864. --- .../traffic/TrafficFourColumnsViewHolder.kt | 56 ++++++++++++++++ .../traffic/TrafficValueViewHolder.kt | 53 +++++++++++++++ .../stats_block_traffic_four_columns_item.xml | 49 ++++++++++++++ .../layout/stats_block_traffic_value_item.xml | 64 +++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt create mode 100644 WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml create mode 100644 WordPress/src/main/res/layout/stats_block_traffic_value_item.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt new file mode 100644 index 000000000000..8e650e541f97 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficFourColumnsViewHolder.kt @@ -0,0 +1,56 @@ +package org.wordpress.android.ui.stats.refresh.lists.sections.traffic + +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import org.wordpress.android.R +import org.wordpress.android.ui.stats.refresh.BlockDiffCallback +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem +import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder + +class TrafficFourColumnsViewHolder(parent: ViewGroup) : BlockListItemViewHolder( + parent, + R.layout.stats_block_traffic_four_columns_item +) { + private val columnLayouts = listOf( + itemView.findViewById(R.id.column1), + itemView.findViewById(R.id.column2), + itemView.findViewById(R.id.column3), + itemView.findViewById(R.id.column4) + ) + + fun bind( + item: BlockListItem.Columns, + payloads: List + ) { + val tabSelected = payloads.contains(BlockDiffCallback.BlockListPayload.SELECTED_COLUMN_CHANGED) + when { + tabSelected -> { + columnLayouts.forEachIndexed { index, layout -> + layout.setSelection(item.selectedColumn == index) + } + } + else -> { + columnLayouts.forEachIndexed { index, layout -> + layout.setOnClickListener { + it.announceForAccessibility(it.resources.getString(R.string.stats_graph_updated)) + item.onColumnSelected?.invoke(index) + } + val currentColumn = item.columns[index] + layout.key().setText(currentColumn.header) + layout.setSelection(item.selectedColumn == null || item.selectedColumn == index) + layout.contentDescription = currentColumn.contentDescription + } + } + } + } + + private fun LinearLayout.setSelection(isSelected: Boolean) { + key().isSelected = isSelected + selector().visibility = if (isSelected) View.VISIBLE else View.GONE + } + + private fun LinearLayout.key(): TextView = this.findViewById(R.id.key) + private fun LinearLayout.selector(): View = this.findViewById(R.id.selector) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt new file mode 100644 index 000000000000..93c7c3c2ae0b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/traffic/TrafficValueViewHolder.kt @@ -0,0 +1,53 @@ +package org.wordpress.android.ui.stats.refresh.lists.sections.traffic + +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.RecyclerView +import org.wordpress.android.R +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.NEGATIVE +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.NEUTRAL +import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.ValueItem.State.POSITIVE +import org.wordpress.android.ui.stats.refresh.lists.sections.viewholders.BlockListItemViewHolder +import org.wordpress.android.util.extensions.getColorResIdFromAttribute +import org.wordpress.android.util.extensions.getString + +class TrafficValueViewHolder(parent: ViewGroup) : BlockListItemViewHolder( + parent, + R.layout.stats_block_traffic_value_item +) { + private val container = itemView.findViewById(R.id.value_container) + private val value = itemView.findViewById(R.id.value) + private val unit = itemView.findViewById(R.id.unit) + private val change = itemView.findViewById(R.id.change) + private val period = itemView.findViewById(R.id.period) + fun bind(item: BlockListItem.ValueItem) { + value.text = item.value + unit.setText(item.unit) + val hasChange = item.change != null + val color = when (item.state) { + POSITIVE -> change.context.getColorResIdFromAttribute(R.attr.wpColorSuccess) + NEGATIVE -> change.context.getColorResIdFromAttribute(R.attr.wpColorError) + NEUTRAL -> change.context.getColorResIdFromAttribute(R.attr.wpColorOnSurfaceMedium) + } + val granularity = if (item.period != 0) itemView.getString(item.period) else "" + val periodText = when (item.state) { + POSITIVE -> String.format(itemView.getString(R.string.stats_traffic_change_higher), granularity) + NEGATIVE -> String.format(itemView.getString(R.string.stats_traffic_change_lower), granularity) + NEUTRAL -> "" + } + + change.setTextColor(AppCompatResources.getColorStateList(change.context, color)) + change.visibility = if (hasChange) View.VISIBLE else View.GONE + change.text = item.change + period.text = periodText + val params = container.layoutParams as RecyclerView.LayoutParams + val topMargin = if (item.isFirst) container.resources.getDimensionPixelSize(R.dimen.margin_medium) else 0 + params.setMargins(0, topMargin, 0, 0) + container.layoutParams = params + container.contentDescription = item.contentDescription + } +} diff --git a/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml b/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml new file mode 100644 index 000000000000..364bb368fd64 --- /dev/null +++ b/WordPress/src/main/res/layout/stats_block_traffic_four_columns_item.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml b/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml new file mode 100644 index 000000000000..166f4ecbf43a --- /dev/null +++ b/WordPress/src/main/res/layout/stats_block_traffic_value_item.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + From e95a1185172a645f8220c6735fe5208053e1bf9e Mon Sep 17 00:00:00 2001 From: Ravi Date: Fri, 23 Feb 2024 16:31:58 +1100 Subject: [PATCH 46/56] Update stats_styles.xml --- WordPress/src/main/res/values/stats_styles.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WordPress/src/main/res/values/stats_styles.xml b/WordPress/src/main/res/values/stats_styles.xml index b7943b50bb21..492eb9b99174 100644 --- a/WordPress/src/main/res/values/stats_styles.xml +++ b/WordPress/src/main/res/values/stats_styles.xml @@ -76,6 +76,10 @@ @color/stats_block_column + +