From 3be9def6094782d3f00e9da0a4ca22cfe5a78439 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 13 Jul 2022 14:25:08 +0300 Subject: [PATCH] Add storage usage to Tools screen --- .../base/ui/widgets/SegmentedBarView.kt | 89 +++++++++++++++++++ .../kotatsu/local/data/LocalStorageManager.kt | 23 ++++- .../kotatsu/settings/SettingsModule.kt | 2 + .../kotatsu/settings/tools/ToolsFragment.kt | 60 +++++++++++++ .../kotatsu/settings/tools/ToolsViewModel.kt | 44 +++++++++ .../settings/tools/model/StorageUsage.kt | 14 +++ .../utils/image/FaviconFallbackDrawable.kt | 4 +- app/src/main/res/drawable/bg_circle.xml | 4 + app/src/main/res/layout/fragment_tools.xml | 11 +++ .../main/res/layout/layout_memory_usage.xml | 69 ++++++++++++++ app/src/main/res/values/strings.xml | 6 ++ app/src/main/res/values/styles.xml | 6 ++ 12 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SegmentedBarView.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/tools/model/StorageUsage.kt create mode 100644 app/src/main/res/layout/layout_memory_usage.xml diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SegmentedBarView.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SegmentedBarView.kt new file mode 100644 index 000000000..0a4a89d98 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/SegmentedBarView.kt @@ -0,0 +1,89 @@ +package org.koitharu.kotatsu.base.ui.widgets + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Outline +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import androidx.annotation.ColorInt +import androidx.annotation.FloatRange +import androidx.core.graphics.ColorUtils +import org.koitharu.kotatsu.parsers.util.replaceWith +import org.koitharu.kotatsu.utils.ext.resolveDp +import kotlin.random.Random + +class SegmentedBarView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : View(context, attrs, defStyleAttr) { + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val segmentsData = ArrayList() + private val minSegmentSize = context.resources.resolveDp(3f) + + var segments: List + get() = segmentsData + set(value) { + segmentsData.replaceWith(value) + invalidate() + } + + init { + paint.style = Paint.Style.FILL + outlineProvider = OutlineProvider() + clipToOutline = true + + if (isInEditMode) { + segments = List(Random.nextInt(3, 5)) { + Segment( + percent = Random.nextFloat(), + color = ColorUtils.HSLToColor(floatArrayOf(Random.nextInt(0, 360).toFloat(), 0.5f, 0.5f)), + ) + } + } + } + + override fun onDraw(canvas: Canvas) { + var x = 0f + val w = width.toFloat() + for (segment in segmentsData) { + paint.color = segment.color + val segmentWidth = (w * segment.percent).coerceAtLeast(minSegmentSize) + canvas.drawRect(x, 0f, x + segmentWidth, height.toFloat(), paint) + x += segmentWidth + } + } + + class Segment( + @FloatRange(from = 0.0, to = 1.0) val percent: Float, + @ColorInt val color: Int, + ) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Segment + + if (percent != other.percent) return false + if (color != other.color) return false + + return true + } + + override fun hashCode(): Int { + var result = percent.hashCode() + result = 31 * result + color + return result + } + } + + private class OutlineProvider : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(0, 0, view.width, view.height, view.height / 2f) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt index 6e9c1e399..9c1407a32 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/LocalStorageManager.kt @@ -4,14 +4,15 @@ import android.content.ContentResolver import android.content.Context import android.os.StatFs import androidx.annotation.WorkerThread -import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import okhttp3.Cache import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.utils.ext.computeSize import org.koitharu.kotatsu.utils.ext.getStorageName +import java.io.File private const val DIR_NAME = "manga" private const val CACHE_DISK_PERCENTAGE = 0.02 @@ -37,6 +38,18 @@ class LocalStorageManager( getCacheDirs(cache.dir).sumOf { it.computeSize() } } + suspend fun computeCacheSize() = withContext(Dispatchers.IO) { + getCacheDirs().sumOf { it.computeSize() } + } + + suspend fun computeStorageSize() = withContext(Dispatchers.IO) { + getAvailableStorageDirs().sumOf { it.computeSize() } + } + + suspend fun computeAvailableSize() = runInterruptible(Dispatchers.IO) { + getAvailableStorageDirs().mapToSet { it.freeSpace }.sum() + } + suspend fun clearCache(cache: CacheDir) = runInterruptible(Dispatchers.IO) { getCacheDirs(cache.dir).forEach { it.deleteRecursively() } } @@ -93,6 +106,14 @@ class LocalStorageManager( return result } + @WorkerThread + private fun getCacheDirs(): MutableSet { + val result = LinkedHashSet() + result += context.cacheDir + context.externalCacheDirs.filterNotNullTo(result) + return result + } + private fun calculateDiskCacheSize(cacheDirectory: File): Long { return try { val cacheDir = StatFs(cacheDirectory.absolutePath) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt index 230ab0d32..f205482d3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -13,6 +13,7 @@ import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel import org.koitharu.kotatsu.settings.onboard.OnboardViewModel import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel +import org.koitharu.kotatsu.settings.tools.ToolsViewModel val settingsModule get() = module { @@ -29,4 +30,5 @@ val settingsModule viewModel { OnboardViewModel(get()) } viewModel { SourcesSettingsViewModel(get()) } viewModel { NewSourcesViewModel(get()) } + viewModel { ToolsViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt index 3c09bb442..7425af696 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt @@ -1,21 +1,35 @@ package org.koitharu.kotatsu.settings.tools +import android.content.res.ColorStateList import android.os.Bundle +import android.transition.TransitionManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.ColorInt +import androidx.core.graphics.ColorUtils import androidx.core.graphics.Insets +import androidx.core.view.isVisible import androidx.core.view.updatePadding +import androidx.core.widget.TextViewCompat +import com.google.android.material.color.MaterialColors +import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment +import org.koitharu.kotatsu.base.ui.widgets.SegmentedBarView import org.koitharu.kotatsu.databinding.FragmentToolsBinding import org.koitharu.kotatsu.download.ui.DownloadsActivity import org.koitharu.kotatsu.settings.AppUpdateChecker import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.tools.model.StorageUsage +import org.koitharu.kotatsu.utils.FileSize +import org.koitharu.kotatsu.utils.ext.getThemeColor +import com.google.android.material.R as materialR class ToolsFragment : BaseFragment(), View.OnClickListener { private var updateChecker: AppUpdateChecker? = null + private val viewModel by viewModel() override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentToolsBinding { return FragmentToolsBinding.inflate(inflater, container, false) @@ -27,6 +41,8 @@ class ToolsFragment : BaseFragment(), View.OnClickListener binding.buttonDownloads.setOnClickListener(this) binding.cardUpdate.root.setOnClickListener(this) binding.cardUpdate.buttonDownload.setOnClickListener(this) + + viewModel.storageUsage.observe(viewLifecycleOwner, ::onStorageUsageChanged) } override fun onClick(v: View) { @@ -44,6 +60,50 @@ class ToolsFragment : BaseFragment(), View.OnClickListener ) } + private fun onStorageUsageChanged(usage: StorageUsage) { + val storageSegment = SegmentedBarView.Segment(usage.savedManga.percent, segmentColor(1)) + val pagesSegment = SegmentedBarView.Segment(usage.pagesCache.percent, segmentColor(2)) + val otherSegment = SegmentedBarView.Segment(usage.otherCache.percent, segmentColor(3)) + + with(binding.layoutStorage) { + bar.segments = listOf(storageSegment, pagesSegment, otherSegment) + val pattern = getString(R.string.memory_usage_pattern) + labelStorage.text = pattern.format( + FileSize.BYTES.format(root.context, usage.savedManga.bytes), + getString(R.string.saved_manga) + ) + labelPagesCache.text = pattern.format( + FileSize.BYTES.format(root.context, usage.pagesCache.bytes), + getString(R.string.pages_cache) + ) + labelOtherCache.text = pattern.format( + FileSize.BYTES.format(root.context, usage.otherCache.bytes), + getString(R.string.other_cache) + ) + labelAvailable.text = pattern.format( + FileSize.BYTES.format(root.context, usage.available.bytes), + getString(R.string.available) + ) + TextViewCompat.setCompoundDrawableTintList(labelStorage, ColorStateList.valueOf(storageSegment.color)) + TextViewCompat.setCompoundDrawableTintList(labelPagesCache, ColorStateList.valueOf(pagesSegment.color)) + TextViewCompat.setCompoundDrawableTintList(labelOtherCache, ColorStateList.valueOf(otherSegment.color)) + if (!labelStorage.isVisible) { + TransitionManager.beginDelayedTransition(root) + } + labelStorage.isVisible = true + labelPagesCache.isVisible = true + labelOtherCache.isVisible = true + } + } + + @ColorInt + private fun segmentColor(i: Int): Int { + val hue = (93.6f * i) % 360 + val color = ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f)) + val backgroundColor = requireContext().getThemeColor(materialR.attr.colorSecondaryContainer) + return MaterialColors.harmonize(color, backgroundColor) + } + companion object { fun newInstance() = ToolsFragment() diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt new file mode 100644 index 000000000..a20393e69 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsViewModel.kt @@ -0,0 +1,44 @@ +package org.koitharu.kotatsu.settings.tools + +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.local.data.CacheDir +import org.koitharu.kotatsu.local.data.LocalStorageManager +import org.koitharu.kotatsu.settings.tools.model.StorageUsage + +class ToolsViewModel( + private val storageManager: LocalStorageManager, +) : BaseViewModel() { + + val storageUsage: LiveData = liveData( + context = viewModelScope.coroutineContext + Dispatchers.Default, + ) { + emit(collectStorageUsage()) + } + + private suspend fun collectStorageUsage(): StorageUsage { + 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 + return 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(), + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/model/StorageUsage.kt b/app/src/main/java/org/koitharu/kotatsu/settings/tools/model/StorageUsage.kt new file mode 100644 index 000000000..0cb0a3c7b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/tools/model/StorageUsage.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.settings.tools.model + +class StorageUsage( + val savedManga: Item, + val pagesCache: Item, + val otherCache: Item, + val available: Item, +) { + + class Item( + val bytes: Long, + val percent: Float, + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt b/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt index 0f5d64406..a03ce5e80 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/image/FaviconFallbackDrawable.kt @@ -5,8 +5,6 @@ import android.graphics.* import android.graphics.drawable.Drawable import androidx.core.graphics.ColorUtils import com.google.android.material.color.MaterialColors -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.utils.ext.getThemeColor import kotlin.math.absoluteValue class FaviconFallbackDrawable( @@ -16,7 +14,7 @@ class FaviconFallbackDrawable( private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val letter = name.take(1).uppercase() - private val color = MaterialColors.harmonize(colorOfString(name), context.getThemeColor(android.R.attr.colorPrimary)) + private val color = MaterialColors.harmonizeWithPrimary(context, colorOfString(name)) private val textBounds = Rect() private val tempRect = Rect() diff --git a/app/src/main/res/drawable/bg_circle.xml b/app/src/main/res/drawable/bg_circle.xml index 258fc4ce3..7b7113bfe 100644 --- a/app/src/main/res/drawable/bg_circle.xml +++ b/app/src/main/res/drawable/bg_circle.xml @@ -5,4 +5,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tools.xml b/app/src/main/res/layout/fragment_tools.xml index ab99ee165..84a60e194 100644 --- a/app/src/main/res/layout/fragment_tools.xml +++ b/app/src/main/res/layout/fragment_tools.xml @@ -16,6 +16,17 @@ android:layout_height="wrap_content" android:layout_margin="@dimen/margin_normal" /> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6dbc8432..5a6427533 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -340,4 +340,10 @@ Press "Back" again to exit Press "Back" twice to exit the app Exit confirmation + Saved manga + Pages cache + Other cache + Storage usage + Available + %s - %s \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 7aa7053f3..c13aa33f4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -150,6 +150,12 @@ ?textAppearanceButton + +