Update download settings

This commit is contained in:
Koitharu
2023-05-07 09:53:22 +03:00
parent f4628f7ab5
commit 0cb1238143
9 changed files with 134 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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