diff --git a/app/build.gradle b/app/build.gradle index 1708156e9..baf6242f6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -104,6 +104,7 @@ dependencies { //noinspection LifecycleAnnotationProcessorWithJava8 kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1' + // TODO https://issuetracker.google.com/issues/254846063 implementation 'androidx.work:work-runtime-ktx:2.8.1' //noinspection GradleDependency implementation('com.google.guava:guava:32.0.0-android') { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index 7a0e3747f..ae4926462 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -28,6 +28,7 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.reader.domain.PageLoader +import org.koitharu.kotatsu.settings.work.WorkScheduleManager import javax.inject.Inject @HiltAndroidApp @@ -51,6 +52,9 @@ class KotatsuApp : Application(), Configuration.Provider { @Inject lateinit var appValidator: AppValidator + @Inject + lateinit var workScheduleManager: WorkScheduleManager + override fun onCreate() { super.onCreate() ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString()) @@ -63,6 +67,7 @@ class KotatsuApp : Application(), Configuration.Provider { processLifecycleScope.launch(Dispatchers.Default) { setupDatabaseObservers() } + workScheduleManager.init() WorkServiceStopHelper(applicationContext).setup() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt index 95e7aaa4e..5d16e9f05 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt @@ -61,6 +61,10 @@ class WorkManagerHelper( return workManagerImpl.getWorkInfoById(id).await() } + suspend fun getUniqueWorkInfoByName(name: String): List { + return workManagerImpl.getWorkInfosForUniqueWork(name).await().orEmpty() + } + suspend fun updateWork(request: WorkRequest): WorkManager.UpdateResult { return workManagerImpl.updateWork(request).await() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt index 995ac48a7..c505d4708 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalStorageCleanupWorker.kt @@ -9,6 +9,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters +import androidx.work.await import dagger.assisted.Assisted import dagger.assisted.AssistedInject import org.koitharu.kotatsu.local.data.LocalMangaRepository @@ -33,7 +34,7 @@ class LocalStorageCleanupWorker @AssistedInject constructor( private const val TAG = "cleanup" - fun enqueue(context: Context) { + suspend fun enqueue(context: Context) { val constraints = Constraints.Builder() .setRequiresBatteryNotLow(true) .build() @@ -42,7 +43,7 @@ class LocalStorageCleanupWorker @AssistedInject constructor( .addTag(TAG) .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES) .build() - WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request) + WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request).await() } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index 395cd5b63..e2edc3295 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -69,8 +69,6 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.shelf.ui.ShelfFragment -import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker -import org.koitharu.kotatsu.tracker.work.TrackWorker import javax.inject.Inject import com.google.android.material.R as materialR @@ -321,8 +319,6 @@ class MainActivity : } } withContext(Dispatchers.Default) { - TrackWorker.setup(applicationContext) - SuggestionsWorker.setup(applicationContext) LocalStorageCleanupWorker.enqueue(applicationContext) } withResumed { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/PeriodicWorkScheduler.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/PeriodicWorkScheduler.kt new file mode 100644 index 000000000..850020ebd --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/PeriodicWorkScheduler.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.settings.work + +import android.content.Context + +interface PeriodicWorkScheduler { + + suspend fun schedule(context: Context) + + suspend fun unschedule(context: Context) + + suspend fun isScheduled(context: Context): Boolean +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/WorkScheduleManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/WorkScheduleManager.kt new file mode 100644 index 000000000..f806ab6e3 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/WorkScheduleManager.kt @@ -0,0 +1,49 @@ +package org.koitharu.kotatsu.settings.work + +import android.content.Context +import android.content.SharedPreferences +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.processLifecycleScope +import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker +import org.koitharu.kotatsu.tracker.work.TrackWorker +import javax.inject.Inject + +class WorkScheduleManager @Inject constructor( + @ApplicationContext private val context: Context, + private val settings: AppSettings, +) : SharedPreferences.OnSharedPreferenceChangeListener { + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + AppSettings.KEY_TRACKER_ENABLED -> updateWorker(TrackWorker, settings.isTrackerEnabled) + AppSettings.KEY_SUGGESTIONS -> updateWorker(SuggestionsWorker, settings.isSuggestionsEnabled) + } + } + + fun init() { + settings.subscribe(this) + processLifecycleScope.launch(Dispatchers.Default) { + updateWorkerImpl(TrackWorker, settings.isTrackerEnabled) + updateWorkerImpl(SuggestionsWorker, settings.isSuggestionsEnabled) + } + } + + private fun updateWorker(scheduler: PeriodicWorkScheduler, isEnabled: Boolean) { + processLifecycleScope.launch(Dispatchers.Default) { + updateWorkerImpl(scheduler, isEnabled) + } + } + + private suspend fun updateWorkerImpl(scheduler: PeriodicWorkScheduler, isEnabled: Boolean) { + if (scheduler.isScheduled(context) != isEnabled) { + if (isEnabled) { + scheduler.schedule(context) + } else { + scheduler.unschedule(context) + } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index f4b6a24fa..5398035a4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -23,6 +23,7 @@ import androidx.work.OutOfQuotaPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters +import androidx.work.await import androidx.work.workDataOf import coil.ImageLoader import coil.request.ImageRequest @@ -38,6 +39,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.distinctById import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.WorkManagerHelper import org.koitharu.kotatsu.core.util.ext.almostEquals import org.koitharu.kotatsu.core.util.ext.asArrayList import org.koitharu.kotatsu.core.util.ext.flatten @@ -55,6 +57,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder +import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist @@ -75,11 +78,11 @@ class SuggestionsWorker @AssistedInject constructor( ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { + trySetForeground() if (!appSettings.isSuggestionsEnabled) { suggestionRepository.clear() return Result.success() } - trySetForeground() val count = doWorkImpl() val outputData = workDataOf(DATA_COUNT to count) return Result.success(outputData) @@ -303,7 +306,7 @@ class SuggestionsWorker @AssistedInject constructor( return -1 } - companion object { + companion object : PeriodicWorkScheduler { private const val TAG = "suggestions" private const val TAG_ONESHOT = "suggestions_oneshot" @@ -324,7 +327,7 @@ class SuggestionsWorker @AssistedInject constructor( SortOrder.RATING, ) - fun setup(context: Context) { + override suspend fun schedule(context: Context) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresBatteryNotLow(true) @@ -336,6 +339,19 @@ class SuggestionsWorker @AssistedInject constructor( .build() WorkManager.getInstance(context) .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) + .await() + } + + override suspend fun unschedule(context: Context) { + WorkManager.getInstance(context) + .cancelUniqueWork(TAG) + .await() + } + + override suspend fun isScheduled(context: Context): Boolean { + return WorkManagerHelper(WorkManager.getInstance(context)) + .getUniqueWorkInfoByName(TAG) + .any { !it.state.isFinished } } fun startNow(context: Context) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index 934a19ac0..6b8f4ffa0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -26,6 +26,7 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkQuery import androidx.work.WorkerParameters +import androidx.work.await import coil.ImageLoader import coil.request.ImageRequest import dagger.assisted.Assisted @@ -42,12 +43,14 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.logs.FileLogger import org.koitharu.kotatsu.core.logs.TrackerLogger import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.WorkManagerHelper import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler import org.koitharu.kotatsu.tracker.domain.Tracker import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import java.util.concurrent.TimeUnit @@ -67,6 +70,7 @@ class TrackWorker @AssistedInject constructor( } override suspend fun doWork(): Result { + trySetForeground() logger.log("doWork()") try { return doWorkImpl() @@ -85,7 +89,6 @@ class TrackWorker @AssistedInject constructor( if (!settings.isTrackerEnabled) { return Result.success(workDataOf(0, 0)) } - trySetForeground() val tracks = tracker.getAllTracks() logger.log("Total ${tracks.size} tracks") if (tracks.isEmpty()) { @@ -234,7 +237,7 @@ class TrackWorker @AssistedInject constructor( .build() } - companion object { + companion object : PeriodicWorkScheduler { private const val WORKER_CHANNEL_ID = "track_worker" private const val WORKER_NOTIFICATION_ID = 35 @@ -244,14 +247,28 @@ class TrackWorker @AssistedInject constructor( private const val DATA_KEY_SUCCESS = "success" private const val DATA_KEY_FAILED = "failed" - fun setup(context: Context) { + override suspend fun schedule(context: Context) { val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() val request = PeriodicWorkRequestBuilder(4, TimeUnit.HOURS) .setConstraints(constraints) .addTag(TAG) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .build() - WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) + .await() + } + + override suspend fun unschedule(context: Context) { + WorkManager.getInstance(context) + .cancelUniqueWork(TAG) + .await() + } + + override suspend fun isScheduled(context: Context): Boolean { + return WorkManagerHelper(WorkManager.getInstance(context)) + .getUniqueWorkInfoByName(TAG) + .any { !it.state.isFinished } } fun startNow(context: Context) { diff --git a/app/src/main/res/layout/item_download.xml b/app/src/main/res/layout/item_download.xml index 00fac21c2..95ba3763a 100644 --- a/app/src/main/res/layout/item_download.xml +++ b/app/src/main/res/layout/item_download.xml @@ -56,7 +56,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/barrier_top" - app:trackColor="?colorPrimaryContainer" + app:trackColor="?android:colorBackground" tools:progress="25" />