Options to run background workers only using wifi

This commit is contained in:
Koitharu
2023-07-20 09:25:12 +03:00
parent dafca9e1e1
commit 297029a659
21 changed files with 250 additions and 185 deletions

View File

@@ -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?) {

View File

@@ -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)
}
}

View File

@@ -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"

View File

@@ -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()
}
}

View File

@@ -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() }

View File

@@ -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

View File

@@ -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),
)
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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()
}
}
}

View File

@@ -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,

View File

@@ -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()
}
}

View File

@@ -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,
)
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>