diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index ef5b8cab..3107cea8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -18,15 +18,19 @@ package com.keylesspalace.tusky import android.app.Application import android.content.SharedPreferences import android.util.Log +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import autodispose2.AutoDisposePlugins -import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.SCHEMA_VERSION import com.keylesspalace.tusky.util.APP_THEME_DEFAULT import com.keylesspalace.tusky.util.LocaleManager 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.HasAndroidInjector import de.c1710.filemojicompat_defaults.DefaultEmojiPackList @@ -35,6 +39,7 @@ import de.c1710.filemojicompat_ui.helpers.EmojiPreference import io.reactivex.rxjava3.plugins.RxJavaPlugins import org.conscrypt.Conscrypt import java.security.Security +import java.util.concurrent.TimeUnit import javax.inject.Inject class TuskyApplication : Application(), HasAndroidInjector { @@ -42,7 +47,7 @@ class TuskyApplication : Application(), HasAndroidInjector { lateinit var androidInjector: DispatchingAndroidInjector @Inject - lateinit var notificationWorkerFactory: NotificationWorkerFactory + lateinit var workerFactory: WorkerFactory @Inject lateinit var localeManager: LocaleManager @@ -93,9 +98,19 @@ class TuskyApplication : Application(), HasAndroidInjector { WorkManager.initialize( this, androidx.work.Configuration.Builder() - .setWorkerFactory(notificationWorkerFactory) + .setWorkerFactory(workerFactory) .build() ) + + // Prune the database every ~ 12 hours when the device is idle. + val pruneCacheWorker = PeriodicWorkRequestBuilder(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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index 1127c261..5e7f2ece 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -63,6 +63,7 @@ import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver; import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.worker.NotificationWorker; import java.util.ArrayList; import java.util.Collections; diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt deleted file mode 100644 index 42b9c869..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt +++ /dev/null @@ -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 . */ - -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 - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt index 71cdcd2f..d33bf7e4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -50,14 +50,11 @@ import com.keylesspalace.tusky.util.EmptyPagingSource import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import retrofit2.HttpException import javax.inject.Inject -import kotlin.time.DurationUnit -import kotlin.time.toDuration /** * TimelineViewModel that caches all statuses in a local database @@ -107,16 +104,6 @@ class CachedTimelineViewModel @Inject constructor( .flowOn(Dispatchers.Default) .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) { // handled by CacheUpdater } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt index 2cf48046..73aceeab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt @@ -34,7 +34,8 @@ import javax.inject.Singleton ActivitiesModule::class, ServicesModule::class, BroadcastReceiverModule::class, - ViewModelModule::class + ViewModelModule::class, + WorkerModule::class ] ) interface AppComponent { diff --git a/app/src/main/java/com/keylesspalace/tusky/di/WorkerModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/WorkerModule.kt new file mode 100644 index 00000000..212d4d31 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/WorkerModule.kt @@ -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 . + */ + +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) + +@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 +} diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt index 45a5ae2b..b95e5310 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt @@ -20,11 +20,11 @@ import android.content.Intent import android.util.Log import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager -import com.keylesspalace.tusky.components.notifications.NotificationWorker import com.keylesspalace.tusky.components.notifications.registerUnifiedPushEndpoint import com.keylesspalace.tusky.components.notifications.unregisterUnifiedPushEndpoint import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.worker.NotificationWorker import dagger.android.AndroidInjection import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt new file mode 100644 index 00000000..84fabd4a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt @@ -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 . + */ + +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) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt new file mode 100644 index 00000000..c0ebdb79 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt @@ -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 . + */ + +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) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/WorkerFactory.kt b/app/src/main/java/com/keylesspalace/tusky/worker/WorkerFactory.kt new file mode 100644 index 00000000..73c87b2e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/worker/WorkerFactory.kt @@ -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 . + */ + +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, @JvmSuppressWildcards Provider> +) : 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 + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19b2f6c1..dc4e4321 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,7 +92,7 @@ androidx-sharetarget = { module = "androidx.sharetarget:sharetarget", version.re androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefresh-layout" } androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } 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" } autodispose-android-lifecycle = { module = "com.uber.autodispose2:autodispose-androidx-lifecycle", 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-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-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"] autodispose = ["autodispose-core", "autodispose-android-lifecycle"] dagger = ["dagger-core", "dagger-android-core", "dagger-android-support"]