Options to run background workers only using wifi
This commit is contained in:
@@ -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<WorkManager>
|
||||
|
||||
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?) {
|
||||
|
||||
@@ -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<LocalManga?>,
|
||||
): 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
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
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<WorkManager>,
|
||||
) {
|
||||
|
||||
fun setup() {
|
||||
processLifecycleScope.launch(Dispatchers.Default) {
|
||||
WorkManager.getInstance(context)
|
||||
workManagerProvider.get()
|
||||
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
|
||||
.asFlow()
|
||||
.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 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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Long>?) {
|
||||
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<UUID>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SuggestionsWorker>(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Unit>()
|
||||
val content = repository.observeTrackingLog(limit)
|
||||
.map { list ->
|
||||
@@ -70,6 +75,10 @@ class FeedViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun update() {
|
||||
scheduler.startNow()
|
||||
}
|
||||
|
||||
private fun List<TrackingLogItem>.mapList(): List<ListModel> {
|
||||
val destination = ArrayList<ListModel>((size * 1.4).toInt())
|
||||
var prevDate: DateTimeAgo? = null
|
||||
|
||||
@@ -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<TrackWorker>(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<TrackWorker>()
|
||||
.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<Boolean> {
|
||||
fun observeIsRunning(): Flow<Boolean> {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,4 +462,5 @@
|
||||
<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="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>
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:dependency="suggestions"
|
||||
android:key="suggestions_wifi"
|
||||
android:summary="@string/suggestions_wifi_only_summary"
|
||||
android:title="@string/only_using_wifi"
|
||||
app:allowDividerAbove="true" />
|
||||
android:title="@string/only_using_wifi" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
android:layout="@layout/preference_toggle_header"
|
||||
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
|
||||
android:defaultValue="@array/values_track_sources_default"
|
||||
android:dependency="tracker_enabled"
|
||||
@@ -44,4 +51,4 @@
|
||||
android:summary="@string/tracker_warning"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user