From 297029a6590d7c7793187fe7065239ab3ba4a10e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 20 Jul 2023 09:25:12 +0300 Subject: [PATCH] Options to run background workers only using wifi --- .../kotlin/org/koitharu/kotatsu/KotatsuApp.kt | 7 +- .../org/koitharu/kotatsu/core/AppModule.kt | 6 ++ .../kotatsu/core/prefs/AppSettings.kt | 4 + .../kotatsu/core/util/WorkManagerHelper.kt | 71 ---------------- .../core/util/WorkServiceStopHelper.kt | 6 +- .../kotatsu/core/util/ext/WorkManager.kt | 73 ++++++++++++++++ .../ui/worker/DownloadNotificationFactory.kt | 5 +- .../download/ui/worker/DownloadWorker.kt | 28 +++---- .../settings/SuggestionsSettingsFragment.kt | 5 +- .../settings/work/PeriodicWorkScheduler.kt | 8 +- .../settings/work/WorkScheduleManager.kt | 36 +++++--- .../suggestions/ui/SuggestionsFragment.kt | 2 +- .../suggestions/ui/SuggestionsViewModel.kt | 5 ++ .../suggestions/ui/SuggestionsWorker.kt | 83 ++++++++++--------- .../kotatsu/tracker/ui/feed/FeedFragment.kt | 13 +-- .../tracker/ui/feed/FeedMenuProvider.kt | 3 +- .../kotatsu/tracker/ui/feed/FeedViewModel.kt | 9 ++ .../kotatsu/tracker/work/TrackWorker.kt | 57 ++++++++----- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_suggestions.xml | 4 +- app/src/main/res/xml/pref_tracker.xml | 9 +- 21 files changed, 250 insertions(+), 185 deletions(-) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/WorkManager.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index 59fbbc62c..f636b4417 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.strictmode.FragmentStrictMode import androidx.hilt.work.HiltWorkerFactory import androidx.room.InvalidationTracker import androidx.work.Configuration +import androidx.work.WorkManager import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -30,6 +31,7 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.settings.work.WorkScheduleManager import javax.inject.Inject +import javax.inject.Provider @HiltAndroidApp class KotatsuApp : Application(), Configuration.Provider { @@ -55,6 +57,9 @@ class KotatsuApp : Application(), Configuration.Provider { @Inject lateinit var workScheduleManager: WorkScheduleManager + @Inject + lateinit var workManagerProvider: Provider + override fun onCreate() { super.onCreate() ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString()) @@ -68,7 +73,7 @@ class KotatsuApp : Application(), Configuration.Provider { setupDatabaseObservers() } workScheduleManager.init() - WorkServiceStopHelper(applicationContext).setup() + WorkServiceStopHelper(workManagerProvider).setup() } override fun attachBaseContext(base: Context?) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index c6d273615..8dc1dc098 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -6,6 +6,7 @@ import android.provider.SearchRecentSuggestions import android.text.Html import androidx.collection.arraySetOf import androidx.room.InvalidationTracker +import androidx.work.WorkManager import coil.ComponentRegistry import coil.ImageLoader import coil.decode.SvgDecoder @@ -172,5 +173,10 @@ interface AppModule { fun provideLocalStorageChangesFlow( @LocalStorageChanges flow: MutableSharedFlow, ): SharedFlow = flow.asSharedFlow() + + @Provides + fun provideWorkManager( + @ApplicationContext context: Context, + ): WorkManager = WorkManager.getInstance(context) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 32093ed71..6ce3e4c25 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -115,6 +115,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isTrackerEnabled: Boolean get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true) + val isTrackerWifiOnly: Boolean + get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false) + val isTrackerNotificationsEnabled: Boolean get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true) @@ -436,6 +439,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_LOCAL_STORAGE = "local_storage" const val KEY_READER_SWITCHERS = "reader_switchers" const val KEY_TRACKER_ENABLED = "tracker_enabled" + const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi" const val KEY_TRACK_SOURCES = "track_sources" const val KEY_TRACK_CATEGORIES = "track_categories" const val KEY_TRACK_WARNING = "track_warning" 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 deleted file mode 100644 index 5d16e9f05..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkManagerHelper.kt +++ /dev/null @@ -1,71 +0,0 @@ -package org.koitharu.kotatsu.core.util - -import android.annotation.SuppressLint -import androidx.work.WorkInfo -import androidx.work.WorkManager -import androidx.work.WorkQuery -import androidx.work.WorkRequest -import androidx.work.await -import androidx.work.impl.WorkManagerImpl -import java.util.UUID -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -@SuppressLint("RestrictedApi") -class WorkManagerHelper( - workManager: WorkManager, -) { - - private val workManagerImpl = workManager as WorkManagerImpl - - suspend fun deleteWork(id: UUID) = suspendCoroutine { cont -> - workManagerImpl.workTaskExecutor.executeOnTaskThread { - try { - workManagerImpl.workDatabase.workSpecDao().delete(id.toString()) - cont.resume(Unit) - } catch (e: Exception) { - cont.resumeWithException(e) - } - } - } - - suspend fun deleteWorks(ids: Collection) = suspendCoroutine { cont -> - workManagerImpl.workTaskExecutor.executeOnTaskThread { - try { - val db = workManagerImpl.workDatabase - db.runInTransaction { - for (id in ids) { - db.workSpecDao().delete(id.toString()) - } - } - cont.resume(Unit) - } catch (e: Exception) { - cont.resumeWithException(e) - } - } - } - - suspend fun getWorkInfosByTag(tag: String): List { - return workManagerImpl.getWorkInfosByTag(tag).await() - } - - suspend fun getFinishedWorkInfosByTag(tag: String): List { - val query = WorkQuery.Builder.fromTags(listOf(tag)) - .addStates(listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.CANCELLED, WorkInfo.State.FAILED)) - .build() - return workManagerImpl.getWorkInfos(query).await() - } - - suspend fun getWorkInfoById(id: UUID): WorkInfo? { - 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/core/util/WorkServiceStopHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt index 533c407a2..6d5b5344c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/WorkServiceStopHelper.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.core.util import android.annotation.SuppressLint -import android.content.Context import androidx.lifecycle.asFlow import androidx.work.WorkInfo import androidx.work.WorkManager @@ -14,6 +13,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.koitharu.kotatsu.core.util.ext.processLifecycleScope +import javax.inject.Provider /** * Workaround for issue @@ -21,12 +21,12 @@ import org.koitharu.kotatsu.core.util.ext.processLifecycleScope * https://issuetracker.google.com/issues/280504155 */ class WorkServiceStopHelper( - private val context: Context, + private val workManagerProvider: Provider, ) { fun setup() { processLifecycleScope.launch(Dispatchers.Default) { - WorkManager.getInstance(context) + workManagerProvider.get() .getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING)) .asFlow() .map { it.isEmpty() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/WorkManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/WorkManager.kt new file mode 100644 index 000000000..b46c13571 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/WorkManager.kt @@ -0,0 +1,73 @@ +package org.koitharu.kotatsu.core.util.ext + +import android.annotation.SuppressLint +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import androidx.work.WorkRequest +import androidx.work.await +import androidx.work.impl.WorkManagerImpl +import java.util.UUID +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@SuppressLint("RestrictedApi") +suspend fun WorkManager.deleteWork(id: UUID) = suspendCoroutine { cont -> + workManagerImpl.workTaskExecutor.executeOnTaskThread { + try { + workManagerImpl.workDatabase.workSpecDao().delete(id.toString()) + cont.resume(Unit) + } catch (e: Exception) { + cont.resumeWithException(e) + } + } +} + +@SuppressLint("RestrictedApi") +suspend fun WorkManager.deleteWorks(ids: Collection) = suspendCoroutine { cont -> + workManagerImpl.workTaskExecutor.executeOnTaskThread { + try { + val db = workManagerImpl.workDatabase + db.runInTransaction { + for (id in ids) { + db.workSpecDao().delete(id.toString()) + } + } + cont.resume(Unit) + } catch (e: Exception) { + cont.resumeWithException(e) + } + } +} + +@SuppressLint("RestrictedApi") +suspend fun WorkManager.awaitWorkInfosByTag(tag: String): List { + return getWorkInfosByTag(tag).await() +} + +@SuppressLint("RestrictedApi") +suspend fun WorkManager.awaitFinishedWorkInfosByTag(tag: String): List { + val query = WorkQuery.Builder.fromTags(listOf(tag)) + .addStates(listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.CANCELLED, WorkInfo.State.FAILED)) + .build() + return getWorkInfos(query).await() +} + +@SuppressLint("RestrictedApi") +suspend fun WorkManager.awaitWorkInfoById(id: UUID): WorkInfo? { + return getWorkInfoById(id).await() +} + +@SuppressLint("RestrictedApi") +suspend fun WorkManager.awaitUniqueWorkInfoByName(name: String): List { + return getWorkInfosForUniqueWork(name).await().orEmpty() +} + +@SuppressLint("RestrictedApi") +suspend fun WorkManager.awaitUpdateWork(request: WorkRequest): WorkManager.UpdateResult { + return updateWork(request).await() +} + +private val WorkManager.workManagerImpl + @SuppressLint("RestrictedApi") inline get() = this as WorkManagerImpl diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt index 41568e832..09cf65c18 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.ui.list.DownloadsActivity @@ -32,7 +33,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.search.ui.MangaListActivity -import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.util.UUID import com.google.android.material.R as materialR @@ -41,6 +41,7 @@ private const val GROUP_ID = "downloads" class DownloadNotificationFactory @AssistedInject constructor( @ApplicationContext private val context: Context, + private val workManager: WorkManager, private val coil: ImageLoader, @Assisted private val uuid: UUID, ) { @@ -67,7 +68,7 @@ class DownloadNotificationFactory @AssistedInject constructor( NotificationCompat.Action( materialR.drawable.material_ic_clear_black_24dp, context.getString(android.R.string.cancel), - WorkManager.getInstance(context).createCancelPendingIntent(uuid), + workManager.createCancelPendingIntent(uuid), ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index e74cd7c21..7bba51708 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -41,8 +41,13 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.Throttler -import org.koitharu.kotatsu.core.util.WorkManagerHelper +import org.koitharu.kotatsu.core.util.ext.awaitFinishedWorkInfosByTag +import org.koitharu.kotatsu.core.util.ext.awaitUpdateWork +import org.koitharu.kotatsu.core.util.ext.awaitWorkInfoById +import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag import org.koitharu.kotatsu.core.util.ext.deleteAwait +import org.koitharu.kotatsu.core.util.ext.deleteWork +import org.koitharu.kotatsu.core.util.ext.deleteWorks import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug @@ -313,7 +318,7 @@ class DownloadWorker @AssistedInject constructor( } private suspend fun getDoneChapters(): LongArray { - val work = WorkManagerHelper(WorkManager.getInstance(applicationContext)).getWorkInfoById(id) + val work = WorkManager.getInstance(applicationContext).awaitWorkInfoById(id) ?: return LongArray(0) return DownloadState.getDownloadedChapters(work.progress) } @@ -346,13 +351,11 @@ class DownloadWorker @AssistedInject constructor( @Reusable class Scheduler @Inject constructor( @ApplicationContext private val context: Context, + private val workManager: WorkManager, private val dataRepository: MangaDataRepository, private val settings: AppSettings, ) { - private val workManager: WorkManager - inline get() = WorkManager.getInstance(context) - suspend fun schedule(manga: Manga, chaptersIds: Collection?) { dataRepository.storeManga(manga) val data = Data.Builder() @@ -396,26 +399,23 @@ class DownloadWorker @AssistedInject constructor( } suspend fun delete(id: UUID) { - WorkManagerHelper(workManager).deleteWork(id) + workManager.deleteWork(id) } suspend fun delete(ids: Collection) { val wm = workManager - val helper = WorkManagerHelper(wm) ids.forEach { id -> wm.cancelWorkById(id).await() } - helper.deleteWorks(ids) + workManager.deleteWorks(ids) } suspend fun removeCompleted() { - val helper = WorkManagerHelper(workManager) - val finishedWorks = helper.getFinishedWorkInfosByTag(TAG) - helper.deleteWorks(finishedWorks.mapToSet { it.id }) + val finishedWorks = workManager.awaitFinishedWorkInfosByTag(TAG) + workManager.deleteWorks(finishedWorks.mapToSet { it.id }) } suspend fun updateConstraints() { val constraints = createConstraints() - val helper = WorkManagerHelper(workManager) - val works = helper.getWorkInfosByTag(TAG) + val works = workManager.awaitWorkInfosByTag(TAG) for (work in works) { if (work.state.isFinished) { continue @@ -425,7 +425,7 @@ class DownloadWorker @AssistedInject constructor( .addTag(TAG) .setId(work.id) .build() - helper.updateWork(request) + workManager.awaitUpdateWork(request) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt index f7be5bdcd..ed6f8ed14 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SuggestionsSettingsFragment.kt @@ -25,6 +25,9 @@ class SuggestionsSettingsFragment : @Inject lateinit var tagsCompletionProvider: TagsAutoCompleteProvider + @Inject + lateinit var suggestionsScheduler: SuggestionsWorker.Scheduler + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) settings.subscribe(this) @@ -53,7 +56,7 @@ class SuggestionsSettingsFragment : private fun onSuggestionsEnabled() { lifecycleScope.launch { if (repository.isEmpty()) { - SuggestionsWorker.startNow(context ?: return@launch) + suggestionsScheduler.startNow() } } } 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 index 850020ebd..5b37d5dfd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/PeriodicWorkScheduler.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/PeriodicWorkScheduler.kt @@ -1,12 +1,10 @@ package org.koitharu.kotatsu.settings.work -import android.content.Context - interface PeriodicWorkScheduler { - suspend fun schedule(context: Context) + suspend fun schedule() - suspend fun unschedule(context: Context) + suspend fun unschedule() - suspend fun isScheduled(context: Context): Boolean + suspend fun isScheduled(): 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 index f806ab6e3..a941c9847 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/WorkScheduleManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/work/WorkScheduleManager.kt @@ -1,8 +1,6 @@ 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 @@ -12,37 +10,49 @@ import org.koitharu.kotatsu.tracker.work.TrackWorker import javax.inject.Inject class WorkScheduleManager @Inject constructor( - @ApplicationContext private val context: Context, private val settings: AppSettings, + private val suggestionScheduler: SuggestionsWorker.Scheduler, + private val trackerScheduler: TrackWorker.Scheduler, ) : 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) + AppSettings.KEY_TRACKER_ENABLED, + AppSettings.KEY_TRACKER_WIFI_ONLY -> updateWorker( + scheduler = trackerScheduler, + isEnabled = settings.isTrackerEnabled, + force = key != AppSettings.KEY_TRACKER_ENABLED, + ) + + AppSettings.KEY_SUGGESTIONS, + AppSettings.KEY_SUGGESTIONS_WIFI_ONLY -> updateWorker( + scheduler = suggestionScheduler, + isEnabled = settings.isSuggestionsEnabled, + force = key != AppSettings.KEY_SUGGESTIONS, + ) } } fun init() { settings.subscribe(this) processLifecycleScope.launch(Dispatchers.Default) { - updateWorkerImpl(TrackWorker, settings.isTrackerEnabled) - updateWorkerImpl(SuggestionsWorker, settings.isSuggestionsEnabled) + updateWorkerImpl(trackerScheduler, settings.isTrackerEnabled, false) + updateWorkerImpl(suggestionScheduler, settings.isSuggestionsEnabled, false) } } - private fun updateWorker(scheduler: PeriodicWorkScheduler, isEnabled: Boolean) { + private fun updateWorker(scheduler: PeriodicWorkScheduler, isEnabled: Boolean, force: Boolean) { processLifecycleScope.launch(Dispatchers.Default) { - updateWorkerImpl(scheduler, isEnabled) + updateWorkerImpl(scheduler, isEnabled, force) } } - private suspend fun updateWorkerImpl(scheduler: PeriodicWorkScheduler, isEnabled: Boolean) { - if (scheduler.isScheduled(context) != isEnabled) { + private suspend fun updateWorkerImpl(scheduler: PeriodicWorkScheduler, isEnabled: Boolean, force: Boolean) { + if (force || scheduler.isScheduled() != isEnabled) { if (isEnabled) { - scheduler.schedule(context) + scheduler.schedule() } else { - scheduler.unschedule(context) + scheduler.unschedule() } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt index 17fdb6830..b0f2888e9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -40,7 +40,7 @@ class SuggestionsFragment : MangaListFragment() { override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { R.id.action_update -> { - SuggestionsWorker.startNow(requireContext()) + viewModel.updateSuggestions() Snackbar.make( requireViewBinding().recyclerView, R.string.feed_will_update_soon, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt index 40338a217..2f9510b30 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt @@ -28,6 +28,7 @@ class SuggestionsViewModel @Inject constructor( settings: AppSettings, private val extraProvider: ListExtraProvider, downloadScheduler: DownloadWorker.Scheduler, + private val suggestionsScheduler: SuggestionsWorker.Scheduler, ) : MangaListViewModel(settings, downloadScheduler) { override val content = combine( @@ -57,4 +58,8 @@ class SuggestionsViewModel @Inject constructor( override fun onRefresh() = Unit override fun onRetry() = Unit + + fun updateSuggestions() { + suggestionsScheduler.startNow() + } } 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 e8e2700f4..3cfc068d3 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 @@ -27,6 +27,7 @@ import androidx.work.await import androidx.work.workDataOf import coil.ImageLoader import coil.request.ImageRequest +import dagger.Reusable import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CancellationException @@ -39,9 +40,9 @@ 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.awaitUniqueWorkInfoByName import org.koitharu.kotatsu.core.util.ext.flatten import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.sanitize @@ -62,6 +63,7 @@ import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist import java.util.concurrent.TimeUnit +import javax.inject.Inject import kotlin.math.pow import kotlin.random.Random @@ -306,55 +308,36 @@ class SuggestionsWorker @AssistedInject constructor( return -1 } - companion object : PeriodicWorkScheduler { + @Reusable + class Scheduler @Inject constructor( + private val workManager: WorkManager, + private val settings: AppSettings, + ) : PeriodicWorkScheduler { - private const val TAG = "suggestions" - private const val TAG_ONESHOT = "suggestions_oneshot" - private const val DATA_COUNT = "count" - private const val WORKER_CHANNEL_ID = "suggestion_worker" - private const val MANGA_CHANNEL_ID = "suggestions" - private const val WORKER_NOTIFICATION_ID = 36 - private const val MAX_RESULTS = 80 - private const val MAX_SOURCE_RESULTS = 14 - private const val MAX_RAW_RESULTS = 200 - private const val TAG_EQ_THRESHOLD = 0.4f - private const val RATING_MIN = 0.5f - - private val preferredSortOrders = listOf( - SortOrder.UPDATED, - SortOrder.NEWEST, - SortOrder.POPULARITY, - SortOrder.RATING, - ) - - override suspend fun schedule(context: Context) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) - .setRequiresBatteryNotLow(true) - .build() + override suspend fun schedule() { val request = PeriodicWorkRequestBuilder(6, TimeUnit.HOURS) - .setConstraints(constraints) + .setConstraints(createConstraints()) .addTag(TAG) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .build() - WorkManager.getInstance(context) - .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) + workManager + .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request) .await() } - override suspend fun unschedule(context: Context) { - WorkManager.getInstance(context) + override suspend fun unschedule() { + workManager .cancelUniqueWork(TAG) .await() } - override suspend fun isScheduled(context: Context): Boolean { - return WorkManagerHelper(WorkManager.getInstance(context)) - .getUniqueWorkInfoByName(TAG) + override suspend fun isScheduled(): Boolean { + return workManager + .awaitUniqueWorkInfoByName(TAG) .any { !it.state.isFinished } } - fun startNow(context: Context) { + fun startNow() { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() @@ -363,8 +346,34 @@ class SuggestionsWorker @AssistedInject constructor( .addTag(TAG_ONESHOT) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() - WorkManager.getInstance(context) - .enqueue(request) + workManager.enqueue(request) } + + private fun createConstraints() = Constraints.Builder() + .setRequiredNetworkType(if (settings.isSuggestionsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + } + + private companion object { + + const val TAG = "suggestions" + const val TAG_ONESHOT = "suggestions_oneshot" + const val DATA_COUNT = "count" + const val WORKER_CHANNEL_ID = "suggestion_worker" + const val MANGA_CHANNEL_ID = "suggestions" + const val WORKER_NOTIFICATION_ID = 36 + const val MAX_RESULTS = 80 + const val MAX_SOURCE_RESULTS = 14 + const val MAX_RAW_RESULTS = 200 + const val TAG_EQ_THRESHOLD = 0.4f + const val RATING_MIN = 0.5f + + val preferredSortOrders = listOf( + SortOrder.UPDATED, + SortOrder.NEWEST, + SortOrder.POPULARITY, + SortOrder.RATING, + ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt index 5579069d6..f912a4647 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedFragment.kt @@ -17,7 +17,6 @@ import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.list.PaginationScrollListener import org.koitharu.kotatsu.core.ui.list.decor.TypedSpacingItemDecoration import org.koitharu.kotatsu.core.util.ext.addMenuProvider -import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.databinding.FragmentFeedBinding @@ -29,7 +28,6 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter -import org.koitharu.kotatsu.tracker.work.TrackWorker import javax.inject.Inject @AndroidEntryPoint @@ -64,11 +62,7 @@ class FeedFragment : ) addItemDecoration(decoration) } - with(binding.swipeRefreshLayout) { - setProgressBackgroundColorSchemeColor(context.getThemeColor(com.google.android.material.R.attr.colorPrimary)) - setColorSchemeColors(context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary)) - setOnRefreshListener(this@FeedFragment) - } + binding.swipeRefreshLayout.setOnRefreshListener(this) addMenuProvider( FeedMenuProvider( binding.recyclerView, @@ -81,8 +75,7 @@ class FeedFragment : viewModel.onFeedCleared.observeEvent(viewLifecycleOwner) { onFeedCleared() } - TrackWorker.observeIsRunning(binding.root.context.applicationContext) - .observe(viewLifecycleOwner, this::onIsTrackerRunningChanged) + viewModel.isRunning.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged) } override fun onDestroyView() { @@ -97,7 +90,7 @@ class FeedFragment : } override fun onRefresh() { - TrackWorker.startNow(context ?: return) + viewModel.update() } override fun onRetryClick(error: Throwable) = Unit diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt index 14fffb485..b0d80db73 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedMenuProvider.kt @@ -8,7 +8,6 @@ import android.view.View import androidx.core.view.MenuProvider import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.dialog.CheckBoxAlertDialog -import org.koitharu.kotatsu.tracker.work.TrackWorker class FeedMenuProvider( private val snackbarHost: View, @@ -24,7 +23,7 @@ class FeedMenuProvider( override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { R.id.action_update -> { - TrackWorker.startNow(context) + viewModel.update() true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt index f54e551ef..49c033704 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/feed/FeedViewModel.kt @@ -21,6 +21,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.ui.feed.model.toFeedItem +import org.koitharu.kotatsu.tracker.work.TrackWorker import java.util.Date import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -31,11 +32,15 @@ private const val PAGE_SIZE = 20 @HiltViewModel class FeedViewModel @Inject constructor( private val repository: TrackingRepository, + private val scheduler: TrackWorker.Scheduler, ) : BaseViewModel() { private val limit = MutableStateFlow(PAGE_SIZE) private val isReady = AtomicBoolean(false) + val isRunning = scheduler.observeIsRunning() + .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) + val onFeedCleared = MutableEventFlow() val content = repository.observeTrackingLog(limit) .map { list -> @@ -70,6 +75,10 @@ class FeedViewModel @Inject constructor( } } + fun update() { + scheduler.startNow() + } + private fun List.mapList(): List { val destination = ArrayList((size * 1.4).toInt()) var prevDate: DateTimeAgo? = null 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 2880969dc..a9d714841 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 @@ -29,6 +29,7 @@ import androidx.work.WorkerParameters import androidx.work.await import coil.ImageLoader import coil.request.ImageRequest +import dagger.Reusable import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers @@ -43,7 +44,7 @@ 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.awaitUniqueWorkInfoByName import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.details.ui.DetailsActivity @@ -54,6 +55,7 @@ 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 +import javax.inject.Inject @HiltWorker class TrackWorker @AssistedInject constructor( @@ -237,57 +239,68 @@ class TrackWorker @AssistedInject constructor( .build() } - companion object : PeriodicWorkScheduler { + @Reusable + class Scheduler @Inject constructor( + private val workManager: WorkManager, + private val settings: AppSettings, + ) : PeriodicWorkScheduler { - private const val WORKER_CHANNEL_ID = "track_worker" - private const val WORKER_NOTIFICATION_ID = 35 - private const val TAG = "tracking" - private const val TAG_ONESHOT = "tracking_oneshot" - private const val MAX_PARALLELISM = 4 - private const val DATA_KEY_SUCCESS = "success" - private const val DATA_KEY_FAILED = "failed" - - override suspend fun schedule(context: Context) { - val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() + override suspend fun schedule() { + val constraints = createConstraints() val request = PeriodicWorkRequestBuilder(4, TimeUnit.HOURS) .setConstraints(constraints) .addTag(TAG) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .build() - WorkManager.getInstance(context) + workManager .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) .await() } - override suspend fun unschedule(context: Context) { - WorkManager.getInstance(context) + override suspend fun unschedule() { + workManager .cancelUniqueWork(TAG) .await() } - override suspend fun isScheduled(context: Context): Boolean { - return WorkManagerHelper(WorkManager.getInstance(context)) - .getUniqueWorkInfoByName(TAG) + override suspend fun isScheduled(): Boolean { + return workManager + .awaitUniqueWorkInfoByName(TAG) .any { !it.state.isFinished } } - fun startNow(context: Context) { + fun startNow() { val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() val request = OneTimeWorkRequestBuilder() .setConstraints(constraints) .addTag(TAG_ONESHOT) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() - WorkManager.getInstance(context).enqueue(request) + workManager.enqueue(request) } - fun observeIsRunning(context: Context): Flow { + fun observeIsRunning(): Flow { val query = WorkQuery.Builder.fromTags(listOf(TAG, TAG_ONESHOT)).build() - return WorkManager.getInstance(context).getWorkInfosLiveData(query) + return workManager.getWorkInfosLiveData(query) .asFlow() .map { works -> works.any { x -> x.state == WorkInfo.State.RUNNING } } } + + private fun createConstraints() = Constraints.Builder() + .setRequiredNetworkType(if (settings.isTrackerWifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED) + .build() + } + + private companion object { + + const val WORKER_CHANNEL_ID = "track_worker" + const val WORKER_NOTIFICATION_ID = 35 + const val TAG = "tracking" + const val TAG_ONESHOT = "tracking_oneshot" + const val MAX_PARALLELISM = 4 + const val DATA_KEY_SUCCESS = "success" + const val DATA_KEY_FAILED = "failed" } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c87cea4bf..0599bcb84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -462,4 +462,5 @@ Make sure you have selected the correct backup file Manage favourites Do not update suggestions using metered network connections + Do not check for new chapters using metered network connections diff --git a/app/src/main/res/xml/pref_suggestions.xml b/app/src/main/res/xml/pref_suggestions.xml index 9c9353e3c..6c000cf11 100644 --- a/app/src/main/res/xml/pref_suggestions.xml +++ b/app/src/main/res/xml/pref_suggestions.xml @@ -11,10 +11,10 @@ + android:title="@string/only_using_wifi" /> + + - \ No newline at end of file +