From 14c5d3fe7d79eb0fa94cb6e902f0b831c72f6a2a Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sat, 20 Dec 2025 17:41:52 -0600 Subject: [PATCH] Fix widget race conditions --- .../java/org/tasks/widget/AppWidgetManager.kt | 37 ++++++++++--------- .../main/java/org/tasks/widget/TasksWidget.kt | 13 +++---- .../org/tasks/widget/TasksWidgetAdapter.kt | 4 +- .../tasks/widget/TasksWidgetViewFactory.kt | 5 +++ 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/tasks/widget/AppWidgetManager.kt b/app/src/main/java/org/tasks/widget/AppWidgetManager.kt index 3fb2bcfca..6b20f105f 100644 --- a/app/src/main/java/org/tasks/widget/AppWidgetManager.kt +++ b/app/src/main/java/org/tasks/widget/AppWidgetManager.kt @@ -6,34 +6,41 @@ import android.content.Context import android.content.Intent import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.tasks.R import org.tasks.compose.throttleLatest import org.tasks.injection.ApplicationScope import timber.log.Timber +import java.util.concurrent.atomic.AtomicLong import javax.inject.Inject +import javax.inject.Singleton +@Singleton class AppWidgetManager @Inject constructor( @param:ApplicationContext private val context: Context, @ApplicationScope private val scope: CoroutineScope, ) { private val appWidgetManager: AppWidgetManager? = AppWidgetManager.getInstance(context) - private val updateChannel = Channel(Channel.CONFLATED) + private val _generation = AtomicLong(0) + val generation: Long get() = _generation.get() + + private val updateChannel = Channel(Channel.CONFLATED) init { updateChannel .consumeAsFlow() .throttleLatest(1000) - .onEach { - val appWidgetIds = widgetIds - Timber.d("updateWidgets: ${appWidgetIds.joinToString { it.toString() }}") - notifyAppWidgetViewDataChanged(appWidgetIds) + .onEach { gen -> + if (gen == _generation.get()) { + val appWidgetIds = widgetIds + Timber.d("updateWidgets: ${appWidgetIds.joinToString { it.toString() }}") + appWidgetManager?.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.list_view) + } else { + Timber.d("Skipping stale widget update") + } } .launchIn(scope) } @@ -43,25 +50,19 @@ class AppWidgetManager @Inject constructor( ?.getAppWidgetIds(ComponentName(context, TasksWidget::class.java)) ?: intArrayOf() - fun reconfigureWidgets(vararg appWidgetIds: Int) = scope.launch { - Timber.d("reconfigureWidgets(${appWidgetIds.joinToString()})") - + fun reconfigureWidgets(vararg appWidgetIds: Int) { + val newGeneration = _generation.incrementAndGet() val ids = appWidgetIds.takeIf { it.isNotEmpty() } ?: widgetIds - + Timber.d("reconfigureWidgets(${ids.joinToString()}) generation=$newGeneration") val intent = Intent(context, TasksWidget::class.java) .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) .apply { action = AppWidgetManager.ACTION_APPWIDGET_UPDATE } - context.sendBroadcast(intent) } fun updateWidgets() { - updateChannel.trySend(Unit) + updateChannel.trySend(_generation.get()) } fun exists(id: Int) = appWidgetManager?.getAppWidgetInfo(id) != null - - private suspend fun notifyAppWidgetViewDataChanged(appWidgetIds: IntArray) = withContext(Dispatchers.Main) { - appWidgetManager?.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.list_view) - } } diff --git a/app/src/main/java/org/tasks/widget/TasksWidget.kt b/app/src/main/java/org/tasks/widget/TasksWidget.kt index 69af5b1eb..54bd21ef1 100644 --- a/app/src/main/java/org/tasks/widget/TasksWidget.kt +++ b/app/src/main/java/org/tasks/widget/TasksWidget.kt @@ -24,7 +24,6 @@ import org.tasks.intents.TaskIntents import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.Preferences import org.tasks.themes.ThemeColor -import org.tasks.time.DateTimeUtils2.currentTimeMillis import timber.log.Timber import javax.inject.Inject @@ -37,10 +36,11 @@ class TasksWidget : AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { Timber.d("onUpdate appWidgetIds=${appWidgetIds.joinToString { it.toString() }}") + val generation = widgetManager.generation appWidgetIds.forEach { id -> try { val options = appWidgetManager.getAppWidgetOptions(id) - appWidgetManager.updateAppWidget(id, createWidget(context, id, options)) + appWidgetManager.updateAppWidget(id, createWidget(context, id, options, generation)) } catch (e: Exception) { Timber.e(e) } @@ -56,12 +56,11 @@ class TasksWidget : AppWidgetProvider() { Timber.d("onAppWidgetOptionsChanged appWidgetId=$appWidgetId") appWidgetManager.updateAppWidget( appWidgetId, - createWidget(context, appWidgetId, newOptions) + createWidget(context, appWidgetId, newOptions, widgetManager.generation) ) - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.list_view) } - private fun createWidget(context: Context, id: Int, options: Bundle): RemoteViews { + private fun createWidget(context: Context, id: Int, options: Bundle, generation: Long): RemoteViews { val widgetPreferences = WidgetPreferences(context, preferences, id) val settings = widgetPreferences.getWidgetHeaderSettings() widgetPreferences.setCompact( @@ -70,7 +69,7 @@ class TasksWidget : AppWidgetProvider() { val filter = runBlocking { defaultFilterProvider.getFilterFromPreference(widgetPreferences.filterId) } - Timber.d("createWidget id=$id filter=$filter") + Timber.d("createWidget id=$id generation=$generation filter=$filter") return RemoteViews(context.packageName, R.layout.scrollable_widget).apply { if (settings.showHeader) { @@ -90,7 +89,7 @@ class TasksWidget : AppWidgetProvider() { opacity = widgetPreferences.footerOpacity, ) setOnClickPendingIntent(R.id.empty_view, getOpenListIntent(context, filter, id)) - val cacheBuster = "tasks://widget/${currentTimeMillis()}".toUri() + val cacheBuster = "tasks://widget/$id/$generation".toUri() setRemoteAdapter( R.id.list_view, Intent(context, TasksWidgetAdapter::class.java) diff --git a/app/src/main/java/org/tasks/widget/TasksWidgetAdapter.kt b/app/src/main/java/org/tasks/widget/TasksWidgetAdapter.kt index 8f4d00d94..7412b9e32 100644 --- a/app/src/main/java/org/tasks/widget/TasksWidgetAdapter.kt +++ b/app/src/main/java/org/tasks/widget/TasksWidgetAdapter.kt @@ -30,14 +30,16 @@ class TasksWidgetAdapter : RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory? { val widgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return null val widgetPreferences = WidgetPreferences(context, preferences, widgetId) + val filterId = widgetPreferences.filterId val filter = runBlocking { - defaultFilterProvider.getFilterFromPreference(widgetPreferences.filterId) + defaultFilterProvider.getFilterFromPreference(filterId) } Timber.d("onGetViewFactory $filter") return TasksWidgetViewFactory( subtasksHelper, widgetPreferences, filter, + filterId, applicationContext, widgetId, taskDao, diff --git a/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt b/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt index 86d5f64c9..19767bcce 100644 --- a/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt +++ b/app/src/main/java/org/tasks/widget/TasksWidgetViewFactory.kt @@ -44,6 +44,7 @@ internal class TasksWidgetViewFactory( private val subtasksHelper: SubtasksHelper, private val widgetPreferences: WidgetPreferences, private val filter: Filter, + private val filterId: String?, private val context: Context, private val widgetId: Int, private val taskDao: TaskDao, @@ -72,6 +73,10 @@ internal class TasksWidgetViewFactory( override fun onDataSetChanged() { Timber.v("onDataSetChanged $filter") + if (widgetPreferences.filterId != filterId) { + Timber.d("Skipping stale factory: expected $filterId, current ${widgetPreferences.filterId}") + return + } runBlocking { val collapsed = widgetPreferences.collapsed tasks = SectionedDataSource(