Convert EH Gallery Updater from a JobService to WorkManager

This commit is contained in:
Jobobby04 2021-07-02 17:50:22 -04:00
parent 0d8af29eeb
commit e9e3340c08
5 changed files with 59 additions and 158 deletions

View File

@ -34,7 +34,7 @@ android {
minSdkVersion(AndroidConfig.minSdk) minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk) targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 20 versionCode = 21
versionName = "1.7.0" versionName = "1.7.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")

View File

@ -189,10 +189,6 @@
android:exported="false" /> android:exported="false" />
<!-- EH --> <!-- EH -->
<service
android:name="exh.eh.EHentaiUpdateWorker"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true" />
<activity <activity
android:name="exh.ui.intercept.InterceptActivity" android:name="exh.ui.intercept.InterceptActivity"
android:label="@string/app_name" android:label="@string/app_name"

View File

@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import exh.eh.EHentaiUpdateWorker
import exh.log.xLogE import exh.log.xLogE
import exh.log.xLogW import exh.log.xLogW
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
@ -331,6 +332,10 @@ object EXHMigrations {
} }
} }
} }
if (oldVersion under 21) {
// Setup EH updater task after migrating to WorkManager
EHentaiUpdateWorker.scheduleBackground(context)
}
// if (oldVersion under 1) { } (1 is current release version) // if (oldVersion under 1) { } (1 is current release version)
// do stuff here when releasing changed crap // do stuff here when releasing changed crap

View File

@ -192,8 +192,8 @@ object DebugFunctions {
fun convertAllExhentaiGalleriesToEhentai() = convertSources(EXH_SOURCE_ID, EH_SOURCE_ID) fun convertAllExhentaiGalleriesToEhentai() = convertSources(EXH_SOURCE_ID, EH_SOURCE_ID)
fun testLaunchEhentaiBackgroundUpdater(): String { fun testLaunchEhentaiBackgroundUpdater() {
return EHentaiUpdateWorker.launchBackgroundTest(app) EHentaiUpdateWorker.launchBackgroundTest(app)
} }
fun rescheduleEhentaiBackgroundUpdater() { fun rescheduleEhentaiBackgroundUpdater() {

View File

@ -1,12 +1,14 @@
package exh.eh package exh.eh
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.os.Build import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.elvishew.xlog.Logger import com.elvishew.xlog.Logger
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -14,7 +16,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier import eu.kanade.tachiyomi.data.library.LibraryUpdateNotifier
import eu.kanade.tachiyomi.data.preference.CHARGING
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.UNMETERED_NETWORK
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.toSChapter import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
@ -30,103 +34,35 @@ import exh.source.isEhBasedManga
import exh.util.cancellable import exh.util.cancellable
import exh.util.days import exh.util.days
import exh.util.executeOnIO import exh.util.executeOnIO
import exh.util.hours
import exh.util.jobScheduler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.ArrayList import java.util.concurrent.TimeUnit
class EHentaiUpdateWorker : JobService() {
private val scope = CoroutineScope(Dispatchers.Default + Job())
class EHentaiUpdateWorker(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val prefs: PreferencesHelper by injectLazy() private val prefs: PreferencesHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val updateHelper: EHentaiUpdateHelper by injectLazy() private val updateHelper: EHentaiUpdateHelper by injectLazy()
private val logger: Logger = xLog() private val logger: Logger = xLog()
private val updateNotifier by lazy { LibraryUpdateNotifier(this) } private val updateNotifier by lazy { LibraryUpdateNotifier(context) }
/** override suspend fun doWork(): Result {
* This method is called if the system has determined that you must stop execution of your job return try {
* even before you've had a chance to call [.jobFinished].
*
*
* This will happen if the requirements specified at schedule time are no longer met. For
* example you may have requested WiFi with
* [android.app.job.JobInfo.Builder.setRequiredNetworkType], yet while your
* job was executing the user toggled WiFi. Another example is if you had specified
* [android.app.job.JobInfo.Builder.setRequiresDeviceIdle], and the phone left its
* idle maintenance window. You are solely responsible for the behavior of your application
* upon receipt of this message; your app will likely start to misbehave if you ignore it.
*
*
* Once this method returns, the system releases the wakelock that it is holding on
* behalf of the job.
*
* @param params The parameters identifying this job, as supplied to
* the job in the [.onStartJob] callback.
* @return `true` to indicate to the JobManager whether you'd like to reschedule
* this job based on the retry criteria provided at job creation-time; or `false`
* to end the job entirely. Regardless of the value returned, your job must stop executing.
*/
override fun onStopJob(params: JobParameters?): Boolean {
runBlocking { scope.coroutineContext[Job]?.cancelAndJoin() }
return false
}
/**
* Called to indicate that the job has begun executing. Override this method with the
* logic for your job. Like all other component lifecycle callbacks, this method executes
* on your application's main thread.
*
*
* Return `true` from this method if your job needs to continue running. If you
* do this, the job remains active until you call
* [.jobFinished] to tell the system that it has completed
* its work, or until the job's required constraints are no longer satisfied. For
* example, if the job was scheduled using
* [setRequiresCharging(true)][JobInfo.Builder.setRequiresCharging],
* it will be immediately halted by the system if the user unplugs the device from power,
* the job's [.onStopJob] callback will be invoked, and the app
* will be expected to shut down all ongoing work connected with that job.
*
*
* The system holds a wakelock on behalf of your app as long as your job is executing.
* This wakelock is acquired before this method is invoked, and is not released until either
* you call [.jobFinished], or after the system invokes
* [.onStopJob] to notify your job that it is being shut down
* prematurely.
*
*
* Returning `false` from this method means your job is already finished. The
* system's wakelock for the job will be released, and [.onStopJob]
* will not be invoked.
*
* @param params Parameters specifying info about this job, including the optional
* extras configured with [ This object serves to identify this specific running job instance when calling][JobInfo.Builder.setExtras]
*/
override fun onStartJob(params: JobParameters): Boolean {
scope.launch {
startUpdating() startUpdating()
logger.d("Update job completed!") logger.d("Update job completed!")
jobFinished(params, false) Result.success()
} catch (e: Exception) {
Result.failure()
} }
return true
} }
private suspend fun startUpdating() { private suspend fun startUpdating() {
@ -165,7 +101,7 @@ class EHentaiUpdateWorker : JobService() {
var failuresThisIteration = 0 var failuresThisIteration = 0
var updatedThisIteration = 0 var updatedThisIteration = 0
val updatedManga = ArrayList<Pair<Manga, Array<Chapter>>>() val updatedManga = mutableListOf<Pair<Manga, Array<Chapter>>>()
val modifiedThisIteration = mutableSetOf<Long>() val modifiedThisIteration = mutableSetOf<Long>()
try { try {
@ -290,86 +226,50 @@ class EHentaiUpdateWorker : JobService() {
private val MIN_BACKGROUND_UPDATE_FREQ = 1.days.inWholeMilliseconds private val MIN_BACKGROUND_UPDATE_FREQ = 1.days.inWholeMilliseconds
private const val JOB_ID_UPDATE_BACKGROUND = 700000 private const val TAG = "EHBackgroundUpdater"
private const val JOB_ID_UPDATE_BACKGROUND_TEST = 700001
private val logger by lazy { XLog.tag("EHUpdaterScheduler") } private val logger by lazy { XLog.tag("EHUpdaterScheduler") }
private fun Context.componentName(): ComponentName = fun launchBackgroundTest(context: Context) {
ComponentName(this, EHentaiUpdateWorker::class.java) WorkManager.getInstance(context).enqueue(OneTimeWorkRequestBuilder<EHentaiUpdateWorker>().build())
}
private fun Context.baseBackgroundJobInfo(isTest: Boolean): JobInfo.Builder =
JobInfo.Builder(
if (isTest) JOB_ID_UPDATE_BACKGROUND_TEST
else JOB_ID_UPDATE_BACKGROUND,
componentName()
)
private fun Context.periodicBackgroundJobInfo(
period: Long,
requireCharging: Boolean,
requireUnmetered: Boolean
): JobInfo = baseBackgroundJobInfo(false)
.setPeriodic(period)
.setPersisted(true)
.setRequiredNetworkType(
if (requireUnmetered) JobInfo.NETWORK_TYPE_UNMETERED
else JobInfo.NETWORK_TYPE_ANY
)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setRequiresBatteryNotLow(true)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setEstimatedNetworkBytes(
15000L * UPDATES_PER_ITERATION,
1000L * UPDATES_PER_ITERATION
)
}
}
.setRequiresCharging(requireCharging)
// .setRequiresDeviceIdle(true) Job never seems to run with this
.build()
private fun Context.testBackgroundJobInfo(): JobInfo = baseBackgroundJobInfo(true)
.setOverrideDeadline(1)
.build()
fun launchBackgroundTest(context: Context): String =
if (context.jobScheduler.schedule(context.testBackgroundJobInfo()) == JobScheduler.RESULT_FAILURE) {
logger.e("Failed to schedule background test job!")
"Failed"
} else {
logger.d("Successfully scheduled background test job!")
"Success"
}
fun scheduleBackground(context: Context, prefInterval: Int? = null) { fun scheduleBackground(context: Context, prefInterval: Int? = null) {
cancelBackground(context)
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val duration = prefInterval ?: preferences.exhAutoUpdateFrequency().get() val interval = prefInterval ?: preferences.exhAutoUpdateFrequency().get()
if (duration > 0) { if (interval > 0) {
val restrictions = preferences.exhAutoUpdateRequirements().get() val restrictions = preferences.exhAutoUpdateRequirements().get()
val acRestriction = "ac" in restrictions val acRestriction = CHARGING in restrictions
val wifiRestriction = "wifi" in restrictions val wifiRestriction = if (UNMETERED_NETWORK in restrictions) {
NetworkType.UNMETERED
val jobInfo = context.periodicBackgroundJobInfo(
duration.hours.inWholeMilliseconds,
acRestriction,
wifiRestriction
)
if (context.jobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
logger.e("Failed to schedule background update job!")
} else { } else {
logger.d("Successfully scheduled background update job!") NetworkType.CONNECTED
} }
val constraints = Constraints.Builder()
.setRequiredNetworkType(wifiRestriction)
.setRequiresCharging(acRestriction)
.build()
val request = PeriodicWorkRequestBuilder<EHentaiUpdateWorker>(
interval.toLong(),
TimeUnit.HOURS,
10,
TimeUnit.MINUTES
)
.addTag(TAG)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
logger.d("Successfully scheduled background update job!")
} else {
cancelBackground(context)
} }
} }
fun cancelBackground(context: Context) { fun cancelBackground(context: Context) {
context.jobScheduler.cancel(JOB_ID_UPDATE_BACKGROUND) WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
} }
} }
} }