Fix widget race conditions

14.8.4
Alex Baker 3 days ago
parent 76baf2ee9e
commit 14c5d3fe7d

@ -6,34 +6,41 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.R import org.tasks.R
import org.tasks.compose.throttleLatest import org.tasks.compose.throttleLatest
import org.tasks.injection.ApplicationScope import org.tasks.injection.ApplicationScope
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicLong
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AppWidgetManager @Inject constructor( class AppWidgetManager @Inject constructor(
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
@ApplicationScope private val scope: CoroutineScope, @ApplicationScope private val scope: CoroutineScope,
) { ) {
private val appWidgetManager: AppWidgetManager? = AppWidgetManager.getInstance(context) private val appWidgetManager: AppWidgetManager? = AppWidgetManager.getInstance(context)
private val updateChannel = Channel<Unit>(Channel.CONFLATED) private val _generation = AtomicLong(0)
val generation: Long get() = _generation.get()
private val updateChannel = Channel<Long>(Channel.CONFLATED)
init { init {
updateChannel updateChannel
.consumeAsFlow() .consumeAsFlow()
.throttleLatest(1000) .throttleLatest(1000)
.onEach { .onEach { gen ->
val appWidgetIds = widgetIds if (gen == _generation.get()) {
Timber.d("updateWidgets: ${appWidgetIds.joinToString { it.toString() }}") val appWidgetIds = widgetIds
notifyAppWidgetViewDataChanged(appWidgetIds) Timber.d("updateWidgets: ${appWidgetIds.joinToString { it.toString() }}")
appWidgetManager?.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.list_view)
} else {
Timber.d("Skipping stale widget update")
}
} }
.launchIn(scope) .launchIn(scope)
} }
@ -43,25 +50,19 @@ class AppWidgetManager @Inject constructor(
?.getAppWidgetIds(ComponentName(context, TasksWidget::class.java)) ?.getAppWidgetIds(ComponentName(context, TasksWidget::class.java))
?: intArrayOf() ?: intArrayOf()
fun reconfigureWidgets(vararg appWidgetIds: Int) = scope.launch { fun reconfigureWidgets(vararg appWidgetIds: Int) {
Timber.d("reconfigureWidgets(${appWidgetIds.joinToString()})") val newGeneration = _generation.incrementAndGet()
val ids = appWidgetIds.takeIf { it.isNotEmpty() } ?: widgetIds val ids = appWidgetIds.takeIf { it.isNotEmpty() } ?: widgetIds
Timber.d("reconfigureWidgets(${ids.joinToString()}) generation=$newGeneration")
val intent = Intent(context, TasksWidget::class.java) val intent = Intent(context, TasksWidget::class.java)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
.apply { action = AppWidgetManager.ACTION_APPWIDGET_UPDATE } .apply { action = AppWidgetManager.ACTION_APPWIDGET_UPDATE }
context.sendBroadcast(intent) context.sendBroadcast(intent)
} }
fun updateWidgets() { fun updateWidgets() {
updateChannel.trySend(Unit) updateChannel.trySend(_generation.get())
} }
fun exists(id: Int) = appWidgetManager?.getAppWidgetInfo(id) != null fun exists(id: Int) = appWidgetManager?.getAppWidgetInfo(id) != null
private suspend fun notifyAppWidgetViewDataChanged(appWidgetIds: IntArray) = withContext(Dispatchers.Main) {
appWidgetManager?.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.list_view)
}
} }

@ -24,7 +24,6 @@ import org.tasks.intents.TaskIntents
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.ThemeColor import org.tasks.themes.ThemeColor
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -37,10 +36,11 @@ class TasksWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
Timber.d("onUpdate appWidgetIds=${appWidgetIds.joinToString { it.toString() }}") Timber.d("onUpdate appWidgetIds=${appWidgetIds.joinToString { it.toString() }}")
val generation = widgetManager.generation
appWidgetIds.forEach { id -> appWidgetIds.forEach { id ->
try { try {
val options = appWidgetManager.getAppWidgetOptions(id) val options = appWidgetManager.getAppWidgetOptions(id)
appWidgetManager.updateAppWidget(id, createWidget(context, id, options)) appWidgetManager.updateAppWidget(id, createWidget(context, id, options, generation))
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
@ -56,12 +56,11 @@ class TasksWidget : AppWidgetProvider() {
Timber.d("onAppWidgetOptionsChanged appWidgetId=$appWidgetId") Timber.d("onAppWidgetOptionsChanged appWidgetId=$appWidgetId")
appWidgetManager.updateAppWidget( appWidgetManager.updateAppWidget(
appWidgetId, 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 widgetPreferences = WidgetPreferences(context, preferences, id)
val settings = widgetPreferences.getWidgetHeaderSettings() val settings = widgetPreferences.getWidgetHeaderSettings()
widgetPreferences.setCompact( widgetPreferences.setCompact(
@ -70,7 +69,7 @@ class TasksWidget : AppWidgetProvider() {
val filter = runBlocking { val filter = runBlocking {
defaultFilterProvider.getFilterFromPreference(widgetPreferences.filterId) 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 { return RemoteViews(context.packageName, R.layout.scrollable_widget).apply {
if (settings.showHeader) { if (settings.showHeader) {
@ -90,7 +89,7 @@ class TasksWidget : AppWidgetProvider() {
opacity = widgetPreferences.footerOpacity, opacity = widgetPreferences.footerOpacity,
) )
setOnClickPendingIntent(R.id.empty_view, getOpenListIntent(context, filter, id)) setOnClickPendingIntent(R.id.empty_view, getOpenListIntent(context, filter, id))
val cacheBuster = "tasks://widget/${currentTimeMillis()}".toUri() val cacheBuster = "tasks://widget/$id/$generation".toUri()
setRemoteAdapter( setRemoteAdapter(
R.id.list_view, R.id.list_view,
Intent(context, TasksWidgetAdapter::class.java) Intent(context, TasksWidgetAdapter::class.java)

@ -30,14 +30,16 @@ class TasksWidgetAdapter : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory? { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory? {
val widgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return null val widgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return null
val widgetPreferences = WidgetPreferences(context, preferences, widgetId) val widgetPreferences = WidgetPreferences(context, preferences, widgetId)
val filterId = widgetPreferences.filterId
val filter = runBlocking { val filter = runBlocking {
defaultFilterProvider.getFilterFromPreference(widgetPreferences.filterId) defaultFilterProvider.getFilterFromPreference(filterId)
} }
Timber.d("onGetViewFactory $filter") Timber.d("onGetViewFactory $filter")
return TasksWidgetViewFactory( return TasksWidgetViewFactory(
subtasksHelper, subtasksHelper,
widgetPreferences, widgetPreferences,
filter, filter,
filterId,
applicationContext, applicationContext,
widgetId, widgetId,
taskDao, taskDao,

@ -44,6 +44,7 @@ internal class TasksWidgetViewFactory(
private val subtasksHelper: SubtasksHelper, private val subtasksHelper: SubtasksHelper,
private val widgetPreferences: WidgetPreferences, private val widgetPreferences: WidgetPreferences,
private val filter: Filter, private val filter: Filter,
private val filterId: String?,
private val context: Context, private val context: Context,
private val widgetId: Int, private val widgetId: Int,
private val taskDao: TaskDao, private val taskDao: TaskDao,
@ -72,6 +73,10 @@ internal class TasksWidgetViewFactory(
override fun onDataSetChanged() { override fun onDataSetChanged() {
Timber.v("onDataSetChanged $filter") Timber.v("onDataSetChanged $filter")
if (widgetPreferences.filterId != filterId) {
Timber.d("Skipping stale factory: expected $filterId, current ${widgetPreferences.filterId}")
return
}
runBlocking { runBlocking {
val collapsed = widgetPreferences.collapsed val collapsed = widgetPreferences.collapsed
tasks = SectionedDataSource( tasks = SectionedDataSource(

Loading…
Cancel
Save