Reorganize storage usage settings
parent
008f2d705a
commit
8ca11b214c
@ -0,0 +1,167 @@
|
||||
package org.koitharu.kotatsu.settings.userdata.storage
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
|
||||
@AndroidEntryPoint
|
||||
class StorageManageSettingsFragment : BasePreferenceFragment(R.string.storage_usage) {
|
||||
|
||||
private val viewModel by viewModels<StorageManageSettingsViewModel>()
|
||||
private val loadingPrefs = HashSet<String>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_storage)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.PAGES]))
|
||||
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.THUMBS]))
|
||||
findPreference<Preference>(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindBytesSizeSummary(viewModel.httpCacheSize)
|
||||
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
||||
viewModel.searchHistoryCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it < 0) {
|
||||
view.context.getString(R.string.loading_)
|
||||
} else {
|
||||
pref.context.resources.getQuantityString(R.plurals.items, it, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref ->
|
||||
viewModel.feedItemsCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it < 0) {
|
||||
view.context.getString(R.string.loading_)
|
||||
} else {
|
||||
pref.context.resources.getQuantityString(R.plurals.items, it, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
findPreference<StorageUsagePreference>(AppSettings.KEY_STORAGE_USAGE)?.let { pref ->
|
||||
viewModel.storageUsage.observe(viewLifecycleOwner, pref)
|
||||
}
|
||||
|
||||
viewModel.loadingKeys.observe(viewLifecycleOwner) { keys ->
|
||||
loadingPrefs.addAll(keys)
|
||||
loadingPrefs.forEach { prefKey ->
|
||||
findPreference<Preference>(prefKey)?.isEnabled = prefKey !in keys
|
||||
}
|
||||
}
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView))
|
||||
viewModel.onChaptersCleanedUp.observeEvent(viewLifecycleOwner, ::onChaptersCleanedUp)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean = when (preference.key) {
|
||||
AppSettings.KEY_COOKIES_CLEAR -> {
|
||||
clearCookies()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
|
||||
clearSearchHistory()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_PAGES_CACHE_CLEAR -> {
|
||||
viewModel.clearCache(preference.key, CacheDir.PAGES)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
||||
viewModel.clearCache(preference.key, CacheDir.THUMBS)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_HTTP_CACHE_CLEAR -> {
|
||||
viewModel.clearHttpCache()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_CHAPTERS_CLEAR -> {
|
||||
cleanupChapters()
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
AppSettings.KEY_CLEAR_MANGA_DATA -> {
|
||||
viewModel.clearMangaData()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
||||
viewModel.clearUpdatesFeed()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
private fun onChaptersCleanedUp(result: Pair<Int, Long>) {
|
||||
val c = context ?: return
|
||||
val text = if (result.first == 0 && result.second == 0L) {
|
||||
c.getString(R.string.no_chapters_deleted)
|
||||
} else {
|
||||
c.getString(
|
||||
R.string.chapters_deleted_pattern,
|
||||
c.resources.getQuantityString(R.plurals.chapters, result.first, result.first),
|
||||
FileSize.BYTES.format(c, result.second),
|
||||
)
|
||||
}
|
||||
Snackbar.make(listView, text, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun Preference.bindBytesSizeSummary(stateFlow: StateFlow<Long>) {
|
||||
stateFlow.observe(viewLifecycleOwner) { size ->
|
||||
summary = if (size < 0) {
|
||||
context.getString(R.string.computing_)
|
||||
} else {
|
||||
FileSize.BYTES.format(context, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearSearchHistory() {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.clear_search_history)
|
||||
.setMessage(R.string.text_clear_search_history_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearSearchHistory()
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun clearCookies() {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.clear_cookies)
|
||||
.setMessage(R.string.text_clear_cookies_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearCookies()
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun cleanupChapters() {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.delete_read_chapters)
|
||||
.setMessage(R.string.delete_read_chapters_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
viewModel.cleanupChapters()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
package org.koitharu.kotatsu.settings.userdata.storage
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.Cache
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.firstNotNull
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.local.domain.DeleteReadChaptersUseCase
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import java.util.EnumMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
@HiltViewModel
|
||||
class StorageManageSettingsViewModel @Inject constructor(
|
||||
private val storageManager: LocalStorageManager,
|
||||
private val httpCache: Cache,
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val deleteReadChaptersUseCase: DeleteReadChaptersUseCase,
|
||||
private val mangaDataRepositoryProvider: Provider<MangaDataRepository>,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val loadingKeys = MutableStateFlow(emptySet<String>())
|
||||
|
||||
val searchHistoryCount = MutableStateFlow(-1)
|
||||
val feedItemsCount = MutableStateFlow(-1)
|
||||
val httpCacheSize = MutableStateFlow(-1L)
|
||||
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
|
||||
val storageUsage = MutableStateFlow<StorageUsage?>(null)
|
||||
|
||||
val onChaptersCleanedUp = MutableEventFlow<Pair<Int, Long>>()
|
||||
|
||||
private var storageUsageJob: Job? = null
|
||||
|
||||
init {
|
||||
CacheDir.entries.forEach {
|
||||
cacheSizes[it] = MutableStateFlow(-1L)
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
searchHistoryCount.value = searchRepository.getSearchHistoryCount()
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
feedItemsCount.value = trackingRepository.getLogsCount()
|
||||
}
|
||||
CacheDir.entries.forEach { cache ->
|
||||
launchJob(Dispatchers.Default) {
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
}
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
httpCacheSize.value = runInterruptible { httpCache.size() }
|
||||
}
|
||||
loadStorageUsage()
|
||||
}
|
||||
|
||||
fun clearCache(key: String, cache: CacheDir) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + key }
|
||||
storageManager.clearCache(cache)
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
loadStorageUsage()
|
||||
} finally {
|
||||
loadingKeys.update { it - key }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearHttpCache() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_HTTP_CACHE_CLEAR }
|
||||
val size = runInterruptible(Dispatchers.IO) {
|
||||
httpCache.evictAll()
|
||||
httpCache.size()
|
||||
}
|
||||
httpCacheSize.value = size
|
||||
loadStorageUsage()
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_HTTP_CACHE_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSearchHistory() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
searchRepository.clearSearchHistory()
|
||||
searchHistoryCount.value = searchRepository.getSearchHistoryCount()
|
||||
onActionDone.call(ReversibleAction(R.string.search_history_cleared, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCookies() {
|
||||
launchJob {
|
||||
cookieJar.clear()
|
||||
onActionDone.call(ReversibleAction(R.string.cookies_cleared, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearUpdatesFeed() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
trackingRepository.clearLogs()
|
||||
feedItemsCount.value = trackingRepository.getLogsCount()
|
||||
onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMangaData() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_CLEAR_MANGA_DATA }
|
||||
trackingRepository.gc()
|
||||
val repository = mangaDataRepositoryProvider.get()
|
||||
repository.cleanupLocalManga()
|
||||
repository.cleanupDatabase()
|
||||
onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_CLEAR_MANGA_DATA }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanupChapters() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_CHAPTERS_CLEAR }
|
||||
val oldSize = storageUsage.firstNotNull().savedManga.bytes
|
||||
val chaptersCount = deleteReadChaptersUseCase.invoke()
|
||||
loadStorageUsage().join()
|
||||
val newSize = storageUsage.firstNotNull().savedManga.bytes
|
||||
onChaptersCleanedUp.call(chaptersCount to oldSize - newSize)
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_CHAPTERS_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadStorageUsage(): Job {
|
||||
val prevJob = storageUsageJob
|
||||
return launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
val pagesCacheSize = storageManager.computeCacheSize(CacheDir.PAGES)
|
||||
val otherCacheSize = storageManager.computeCacheSize() - pagesCacheSize
|
||||
val storageSize = storageManager.computeStorageSize()
|
||||
val availableSpace = storageManager.computeAvailableSize()
|
||||
val totalBytes = pagesCacheSize + otherCacheSize + storageSize + availableSpace
|
||||
storageUsage.value = StorageUsage(
|
||||
savedManga = StorageUsage.Item(
|
||||
bytes = storageSize,
|
||||
percent = (storageSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
pagesCache = StorageUsage.Item(
|
||||
bytes = pagesCacheSize,
|
||||
percent = (pagesCacheSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
otherCache = StorageUsage.Item(
|
||||
bytes = otherCacheSize,
|
||||
percent = (otherCacheSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
available = StorageUsage.Item(
|
||||
bytes = availableSpace,
|
||||
percent = (availableSpace.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
)
|
||||
}.also {
|
||||
storageUsageJob = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.settings.userdata
|
||||
package org.koitharu.kotatsu.settings.userdata.storage
|
||||
|
||||
data class StorageUsage(
|
||||
val savedManga: Item,
|
||||
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/storage_usage">
|
||||
|
||||
<org.koitharu.kotatsu.settings.userdata.storage.StorageUsagePreference android:key="storage_usage" />
|
||||
|
||||
<Preference
|
||||
android:key="search_history_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/loading_"
|
||||
android:title="@string/clear_search_history"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<Preference
|
||||
android:key="updates_feed_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/loading_"
|
||||
android:title="@string/clear_updates_feed" />
|
||||
|
||||
<Preference
|
||||
android:key="thumbs_cache_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/computing_"
|
||||
android:title="@string/clear_thumbs_cache" />
|
||||
|
||||
<Preference
|
||||
android:key="pages_cache_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/computing_"
|
||||
android:title="@string/clear_pages_cache" />
|
||||
|
||||
<Preference
|
||||
android:key="http_cache_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/loading_"
|
||||
android:title="@string/clear_network_cache" />
|
||||
|
||||
<Preference
|
||||
android:key="manga_data_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/clear_database_summary"
|
||||
android:title="@string/clear_database" />
|
||||
|
||||
<Preference
|
||||
android:key="cookies_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/clear_cookies_summary"
|
||||
android:title="@string/clear_cookies" />
|
||||
|
||||
<Preference
|
||||
android:key="chapters_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/delete_read_chapters_summary"
|
||||
android:title="@string/delete_read_chapters"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="chapters_clear_auto"
|
||||
android:summary="@string/runs_on_app_start"
|
||||
android:title="@string/delete_read_chapters_auto" />
|
||||
|
||||
</PreferenceScreen>
|
||||
Loading…
Reference in New Issue