From 045087072facc281852b525f5e70083dbbdbc880 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 12 May 2026 10:43:40 -0400 Subject: [PATCH 1/5] Fix Sentry: swallow InterruptedException in stats widget VMs The stats `RemoteViewsService.onDataSetChanged()` path uses `runBlocking` to fetch fresh data on the widget host thread. When the host kills the service mid-fetch the coroutine is cancelled and the resulting `InterruptedException` propagates uncaught, crashing the app. Add a shared `runBlockingForWidget(block)` helper in `widget/utils/` that wraps `runBlocking` and swallows `InterruptedException` / `CancellationException` so the VM can still fall through to its cached data read. Update every widget VM to use the helper. VMs affected: - ViewsWidgetListViewModel (Sentry JETPACK-ANDROID-1ATH) - TodayWidgetBlockListViewModel (Sentry JETPACK-ANDROID-1AV2) - WeekWidgetBlockListViewModel (Sentry JETPACK-ANDROID-1AZW) - TodayWidgetListViewModel (Sentry JETPACK-ANDROID-1AZQ) - AllTimeWidgetBlockListViewModel (Sentry JETPACK-ANDROID-1AWZ) - AllTimeWidgetListViewModel (Sentry JETPACK-ANDROID-1B0V) - WeekViewsWidgetListViewModel (Sentry JETPACK-ANDROID-1C2Y) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../AllTimeWidgetBlockListViewModel.kt | 4 +-- .../alltime/AllTimeWidgetListViewModel.kt | 4 +-- .../today/TodayWidgetBlockListViewModel.kt | 4 +-- .../widget/today/TodayWidgetListViewModel.kt | 4 +-- .../widget/utils/WidgetCoroutineHelper.kt | 25 +++++++++++++++++++ .../widget/views/ViewsWidgetListViewModel.kt | 4 +-- .../weeks/WeekViewsWidgetListViewModel.kt | 4 +-- .../weeks/WeekWidgetBlockListViewModel.kt | 4 +-- 8 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/AllTimeWidgetBlockListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/AllTimeWidgetBlockListViewModel.kt index 131116add589..ac6c71ca7b69 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/AllTimeWidgetBlockListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/AllTimeWidgetBlockListViewModel.kt @@ -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 @@ -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 @@ -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 -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/AllTimeWidgetListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/AllTimeWidgetListViewModel.kt index 0c26465db1bf..0e1f83265dd3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/AllTimeWidgetListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/AllTimeWidgetListViewModel.kt @@ -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 @@ -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 -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/TodayWidgetBlockListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/TodayWidgetBlockListViewModel.kt index b811c79a9c1a..578aeca13f2a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/TodayWidgetBlockListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/TodayWidgetBlockListViewModel.kt @@ -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 @@ -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 @@ -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 -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/TodayWidgetListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/TodayWidgetListViewModel.kt index 9b05b9fad60f..6adca39d4131 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/TodayWidgetListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/TodayWidgetListViewModel.kt @@ -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 @@ -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 -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt new file mode 100644 index 000000000000..bbb34dcfe842 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt @@ -0,0 +1,25 @@ +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. + * + * Sentry: JETPACK-ANDROID-1ATH (and the same shape across all stats widget VMs: + * 1AV2, 1AZW, 1AZQ, 1AWZ, 1B0V, 1C2Y). + */ +@Suppress("TooGenericExceptionCaught") +internal fun runBlockingForWidget(block: suspend () -> Unit) { + try { + runBlocking { block() } + } catch (e: InterruptedException) { + AppLog.w(AppLog.T.STATS, "Widget data fetch interrupted: ${e.message}") + } catch (e: CancellationException) { + AppLog.w(AppLog.T.STATS, "Widget data fetch cancelled: ${e.message}") + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/ViewsWidgetListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/ViewsWidgetListViewModel.kt index 91aa5dbf2d35..b2ebf83dc17e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/ViewsWidgetListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/ViewsWidgetListViewModel.kt @@ -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 @@ -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 @@ -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( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/WeekViewsWidgetListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/WeekViewsWidgetListViewModel.kt index 121c74fec8b2..b2714a8e5453 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/WeekViewsWidgetListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/WeekViewsWidgetListViewModel.kt @@ -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 @@ -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 @@ -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 -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/WeekWidgetBlockListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/WeekWidgetBlockListViewModel.kt index 30b198f7dda4..9bd5b7b77ded 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/WeekWidgetBlockListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/WeekWidgetBlockListViewModel.kt @@ -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 @@ -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 @@ -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 -> From 3158ddf2fd12ea163a6564a93b6f69eb995a64d4 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 13 May 2026 07:26:30 -0400 Subject: [PATCH 2/5] Restore interrupt flag, drop unused detekt suppression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the thread interrupt flag after swallowing InterruptedException in runBlockingForWidget, per JVM convention. Remove @Suppress("TooGenericExceptionCaught") — the rule doesn't fire on these specific exception types. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt index bbb34dcfe842..49a26385f6d4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt @@ -13,12 +13,12 @@ import org.wordpress.android.util.AppLog * Sentry: JETPACK-ANDROID-1ATH (and the same shape across all stats widget VMs: * 1AV2, 1AZW, 1AZQ, 1AWZ, 1B0V, 1C2Y). */ -@Suppress("TooGenericExceptionCaught") internal fun runBlockingForWidget(block: suspend () -> Unit) { try { runBlocking { block() } } catch (e: InterruptedException) { AppLog.w(AppLog.T.STATS, "Widget data fetch interrupted: ${e.message}") + Thread.currentThread().interrupt() } catch (e: CancellationException) { AppLog.w(AppLog.T.STATS, "Widget data fetch cancelled: ${e.message}") } From 6851c1776e54bac6d842af9bcce80d5d7937628d Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 13 May 2026 07:32:06 -0400 Subject: [PATCH 3/5] Updated comment --- .../stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt index 49a26385f6d4..71d23f4e34ce 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt @@ -9,9 +9,6 @@ import org.wordpress.android.util.AppLog * 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. - * - * Sentry: JETPACK-ANDROID-1ATH (and the same shape across all stats widget VMs: - * 1AV2, 1AZW, 1AZQ, 1AWZ, 1B0V, 1C2Y). */ internal fun runBlockingForWidget(block: suspend () -> Unit) { try { From 684d1d4395364ff71708f526122b5e95417f198f Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 13 May 2026 07:51:36 -0400 Subject: [PATCH 4/5] Drop noisy exception message interpolation from widget log lines Co-Authored-By: Claude Opus 4.7 (1M context) --- .../refresh/lists/widget/utils/WidgetCoroutineHelper.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt index 71d23f4e34ce..a4973ddee066 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt @@ -13,10 +13,10 @@ import org.wordpress.android.util.AppLog internal fun runBlockingForWidget(block: suspend () -> Unit) { try { runBlocking { block() } - } catch (e: InterruptedException) { - AppLog.w(AppLog.T.STATS, "Widget data fetch interrupted: ${e.message}") + } catch (_: InterruptedException) { + AppLog.w(AppLog.T.STATS, "Widget data fetch interrupted") Thread.currentThread().interrupt() - } catch (e: CancellationException) { - AppLog.w(AppLog.T.STATS, "Widget data fetch cancelled: ${e.message}") + } catch (_: CancellationException) { + AppLog.w(AppLog.T.STATS, "Widget data fetch cancelled") } } From 013610475163b40a393e13500087a8c8877fba9f Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 14 May 2026 09:39:42 -0400 Subject: [PATCH 5/5] Clarify widget runBlocking helper scope and restore interrupt flag on cancel Co-Authored-By: Claude Opus 4.7 (1M context) --- .../refresh/lists/widget/utils/WidgetCoroutineHelper.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt index a4973ddee066..84cb0c22c46f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetCoroutineHelper.kt @@ -9,6 +9,11 @@ import org.wordpress.android.util.AppLog * 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 { @@ -18,5 +23,6 @@ internal fun runBlockingForWidget(block: suspend () -> Unit) { Thread.currentThread().interrupt() } catch (_: CancellationException) { AppLog.w(AppLog.T.STATS, "Widget data fetch cancelled") + Thread.currentThread().interrupt() } }