Options to run background workers only using wifi

pull/440/head
Koitharu 3 years ago
parent dafca9e1e1
commit 297029a659
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -9,6 +9,7 @@ import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import androidx.work.Configuration import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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.reader.domain.PageLoader
import org.koitharu.kotatsu.settings.work.WorkScheduleManager import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
@HiltAndroidApp @HiltAndroidApp
class KotatsuApp : Application(), Configuration.Provider { class KotatsuApp : Application(), Configuration.Provider {
@ -55,6 +57,9 @@ class KotatsuApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var workScheduleManager: WorkScheduleManager lateinit var workScheduleManager: WorkScheduleManager
@Inject
lateinit var workManagerProvider: Provider<WorkManager>
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString()) ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
@ -68,7 +73,7 @@ class KotatsuApp : Application(), Configuration.Provider {
setupDatabaseObservers() setupDatabaseObservers()
} }
workScheduleManager.init() workScheduleManager.init()
WorkServiceStopHelper(applicationContext).setup() WorkServiceStopHelper(workManagerProvider).setup()
} }
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {

@ -6,6 +6,7 @@ import android.provider.SearchRecentSuggestions
import android.text.Html import android.text.Html
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import androidx.work.WorkManager
import coil.ComponentRegistry import coil.ComponentRegistry
import coil.ImageLoader import coil.ImageLoader
import coil.decode.SvgDecoder import coil.decode.SvgDecoder
@ -172,5 +173,10 @@ interface AppModule {
fun provideLocalStorageChangesFlow( fun provideLocalStorageChangesFlow(
@LocalStorageChanges flow: MutableSharedFlow<LocalManga?>, @LocalStorageChanges flow: MutableSharedFlow<LocalManga?>,
): SharedFlow<LocalManga?> = flow.asSharedFlow() ): SharedFlow<LocalManga?> = flow.asSharedFlow()
@Provides
fun provideWorkManager(
@ApplicationContext context: Context,
): WorkManager = WorkManager.getInstance(context)
} }
} }

@ -115,6 +115,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isTrackerEnabled: Boolean val isTrackerEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true) get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true)
val isTrackerWifiOnly: Boolean
get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false)
val isTrackerNotificationsEnabled: Boolean val isTrackerNotificationsEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true) 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_LOCAL_STORAGE = "local_storage"
const val KEY_READER_SWITCHERS = "reader_switchers" const val KEY_READER_SWITCHERS = "reader_switchers"
const val KEY_TRACKER_ENABLED = "tracker_enabled" 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_SOURCES = "track_sources"
const val KEY_TRACK_CATEGORIES = "track_categories" const val KEY_TRACK_CATEGORIES = "track_categories"
const val KEY_TRACK_WARNING = "track_warning" const val KEY_TRACK_WARNING = "track_warning"

@ -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<UUID>) = 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<WorkInfo> {
return workManagerImpl.getWorkInfosByTag(tag).await()
}
suspend fun getFinishedWorkInfosByTag(tag: String): List<WorkInfo> {
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<WorkInfo> {
return workManagerImpl.getWorkInfosForUniqueWork(name).await().orEmpty()
}
suspend fun updateWork(request: WorkRequest): WorkManager.UpdateResult {
return workManagerImpl.updateWork(request).await()
}
}

@ -1,7 +1,6 @@
package org.koitharu.kotatsu.core.util package org.koitharu.kotatsu.core.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
@ -14,6 +13,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import javax.inject.Provider
/** /**
* Workaround for issue * Workaround for issue
@ -21,12 +21,12 @@ import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
* https://issuetracker.google.com/issues/280504155 * https://issuetracker.google.com/issues/280504155
*/ */
class WorkServiceStopHelper( class WorkServiceStopHelper(
private val context: Context, private val workManagerProvider: Provider<WorkManager>,
) { ) {
fun setup() { fun setup() {
processLifecycleScope.launch(Dispatchers.Default) { processLifecycleScope.launch(Dispatchers.Default) {
WorkManager.getInstance(context) workManagerProvider.get()
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING)) .getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.asFlow() .asFlow()
.map { it.isEmpty() } .map { it.isEmpty() }

@ -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<UUID>) = 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<WorkInfo> {
return getWorkInfosByTag(tag).await()
}
@SuppressLint("RestrictedApi")
suspend fun WorkManager.awaitFinishedWorkInfosByTag(tag: String): List<WorkInfo> {
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<WorkInfo> {
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

@ -24,6 +24,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow 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.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity 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.format
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.util.UUID import java.util.UUID
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@ -41,6 +41,7 @@ private const val GROUP_ID = "downloads"
class DownloadNotificationFactory @AssistedInject constructor( class DownloadNotificationFactory @AssistedInject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val workManager: WorkManager,
private val coil: ImageLoader, private val coil: ImageLoader,
@Assisted private val uuid: UUID, @Assisted private val uuid: UUID,
) { ) {
@ -67,7 +68,7 @@ class DownloadNotificationFactory @AssistedInject constructor(
NotificationCompat.Action( NotificationCompat.Action(
materialR.drawable.material_ic_clear_black_24dp, materialR.drawable.material_ic_clear_black_24dp,
context.getString(android.R.string.cancel), context.getString(android.R.string.cancel),
WorkManager.getInstance(context).createCancelPendingIntent(uuid), workManager.createCancelPendingIntent(uuid),
) )
} }

@ -41,8 +41,13 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.Throttler 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.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.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@ -313,7 +318,7 @@ class DownloadWorker @AssistedInject constructor(
} }
private suspend fun getDoneChapters(): LongArray { private suspend fun getDoneChapters(): LongArray {
val work = WorkManagerHelper(WorkManager.getInstance(applicationContext)).getWorkInfoById(id) val work = WorkManager.getInstance(applicationContext).awaitWorkInfoById(id)
?: return LongArray(0) ?: return LongArray(0)
return DownloadState.getDownloadedChapters(work.progress) return DownloadState.getDownloadedChapters(work.progress)
} }
@ -346,13 +351,11 @@ class DownloadWorker @AssistedInject constructor(
@Reusable @Reusable
class Scheduler @Inject constructor( class Scheduler @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val workManager: WorkManager,
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val settings: AppSettings, private val settings: AppSettings,
) { ) {
private val workManager: WorkManager
inline get() = WorkManager.getInstance(context)
suspend fun schedule(manga: Manga, chaptersIds: Collection<Long>?) { suspend fun schedule(manga: Manga, chaptersIds: Collection<Long>?) {
dataRepository.storeManga(manga) dataRepository.storeManga(manga)
val data = Data.Builder() val data = Data.Builder()
@ -396,26 +399,23 @@ class DownloadWorker @AssistedInject constructor(
} }
suspend fun delete(id: UUID) { suspend fun delete(id: UUID) {
WorkManagerHelper(workManager).deleteWork(id) workManager.deleteWork(id)
} }
suspend fun delete(ids: Collection<UUID>) { suspend fun delete(ids: Collection<UUID>) {
val wm = workManager val wm = workManager
val helper = WorkManagerHelper(wm)
ids.forEach { id -> wm.cancelWorkById(id).await() } ids.forEach { id -> wm.cancelWorkById(id).await() }
helper.deleteWorks(ids) workManager.deleteWorks(ids)
} }
suspend fun removeCompleted() { suspend fun removeCompleted() {
val helper = WorkManagerHelper(workManager) val finishedWorks = workManager.awaitFinishedWorkInfosByTag(TAG)
val finishedWorks = helper.getFinishedWorkInfosByTag(TAG) workManager.deleteWorks(finishedWorks.mapToSet { it.id })
helper.deleteWorks(finishedWorks.mapToSet { it.id })
} }
suspend fun updateConstraints() { suspend fun updateConstraints() {
val constraints = createConstraints() val constraints = createConstraints()
val helper = WorkManagerHelper(workManager) val works = workManager.awaitWorkInfosByTag(TAG)
val works = helper.getWorkInfosByTag(TAG)
for (work in works) { for (work in works) {
if (work.state.isFinished) { if (work.state.isFinished) {
continue continue
@ -425,7 +425,7 @@ class DownloadWorker @AssistedInject constructor(
.addTag(TAG) .addTag(TAG)
.setId(work.id) .setId(work.id)
.build() .build()
helper.updateWork(request) workManager.awaitUpdateWork(request)
} }
} }

@ -25,6 +25,9 @@ class SuggestionsSettingsFragment :
@Inject @Inject
lateinit var tagsCompletionProvider: TagsAutoCompleteProvider lateinit var tagsCompletionProvider: TagsAutoCompleteProvider
@Inject
lateinit var suggestionsScheduler: SuggestionsWorker.Scheduler
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
settings.subscribe(this) settings.subscribe(this)
@ -53,7 +56,7 @@ class SuggestionsSettingsFragment :
private fun onSuggestionsEnabled() { private fun onSuggestionsEnabled() {
lifecycleScope.launch { lifecycleScope.launch {
if (repository.isEmpty()) { if (repository.isEmpty()) {
SuggestionsWorker.startNow(context ?: return@launch) suggestionsScheduler.startNow()
} }
} }
} }

@ -1,12 +1,10 @@
package org.koitharu.kotatsu.settings.work package org.koitharu.kotatsu.settings.work
import android.content.Context
interface PeriodicWorkScheduler { 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
} }

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.settings.work package org.koitharu.kotatsu.settings.work
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@ -12,37 +10,49 @@ import org.koitharu.kotatsu.tracker.work.TrackWorker
import javax.inject.Inject import javax.inject.Inject
class WorkScheduleManager @Inject constructor( class WorkScheduleManager @Inject constructor(
@ApplicationContext private val context: Context,
private val settings: AppSettings, private val settings: AppSettings,
private val suggestionScheduler: SuggestionsWorker.Scheduler,
private val trackerScheduler: TrackWorker.Scheduler,
) : SharedPreferences.OnSharedPreferenceChangeListener { ) : SharedPreferences.OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
AppSettings.KEY_TRACKER_ENABLED -> updateWorker(TrackWorker, settings.isTrackerEnabled) AppSettings.KEY_TRACKER_ENABLED,
AppSettings.KEY_SUGGESTIONS -> updateWorker(SuggestionsWorker, settings.isSuggestionsEnabled) 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() { fun init() {
settings.subscribe(this) settings.subscribe(this)
processLifecycleScope.launch(Dispatchers.Default) { processLifecycleScope.launch(Dispatchers.Default) {
updateWorkerImpl(TrackWorker, settings.isTrackerEnabled) updateWorkerImpl(trackerScheduler, settings.isTrackerEnabled, false)
updateWorkerImpl(SuggestionsWorker, settings.isSuggestionsEnabled) 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) { processLifecycleScope.launch(Dispatchers.Default) {
updateWorkerImpl(scheduler, isEnabled) updateWorkerImpl(scheduler, isEnabled, force)
} }
} }
private suspend fun updateWorkerImpl(scheduler: PeriodicWorkScheduler, isEnabled: Boolean) { private suspend fun updateWorkerImpl(scheduler: PeriodicWorkScheduler, isEnabled: Boolean, force: Boolean) {
if (scheduler.isScheduled(context) != isEnabled) { if (force || scheduler.isScheduled() != isEnabled) {
if (isEnabled) { if (isEnabled) {
scheduler.schedule(context) scheduler.schedule()
} else { } else {
scheduler.unschedule(context) scheduler.unschedule()
} }
} }
} }

@ -40,7 +40,7 @@ class SuggestionsFragment : MangaListFragment() {
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_update -> { R.id.action_update -> {
SuggestionsWorker.startNow(requireContext()) viewModel.updateSuggestions()
Snackbar.make( Snackbar.make(
requireViewBinding().recyclerView, requireViewBinding().recyclerView,
R.string.feed_will_update_soon, R.string.feed_will_update_soon,

@ -28,6 +28,7 @@ class SuggestionsViewModel @Inject constructor(
settings: AppSettings, settings: AppSettings,
private val extraProvider: ListExtraProvider, private val extraProvider: ListExtraProvider,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
private val suggestionsScheduler: SuggestionsWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler) { ) : MangaListViewModel(settings, downloadScheduler) {
override val content = combine( override val content = combine(
@ -57,4 +58,8 @@ class SuggestionsViewModel @Inject constructor(
override fun onRefresh() = Unit override fun onRefresh() = Unit
override fun onRetry() = Unit override fun onRetry() = Unit
fun updateSuggestions() {
suggestionsScheduler.startNow()
}
} }

@ -27,6 +27,7 @@ import androidx.work.await
import androidx.work.workDataOf import androidx.work.workDataOf
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dagger.Reusable
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException 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.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings 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.almostEquals
import org.koitharu.kotatsu.core.util.ext.asArrayList 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.flatten
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sanitize 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.SuggestionRepository
import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.pow import kotlin.math.pow
import kotlin.random.Random import kotlin.random.Random
@ -306,55 +308,36 @@ class SuggestionsWorker @AssistedInject constructor(
return -1 return -1
} }
companion object : PeriodicWorkScheduler { @Reusable
class Scheduler @Inject constructor(
private val workManager: WorkManager,
private val settings: AppSettings,
) : PeriodicWorkScheduler {
private const val TAG = "suggestions" override suspend fun schedule() {
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()
val request = PeriodicWorkRequestBuilder<SuggestionsWorker>(6, TimeUnit.HOURS) val request = PeriodicWorkRequestBuilder<SuggestionsWorker>(6, TimeUnit.HOURS)
.setConstraints(constraints) .setConstraints(createConstraints())
.addTag(TAG) .addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
.build() .build()
WorkManager.getInstance(context) workManager
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
.await() .await()
} }
override suspend fun unschedule(context: Context) { override suspend fun unschedule() {
WorkManager.getInstance(context) workManager
.cancelUniqueWork(TAG) .cancelUniqueWork(TAG)
.await() .await()
} }
override suspend fun isScheduled(context: Context): Boolean { override suspend fun isScheduled(): Boolean {
return WorkManagerHelper(WorkManager.getInstance(context)) return workManager
.getUniqueWorkInfoByName(TAG) .awaitUniqueWorkInfoByName(TAG)
.any { !it.state.isFinished } .any { !it.state.isFinished }
} }
fun startNow(context: Context) { fun startNow() {
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(NetworkType.CONNECTED)
.build() .build()
@ -363,8 +346,34 @@ class SuggestionsWorker @AssistedInject constructor(
.addTag(TAG_ONESHOT) .addTag(TAG_ONESHOT)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build() .build()
WorkManager.getInstance(context) workManager.enqueue(request)
.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,
)
} }
} }

@ -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.PaginationScrollListener
import org.koitharu.kotatsu.core.ui.list.decor.TypedSpacingItemDecoration import org.koitharu.kotatsu.core.ui.list.decor.TypedSpacingItemDecoration
import org.koitharu.kotatsu.core.util.ext.addMenuProvider 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.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentFeedBinding 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter import org.koitharu.kotatsu.tracker.ui.feed.adapter.FeedAdapter
import org.koitharu.kotatsu.tracker.work.TrackWorker
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -64,11 +62,7 @@ class FeedFragment :
) )
addItemDecoration(decoration) addItemDecoration(decoration)
} }
with(binding.swipeRefreshLayout) { binding.swipeRefreshLayout.setOnRefreshListener(this)
setProgressBackgroundColorSchemeColor(context.getThemeColor(com.google.android.material.R.attr.colorPrimary))
setColorSchemeColors(context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary))
setOnRefreshListener(this@FeedFragment)
}
addMenuProvider( addMenuProvider(
FeedMenuProvider( FeedMenuProvider(
binding.recyclerView, binding.recyclerView,
@ -81,8 +75,7 @@ class FeedFragment :
viewModel.onFeedCleared.observeEvent(viewLifecycleOwner) { viewModel.onFeedCleared.observeEvent(viewLifecycleOwner) {
onFeedCleared() onFeedCleared()
} }
TrackWorker.observeIsRunning(binding.root.context.applicationContext) viewModel.isRunning.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged)
.observe(viewLifecycleOwner, this::onIsTrackerRunningChanged)
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -97,7 +90,7 @@ class FeedFragment :
} }
override fun onRefresh() { override fun onRefresh() {
TrackWorker.startNow(context ?: return) viewModel.update()
} }
override fun onRetryClick(error: Throwable) = Unit override fun onRetryClick(error: Throwable) = Unit

@ -8,7 +8,6 @@ import android.view.View
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.core.ui.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.tracker.work.TrackWorker
class FeedMenuProvider( class FeedMenuProvider(
private val snackbarHost: View, private val snackbarHost: View,
@ -24,7 +23,7 @@ class FeedMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_update -> { R.id.action_update -> {
TrackWorker.startNow(context) viewModel.update()
true true
} }

@ -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.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.ui.feed.model.toFeedItem import org.koitharu.kotatsu.tracker.ui.feed.model.toFeedItem
import org.koitharu.kotatsu.tracker.work.TrackWorker
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -31,11 +32,15 @@ private const val PAGE_SIZE = 20
@HiltViewModel @HiltViewModel
class FeedViewModel @Inject constructor( class FeedViewModel @Inject constructor(
private val repository: TrackingRepository, private val repository: TrackingRepository,
private val scheduler: TrackWorker.Scheduler,
) : BaseViewModel() { ) : BaseViewModel() {
private val limit = MutableStateFlow(PAGE_SIZE) private val limit = MutableStateFlow(PAGE_SIZE)
private val isReady = AtomicBoolean(false) private val isReady = AtomicBoolean(false)
val isRunning = scheduler.observeIsRunning()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val onFeedCleared = MutableEventFlow<Unit>() val onFeedCleared = MutableEventFlow<Unit>()
val content = repository.observeTrackingLog(limit) val content = repository.observeTrackingLog(limit)
.map { list -> .map { list ->
@ -70,6 +75,10 @@ class FeedViewModel @Inject constructor(
} }
} }
fun update() {
scheduler.startNow()
}
private fun List<TrackingLogItem>.mapList(): List<ListModel> { private fun List<TrackingLogItem>.mapList(): List<ListModel> {
val destination = ArrayList<ListModel>((size * 1.4).toInt()) val destination = ArrayList<ListModel>((size * 1.4).toInt())
var prevDate: DateTimeAgo? = null var prevDate: DateTimeAgo? = null

@ -29,6 +29,7 @@ import androidx.work.WorkerParameters
import androidx.work.await import androidx.work.await
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dagger.Reusable
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers 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.FileLogger
import org.koitharu.kotatsu.core.logs.TrackerLogger import org.koitharu.kotatsu.core.logs.TrackerLogger
import org.koitharu.kotatsu.core.prefs.AppSettings 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.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.core.util.ext.trySetForeground
import org.koitharu.kotatsu.details.ui.DetailsActivity 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.Tracker
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltWorker @HiltWorker
class TrackWorker @AssistedInject constructor( class TrackWorker @AssistedInject constructor(
@ -237,57 +239,68 @@ class TrackWorker @AssistedInject constructor(
.build() .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" override suspend fun schedule() {
private const val WORKER_NOTIFICATION_ID = 35 val constraints = createConstraints()
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()
val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS) val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS)
.setConstraints(constraints) .setConstraints(constraints)
.addTag(TAG) .addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
.build() .build()
WorkManager.getInstance(context) workManager
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
.await() .await()
} }
override suspend fun unschedule(context: Context) { override suspend fun unschedule() {
WorkManager.getInstance(context) workManager
.cancelUniqueWork(TAG) .cancelUniqueWork(TAG)
.await() .await()
} }
override suspend fun isScheduled(context: Context): Boolean { override suspend fun isScheduled(): Boolean {
return WorkManagerHelper(WorkManager.getInstance(context)) return workManager
.getUniqueWorkInfoByName(TAG) .awaitUniqueWorkInfoByName(TAG)
.any { !it.state.isFinished } .any { !it.state.isFinished }
} }
fun startNow(context: Context) { fun startNow() {
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val request = OneTimeWorkRequestBuilder<TrackWorker>() val request = OneTimeWorkRequestBuilder<TrackWorker>()
.setConstraints(constraints) .setConstraints(constraints)
.addTag(TAG_ONESHOT) .addTag(TAG_ONESHOT)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build() .build()
WorkManager.getInstance(context).enqueue(request) workManager.enqueue(request)
} }
fun observeIsRunning(context: Context): Flow<Boolean> { fun observeIsRunning(): Flow<Boolean> {
val query = WorkQuery.Builder.fromTags(listOf(TAG, TAG_ONESHOT)).build() val query = WorkQuery.Builder.fromTags(listOf(TAG, TAG_ONESHOT)).build()
return WorkManager.getInstance(context).getWorkInfosLiveData(query) return workManager.getWorkInfosLiveData(query)
.asFlow() .asFlow()
.map { works -> .map { works ->
works.any { x -> x.state == WorkInfo.State.RUNNING } 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"
} }
} }

@ -462,4 +462,5 @@
<string name="data_not_restored_text">Make sure you have selected the correct backup file</string> <string name="data_not_restored_text">Make sure you have selected the correct backup file</string>
<string name="manage_favourites">Manage favourites</string> <string name="manage_favourites">Manage favourites</string>
<string name="suggestions_wifi_only_summary">Do not update suggestions using metered network connections</string> <string name="suggestions_wifi_only_summary">Do not update suggestions using metered network connections</string>
<string name="tracker_wifi_only_summary">Do not check for new chapters using metered network connections</string>
</resources> </resources>

@ -11,10 +11,10 @@
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:dependency="suggestions"
android:key="suggestions_wifi" android:key="suggestions_wifi"
android:summary="@string/suggestions_wifi_only_summary" android:summary="@string/suggestions_wifi_only_summary"
android:title="@string/only_using_wifi" android:title="@string/only_using_wifi" />
app:allowDividerAbove="true" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"

@ -9,6 +9,13 @@
android:layout="@layout/preference_toggle_header" android:layout="@layout/preference_toggle_header"
android:title="@string/check_new_chapters_title" /> android:title="@string/check_new_chapters_title" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:dependency="tracker_enabled"
android:key="tracker_wifi"
android:summary="@string/tracker_wifi_only_summary"
android:title="@string/only_using_wifi" />
<MultiSelectListPreference <MultiSelectListPreference
android:defaultValue="@array/values_track_sources_default" android:defaultValue="@array/values_track_sources_default"
android:dependency="tracker_enabled" android:dependency="tracker_enabled"
@ -44,4 +51,4 @@
android:summary="@string/tracker_warning" android:summary="@string/tracker_warning"
app:allowDividerAbove="true" /> app:allowDividerAbove="true" />
</PreferenceScreen> </PreferenceScreen>

Loading…
Cancel
Save