Skip to content
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.wordpress.android.ui.stats.refresh.lists.widget.alltime

import android.content.Context
import kotlinx.coroutines.runBlocking
import org.wordpress.android.R
import org.wordpress.android.fluxc.model.stats.InsightsAllTimeModel
import org.wordpress.android.fluxc.store.SiteStore
Expand All @@ -10,6 +9,7 @@ import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetBlockListProvider.BlockItemUiModel
import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetBlockListProvider.WidgetBlockListViewModel
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color
import org.wordpress.android.ui.stats.refresh.lists.widget.utils.runBlockingForWidget
import org.wordpress.android.ui.stats.refresh.utils.MILLION
import org.wordpress.android.ui.stats.refresh.utils.StatsUtils
import org.wordpress.android.viewmodel.ResourceProvider
Expand Down Expand Up @@ -39,7 +39,7 @@ class AllTimeWidgetBlockListViewModel
siteId?.apply {
val site = siteStore.getSiteByLocalId(this)
if (site != null) {
runBlocking {
runBlockingForWidget {
allTimeStore.fetchAllTimeInsights(site)
}
allTimeStore.getAllTimeInsights(site)?.let { visitsAndViewsModel ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.wordpress.android.ui.stats.refresh.lists.widget.alltime

import androidx.annotation.LayoutRes
import kotlinx.coroutines.runBlocking
import org.wordpress.android.R
import org.wordpress.android.fluxc.model.stats.InsightsAllTimeModel
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.fluxc.store.stats.insights.AllTimeInsightsStore
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color
import org.wordpress.android.ui.stats.refresh.lists.widget.utils.runBlockingForWidget
import org.wordpress.android.ui.stats.refresh.utils.ONE_THOUSAND
import org.wordpress.android.ui.stats.refresh.utils.StatsUtils
import org.wordpress.android.viewmodel.ResourceProvider
Expand Down Expand Up @@ -36,7 +36,7 @@ class AllTimeWidgetListViewModel
siteId?.apply {
val site = siteStore.getSiteByLocalId(this)
if (site != null) {
runBlocking {
runBlockingForWidget {
allTimeStore.fetchAllTimeInsights(site)
}
allTimeStore.getAllTimeInsights(site)?.let { visitsAndViewsModel ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.wordpress.android.ui.stats.refresh.lists.widget.today

import android.content.Context
import kotlinx.coroutines.runBlocking
import org.wordpress.android.R
import org.wordpress.android.fluxc.model.stats.VisitsModel
import org.wordpress.android.fluxc.store.SiteStore
Expand All @@ -11,6 +10,7 @@ import org.wordpress.android.ui.stats.StatsTimeframe
import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetBlockListProvider.BlockItemUiModel
import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetBlockListProvider.WidgetBlockListViewModel
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color
import org.wordpress.android.ui.stats.refresh.lists.widget.utils.runBlockingForWidget
import org.wordpress.android.ui.stats.refresh.utils.MILLION
import org.wordpress.android.ui.stats.refresh.utils.StatsUtils
import org.wordpress.android.util.config.StatsTrafficSubscribersTabsFeatureConfig
Expand Down Expand Up @@ -46,7 +46,7 @@ class TodayWidgetBlockListViewModel
siteId?.apply {
val site = siteStore.getSiteByLocalId(this)
if (site != null) {
runBlocking {
runBlockingForWidget {
todayInsightsStore.fetchTodayInsights(site)
}
todayInsightsStore.getTodayInsights(site)?.let { visitsAndViewsModel ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.wordpress.android.ui.stats.refresh.lists.widget.today

import androidx.annotation.LayoutRes
import kotlinx.coroutines.runBlocking
import org.wordpress.android.R
import org.wordpress.android.fluxc.model.stats.VisitsModel
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.fluxc.store.stats.insights.TodayInsightsStore
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color
import org.wordpress.android.ui.stats.refresh.lists.widget.utils.runBlockingForWidget
import org.wordpress.android.ui.stats.refresh.utils.ONE_THOUSAND
import org.wordpress.android.ui.stats.refresh.utils.StatsUtils
import org.wordpress.android.viewmodel.ResourceProvider
Expand Down Expand Up @@ -36,7 +36,7 @@ class TodayWidgetListViewModel
siteId?.let { nonNullSiteId ->
val site = siteStore.getSiteByLocalId(nonNullSiteId)
if (site != null) {
runBlocking {
runBlockingForWidget {
todayInsightsStore.fetchTodayInsights(site)
}
todayInsightsStore.getTodayInsights(site)?.let { visitsAndViewsModel ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.wordpress.android.ui.stats.refresh.lists.widget.utils

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.runBlocking
import org.wordpress.android.util.AppLog

/**
* Runs [block] via [runBlocking] from inside a `RemoteViewsService.onDataSetChanged()`
* call on a stats widget. Swallows [InterruptedException] / [CancellationException]
* thrown when the widget host kills the service mid-fetch — the VM can still fall
* through to its cached read and render whatever data is already on disk.
*
* Only safe because this is a root [runBlocking] on the widget worker thread with no
* parent [kotlinx.coroutines.Job] above it — there is nothing upstream that needs to
* observe the cancellation. Do NOT call this from inside an existing coroutine: there
* the swallow would hide a cancellation signal from a surviving parent.
*/
internal fun runBlockingForWidget(block: suspend () -> Unit) {
try {
runBlocking { block() }
} catch (_: InterruptedException) {
AppLog.w(AppLog.T.STATS, "Widget data fetch interrupted")
Thread.currentThread().interrupt()
} catch (_: CancellationException) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm I'm not sure that swallowing CancellationException is a good idea because then the cancel propagation is stopped and upper jobs keep doing their thing.
We should limit calling this function to very specific scenarios where we are sure it has no side effects. (Which I think is the case, but still I'm concerned)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that swallowing CancellationException is a good idea because then the cancel propagation is stopped and upper jobs keep doing their thing.

We could get rid of this and just catch the InterruptedException, since that's the main source of the crashes. WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering what would happen then when throwing the thread interrupt. But I guess it could work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked for Claude's input on this discussion:

Good instinct to flag this — but I think it's safe in this specific spot, and worth me spelling out why in the KDoc so the next reader doesn't have to re-derive it.

The "swallowing CancellationException stops propagation and upper jobs keep running" failure mode applies when there's a parent Job above. Here, runBlocking creates a brand-new root coroutine on the RemoteViewsService worker thread — there's nothing upstream to notify. The CancellationException we see is what runBlocking re-throws when the worker thread is interrupted mid-fetch by the widget host; it's terminal for the local scope, not a signal we're hiding from a surviving caller.

The real risk is someone reusing this helper from inside an existing coroutine, where the concern would apply. I'll tighten the KDoc to call that out explicitly ("only safe because this is a root runBlocking — do not call from inside another coroutine").

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable.

AppLog.w(AppLog.T.STATS, "Widget data fetch cancelled")
Thread.currentThread().interrupt()
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.wordpress.android.ui.stats.refresh.lists.widget.views

import androidx.annotation.LayoutRes
import kotlinx.coroutines.runBlocking
import org.wordpress.android.R
import org.wordpress.android.fluxc.model.stats.LimitMode
import org.wordpress.android.fluxc.model.stats.LimitMode.Top
Expand All @@ -16,6 +15,7 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Value
import org.wordpress.android.ui.stats.refresh.lists.sections.granular.usecases.OVERVIEW_ITEMS_TO_LOAD
import org.wordpress.android.ui.stats.refresh.lists.sections.granular.usecases.OverviewMapper
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color
import org.wordpress.android.ui.stats.refresh.lists.widget.utils.runBlockingForWidget
import org.wordpress.android.ui.stats.refresh.utils.MILLION
import org.wordpress.android.ui.stats.refresh.utils.ONE_THOUSAND
import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter
Expand Down Expand Up @@ -50,7 +50,7 @@ class ViewsWidgetListViewModel
siteId?.apply {
val site = siteStore.getSiteByLocalId(this)
if (site != null) {
runBlocking {
runBlockingForWidget {
visitsAndViewsStore.fetchVisits(site, DAYS, Top(OVERVIEW_ITEMS_TO_LOAD))
}
val visitsAndViewsModel = visitsAndViewsStore.getVisits(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.wordpress.android.ui.stats.refresh.lists.widget.weeks

import androidx.annotation.LayoutRes
import kotlinx.coroutines.runBlocking
import org.wordpress.android.R
import org.wordpress.android.fluxc.model.stats.LimitMode
import org.wordpress.android.fluxc.model.stats.time.VisitsAndViewsModel
Expand All @@ -10,6 +9,7 @@ import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.fluxc.store.stats.time.VisitsAndViewsStore
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color
import org.wordpress.android.ui.stats.refresh.lists.widget.utils.runBlockingForWidget
import org.wordpress.android.ui.stats.refresh.utils.ONE_THOUSAND
import org.wordpress.android.ui.stats.refresh.utils.StatsUtils
import org.wordpress.android.viewmodel.ResourceProvider
Expand Down Expand Up @@ -38,7 +38,7 @@ class WeekViewsWidgetListViewModel @Inject constructor(
siteId?.let { nonNullSiteId ->
val site = siteStore.getSiteByLocalId(nonNullSiteId)
if (site != null) {
runBlocking {
runBlockingForWidget {
visitsAndViewsStore.fetchVisits(site, WEEKS, LimitMode.Top(1))
}
visitsAndViewsStore.getVisits(site, WEEKS, LimitMode.All)?.let { visitsAndViewsModel ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.wordpress.android.ui.stats.refresh.lists.widget.weeks

import android.content.Context
import kotlinx.coroutines.runBlocking
import org.wordpress.android.R
import org.wordpress.android.fluxc.model.stats.LimitMode
import org.wordpress.android.fluxc.model.stats.time.VisitsAndViewsModel
Expand All @@ -12,6 +11,7 @@ import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetBlockListProvider.BlockItemUiModel
import org.wordpress.android.ui.stats.refresh.lists.widget.WidgetBlockListProvider.WidgetBlockListViewModel
import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color
import org.wordpress.android.ui.stats.refresh.lists.widget.utils.runBlockingForWidget
import org.wordpress.android.ui.stats.refresh.utils.MILLION
import org.wordpress.android.ui.stats.refresh.utils.StatsUtils
import org.wordpress.android.viewmodel.ResourceProvider
Expand Down Expand Up @@ -46,7 +46,7 @@ class WeekWidgetBlockListViewModel
siteId?.let { nonNullSiteId ->
val site = siteStore.getSiteByLocalId(nonNullSiteId)
if (site != null) {
runBlocking {
runBlockingForWidget {
visitsAndViewsStore.fetchVisits(site, WEEKS, LimitMode.Top(1))
}
visitsAndViewsStore.getVisits(site, WEEKS, LimitMode.All)?.let { visitsAndViewsModel ->
Expand Down
Loading