Update download settings
This commit is contained in:
@@ -103,6 +103,8 @@ dependencies {
|
||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
// implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
|
||||
implementation 'com.google.guava:guava:31.1-android'
|
||||
|
||||
implementation 'androidx.room:room-runtime:2.5.1'
|
||||
implementation 'androidx.room:room-ktx:2.5.1'
|
||||
|
||||
@@ -238,8 +238,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isDownloadsSlowdownEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_DOWNLOADS_SLOWDOWN, false)
|
||||
|
||||
val downloadsParallelism: Int
|
||||
get() = prefs.getInt(KEY_DOWNLOADS_PARALLELISM, 2)
|
||||
val isDownloadsWiFiOnly: Boolean
|
||||
get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false)
|
||||
|
||||
val isSuggestionsEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
|
||||
@@ -381,8 +381,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_SHIKIMORI = "shikimori"
|
||||
const val KEY_ANILIST = "anilist"
|
||||
const val KEY_MAL = "mal"
|
||||
const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism"
|
||||
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown"
|
||||
const val KEY_DOWNLOADS_WIFI = "downloads_wifi"
|
||||
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
|
||||
const val KEY_DOH = "doh"
|
||||
const val KEY_EXIT_CONFIRM = "exit_confirm"
|
||||
|
||||
@@ -43,22 +43,22 @@ class DownloadsViewModel @Inject constructor(
|
||||
private val cacheMutex = Mutex()
|
||||
private val works = workScheduler.observeWorks()
|
||||
.mapLatest { it.toDownloadsList() }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val items = works.map {
|
||||
it.toUiList()
|
||||
it?.toUiList() ?: listOf(LoadingState)
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||
|
||||
val hasPausedWorks = works.map {
|
||||
it.any { x -> x.canResume }
|
||||
it?.any { x -> x.canResume } == true
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
|
||||
val hasActiveWorks = works.map {
|
||||
it.any { x -> x.canPause }
|
||||
it?.any { x -> x.canPause } == true
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
|
||||
val hasCancellableWorks = works.map {
|
||||
it.any { x -> !x.workState.isFinished }
|
||||
it?.any { x -> !x.workState.isFinished } == true
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
|
||||
fun cancel(id: UUID) {
|
||||
@@ -69,7 +69,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
|
||||
fun cancel(ids: Set<Long>) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val snapshot = works.value
|
||||
val snapshot = works.value ?: return@launchJob
|
||||
for (work in snapshot) {
|
||||
if (work.id.mostSignificantBits in ids) {
|
||||
workScheduler.cancel(work.id)
|
||||
@@ -85,7 +85,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun pause(ids: Set<Long>) {
|
||||
val snapshot = works.value
|
||||
val snapshot = works.value ?: return
|
||||
for (work in snapshot) {
|
||||
if (work.id.mostSignificantBits in ids) {
|
||||
workScheduler.pause(work.id)
|
||||
@@ -94,7 +94,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun pauseAll() {
|
||||
val snapshot = works.value
|
||||
val snapshot = works.value ?: return
|
||||
for (work in snapshot) {
|
||||
if (work.canPause) {
|
||||
workScheduler.pause(work.id)
|
||||
@@ -103,7 +103,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun resumeAll() {
|
||||
val snapshot = works.value
|
||||
val snapshot = works.value ?: return
|
||||
for (work in snapshot) {
|
||||
if (work.workState == WorkInfo.State.RUNNING && work.isPaused) {
|
||||
workScheduler.resume(work.id)
|
||||
@@ -112,7 +112,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun resume(ids: Set<Long>) {
|
||||
val snapshot = works.value
|
||||
val snapshot = works.value ?: return
|
||||
for (work in snapshot) {
|
||||
if (work.id.mostSignificantBits in ids) {
|
||||
workScheduler.resume(work.id)
|
||||
@@ -122,7 +122,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
|
||||
fun remove(ids: Set<Long>) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val snapshot = works.value
|
||||
val snapshot = works.value ?: return@launchJob
|
||||
for (work in snapshot) {
|
||||
if (work.id.mostSignificantBits in ids) {
|
||||
workScheduler.delete(work.id)
|
||||
@@ -138,12 +138,12 @@ class DownloadsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun snapshot(ids: Set<Long>): Collection<DownloadItemModel> {
|
||||
return works.value.filterTo(ArrayList(ids.size)) { x -> x.id.mostSignificantBits in ids }
|
||||
return works.value?.filterTo(ArrayList(ids.size)) { x -> x.id.mostSignificantBits in ids }.orEmpty()
|
||||
}
|
||||
|
||||
fun allIds(): Set<Long> = works.value.mapToSet {
|
||||
fun allIds(): Set<Long> = works.value?.mapToSet {
|
||||
it.id.mostSignificantBits
|
||||
}
|
||||
} ?: emptySet()
|
||||
|
||||
private suspend fun List<WorkInfo>.toDownloadsList(): List<DownloadItemModel> {
|
||||
if (isEmpty()) {
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.utils.WorkManagerHelper
|
||||
import org.koitharu.kotatsu.utils.ext.copyToSuspending
|
||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||
@@ -297,6 +298,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
class Scheduler @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
private val workManager: WorkManager
|
||||
@@ -349,13 +351,34 @@ class DownloadWorker @AssistedInject constructor(
|
||||
}
|
||||
|
||||
suspend fun removeCompleted() {
|
||||
workManager.pruneWork().await()
|
||||
val helper = WorkManagerHelper(workManager)
|
||||
val finishedWorks = helper.getFinishedWorkInfosByTag(TAG)
|
||||
helper.deleteWorks(finishedWorks.mapToSet { it.id })
|
||||
}
|
||||
|
||||
suspend fun updateConstraints() {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresStorageNotLow(true)
|
||||
.setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||
.build()
|
||||
val helper = WorkManagerHelper(workManager)
|
||||
val works = helper.getWorkInfosByTag(TAG)
|
||||
for (work in works) {
|
||||
if (work.state.isFinished) {
|
||||
continue
|
||||
}
|
||||
val request = OneTimeWorkRequestBuilder<DownloadWorker>()
|
||||
.setConstraints(constraints)
|
||||
.setId(work.id)
|
||||
.build()
|
||||
helper.updateWork(request)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun scheduleImpl(data: Collection<Data>) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresStorageNotLow(true)
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||
.build()
|
||||
val requests = data.map { inputData ->
|
||||
OneTimeWorkRequestBuilder<DownloadWorker>()
|
||||
@@ -364,7 +387,7 @@ class DownloadWorker @AssistedInject constructor(
|
||||
.keepResultsForAtLeast(7, TimeUnit.DAYS)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
|
||||
.setInputData(inputData)
|
||||
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
}
|
||||
workManager.enqueue(requests).await()
|
||||
|
||||
@@ -7,17 +7,20 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.parsers.util.names
|
||||
import org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
@@ -35,16 +38,12 @@ class ContentSettingsFragment :
|
||||
@Inject
|
||||
lateinit var contentCache: ContentCache
|
||||
|
||||
@Inject
|
||||
lateinit var downloadsScheduler: DownloadWorker.Scheduler
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_content)
|
||||
findPreference<Preference>(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled
|
||||
findPreference<SliderPreference>(AppSettings.KEY_DOWNLOADS_PARALLELISM)?.run {
|
||||
summary = value.toString()
|
||||
setOnPreferenceChangeListener { preference, newValue ->
|
||||
preference.summary = newValue.toString()
|
||||
true
|
||||
}
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_DOH)?.run {
|
||||
entryValues = arrayOf(
|
||||
DoHProvider.NONE,
|
||||
@@ -87,6 +86,10 @@ class ContentSettingsFragment :
|
||||
bindRemoteSourcesSummary()
|
||||
}
|
||||
|
||||
AppSettings.KEY_DOWNLOADS_WIFI -> {
|
||||
updateDownloadsConstraints()
|
||||
}
|
||||
|
||||
AppSettings.KEY_SSL_BYPASS -> {
|
||||
Snackbar.make(listView, R.string.settings_apply_restart_required, Snackbar.LENGTH_INDEFINITE).show()
|
||||
}
|
||||
@@ -126,4 +129,20 @@ class ContentSettingsFragment :
|
||||
summary = getString(R.string.enabled_d_of_d, total - settings.hiddenSources.size, total)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadsConstraints() {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_DOWNLOADS_WIFI)
|
||||
viewLifecycleScope.launch {
|
||||
try {
|
||||
preference?.isEnabled = false
|
||||
withContext(Dispatchers.Default) {
|
||||
downloadsScheduler.updateConstraints()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTraceDebug()
|
||||
} finally {
|
||||
preference?.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
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
|
||||
@@ -25,4 +29,35 @@ class WorkManagerHelper(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 updateWork(request: WorkRequest): WorkManager.UpdateResult {
|
||||
return workManagerImpl.updateWork(request).await()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,4 +440,6 @@
|
||||
<string name="paused">Paused</string>
|
||||
<string name="remove_completed">Remove completed</string>
|
||||
<string name="cancel_all">Cancel all</string>
|
||||
<string name="downloads_wifi_only">Download only via Wi-Fi</string>
|
||||
<string name="downloads_wifi_only_summary">Stop downloading when switching to a mobile network</string>
|
||||
</resources>
|
||||
|
||||
@@ -30,26 +30,6 @@
|
||||
app:useSimpleSummaryProvider="true"
|
||||
tools:isPreferenceVisible="true" />
|
||||
|
||||
<Preference
|
||||
android:key="local_storage"
|
||||
android:persistent="false"
|
||||
android:title="@string/manga_save_location"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="downloads_slowdown"
|
||||
android:summary="@string/download_slowdown_summary"
|
||||
android:title="@string/download_slowdown" />
|
||||
|
||||
<org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||
android:key="downloads_parallelism"
|
||||
android:stepSize="1"
|
||||
android:title="@string/parallel_downloads"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="5"
|
||||
app:defaultValue="2" />
|
||||
|
||||
<ListPreference
|
||||
android:entries="@array/doh_providers"
|
||||
android:key="doh"
|
||||
@@ -61,9 +41,25 @@
|
||||
android:key="ssl_bypass"
|
||||
android:title="Ignore SSL errors" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"
|
||||
android:title="@string/backup_restore"
|
||||
app:allowDividerAbove="true" />
|
||||
<PreferenceCategory android:title="@string/downloads">
|
||||
|
||||
<Preference
|
||||
android:key="local_storage"
|
||||
android:persistent="false"
|
||||
android:title="@string/manga_save_location" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="downloads_wifi"
|
||||
android:summary="@string/downloads_wifi_only_summary"
|
||||
android:title="@string/downloads_wifi_only" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="downloads_slowdown"
|
||||
android:summary="@string/download_slowdown_summary"
|
||||
android:title="@string/download_slowdown" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
android:icon="@drawable/ic_feed"
|
||||
android:title="@string/check_for_new_chapters" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"
|
||||
android:icon="@drawable/ic_backup_restore"
|
||||
android:title="@string/backup_restore" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.about.AboutSettingsFragment"
|
||||
android:icon="@drawable/ic_info_outline"
|
||||
|
||||
Reference in New Issue
Block a user