Add storage usage to Tools screen

This commit is contained in:
Koitharu
2022-07-13 14:25:08 +03:00
parent bd3d800cde
commit 3be9def609
12 changed files with 328 additions and 4 deletions

View File

@@ -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<Segment>()
private val minSegmentSize = context.resources.resolveDp(3f)
var segments: List<Segment>
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)
}
}
}

View File

@@ -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<File> {
val result = LinkedHashSet<File>()
result += context.cacheDir
context.externalCacheDirs.filterNotNullTo(result)
return result
}
private fun calculateDiskCacheSize(cacheDirectory: File): Long {
return try {
val cacheDir = StatFs(cacheDirectory.absolutePath)

View File

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

View File

@@ -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<FragmentToolsBinding>(), View.OnClickListener {
private var updateChecker: AppUpdateChecker? = null
private val viewModel by viewModel<ToolsViewModel>()
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentToolsBinding {
return FragmentToolsBinding.inflate(inflater, container, false)
@@ -27,6 +41,8 @@ class ToolsFragment : BaseFragment<FragmentToolsBinding>(), 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<FragmentToolsBinding>(), 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()

View File

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

View File

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

View File

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

View File

@@ -5,4 +5,8 @@
<solid android:color="?colorSurface" />
<size
android:width="12dp"
android:height="12dp" />
</shape>

View File

@@ -16,6 +16,17 @@
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_normal" />
<include
android:id="@+id/layout_storage"
layout="@layout/layout_memory_usage"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/margin_small" />
<org.koitharu.kotatsu.base.ui.widgets.ListItemTextView
android:id="@+id/button_downloads"
android:layout_width="match_parent"

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/screen_padding">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/storage_usage"
android:textAppearance="?textAppearanceTitleMedium" />
<org.koitharu.kotatsu.base.ui.widgets.SegmentedBarView
android:id="@+id/bar"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginVertical="@dimen/margin_normal"
android:background="?colorSecondaryContainer" />
<TextView
android:id="@+id/label_storage"
style="@style/Widget.Kotatsu.TextView.Indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/saved_manga"
android:visibility="gone"
app:drawableStartCompat="@drawable/bg_circle"
tools:drawableTint="?colorPrimary"
tools:visibility="visible" />
<TextView
android:id="@+id/label_pages_cache"
style="@style/Widget.Kotatsu.TextView.Indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:text="@string/pages_cache"
android:visibility="gone"
app:drawableStartCompat="@drawable/bg_circle"
tools:drawableTint="?colorSecondary"
tools:visibility="visible" />
<TextView
android:id="@+id/label_other_cache"
style="@style/Widget.Kotatsu.TextView.Indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:text="@string/other_cache"
android:visibility="gone"
app:drawableStartCompat="@drawable/bg_circle"
tools:drawableTint="?colorTertiary"
tools:visibility="visible" />
<TextView
android:id="@+id/label_available"
style="@style/Widget.Kotatsu.TextView.Indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:text="@string/computing_"
app:drawableStartCompat="@drawable/bg_circle"
app:drawableTint="?colorSecondaryContainer" />
</LinearLayout>

View File

@@ -340,4 +340,10 @@
<string name="confirm_exit">Press "Back" again to exit</string>
<string name="exit_confirmation_summary">Press "Back" twice to exit the app</string>
<string name="exit_confirmation">Exit confirmation</string>
<string name="saved_manga">Saved manga</string>
<string name="pages_cache">Pages cache</string>
<string name="other_cache">Other cache</string>
<string name="storage_usage">Storage usage</string>
<string name="available">Available</string>
<string name="memory_usage_pattern">%s - %s</string>
</resources>

View File

@@ -150,6 +150,12 @@
<item name="android:textAppearance">?textAppearanceButton</item>
</style>
<style name="Widget.Kotatsu.TextView.Indicator" parent="Widget.MaterialComponents.TextView">
<item name="android:drawablePadding">12dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?textAppearanceLabelMedium</item>
</style>
<style name="ThemeOverlay.Kotatsu.MainToolbar" parent="">
<item name="colorControlHighlight">@color/selector_overlay</item>
</style>