feat: implement data and power settings sections
Some checks failed
Android CI / android (push) Failing after 4m5s
Android Release / release (push) Failing after 4m18s
CI / test (push) Failing after 2m20s

This commit is contained in:
2026-04-06 02:24:50 +03:00
parent 09ddb1ef41
commit 53303a5a5b
3 changed files with 315 additions and 13 deletions

View File

@@ -1,5 +1,9 @@
package ru.daemonlord.messenger.ui.settings
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -22,14 +26,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Chat
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.BatterySaver
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Notifications
@@ -58,7 +61,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@@ -66,6 +71,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -77,6 +83,10 @@ import ru.daemonlord.messenger.domain.settings.model.AppLanguage
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
import ru.daemonlord.messenger.ui.account.AccountUiState
import ru.daemonlord.messenger.ui.account.AccountViewModel
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private enum class SettingsFolder {
Account,
@@ -84,7 +94,6 @@ private enum class SettingsFolder {
Privacy,
Notifications,
Data,
Folders,
Devices,
Power,
Language,
@@ -200,15 +209,41 @@ private fun SettingsHome(
SettingsCard {
SettingsRow(Icons.Filled.AccountCircle, stringResource(id = R.string.settings_folder_account), stringResource(id = R.string.settings_account_subtitle)) { onOpenFolder(SettingsFolder.Account) }
SettingsRow(Icons.Filled.Chat, stringResource(id = R.string.settings_folder_chat), stringResource(id = R.string.settings_chat_subtitle)) { onOpenFolder(SettingsFolder.Chat) }
SettingsRow(Icons.AutoMirrored.Filled.Chat, stringResource(id = R.string.settings_folder_chat), stringResource(id = R.string.settings_chat_subtitle)) { onOpenFolder(SettingsFolder.Chat) }
SettingsRow(Icons.Filled.Lock, stringResource(id = R.string.settings_folder_privacy), stringResource(id = R.string.settings_privacy_subtitle)) { onOpenFolder(SettingsFolder.Privacy) }
SettingsRow(Icons.Filled.Notifications, stringResource(id = R.string.settings_folder_notifications), stringResource(id = R.string.settings_notifications_subtitle)) { onOpenFolder(SettingsFolder.Notifications) }
SettingsRow(Icons.Filled.Storage, stringResource(id = R.string.settings_folder_data), stringResource(id = R.string.settings_data_subtitle)) { onOpenFolder(SettingsFolder.Data) }
SettingsRow(Icons.Filled.Folder, stringResource(id = R.string.settings_folder_folders), stringResource(id = R.string.settings_folders_subtitle)) { onOpenFolder(SettingsFolder.Folders) }
SettingsRow(Icons.Filled.Devices, stringResource(id = R.string.settings_folder_devices), stringResource(id = R.string.settings_devices_subtitle)) { onOpenFolder(SettingsFolder.Devices) }
SettingsRow(Icons.Filled.BatterySaver, stringResource(id = R.string.settings_folder_power), stringResource(id = R.string.settings_power_subtitle)) { onOpenFolder(SettingsFolder.Power) }
SettingsRow(Icons.Filled.Language, stringResource(id = R.string.settings_folder_language), languageLabel(state.appLanguage), divider = false) { onOpenFolder(SettingsFolder.Language) }
}
SettingsCard {
Text(
text = stringResource(id = R.string.settings_preferences_header),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelMedium,
)
SettingsToggle(
icon = Icons.Filled.Notifications,
title = stringResource(id = R.string.settings_enable_notifications),
checked = state.notificationsEnabled,
onCheckedChange = {},
enabled = false,
)
SettingsToggle(
icon = Icons.Filled.Visibility,
title = stringResource(id = R.string.settings_show_preview),
checked = state.notificationsPreviewEnabled,
onCheckedChange = {},
enabled = false,
)
SettingsShortcut(
title = stringResource(id = R.string.settings_folder_language),
subtitle = languageLabel(state.appLanguage),
onClick = { onOpenFolder(SettingsFolder.Language) },
)
}
}
}
@@ -247,10 +282,9 @@ private fun SettingsFolderView(
SettingsFolder.Chat -> ChatFolder(state, viewModel)
SettingsFolder.Privacy -> PrivacyFolder(state, viewModel)
SettingsFolder.Notifications -> NotificationsFolder(state, viewModel)
SettingsFolder.Data -> DataFolder()
SettingsFolder.Devices -> DevicesFolder(state, viewModel)
SettingsFolder.Data -> PlaceholderFolder(stringResource(id = R.string.settings_placeholder_data))
SettingsFolder.Folders -> PlaceholderFolder(stringResource(id = R.string.settings_placeholder_folders))
SettingsFolder.Power -> PlaceholderFolder(stringResource(id = R.string.settings_placeholder_power))
SettingsFolder.Power -> PowerFolder()
SettingsFolder.Language -> LanguageFolder(state, viewModel)
}
@@ -326,7 +360,6 @@ private fun folderTitle(folder: SettingsFolder): String = when (folder) {
SettingsFolder.Privacy -> stringResource(id = R.string.settings_folder_privacy)
SettingsFolder.Notifications -> stringResource(id = R.string.settings_folder_notifications)
SettingsFolder.Data -> stringResource(id = R.string.settings_folder_data)
SettingsFolder.Folders -> stringResource(id = R.string.settings_folder_folders)
SettingsFolder.Devices -> stringResource(id = R.string.settings_folder_devices)
SettingsFolder.Power -> stringResource(id = R.string.settings_folder_power)
SettingsFolder.Language -> stringResource(id = R.string.settings_folder_language)
@@ -517,8 +550,231 @@ private fun DevicesFolder(state: AccountUiState, viewModel: AccountViewModel) {
}
@Composable
private fun PlaceholderFolder(text: String) {
SettingsCard { Text(text) }
private fun DataFolder() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var cacheStats by remember { mutableStateOf<SettingsCacheStats?>(null) }
var isClearing by remember { mutableStateOf(false) }
suspend fun refreshStats() {
cacheStats = readSettingsCacheStats(context)
}
LaunchedEffect(Unit) {
refreshStats()
}
SettingsCard {
Text(stringResource(id = R.string.settings_data_usage_title), style = MaterialTheme.typography.titleSmall)
val stats = cacheStats
if (stats == null) {
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
} else {
SettingsMetricRow(
title = stringResource(id = R.string.settings_data_images_cache),
value = formatSettingsBytes(stats.coilImagesBytes),
)
SettingsMetricRow(
title = stringResource(id = R.string.settings_data_media_cache),
value = formatSettingsBytes(stats.exoMediaBytes),
)
SettingsMetricRow(
title = stringResource(id = R.string.settings_data_temp_captures),
value = formatSettingsBytes(stats.capturesBytes),
)
SettingsMetricRow(
title = stringResource(id = R.string.settings_data_total_cache),
value = formatSettingsBytes(stats.totalBytes),
)
}
}
SettingsCard {
Text(stringResource(id = R.string.settings_data_actions_title), style = MaterialTheme.typography.titleSmall)
OutlinedButton(
onClick = {
scope.launch {
isClearing = true
withContext(Dispatchers.IO) {
clearSettingsCacheDirectories(context)
}
refreshStats()
isClearing = false
}
},
enabled = !isClearing,
modifier = Modifier.fillMaxWidth(),
) {
if (isClearing) {
CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
} else {
Icon(Icons.Filled.DeleteOutline, contentDescription = null)
}
Text(
text = stringResource(id = R.string.settings_data_clear_cache),
modifier = Modifier.padding(start = 6.dp),
)
}
Text(
text = stringResource(id = R.string.settings_data_clear_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun PowerFolder() {
val context = LocalContext.current
val powerManager = remember {
context.getSystemService(PowerManager::class.java)
}
val powerSnapshot = produceState(initialValue = readPowerState(context, powerManager)) {
value = readPowerState(context, powerManager)
}
SettingsCard {
Text(stringResource(id = R.string.settings_power_status_title), style = MaterialTheme.typography.titleSmall)
SettingsMetricRow(
title = stringResource(id = R.string.settings_power_saver_enabled),
value = if (powerSnapshot.value.isPowerSaveMode) {
context.getString(R.string.settings_status_enabled)
} else {
context.getString(R.string.settings_status_disabled)
},
)
SettingsMetricRow(
title = stringResource(id = R.string.settings_power_optimizations),
value = if (powerSnapshot.value.ignoresOptimizations) {
context.getString(R.string.settings_power_not_optimized)
} else {
context.getString(R.string.settings_power_optimized)
},
)
Text(
text = stringResource(id = R.string.settings_power_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
SettingsCard {
Text(stringResource(id = R.string.settings_power_actions_title), style = MaterialTheme.typography.titleSmall)
OutlinedButton(
onClick = {
context.startActivity(Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
},
modifier = Modifier.fillMaxWidth(),
) {
Icon(Icons.Filled.BatterySaver, contentDescription = null)
Text(
text = stringResource(id = R.string.settings_power_open_battery_saver),
modifier = Modifier.padding(start = 6.dp),
)
}
OutlinedButton(
onClick = {
val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
} else {
Settings.ACTION_SETTINGS
}
context.startActivity(Intent(action).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
},
modifier = Modifier.fillMaxWidth(),
) {
Icon(Icons.Filled.Security, contentDescription = null)
Text(
text = stringResource(id = R.string.settings_power_open_optimization_settings),
modifier = Modifier.padding(start = 6.dp),
)
}
}
}
private data class SettingsCacheStats(
val coilImagesBytes: Long,
val exoMediaBytes: Long,
val capturesBytes: Long,
) {
val totalBytes: Long = coilImagesBytes + exoMediaBytes + capturesBytes
}
private data class PowerSnapshot(
val isPowerSaveMode: Boolean,
val ignoresOptimizations: Boolean,
)
@Composable
private fun SettingsMetricRow(title: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f),
)
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
)
}
}
private suspend fun readSettingsCacheStats(context: android.content.Context): SettingsCacheStats = withContext(Dispatchers.IO) {
SettingsCacheStats(
coilImagesBytes = File(context.cacheDir, "coil_images").directorySize(),
exoMediaBytes = File(context.cacheDir, "exo_media_cache").directorySize(),
capturesBytes = File(context.cacheDir, "captures").directorySize(),
)
}
private suspend fun clearSettingsCacheDirectories(context: android.content.Context) = withContext(Dispatchers.IO) {
listOf(
File(context.cacheDir, "coil_images"),
File(context.cacheDir, "exo_media_cache"),
File(context.cacheDir, "captures"),
).forEach { file ->
if (file.exists()) {
file.deleteRecursively()
file.mkdirs()
}
}
}
private fun File.directorySize(): Long {
if (!exists()) return 0L
if (isFile) return length()
return walkTopDown()
.filter { it.isFile }
.sumOf { it.length() }
}
private fun formatSettingsBytes(value: Long): String {
if (value < 1024) return "$value B"
val kb = value / 1024.0
if (kb < 1024) return String.format("%.1f KB", kb)
val mb = kb / 1024.0
if (mb < 1024) return String.format("%.1f MB", mb)
val gb = mb / 1024.0
return String.format("%.1f GB", gb)
}
private fun readPowerState(context: android.content.Context, powerManager: PowerManager?): PowerSnapshot {
val isSaver = powerManager?.isPowerSaveMode == true
val ignores = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
powerManager?.isIgnoringBatteryOptimizations(context.packageName) == true
} else {
true
}
return PowerSnapshot(
isPowerSaveMode = isSaver,
ignoresOptimizations = ignores,
)
}
@Composable
@@ -600,7 +856,13 @@ private fun SettingsRow(icon: ImageVector, title: String, subtitle: String, divi
}
@Composable
private fun SettingsToggle(icon: ImageVector, title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
private fun SettingsToggle(
icon: ImageVector,
title: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true,
) {
Row(
modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(14.dp)).background(MaterialTheme.colorScheme.surfaceContainerHighest).padding(horizontal = 10.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -608,7 +870,7 @@ private fun SettingsToggle(icon: ImageVector, title: String, checked: Boolean, o
) {
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
Text(title, modifier = Modifier.weight(1f))
Switch(checked = checked, onCheckedChange = onCheckedChange)
Switch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled)
}
}

View File

@@ -260,6 +260,26 @@
<string name="settings_folders_subtitle">Сортировка чатов по папкам</string>
<string name="settings_devices_subtitle">Управление активными сессиями</string>
<string name="settings_power_subtitle">Экономия энергии при низком заряде</string>
<string name="settings_preferences_header">Быстрые настройки</string>
<string name="settings_data_usage_title">Кэшированные медиа</string>
<string name="settings_data_images_cache">Кэш изображений</string>
<string name="settings_data_media_cache">Кэш видео и аудио</string>
<string name="settings_data_temp_captures">Временные записи</string>
<string name="settings_data_total_cache">Всего кэша</string>
<string name="settings_data_actions_title">Действия с данными</string>
<string name="settings_data_clear_cache">Очистить кэш медиа</string>
<string name="settings_data_clear_hint">Это удалит временные копии медиа и освободит место. Сами чаты и файлы на сервере не пострадают.</string>
<string name="settings_power_status_title">Состояние энергосбережения</string>
<string name="settings_power_saver_enabled">Режим энергосбережения</string>
<string name="settings_power_optimizations">Оптимизация батареи</string>
<string name="settings_power_not_optimized">Без ограничений</string>
<string name="settings_power_optimized">Оптимизируется системой</string>
<string name="settings_power_hint">Используйте системные настройки, если уведомления или синхронизация в фоне становятся слишком агрессивно ограничены.</string>
<string name="settings_power_actions_title">Действия</string>
<string name="settings_power_open_battery_saver">Открыть настройки энергосбережения</string>
<string name="settings_power_open_optimization_settings">Открыть настройки оптимизации</string>
<string name="settings_status_enabled">Включено</string>
<string name="settings_status_disabled">Выключено</string>
<string name="settings_placeholder_data">Этот раздел будет расширен на следующем шаге.</string>
<string name="settings_placeholder_folders">Управление папками чатов будет добавлено на следующей итерации.</string>
<string name="settings_placeholder_power">Настройки энергосбережения будут добавлены отдельным этапом.</string>

View File

@@ -260,6 +260,26 @@
<string name="settings_folders_subtitle">Sort chats by folders</string>
<string name="settings_devices_subtitle">Manage active sessions</string>
<string name="settings_power_subtitle">Save power on low battery</string>
<string name="settings_preferences_header">Quick preferences</string>
<string name="settings_data_usage_title">Cached media</string>
<string name="settings_data_images_cache">Image cache</string>
<string name="settings_data_media_cache">Video and audio cache</string>
<string name="settings_data_temp_captures">Temporary captures</string>
<string name="settings_data_total_cache">Total cached data</string>
<string name="settings_data_actions_title">Data actions</string>
<string name="settings_data_clear_cache">Clear cached media</string>
<string name="settings_data_clear_hint">This clears temporary media copies and frees local space. Chats and server files stay intact.</string>
<string name="settings_power_status_title">Power status</string>
<string name="settings_power_saver_enabled">Battery saver</string>
<string name="settings_power_optimizations">Battery optimization</string>
<string name="settings_power_not_optimized">Not restricted</string>
<string name="settings_power_optimized">Optimized by the system</string>
<string name="settings_power_hint">Use system settings if message delivery or background sync become aggressive under battery restrictions.</string>
<string name="settings_power_actions_title">Power actions</string>
<string name="settings_power_open_battery_saver">Open battery saver settings</string>
<string name="settings_power_open_optimization_settings">Open optimization settings</string>
<string name="settings_status_enabled">Enabled</string>
<string name="settings_status_disabled">Disabled</string>
<string name="settings_placeholder_data">This section will be expanded in the next step.</string>
<string name="settings_placeholder_folders">Chat folders management will be added in next iteration.</string>
<string name="settings_placeholder_power">Power saving settings will be added in a separate step.</string>