Move cache pruning to a WorkManager worker (#3649)

- Extend what was `NotificationWorkerFactory` to `WorkerFactory`. This
  can construct arbitrary Workers as long as they provide their own
  Factory for construction.

  The per-Worker factory contains any injected components just for that
  worker type, keeping `WorkerFactory` clean.

- Move `NotificationWorkerFactory` to the new model.

- Implement `PruneCacheWorker`, and remove the code from
 `CachedTimelineViewModel`.

- Create the periodic worker in `TuskyApplication`, ensuring that the
  database is only pruned when the device is idle.
This commit is contained in:
Nik Clayton 2023-06-11 13:17:30 +02:00 committed by GitHub
parent 85b7caa887
commit 4025ab35ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 227 additions and 71 deletions

View file

@ -18,15 +18,19 @@ package com.keylesspalace.tusky
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import autodispose2.AutoDisposePlugins import autodispose2.AutoDisposePlugins
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.SCHEMA_VERSION import com.keylesspalace.tusky.settings.SCHEMA_VERSION
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.util.LocaleManager import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.setAppNightMode import com.keylesspalace.tusky.util.setAppNightMode
import com.keylesspalace.tusky.worker.PruneCacheWorker
import com.keylesspalace.tusky.worker.WorkerFactory
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
@ -35,6 +39,7 @@ import de.c1710.filemojicompat_ui.helpers.EmojiPreference
import io.reactivex.rxjava3.plugins.RxJavaPlugins import io.reactivex.rxjava3.plugins.RxJavaPlugins
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import java.security.Security import java.security.Security
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
class TuskyApplication : Application(), HasAndroidInjector { class TuskyApplication : Application(), HasAndroidInjector {
@ -42,7 +47,7 @@ class TuskyApplication : Application(), HasAndroidInjector {
lateinit var androidInjector: DispatchingAndroidInjector<Any> lateinit var androidInjector: DispatchingAndroidInjector<Any>
@Inject @Inject
lateinit var notificationWorkerFactory: NotificationWorkerFactory lateinit var workerFactory: WorkerFactory
@Inject @Inject
lateinit var localeManager: LocaleManager lateinit var localeManager: LocaleManager
@ -93,9 +98,19 @@ class TuskyApplication : Application(), HasAndroidInjector {
WorkManager.initialize( WorkManager.initialize(
this, this,
androidx.work.Configuration.Builder() androidx.work.Configuration.Builder()
.setWorkerFactory(notificationWorkerFactory) .setWorkerFactory(workerFactory)
.build() .build()
) )
// Prune the database every ~ 12 hours when the device is idle.
val pruneCacheWorker = PeriodicWorkRequestBuilder<PruneCacheWorker>(12, TimeUnit.HOURS)
.setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build())
.build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
PruneCacheWorker.PERIODIC_WORK_TAG,
ExistingPeriodicWorkPolicy.KEEP,
pruneCacheWorker
)
} }
override fun androidInjector() = androidInjector override fun androidInjector() = androidInjector

View file

@ -63,6 +63,7 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver; import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.viewdata.PollViewDataKt; import com.keylesspalace.tusky.viewdata.PollViewDataKt;
import com.keylesspalace.tusky.worker.NotificationWorker;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;

View file

@ -1,51 +0,0 @@
/* Copyright 2020 Tusky Contributors
*
* This file is part of Tusky.
*
* Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with Tusky. If
* not, see <http://www.gnu.org/licenses/>. */
package com.keylesspalace.tusky.components.notifications
import android.content.Context
import androidx.work.ListenableWorker
import androidx.work.Worker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import javax.inject.Inject
class NotificationWorker(
context: Context,
params: WorkerParameters,
private val notificationsFetcher: NotificationFetcher
) : Worker(context, params) {
override fun doWork(): Result {
notificationsFetcher.fetchAndShow()
return Result.success()
}
}
class NotificationWorkerFactory @Inject constructor(
private val notificationsFetcher: NotificationFetcher
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
if (workerClassName == NotificationWorker::class.java.name) {
return NotificationWorker(appContext, workerParameters, notificationsFetcher)
}
return null
}
}

View file

@ -50,14 +50,11 @@ import com.keylesspalace.tusky.util.EmptyPagingSource
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
/** /**
* TimelineViewModel that caches all statuses in a local database * TimelineViewModel that caches all statuses in a local database
@ -107,16 +104,6 @@ class CachedTimelineViewModel @Inject constructor(
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.cachedIn(viewModelScope) .cachedIn(viewModelScope)
init {
viewModelScope.launch {
delay(5.toDuration(DurationUnit.SECONDS)) // delay so the db is not locked during initial ui refresh
accountManager.activeAccount?.id?.let { accountId ->
db.timelineDao().cleanup(accountId, MAX_STATUSES_IN_CACHE)
db.timelineDao().cleanupAccounts(accountId)
}
}
}
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) { override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
// handled by CacheUpdater // handled by CacheUpdater
} }

View file

@ -34,7 +34,8 @@ import javax.inject.Singleton
ActivitiesModule::class, ActivitiesModule::class,
ServicesModule::class, ServicesModule::class,
BroadcastReceiverModule::class, BroadcastReceiverModule::class,
ViewModelModule::class ViewModelModule::class,
WorkerModule::class
] ]
) )
interface AppComponent { interface AppComponent {

View file

@ -0,0 +1,45 @@
/*
* Copyright 2023 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.di
import androidx.work.ListenableWorker
import com.keylesspalace.tusky.worker.ChildWorkerFactory
import com.keylesspalace.tusky.worker.NotificationWorker
import com.keylesspalace.tusky.worker.PruneCacheWorker
import dagger.Binds
import dagger.MapKey
import dagger.Module
import dagger.multibindings.IntoMap
import kotlin.reflect.KClass
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class WorkerKey(val value: KClass<out ListenableWorker>)
@Module
abstract class WorkerModule {
@Binds
@IntoMap
@WorkerKey(NotificationWorker::class)
internal abstract fun bindNotificationWorkerFactory(worker: NotificationWorker.Factory): ChildWorkerFactory
@Binds
@IntoMap
@WorkerKey(PruneCacheWorker::class)
internal abstract fun bindPruneCacheWorkerFactory(worker: PruneCacheWorker.Factory): ChildWorkerFactory
}

View file

@ -20,11 +20,11 @@ import android.content.Intent
import android.util.Log import android.util.Log
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import com.keylesspalace.tusky.components.notifications.NotificationWorker
import com.keylesspalace.tusky.components.notifications.registerUnifiedPushEndpoint import com.keylesspalace.tusky.components.notifications.registerUnifiedPushEndpoint
import com.keylesspalace.tusky.components.notifications.unregisterUnifiedPushEndpoint import com.keylesspalace.tusky.components.notifications.unregisterUnifiedPushEndpoint
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.worker.NotificationWorker
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope

View file

@ -0,0 +1,44 @@
/*
* Copyright 2023 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.worker
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.keylesspalace.tusky.components.notifications.NotificationFetcher
import javax.inject.Inject
/** Fetch and show new notifications. */
class NotificationWorker(
appContext: Context,
params: WorkerParameters,
private val notificationsFetcher: NotificationFetcher
) : Worker(appContext, params) {
override fun doWork(): Result {
notificationsFetcher.fetchAndShow()
return Result.success()
}
class Factory @Inject constructor(
private val notificationsFetcher: NotificationFetcher
) : ChildWorkerFactory {
override fun createWorker(appContext: Context, params: WorkerParameters): Worker {
return NotificationWorker(appContext, params, notificationsFetcher)
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2023 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.worker
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import javax.inject.Inject
/** Prune the database cache of old statuses. */
class PruneCacheWorker(
appContext: Context,
workerParams: WorkerParameters,
private val appDatabase: AppDatabase,
private val accountManager: AccountManager
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
for (account in accountManager.accounts) {
Log.d(TAG, "Pruning database using account ID: ${account.id}")
appDatabase.timelineDao().cleanup(account.id, MAX_STATUSES_IN_CACHE)
}
return Result.success()
}
companion object {
private const val TAG = "PruneCacheWorker"
private const val MAX_STATUSES_IN_CACHE = 1000
const val PERIODIC_WORK_TAG = "PruneCacheWorker_periodic"
}
class Factory @Inject constructor(
private val appDatabase: AppDatabase,
private val accountManager: AccountManager
) : ChildWorkerFactory {
override fun createWorker(appContext: Context, params: WorkerParameters): ListenableWorker {
return PruneCacheWorker(appContext, params, appDatabase, accountManager)
}
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2023 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.worker
import android.content.Context
import androidx.work.ListenableWorker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import javax.inject.Inject
import javax.inject.Provider
/**
* Workers implement this and are added to the map in [com.keylesspalace.tusky.di.WorkerModule]
* so they can be created by [WorkerFactory.createWorker].
*/
interface ChildWorkerFactory {
/** Create a new instance of the given worker. */
fun createWorker(appContext: Context, params: WorkerParameters): ListenableWorker
}
/**
* Creates workers, delegating to each worker's [ChildWorkerFactory.createWorker] to do the
* creation.
*
* @see [com.keylesspalace.tusky.components.notifications.NotificationWorker]
*/
class WorkerFactory @Inject constructor(
val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
val key = Class.forName(workerClassName)
workerFactories[key]?.let {
return it.get().createWorker(appContext, workerParameters)
}
return null
}
}

View file

@ -92,7 +92,7 @@ androidx-sharetarget = { module = "androidx.sharetarget:sharetarget", version.re
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefresh-layout" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefresh-layout" }
androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" } androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" }
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "androidx-work" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" } androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" }
autodispose-android-lifecycle = { module = "com.uber.autodispose2:autodispose-androidx-lifecycle", version.ref = "autodispose" } autodispose-android-lifecycle = { module = "com.uber.autodispose2:autodispose-androidx-lifecycle", version.ref = "autodispose" }
autodispose-core = { module = "com.uber.autodispose2:autodispose", version.ref = "autodispose" } autodispose-core = { module = "com.uber.autodispose2:autodispose", version.ref = "autodispose" }
@ -146,7 +146,7 @@ androidx = ["androidx-core-ktx", "androidx-appcompat", "androidx-fragment-ktx",
"androidx-recyclerview", "androidx-exifinterface", "androidx-cardview", "androidx-preference-ktx", "androidx-sharetarget", "androidx-recyclerview", "androidx-exifinterface", "androidx-cardview", "androidx-preference-ktx", "androidx-sharetarget",
"androidx-emoji2-core", "androidx-emoji2-views-core", "androidx-emoji2-view-helper", "androidx-lifecycle-viewmodel-ktx", "androidx-emoji2-core", "androidx-emoji2-views-core", "androidx-emoji2-view-helper", "androidx-lifecycle-viewmodel-ktx",
"androidx-lifecycle-livedata-ktx", "androidx-lifecycle-common-java8", "androidx-lifecycle-reactivestreams-ktx", "androidx-lifecycle-livedata-ktx", "androidx-lifecycle-common-java8", "androidx-lifecycle-reactivestreams-ktx",
"androidx-constraintlayout", "androidx-paging-runtime-ktx", "androidx-viewpager2", "androidx-work-runtime", "androidx-constraintlayout", "androidx-paging-runtime-ktx", "androidx-viewpager2", "androidx-work-runtime-ktx",
"androidx-core-splashscreen", "androidx-activity"] "androidx-core-splashscreen", "androidx-activity"]
autodispose = ["autodispose-core", "autodispose-android-lifecycle"] autodispose = ["autodispose-core", "autodispose-android-lifecycle"]
dagger = ["dagger-core", "dagger-android-core", "dagger-android-support"] dagger = ["dagger-core", "dagger-android-core", "dagger-android-support"]