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)
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.activity)
implementation(projects.core.common)

View File

@ -212,17 +212,11 @@
android:resource="@xml/file_paths" />
</provider>
<!-- disable automatic WorkManager initialization -->
<!-- Disable automatic WorkManager initialization, use HiltWorkerFactory -->
<provider
android:name="androidx.startup.InitializationProvider"
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" />
</provider>
tools:node="remove" />
</application>

View File

@ -20,6 +20,7 @@ package app.pachli
import android.app.Application
import android.content.Context
import androidx.core.content.edit
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
@ -37,7 +38,6 @@ import app.pachli.util.LocaleManager
import app.pachli.util.setAppNightMode
import app.pachli.worker.PruneCacheWorker
import app.pachli.worker.PruneLogEntryEntityWorker
import app.pachli.worker.WorkerFactory
import dagger.hilt.android.HiltAndroidApp
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
@ -51,7 +51,7 @@ import timber.log.Timber
@HiltAndroidApp
class PachliApplication : Application() {
@Inject
lateinit var workerFactory: WorkerFactory
lateinit var workerFactory: HiltWorkerFactory
@Inject
lateinit var localeManager: LocaleManager
@ -110,9 +110,7 @@ class PachliApplication : Application() {
WorkManager.initialize(
this,
androidx.work.Configuration.Builder()
.setWorkerFactory(workerFactory)
.build(),
androidx.work.Configuration.Builder().setWorkerFactory(workerFactory).build(),
)
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.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
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.NotificationFetcher
import app.pachli.components.notifications.createWorkerNotification
import javax.inject.Inject
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber
/** Fetch and show new notifications. */
class NotificationWorker(
appContext: Context,
params: WorkerParameters,
@HiltWorker
class NotificationWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val notificationsFetcher: NotificationFetcher,
) : CoroutineWorker(appContext, params) {
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)
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.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import app.pachli.R
import app.pachli.components.notifications.NOTIFICATION_ID_PRUNE_CACHE
import app.pachli.components.notifications.createWorkerNotification
import app.pachli.core.accounts.AccountManager
import app.pachli.core.database.dao.TimelineDao
import javax.inject.Inject
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber
/** Prune the database cache of old statuses. */
class PruneCacheWorker(
appContext: Context,
workerParams: WorkerParameters,
@HiltWorker
class PruneCacheWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
private val timelineDao: TimelineDao,
private val accountManager: AccountManager,
) : CoroutineWorker(appContext, workerParams) {
@ -54,13 +56,4 @@ class PruneCacheWorker(
private const val MAX_STATUSES_IN_CACHE = 1000
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.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import app.pachli.R
import app.pachli.components.notifications.NOTIFICATION_ID_PRUNE_CACHE
import app.pachli.components.notifications.createWorkerNotification
import app.pachli.core.database.dao.LogEntryDao
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.time.Instant
import javax.inject.Inject
import kotlin.time.Duration.Companion.hours
import timber.log.Timber
/** Prune the database cache of old statuses. */
class PruneLogEntryEntityWorker(
appContext: Context,
workerParams: WorkerParameters,
@HiltWorker
class PruneLogEntryEntityWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
private val logEntryDao: LogEntryDao,
) : CoroutineWorker(appContext, workerParams) {
val notification: Notification = createWorkerNotification(applicationContext, R.string.notification_prune_cache)
override suspend fun doWork(): Result {
val now = Instant.now()
val oldest = now.minusMillis(OLDEST_ENTRY.inWholeMilliseconds)
logEntryDao.prune(oldest)
return Result.success()
return try {
val now = Instant.now()
val oldest = now.minusMillis(OLDEST_ENTRY.inWholeMilliseconds)
logEntryDao.prune(oldest)
Result.success()
} catch (e: Exception) {
Timber.e(e, "error in PruneLogEntryEntityWorker.doWork")
Result.failure()
}
}
override suspend fun getForegroundInfo() = ForegroundInfo(NOTIFICATION_ID_PRUNE_CACHE, notification)
@ -52,12 +60,4 @@ class PruneLogEntryEntityWorker(
private val OLDEST_ENTRY = 48.hours
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-exifinterface = "1.3.7"
androidx-fragment = "1.6.2"
androidx-hilt = "1.2.0"
androidx-junit = "1.1.5"
androidx-lifecycle = "2.7.0"
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-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exifinterface" }
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-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" }