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 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<Unit>(Channel.CONFLATED)
private val _generation = AtomicLong(0)
val generation: Long get() = _generation.get()
private val updateChannel = Channel<Long>(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)
}
}

@ -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)

@ -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,

@ -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(

Loading…
Cancel
Save