Update download settings

pull/359/head
Koitharu 3 years ago
parent f4628f7ab5
commit 0cb1238143
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -103,6 +103,8 @@ dependencies {
//noinspection LifecycleAnnotationProcessorWithJava8 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1' kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
implementation 'androidx.work:work-runtime-ktx:2.8.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-runtime:2.5.1'
implementation 'androidx.room:room-ktx: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 val isDownloadsSlowdownEnabled: Boolean
get() = prefs.getBoolean(KEY_DOWNLOADS_SLOWDOWN, false) get() = prefs.getBoolean(KEY_DOWNLOADS_SLOWDOWN, false)
val downloadsParallelism: Int val isDownloadsWiFiOnly: Boolean
get() = prefs.getInt(KEY_DOWNLOADS_PARALLELISM, 2) get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false)
val isSuggestionsEnabled: Boolean val isSuggestionsEnabled: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS, false) 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_SHIKIMORI = "shikimori"
const val KEY_ANILIST = "anilist" const val KEY_ANILIST = "anilist"
const val KEY_MAL = "mal" const val KEY_MAL = "mal"
const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism"
const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown" 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_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
const val KEY_DOH = "doh" const val KEY_DOH = "doh"
const val KEY_EXIT_CONFIRM = "exit_confirm" const val KEY_EXIT_CONFIRM = "exit_confirm"

@ -43,22 +43,22 @@ class DownloadsViewModel @Inject constructor(
private val cacheMutex = Mutex() private val cacheMutex = Mutex()
private val works = workScheduler.observeWorks() private val works = workScheduler.observeWorks()
.mapLatest { it.toDownloadsList() } .mapLatest { it.toDownloadsList() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList()) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val items = works.map { val items = works.map {
it.toUiList() it?.toUiList() ?: listOf(LoadingState)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
val hasPausedWorks = works.map { val hasPausedWorks = works.map {
it.any { x -> x.canResume } it?.any { x -> x.canResume } == true
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
val hasActiveWorks = works.map { val hasActiveWorks = works.map {
it.any { x -> x.canPause } it?.any { x -> x.canPause } == true
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
val hasCancellableWorks = works.map { val hasCancellableWorks = works.map {
it.any { x -> !x.workState.isFinished } it?.any { x -> !x.workState.isFinished } == true
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
fun cancel(id: UUID) { fun cancel(id: UUID) {
@ -69,7 +69,7 @@ class DownloadsViewModel @Inject constructor(
fun cancel(ids: Set<Long>) { fun cancel(ids: Set<Long>) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val snapshot = works.value val snapshot = works.value ?: return@launchJob
for (work in snapshot) { for (work in snapshot) {
if (work.id.mostSignificantBits in ids) { if (work.id.mostSignificantBits in ids) {
workScheduler.cancel(work.id) workScheduler.cancel(work.id)
@ -85,7 +85,7 @@ class DownloadsViewModel @Inject constructor(
} }
fun pause(ids: Set<Long>) { fun pause(ids: Set<Long>) {
val snapshot = works.value val snapshot = works.value ?: return
for (work in snapshot) { for (work in snapshot) {
if (work.id.mostSignificantBits in ids) { if (work.id.mostSignificantBits in ids) {
workScheduler.pause(work.id) workScheduler.pause(work.id)
@ -94,7 +94,7 @@ class DownloadsViewModel @Inject constructor(
} }
fun pauseAll() { fun pauseAll() {
val snapshot = works.value val snapshot = works.value ?: return
for (work in snapshot) { for (work in snapshot) {
if (work.canPause) { if (work.canPause) {
workScheduler.pause(work.id) workScheduler.pause(work.id)
@ -103,7 +103,7 @@ class DownloadsViewModel @Inject constructor(
} }
fun resumeAll() { fun resumeAll() {
val snapshot = works.value val snapshot = works.value ?: return
for (work in snapshot) { for (work in snapshot) {
if (work.workState == WorkInfo.State.RUNNING && work.isPaused) { if (work.workState == WorkInfo.State.RUNNING && work.isPaused) {
workScheduler.resume(work.id) workScheduler.resume(work.id)
@ -112,7 +112,7 @@ class DownloadsViewModel @Inject constructor(
} }
fun resume(ids: Set<Long>) { fun resume(ids: Set<Long>) {
val snapshot = works.value val snapshot = works.value ?: return
for (work in snapshot) { for (work in snapshot) {
if (work.id.mostSignificantBits in ids) { if (work.id.mostSignificantBits in ids) {
workScheduler.resume(work.id) workScheduler.resume(work.id)
@ -122,7 +122,7 @@ class DownloadsViewModel @Inject constructor(
fun remove(ids: Set<Long>) { fun remove(ids: Set<Long>) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val snapshot = works.value val snapshot = works.value ?: return@launchJob
for (work in snapshot) { for (work in snapshot) {
if (work.id.mostSignificantBits in ids) { if (work.id.mostSignificantBits in ids) {
workScheduler.delete(work.id) workScheduler.delete(work.id)
@ -138,12 +138,12 @@ class DownloadsViewModel @Inject constructor(
} }
fun snapshot(ids: Set<Long>): Collection<DownloadItemModel> { 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 it.id.mostSignificantBits
} } ?: emptySet()
private suspend fun List<WorkInfo>.toDownloadsList(): List<DownloadItemModel> { private suspend fun List<WorkInfo>.toDownloadsList(): List<DownloadItemModel> {
if (isEmpty()) { 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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await 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.WorkManagerHelper
import org.koitharu.kotatsu.utils.ext.copyToSuspending import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.deleteAwait
@ -297,6 +298,7 @@ class DownloadWorker @AssistedInject constructor(
class Scheduler @Inject constructor( class Scheduler @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val settings: AppSettings,
) { ) {
private val workManager: WorkManager private val workManager: WorkManager
@ -349,13 +351,34 @@ class DownloadWorker @AssistedInject constructor(
} }
suspend fun removeCompleted() { 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>) { private suspend fun scheduleImpl(data: Collection<Data>) {
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true) .setRequiresStorageNotLow(true)
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED)
.build() .build()
val requests = data.map { inputData -> val requests = data.map { inputData ->
OneTimeWorkRequestBuilder<DownloadWorker>() OneTimeWorkRequestBuilder<DownloadWorker>()
@ -364,7 +387,7 @@ class DownloadWorker @AssistedInject constructor(
.keepResultsForAtLeast(7, TimeUnit.DAYS) .keepResultsForAtLeast(7, TimeUnit.DAYS)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.setInputData(inputData) .setInputData(inputData)
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build() .build()
} }
workManager.enqueue(requests).await() workManager.enqueue(requests).await()

@ -7,17 +7,20 @@ import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.prefs.AppSettings 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.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.util.names 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.getStorageName
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import java.io.File import java.io.File
@ -35,16 +38,12 @@ class ContentSettingsFragment :
@Inject @Inject
lateinit var contentCache: ContentCache lateinit var contentCache: ContentCache
@Inject
lateinit var downloadsScheduler: DownloadWorker.Scheduler
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_content) addPreferencesFromResource(R.xml.pref_content)
findPreference<Preference>(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled 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 { findPreference<ListPreference>(AppSettings.KEY_DOH)?.run {
entryValues = arrayOf( entryValues = arrayOf(
DoHProvider.NONE, DoHProvider.NONE,
@ -87,6 +86,10 @@ class ContentSettingsFragment :
bindRemoteSourcesSummary() bindRemoteSourcesSummary()
} }
AppSettings.KEY_DOWNLOADS_WIFI -> {
updateDownloadsConstraints()
}
AppSettings.KEY_SSL_BYPASS -> { AppSettings.KEY_SSL_BYPASS -> {
Snackbar.make(listView, R.string.settings_apply_restart_required, Snackbar.LENGTH_INDEFINITE).show() 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) 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 package org.koitharu.kotatsu.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.WorkRequest
import androidx.work.await
import androidx.work.impl.WorkManagerImpl import androidx.work.impl.WorkManagerImpl
import java.util.UUID import java.util.UUID
import kotlin.coroutines.resume 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="paused">Paused</string>
<string name="remove_completed">Remove completed</string> <string name="remove_completed">Remove completed</string>
<string name="cancel_all">Cancel all</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> </resources>

@ -30,26 +30,6 @@
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
tools:isPreferenceVisible="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 <ListPreference
android:entries="@array/doh_providers" android:entries="@array/doh_providers"
android:key="doh" android:key="doh"
@ -61,9 +41,25 @@
android:key="ssl_bypass" android:key="ssl_bypass"
android:title="Ignore SSL errors" /> android:title="Ignore SSL errors" />
<PreferenceScreen <PreferenceCategory android:title="@string/downloads">
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"
android:title="@string/backup_restore" <Preference
app:allowDividerAbove="true" /> 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> </PreferenceScreen>

@ -34,6 +34,11 @@
android:icon="@drawable/ic_feed" android:icon="@drawable/ic_feed"
android:title="@string/check_for_new_chapters" /> 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 <PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.about.AboutSettingsFragment" android:fragment="org.koitharu.kotatsu.settings.about.AboutSettingsFragment"
android:icon="@drawable/ic_info_outline" android:icon="@drawable/ic_info_outline"

Loading…
Cancel
Save