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.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>
|
||||||
|
|||||||
Reference in New Issue
Block a user