Add Android backup status summary
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* Copyright (C) 2026 QSfera.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License version 2,
|
||||
* as published by the Free Software Foundation.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.presentation.settings.automaticuploads
|
||||
|
||||
import android.content.Context
|
||||
import eu.qsfera.android.R
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
|
||||
|
||||
internal object AutomaticUploadStatusFormatter {
|
||||
|
||||
fun format(
|
||||
context: Context,
|
||||
configuration: FolderBackUpConfiguration?,
|
||||
sourcePaths: List<String>,
|
||||
lastSyncSummary: String,
|
||||
): String {
|
||||
if (configuration == null) {
|
||||
return context.getString(R.string.prefs_camera_upload_status_off)
|
||||
}
|
||||
|
||||
return listOf(
|
||||
context.getString(R.string.prefs_camera_upload_status_on),
|
||||
formatSourceCount(context, sourcePaths.size),
|
||||
formatConditions(context, configuration),
|
||||
context.getString(R.string.prefs_camera_upload_status_last_scan, lastSyncSummary),
|
||||
).joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
private fun formatSourceCount(context: Context, sourceCount: Int): String =
|
||||
if (sourceCount == 0) {
|
||||
context.getString(R.string.prefs_camera_upload_source_paths_empty)
|
||||
} else {
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.prefs_camera_upload_status_source_count,
|
||||
sourceCount,
|
||||
sourceCount
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatConditions(context: Context, configuration: FolderBackUpConfiguration): String {
|
||||
val conditions = mutableListOf<String>()
|
||||
if (configuration.wifiOnly) {
|
||||
conditions += context.getString(R.string.prefs_camera_upload_status_wifi_only)
|
||||
} else {
|
||||
conditions += context.getString(
|
||||
R.string.prefs_camera_upload_status_mobile_data,
|
||||
formatMobileDataLimit(context, configuration.mobileDataLimitBytes)
|
||||
)
|
||||
if (configuration.allowRoaming) {
|
||||
conditions += context.getString(R.string.prefs_camera_upload_status_roaming_allowed)
|
||||
}
|
||||
}
|
||||
if (configuration.chargingOnly) {
|
||||
conditions += context.getString(R.string.prefs_camera_upload_status_charging_only)
|
||||
}
|
||||
return conditions.joinToString(separator = " • ")
|
||||
}
|
||||
|
||||
private fun formatMobileDataLimit(context: Context, mobileDataLimitBytes: Long): String {
|
||||
val values = context.resources.getStringArray(R.array.prefs_camera_upload_mobile_data_limit_values)
|
||||
val entries = context.resources.getStringArray(R.array.prefs_camera_upload_mobile_data_limit_entries)
|
||||
val index = values.indexOf(mobileDataLimitBytes.toString())
|
||||
return entries.getOrNull(index)
|
||||
?: context.getString(R.string.prefs_camera_upload_status_custom_mobile_data_limit, mobileDataLimitBytes)
|
||||
}
|
||||
}
|
||||
+15
@@ -67,6 +67,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
private val picturesViewModel by viewModel<SettingsPictureUploadsViewModel>()
|
||||
private val manageAccountsViewModel by viewModel<ManageAccountsViewModel>()
|
||||
|
||||
private var prefPictureUploadsStatus: Preference? = null
|
||||
private var prefEnablePictureUploads: SwitchPreferenceCompat? = null
|
||||
private var prefPictureUploadsPath: Preference? = null
|
||||
private var prefPictureUploadsOnWifi: CheckBoxPreference? = null
|
||||
@@ -101,6 +102,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_picture_uploads, rootKey)
|
||||
|
||||
prefPictureUploadsStatus = findPreference(PREF_PICTURE_UPLOADS_STATUS)
|
||||
prefEnablePictureUploads = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ENABLED)
|
||||
prefPictureUploadsPath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH)
|
||||
prefPictureUploadsOnWifi = findPreference(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY)
|
||||
@@ -162,6 +164,12 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefPictureUploadsAccount?.value = it.accountName
|
||||
prefPictureUploadsPath?.summary = picturesViewModel.getUploadPathString()
|
||||
val sourcePaths = picturesViewModel.getPictureUploadsSourcePaths()
|
||||
prefPictureUploadsStatus?.summary = AutomaticUploadStatusFormatter.format(
|
||||
context = requireContext(),
|
||||
configuration = it,
|
||||
sourcePaths = sourcePaths,
|
||||
lastSyncSummary = formatLastSyncSummary(it.lastSyncTimestamp),
|
||||
)
|
||||
prefPictureUploadsSourcePath?.summary = getSourcePathsSummary(sourcePaths)
|
||||
prefPictureUploadsClearSourcePaths?.isEnabled = sourcePaths.isNotEmpty()
|
||||
prefPictureUploadsOnWifi?.isChecked = it.wifiOnly
|
||||
@@ -314,6 +322,12 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
private fun resetFields() {
|
||||
prefPictureUploadsStatus?.summary = AutomaticUploadStatusFormatter.format(
|
||||
context = requireContext(),
|
||||
configuration = null,
|
||||
sourcePaths = emptyList(),
|
||||
lastSyncSummary = getString(R.string.prefs_camera_upload_last_sync_never),
|
||||
)
|
||||
prefPictureUploadsAccount?.value = null
|
||||
prefPictureUploadsPath?.summary = null
|
||||
prefPictureUploadsSourcePath?.summary = getString(R.string.prefs_camera_upload_source_paths_empty)
|
||||
@@ -343,6 +357,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_PICTURE_UPLOADS_STATUS = "picture_uploads_status"
|
||||
private const val PREF_PICTURE_UPLOADS_CLEAR_SOURCE_PATHS = "picture_uploads_clear_source_paths"
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -67,6 +67,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
private val videosViewModel by viewModel<SettingsVideoUploadsViewModel>()
|
||||
private val manageAccountsViewModel by viewModel<ManageAccountsViewModel>()
|
||||
|
||||
private var prefVideoUploadsStatus: Preference? = null
|
||||
private var prefEnableVideoUploads: SwitchPreferenceCompat? = null
|
||||
private var prefVideoUploadsPath: Preference? = null
|
||||
private var prefVideoUploadsOnWifi: CheckBoxPreference? = null
|
||||
@@ -101,6 +102,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_video_uploads, rootKey)
|
||||
|
||||
prefVideoUploadsStatus = findPreference(PREF_VIDEO_UPLOADS_STATUS)
|
||||
prefEnableVideoUploads = findPreference(PREF__CAMERA_VIDEO_UPLOADS_ENABLED)
|
||||
prefVideoUploadsPath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH)
|
||||
prefVideoUploadsOnWifi = findPreference(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY)
|
||||
@@ -160,6 +162,12 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefVideoUploadsAccount?.value = it.accountName
|
||||
prefVideoUploadsPath?.summary = videosViewModel.getUploadPathString()
|
||||
val sourcePaths = videosViewModel.getVideoUploadsSourcePaths()
|
||||
prefVideoUploadsStatus?.summary = AutomaticUploadStatusFormatter.format(
|
||||
context = requireContext(),
|
||||
configuration = it,
|
||||
sourcePaths = sourcePaths,
|
||||
lastSyncSummary = formatLastSyncSummary(it.lastSyncTimestamp),
|
||||
)
|
||||
prefVideoUploadsSourcePath?.summary = getSourcePathsSummary(sourcePaths)
|
||||
prefVideoUploadsClearSourcePaths?.isEnabled = sourcePaths.isNotEmpty()
|
||||
prefVideoUploadsOnWifi?.isChecked = it.wifiOnly
|
||||
@@ -312,6 +320,12 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
private fun resetFields() {
|
||||
prefVideoUploadsStatus?.summary = AutomaticUploadStatusFormatter.format(
|
||||
context = requireContext(),
|
||||
configuration = null,
|
||||
sourcePaths = emptyList(),
|
||||
lastSyncSummary = getString(R.string.prefs_camera_upload_last_sync_never),
|
||||
)
|
||||
prefVideoUploadsAccount?.value = null
|
||||
prefVideoUploadsPath?.summary = null
|
||||
prefVideoUploadsSourcePath?.summary = getString(R.string.prefs_camera_upload_source_paths_empty)
|
||||
@@ -341,6 +355,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_VIDEO_UPLOADS_STATUS = "video_uploads_status"
|
||||
private const val PREF_VIDEO_UPLOADS_CLEAR_SOURCE_PATHS = "video_uploads_clear_source_paths"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,6 +563,21 @@
|
||||
<string name="prefs_camera_upload_source_paths_empty">Папки телефона не выбраны</string>
|
||||
<string name="confirmation_clear_camera_upload_sources_title">Убрать выбранные папки</string>
|
||||
<string name="confirmation_clear_camera_upload_sources_message">Копирование перестанет проверять папки телефона, пока вы снова не добавите папку.</string>
|
||||
<string name="prefs_camera_upload_status_title">Состояние копирования</string>
|
||||
<string name="prefs_camera_upload_status_off">Выключено</string>
|
||||
<string name="prefs_camera_upload_status_on">Включено</string>
|
||||
<plurals name="prefs_camera_upload_status_source_count">
|
||||
<item quantity="one">%d папка телефона</item>
|
||||
<item quantity="few">%d папки телефона</item>
|
||||
<item quantity="many">%d папок телефона</item>
|
||||
<item quantity="other">%d папки телефона</item>
|
||||
</plurals>
|
||||
<string name="prefs_camera_upload_status_wifi_only">Только Wi-Fi</string>
|
||||
<string name="prefs_camera_upload_status_mobile_data">Мобильные данные: %1$s</string>
|
||||
<string name="prefs_camera_upload_status_custom_mobile_data_limit">%1$d байт в день</string>
|
||||
<string name="prefs_camera_upload_status_roaming_allowed">роуминг разрешён</string>
|
||||
<string name="prefs_camera_upload_status_charging_only">только во время зарядки</string>
|
||||
<string name="prefs_camera_upload_status_last_scan">Последняя проверка: %1$s</string>
|
||||
<string name="prefs_camera_upload_allow_roaming">Разрешить копирование в роуминге</string>
|
||||
<string name="prefs_camera_upload_allow_roaming_summary">Применяется только если копирование только по Wi-Fi выключено</string>
|
||||
<string name="prefs_camera_upload_behaviour_dialog_title">Исходный файл после копирования</string>
|
||||
|
||||
@@ -602,6 +602,19 @@
|
||||
<string name="prefs_camera_upload_source_paths_empty">No phone folders selected</string>
|
||||
<string name="confirmation_clear_camera_upload_sources_title">Remove selected folders</string>
|
||||
<string name="confirmation_clear_camera_upload_sources_message">Backup will stop scanning phone folders until you add a folder again.</string>
|
||||
<string name="prefs_camera_upload_status_title">Backup status</string>
|
||||
<string name="prefs_camera_upload_status_off">Off</string>
|
||||
<string name="prefs_camera_upload_status_on">On</string>
|
||||
<plurals name="prefs_camera_upload_status_source_count">
|
||||
<item quantity="one">%d phone folder</item>
|
||||
<item quantity="other">%d phone folders</item>
|
||||
</plurals>
|
||||
<string name="prefs_camera_upload_status_wifi_only">Wi-Fi only</string>
|
||||
<string name="prefs_camera_upload_status_mobile_data">Mobile data: %1$s</string>
|
||||
<string name="prefs_camera_upload_status_custom_mobile_data_limit">%1$d bytes per day</string>
|
||||
<string name="prefs_camera_upload_status_roaming_allowed">roaming allowed</string>
|
||||
<string name="prefs_camera_upload_status_charging_only">charging only</string>
|
||||
<string name="prefs_camera_upload_status_last_scan">Last scan: %1$s</string>
|
||||
<string name="prefs_camera_upload_allow_roaming">Allow backup while roaming</string>
|
||||
<string name="prefs_camera_upload_allow_roaming_summary">Only applies when Wi-Fi-only backup is off</string>
|
||||
<string name="prefs_camera_upload_mobile_data_limit_title">Mobile data daily limit</string>
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
app:key="enable_picture_uploads"
|
||||
app:summary="@string/prefs_camera_picture_upload_summary"
|
||||
app:title="@string/prefs_camera_picture_upload" />
|
||||
<Preference
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="picture_uploads_status"
|
||||
app:selectable="false"
|
||||
app:summary="@string/prefs_camera_upload_status_off"
|
||||
app:title="@string/prefs_camera_upload_status_title" />
|
||||
<ListPreference
|
||||
app:dialogTitle="@string/prefs_picture_upload_account"
|
||||
app:iconSpaceReserved="false"
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
app:key="enable_video_uploads"
|
||||
app:summary="@string/prefs_camera_video_upload_summary"
|
||||
app:title="@string/prefs_camera_video_upload" />
|
||||
<Preference
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="video_uploads_status"
|
||||
app:selectable="false"
|
||||
app:summary="@string/prefs_camera_upload_status_off"
|
||||
app:title="@string/prefs_camera_upload_status_title" />
|
||||
<ListPreference
|
||||
app:dialogTitle="@string/prefs_video_upload_account"
|
||||
app:iconSpaceReserved="false"
|
||||
|
||||
@@ -33,6 +33,7 @@ Date: 2026-06-08.
|
||||
- Android auto-upload now exposes a no-roaming control for photo and video backup, stores it in Room schema version `50`, and maps mobile backup without roaming to WorkManager `NetworkType.NOT_ROAMING`.
|
||||
- Android auto-upload now exposes a photo/video daily mobile-data limit, stores it in Room schema version `51`, and stops queuing additional uploads on metered automatic-upload runs after the configured daily byte limit is reached while keeping skipped files eligible for the next scan period.
|
||||
- Android video backup settings now label the Wi-Fi-only control as "Never use mobile data for videos" and explain that turning it off enables metered-network limits and roaming controls.
|
||||
- Android photo/video backup settings now show a compact overall backup status: enabled state, selected phone-folder count, network/roaming/charging conditions, and last backup scan.
|
||||
- Server `qsfera_full` compose files now require explicit admin, Keycloak, and S3/MinIO secrets instead of demo defaults.
|
||||
- Server `qsfera_full/env.production.example` provides a tracked production-safe starting point with TLS validation enabled and demo users disabled.
|
||||
- Server `qsfera_full/validate-production-env.sh` checks production `.env` invariants before compose validation: TLS validation enabled, demo users disabled, pinned production image, required domains, required secrets, and no public-production MinIO.
|
||||
@@ -84,7 +85,7 @@ Date: 2026-06-08.
|
||||
|
||||
## Remaining P1
|
||||
|
||||
- Add Google Photos-style backup status UX: overall status, per-item status, queued/paused/permanent-failure states, and low-storage messaging.
|
||||
- Extend Android backup status UX beyond the new overall settings summary with per-item status, queued/paused/permanent-failure states, and low-storage messaging.
|
||||
- Store auto-upload sync cursors per source folder rather than one timestamp for all folders.
|
||||
- Configure `QSFERA_ALERT_WEBHOOK_URL` in `/etc/qsfera/rpi-production-check.env` if off-host alert delivery is required; the installed failure service currently logs through `systemd-cat` and journal without an external endpoint.
|
||||
- Verify sharing controls against Google Drive and Yandex Disk expectations: link expiration, password, disable downloads, organization-only access, and role inheritance.
|
||||
|
||||
Reference in New Issue
Block a user