refactor: Replace custom worker factory with HiltWorkerFactory (#517)

Removes the need for a separate `WorkerModule` and factory methods in
each worker.
This commit is contained in:
Nik Clayton 2024-03-11 10:49:58 +01:00 committed by GitHub
parent 442f3bc80c
commit 4762411147
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 45 additions and 171 deletions

View File

@ -116,6 +116,11 @@ dependencies {
compileOnly(libs.bundles.room) compileOnly(libs.bundles.room)
testCompileOnly(libs.bundles.room) testCompileOnly(libs.bundles.room)
// @HiltWorker annotation
implementation(libs.androidx.hilt.common)
implementation(libs.androidx.hilt.work)
ksp(libs.androidx.hilt.compiler)
implementation(projects.core.accounts) implementation(projects.core.accounts)
implementation(projects.core.activity) implementation(projects.core.activity)
implementation(projects.core.common) implementation(projects.core.common)

View File

@ -212,17 +212,11 @@
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<!-- disable automatic WorkManager initialization --> <!-- Disable automatic WorkManager initialization, use HiltWorkerFactory -->
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" /> tools:node="remove" />
</provider>
</application> </application>

View File

@ -20,6 +20,7 @@ package app.pachli
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
@ -37,7 +38,6 @@ import app.pachli.util.LocaleManager
import app.pachli.util.setAppNightMode import app.pachli.util.setAppNightMode
import app.pachli.worker.PruneCacheWorker import app.pachli.worker.PruneCacheWorker
import app.pachli.worker.PruneLogEntryEntityWorker import app.pachli.worker.PruneLogEntryEntityWorker
import app.pachli.worker.WorkerFactory
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
@ -51,7 +51,7 @@ import timber.log.Timber
@HiltAndroidApp @HiltAndroidApp
class PachliApplication : Application() { class PachliApplication : Application() {
@Inject @Inject
lateinit var workerFactory: WorkerFactory lateinit var workerFactory: HiltWorkerFactory
@Inject @Inject
lateinit var localeManager: LocaleManager lateinit var localeManager: LocaleManager
@ -110,9 +110,7 @@ class PachliApplication : Application() {
WorkManager.initialize( WorkManager.initialize(
this, this,
androidx.work.Configuration.Builder() androidx.work.Configuration.Builder().setWorkerFactory(workerFactory).build(),
.setWorkerFactory(workerFactory)
.build(),
) )
val workManager = WorkManager.getInstance(this) val workManager = WorkManager.getInstance(this)

View File

@ -1,48 +0,0 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* 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.
*
* Pachli 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 Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.di
import androidx.work.ListenableWorker
import app.pachli.worker.ChildWorkerFactory
import app.pachli.worker.NotificationWorker
import app.pachli.worker.PruneCacheWorker
import dagger.Binds
import dagger.MapKey
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoMap
import kotlin.reflect.KClass
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class WorkerKey(val value: KClass<out ListenableWorker>)
@InstallIn(SingletonComponent::class)
@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

@ -19,6 +19,7 @@ package app.pachli.worker
import android.app.Notification import android.app.Notification
import android.content.Context import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
@ -26,13 +27,15 @@ import app.pachli.R
import app.pachli.components.notifications.NOTIFICATION_ID_FETCH_NOTIFICATION import app.pachli.components.notifications.NOTIFICATION_ID_FETCH_NOTIFICATION
import app.pachli.components.notifications.NotificationFetcher import app.pachli.components.notifications.NotificationFetcher
import app.pachli.components.notifications.createWorkerNotification import app.pachli.components.notifications.createWorkerNotification
import javax.inject.Inject import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber import timber.log.Timber
/** Fetch and show new notifications. */ /** Fetch and show new notifications. */
class NotificationWorker( @HiltWorker
appContext: Context, class NotificationWorker @AssistedInject constructor(
params: WorkerParameters, @Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val notificationsFetcher: NotificationFetcher, private val notificationsFetcher: NotificationFetcher,
) : CoroutineWorker(appContext, params) { ) : CoroutineWorker(appContext, params) {
val notification: Notification = createWorkerNotification(applicationContext, R.string.notification_notification_worker) val notification: Notification = createWorkerNotification(applicationContext, R.string.notification_notification_worker)
@ -44,12 +47,4 @@ class NotificationWorker(
} }
override suspend fun getForegroundInfo() = ForegroundInfo(NOTIFICATION_ID_FETCH_NOTIFICATION, notification) override suspend fun getForegroundInfo() = ForegroundInfo(NOTIFICATION_ID_FETCH_NOTIFICATION, notification)
class Factory @Inject constructor(
private val notificationsFetcher: NotificationFetcher,
) : ChildWorkerFactory {
override fun createWorker(appContext: Context, params: WorkerParameters): CoroutineWorker {
return NotificationWorker(appContext, params, notificationsFetcher)
}
}
} }

View File

@ -19,22 +19,24 @@ package app.pachli.worker
import android.app.Notification import android.app.Notification
import android.content.Context import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import app.pachli.R import app.pachli.R
import app.pachli.components.notifications.NOTIFICATION_ID_PRUNE_CACHE import app.pachli.components.notifications.NOTIFICATION_ID_PRUNE_CACHE
import app.pachli.components.notifications.createWorkerNotification import app.pachli.components.notifications.createWorkerNotification
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.database.dao.TimelineDao import app.pachli.core.database.dao.TimelineDao
import javax.inject.Inject import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber import timber.log.Timber
/** Prune the database cache of old statuses. */ /** Prune the database cache of old statuses. */
class PruneCacheWorker( @HiltWorker
appContext: Context, class PruneCacheWorker @AssistedInject constructor(
workerParams: WorkerParameters, @Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
private val timelineDao: TimelineDao, private val timelineDao: TimelineDao,
private val accountManager: AccountManager, private val accountManager: AccountManager,
) : CoroutineWorker(appContext, workerParams) { ) : CoroutineWorker(appContext, workerParams) {
@ -54,13 +56,4 @@ class PruneCacheWorker(
private const val MAX_STATUSES_IN_CACHE = 1000 private const val MAX_STATUSES_IN_CACHE = 1000
const val PERIODIC_WORK_TAG = "PruneCacheWorker_periodic" const val PERIODIC_WORK_TAG = "PruneCacheWorker_periodic"
} }
class Factory @Inject constructor(
private val timelineDao: TimelineDao,
private val accountManager: AccountManager,
) : ChildWorkerFactory {
override fun createWorker(appContext: Context, params: WorkerParameters): ListenableWorker {
return PruneCacheWorker(appContext, params, timelineDao, accountManager)
}
}
} }

View File

@ -19,31 +19,39 @@ package app.pachli.worker
import android.app.Notification import android.app.Notification
import android.content.Context import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import app.pachli.R import app.pachli.R
import app.pachli.components.notifications.NOTIFICATION_ID_PRUNE_CACHE import app.pachli.components.notifications.NOTIFICATION_ID_PRUNE_CACHE
import app.pachli.components.notifications.createWorkerNotification import app.pachli.components.notifications.createWorkerNotification
import app.pachli.core.database.dao.LogEntryDao import app.pachli.core.database.dao.LogEntryDao
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.time.Instant import java.time.Instant
import javax.inject.Inject
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import timber.log.Timber
/** Prune the database cache of old statuses. */ /** Prune the database cache of old statuses. */
class PruneLogEntryEntityWorker( @HiltWorker
appContext: Context, class PruneLogEntryEntityWorker @AssistedInject constructor(
workerParams: WorkerParameters, @Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
private val logEntryDao: LogEntryDao, private val logEntryDao: LogEntryDao,
) : CoroutineWorker(appContext, workerParams) { ) : CoroutineWorker(appContext, workerParams) {
val notification: Notification = createWorkerNotification(applicationContext, R.string.notification_prune_cache) val notification: Notification = createWorkerNotification(applicationContext, R.string.notification_prune_cache)
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
return try {
val now = Instant.now() val now = Instant.now()
val oldest = now.minusMillis(OLDEST_ENTRY.inWholeMilliseconds) val oldest = now.minusMillis(OLDEST_ENTRY.inWholeMilliseconds)
logEntryDao.prune(oldest) logEntryDao.prune(oldest)
return Result.success() Result.success()
} catch (e: Exception) {
Timber.e(e, "error in PruneLogEntryEntityWorker.doWork")
Result.failure()
}
} }
override suspend fun getForegroundInfo() = ForegroundInfo(NOTIFICATION_ID_PRUNE_CACHE, notification) override suspend fun getForegroundInfo() = ForegroundInfo(NOTIFICATION_ID_PRUNE_CACHE, notification)
@ -52,12 +60,4 @@ class PruneLogEntryEntityWorker(
private val OLDEST_ENTRY = 48.hours private val OLDEST_ENTRY = 48.hours
const val PERIODIC_WORK_TAG = "PruneLogEntryEntityWorker_periodic" const val PERIODIC_WORK_TAG = "PruneLogEntryEntityWorker_periodic"
} }
class Factory @Inject constructor(
private val logEntryDao: LogEntryDao,
) : ChildWorkerFactory {
override fun createWorker(appContext: Context, params: WorkerParameters): ListenableWorker {
return PruneLogEntryEntityWorker(appContext, params, logEntryDao)
}
}
} }

View File

@ -1,67 +0,0 @@
/*
* Copyright 2023 Pachli Association
*
* This file is a part of Pachli.
*
* 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.
*
* Pachli 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 Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.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
import javax.inject.Singleton
import timber.log.Timber
/**
* Workers implement this and are added to the map in [app.pachli.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 [app.pachli.worker.NotificationWorker]
*/
@Singleton
class WorkerFactory @Inject constructor(
private val workerFactories: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<ChildWorkerFactory>>,
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters,
): ListenableWorker? {
val key = try {
Class.forName(workerClassName)
} catch (e: ClassNotFoundException) {
// Class might be missing if it was renamed / moved to a different package, as
// periodic work requests from before the rename might still exist. Catch and
// return null, which should stop future requests.
Timber.d(e, "Invalid class: %s", workerClassName)
null
}
workerFactories[key]?.let {
return it.get().createWorker(appContext, workerParameters)
}
return null
}
}

View File

@ -10,6 +10,7 @@ androidx-constraintlayout = "2.1.4"
androidx-core = "1.12.0" androidx-core = "1.12.0"
androidx-exifinterface = "1.3.7" androidx-exifinterface = "1.3.7"
androidx-fragment = "1.6.2" androidx-fragment = "1.6.2"
androidx-hilt = "1.2.0"
androidx-junit = "1.1.5" androidx-junit = "1.1.5"
androidx-lifecycle = "2.7.0" androidx-lifecycle = "2.7.0"
androidx-media3 = "1.2.1" androidx-media3 = "1.2.1"
@ -109,6 +110,9 @@ androidx-emoji2-views-core = { module = "androidx.emoji2:emoji2-views", version.
androidx-emoji2-view-helper = { module = "androidx.emoji2:emoji2-views-helper", version.ref = "emoji2" } androidx-emoji2-view-helper = { module = "androidx.emoji2:emoji2-views-helper", version.ref = "emoji2" }
androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exifinterface" } androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exifinterface" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" }
androidx-hilt-common = { module = "androidx.hilt:hilt-common", version.ref = "androidx-hilt" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidx-hilt" }
androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidx-hilt" }
androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" } androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" }
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-reactivestreams-ktx = { module = "androidx.lifecycle:lifecycle-reactivestreams-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-reactivestreams-ktx = { module = "androidx.lifecycle:lifecycle-reactivestreams-ktx", version.ref = "androidx-lifecycle" }