From 1d78c643500ba5148187b7df623414e996b59471 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 19 Jul 2023 13:32:02 +0300 Subject: [PATCH] Move coroutines from UserDataSettingsFragment to ViewModel --- .../kotatsu/core/parser/DummyParser.kt | 2 +- .../settings/UserDataSettingsFragment.kt | 154 +++++------------- .../settings/UserDataSettingsViewModel.kt | 109 +++++++++++++ 3 files changed, 147 insertions(+), 118 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsViewModel.kt diff --git a/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt b/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt index fb96f6e64..5ce1c1fce 100644 --- a/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt +++ b/app/src/debug/java/org/koitharu/kotatsu/core/parser/DummyParser.kt @@ -17,7 +17,7 @@ import java.util.EnumSet class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) { override val configKeyDomain: ConfigKey.Domain - get() = ConfigKey.Domain() + get() = ConfigKey.Domain("") override val sortOrders: Set get() = EnumSet.allOf(SortOrder::class.java) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsFragment.kt index d280e2bd0..4db1df275 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsFragment.kt @@ -8,34 +8,28 @@ import android.os.Bundle import android.view.View import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts -import androidx.lifecycle.Lifecycle +import androidx.fragment.app.viewModels import androidx.preference.Preference import androidx.preference.TwoStatePreference +import androidx.preference.forEach import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runInterruptible -import okhttp3.Cache +import kotlinx.coroutines.flow.StateFlow import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar +import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.os.AppShortcutManager 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.awaitStateAtLeast -import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug -import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.local.data.CacheDir -import org.koitharu.kotatsu.local.data.LocalStorageManager -import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.settings.backup.BackupDialogFragment import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity -import org.koitharu.kotatsu.tracker.domain.TrackingRepository import javax.inject.Inject @AndroidEntryPoint @@ -43,24 +37,11 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac SharedPreferences.OnSharedPreferenceChangeListener, ActivityResultCallback { - @Inject - lateinit var trackerRepo: TrackingRepository - - @Inject - lateinit var searchRepository: MangaSearchRepository - - @Inject - lateinit var storageManager: LocalStorageManager - - @Inject - lateinit var cookieJar: MutableCookieJar - - @Inject - lateinit var cache: Cache - @Inject lateinit var appShortcutManager: AppShortcutManager + private val viewModel: UserDataSettingsViewModel by viewModels() + private val backupSelectCall = registerForActivityResult( ActivityResultContracts.OpenDocument(), this, @@ -76,23 +57,26 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - findPreference(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.PAGES) - findPreference(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.THUMBS) - findPreference(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindSummaryToHttpCacheSize() + findPreference(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.PAGES])) + findPreference(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.THUMBS])) + findPreference(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindBytesSizeSummary(viewModel.httpCacheSize) findPreference(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref -> - viewLifecycleScope.launch { - lifecycle.awaitStateAtLeast(Lifecycle.State.RESUMED) - val items = searchRepository.getSearchHistoryCount() - pref.summary = pref.context.resources.getQuantityString(R.plurals.items, items, items) + viewModel.searchHistoryCount.observe(viewLifecycleOwner) { + pref.summary = pref.context.resources.getQuantityString(R.plurals.items, it, it) } } findPreference(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref -> - viewLifecycleScope.launch { - lifecycle.awaitStateAtLeast(Lifecycle.State.RESUMED) - val items = trackerRepo.getLogsCount() - pref.summary = pref.context.resources.getQuantityString(R.plurals.items, items, items) + viewModel.feedItemsCount.observe(viewLifecycleOwner) { + pref.summary = pref.context.resources.getQuantityString(R.plurals.items, it, it) } } + viewModel.loadingKeys.observe(viewLifecycleOwner) { keys -> + preferenceScreen.forEach { pref -> + pref.isEnabled = pref.key !in keys + } + } + viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this)) + viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView)) settings.subscribe(this) } @@ -104,12 +88,12 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { AppSettings.KEY_PAGES_CACHE_CLEAR -> { - clearCache(preference, CacheDir.PAGES) + viewModel.clearCache(preference.key, CacheDir.PAGES) true } AppSettings.KEY_THUMBS_CACHE_CLEAR -> { - clearCache(preference, CacheDir.THUMBS) + viewModel.clearCache(preference.key, CacheDir.THUMBS) true } @@ -119,26 +103,17 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac } AppSettings.KEY_SEARCH_HISTORY_CLEAR -> { - clearSearchHistory(preference) + clearSearchHistory() true } AppSettings.KEY_HTTP_CACHE_CLEAR -> { - clearHttpCache() + viewModel.clearHttpCache() true } AppSettings.KEY_UPDATES_FEED_CLEAR -> { - viewLifecycleScope.launch { - trackerRepo.clearLogs() - preference.summary = preference.context.resources - .getQuantityString(R.plurals.items, 0, 0) - Snackbar.make( - view ?: return@launch, - R.string.updates_feed_cleared, - Snackbar.LENGTH_SHORT, - ).show() - } + viewModel.clearUpdatesFeed() true } @@ -189,71 +164,23 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac } } - private fun clearCache(preference: Preference, cache: CacheDir) { - val ctx = preference.context.applicationContext - viewLifecycleScope.launch { - try { - preference.isEnabled = false - storageManager.clearCache(cache) - val size = storageManager.computeCacheSize(cache) - preference.summary = FileSize.BYTES.format(ctx, size) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - preference.summary = e.getDisplayMessage(ctx.resources) - } finally { - preference.isEnabled = true + private fun Preference.bindBytesSizeSummary(stateFlow: StateFlow) { + stateFlow.observe(viewLifecycleOwner) { size -> + summary = if (size < 0) { + context.getString(R.string.computing_) + } else { + FileSize.BYTES.format(context, size) } } } - private fun Preference.bindSummaryToCacheSize(dir: CacheDir) = viewLifecycleScope.launch { - val size = storageManager.computeCacheSize(dir) - summary = FileSize.BYTES.format(context, size) - } - - private fun Preference.bindSummaryToHttpCacheSize() = viewLifecycleScope.launch { - val size = runInterruptible(Dispatchers.IO) { cache.size() } - summary = FileSize.BYTES.format(context, size) - } - - private fun clearHttpCache() { - val preference = findPreference(AppSettings.KEY_HTTP_CACHE_CLEAR) ?: return - val ctx = preference.context.applicationContext - viewLifecycleScope.launch { - try { - preference.isEnabled = false - val size = runInterruptible(Dispatchers.IO) { - cache.evictAll() - cache.size() - } - preference.summary = FileSize.BYTES.format(ctx, size) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - preference.summary = e.getDisplayMessage(ctx.resources) - } finally { - preference.isEnabled = true - } - } - } - - private fun clearSearchHistory(preference: Preference) { + 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) { _, _ -> - viewLifecycleScope.launch { - searchRepository.clearSearchHistory() - preference.summary = preference.context.resources - .getQuantityString(R.plurals.items, 0, 0) - Snackbar.make( - view ?: return@launch, - R.string.search_history_cleared, - Snackbar.LENGTH_SHORT, - ).show() - } + viewModel.clearSearchHistory() }.show() } @@ -263,14 +190,7 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac .setMessage(R.string.text_clear_cookies_prompt) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.clear) { _, _ -> - viewLifecycleScope.launch { - cookieJar.clear() - Snackbar.make( - listView ?: return@launch, - R.string.cookies_cleared, - Snackbar.LENGTH_SHORT, - ).show() - } + viewModel.clearCookies() }.show() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsViewModel.kt new file mode 100644 index 000000000..5739402ae --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/UserDataSettingsViewModel.kt @@ -0,0 +1,109 @@ +package org.koitharu.kotatsu.settings + +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +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.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.local.data.CacheDir +import org.koitharu.kotatsu.local.data.LocalStorageManager +import org.koitharu.kotatsu.search.domain.MangaSearchRepository +import org.koitharu.kotatsu.tracker.domain.TrackingRepository +import java.util.EnumMap +import javax.inject.Inject + +@HiltViewModel +class UserDataSettingsViewModel @Inject constructor( + private val storageManager: LocalStorageManager, + private val httpCache: Cache, + private val searchRepository: MangaSearchRepository, + private val trackingRepository: TrackingRepository, + private val cookieJar: MutableCookieJar, +) : BaseViewModel() { + + val onActionDone = MutableEventFlow() + val loadingKeys = MutableStateFlow(emptySet()) + + val searchHistoryCount = MutableStateFlow(-1) + val feedItemsCount = MutableStateFlow(-1) + val httpCacheSize = MutableStateFlow(-1L) + val cacheSizes = EnumMap>(CacheDir::class.java) + + init { + CacheDir.values().forEach { + cacheSizes[it] = MutableStateFlow(-1L) + } + launchJob(Dispatchers.Default) { + searchHistoryCount.value = searchRepository.getSearchHistoryCount() + } + launchJob(Dispatchers.Default) { + feedItemsCount.value = trackingRepository.getLogsCount() + } + CacheDir.values().forEach { cache -> + launchJob(Dispatchers.Default) { + checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache) + } + } + launchJob(Dispatchers.Default) { + httpCacheSize.value = runInterruptible { httpCache.size() } + } + } + + fun clearCache(key: String, cache: CacheDir) { + launchJob(Dispatchers.Default) { + try { + loadingKeys.update { it + key } + storageManager.clearCache(cache) + checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache) + } 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 + } 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)) + } + } +}