android, desktop: storage breakdown in developer options (#7603)

* android, desktop: storage breakdown in developer options

Port of the iOS Storage screen (#5529): lists each top-level entry of the app's data, config and temp folders with its recursive size, so it is possible to see what takes space.

* android, desktop: simplify storage view

Use DefaultProgressView, drop the bold total and redundant absoluteFile, and do not follow top-level symlinks (matches iOS; avoids counting the Android lib link).

* android, desktop: storage view: do not crash on names the JVM cannot convert to Path

Under a non-UTF-8 locale on Linux, File.toPath() throws InvalidPathException for a non-ASCII top-level name (e.g. a received file decrypted into tmpDir); log it and show 0 bytes. Also correct plan wording.

* android, desktop: storage view: fold path conversion into treeSize, use produceState

Also correct plan wording (du per-directory overhead, unlistable roots, Windows junctions).

* android, desktop: storage view: open as card screen like other settings screens

* android, desktop: storage view: list top-level entries as Path to keep their exact names

File.listFiles + toPath loses names that are not valid in the JVM file-name encoding (non-ASCII under a C locale, invalid UTF-8), so those entries showed 0 bytes. Files.newDirectoryStream keeps the raw bytes.

* android, desktop: storage view: normalize root paths before dropping nested roots

A root spelled with .. (e.g. XDG_DATA_HOME=/x/data/../tmp) compared as nested in another root and its size was shown nowhere.

* android, desktop: document storage view in spec and product docs

* android, desktop: storage view: single catch when listing a root

* android, desktop: do not log file paths in storage view

The walk's IOException messages contain the full path, which in tmpDir can be a received file's original name.

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Narasimha-scandEvgeny Poberezkin authored and GitHub committed 2026-10-10 21:55:08 +01:00
1 parent d72f68e6dc
commit afcaf22100
7 files changed
+159

No files matched your search

@@ -60,6 +60,7 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
}
SettingsPreferenceItem(painterResource(MR.images.ic_report), stringResource(MR.strings.show_internal_errors), appPreferences.showInternalErrors)
SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls)
SettingsActionItem(painterResource(MR.images.ic_folder_open), stringResource(MR.strings.storage), { ModalManager.start.showModal(cardScreen = true) { StorageView() } })
}
}
SectionDividerSpaced()
@@ -0,0 +1,100 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemViewSpaceBetween
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.*
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.nio.file.*
import java.nio.file.attribute.BasicFileAttributes
private class EntryUsage(val name: String, val bytes: Long)
private class RootUsage(val dir: File, val entries: List<EntryUsage>) {
val bytes: Long = entries.sumOf { it.bytes }
}
@Composable
fun StorageView() {
val usage by produceState<List<RootUsage>?>(initialValue = null) {
value = withContext(Dispatchers.IO) { storageRoots().map(::rootUsage) }
}
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.storage))
val roots = usage
if (roots == null) {
DefaultProgressView(null)
} else {
roots.forEachIndexed { i, root ->
if (i > 0) SectionDividerSpaced()
SectionView(root.dir.path) {
UsageRow(stringResource(MR.strings.storage_total), root.bytes)
root.entries.forEach { UsageRow(it.name, it.bytes) }
}
}
}
SectionBottomSpacer()
}
}
@Composable
private fun UsageRow(name: String, bytes: Long) {
SectionItemViewSpaceBetween {
Text(name, Modifier.weight(1f))
Text(formatBytes(bytes), Modifier.padding(start = DEFAULT_PADDING), color = MaterialTheme.colors.secondary)
}
}
private fun storageRoots(): List<File> {
val dirs = listOf(dataDir, preferencesDir, tmpDir).map { it.normalize() }.distinct()
return dirs.filter { dir -> dirs.none { it != dir && dir.startsWith(it) } }
}
private fun rootUsage(root: File): RootUsage =
RootUsage(
root,
rootEntries(root)
.map { EntryUsage(it.fileName.toString(), treeSize(it)) }
.sortedByDescending { it.bytes }
)
private fun rootEntries(root: File): List<Path> =
try {
Files.newDirectoryStream(root.toPath()).use { it.toList() }
} catch (e: Exception) {
Log.e(TAG, "StorageView rootEntries: ${e.javaClass.simpleName}")
emptyList()
}
private fun treeSize(path: Path): Long {
var bytes = 0L
Files.walkFileTree(path, object : SimpleFileVisitor<Path>() {
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
bytes += attrs.size()
return FileVisitResult.CONTINUE
}
override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult {
Log.e(TAG, "StorageView visitFileFailed: ${exc.javaClass.simpleName}")
return FileVisitResult.CONTINUE
}
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
if (exc != null) Log.e(TAG, "StorageView postVisitDirectory: ${exc.javaClass.simpleName}")
return FileVisitResult.CONTINUE
}
})
return bytes
}
@@ -1160,6 +1160,8 @@
<string name="deprecated_options_section">Deprecated options</string>
<string name="show_internal_errors">Show internal errors</string>
<string name="show_slow_api_calls">Show slow API calls</string>
<string name="storage">Storage</string>
<string name="storage_total">Total</string>
<string name="shutdown_alert_question">Shutdown?</string>
<string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string>
<string name="prefs_error_saving_settings">Error saving settings</string>