Add Android backup roaming control

This commit is contained in:
Курнат Андрей
2026-06-08 21:50:33 +03:00
parent c9c4b1b17d
commit cf366b2699
22 changed files with 1375 additions and 4 deletions
@@ -48,8 +48,10 @@ public abstract class PreferenceManager {
public static final String PREF__CAMERA_VIDEO_UPLOADS_ENABLED = "enable_video_uploads";
public static final String PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "picture_uploads_on_wifi";
public static final String PREF__CAMERA_PICTURE_UPLOADS_CHARGING_ONLY = "picture_uploads_on_charging";
public static final String PREF__CAMERA_PICTURE_UPLOADS_ALLOW_ROAMING = "picture_uploads_allow_roaming";
public static final String PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY = "video_uploads_on_wifi";
public static final String PREF__CAMERA_VIDEO_UPLOADS_CHARGING_ONLY = "video_uploads_on_charging";
public static final String PREF__CAMERA_VIDEO_UPLOADS_ALLOW_ROAMING = "video_uploads_allow_roaming";
public static final String PREF__CAMERA_PICTURE_UPLOADS_PATH = "picture_uploads_path";
public static final String PREF__CAMERA_VIDEO_UPLOADS_PATH = "video_uploads_path";
public static final String PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR = "picture_uploads_behaviour";
@@ -41,6 +41,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import eu.qsfera.android.R
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ALLOW_ROAMING
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_CHARGING_ONLY
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ENABLED
@@ -68,6 +69,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
private var prefEnablePictureUploads: SwitchPreferenceCompat? = null
private var prefPictureUploadsPath: Preference? = null
private var prefPictureUploadsOnWifi: CheckBoxPreference? = null
private var prefPictureUploadsAllowRoaming: CheckBoxPreference? = null
private var prefPictureUploadsOnCharging: CheckBoxPreference? = null
private var prefPictureUploadsSourcePath: Preference? = null
private var prefPictureUploadsClearSourcePaths: Preference? = null
@@ -100,6 +102,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
prefEnablePictureUploads = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ENABLED)
prefPictureUploadsPath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH)
prefPictureUploadsOnWifi = findPreference(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY)
prefPictureUploadsAllowRoaming = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ALLOW_ROAMING)
prefPictureUploadsOnCharging = findPreference(PREF__CAMERA_PICTURE_UPLOADS_CHARGING_ONLY)
prefPictureUploadsSourcePath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE)
prefPictureUploadsClearSourcePaths = findPreference(PREF_PICTURE_UPLOADS_CLEAR_SOURCE_PATHS)
@@ -157,6 +160,8 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
prefPictureUploadsSourcePath?.summary = getSourcePathsSummary(sourcePaths)
prefPictureUploadsClearSourcePaths?.isEnabled = sourcePaths.isNotEmpty()
prefPictureUploadsOnWifi?.isChecked = it.wifiOnly
prefPictureUploadsAllowRoaming?.isChecked = it.allowRoaming
prefPictureUploadsAllowRoaming?.isEnabled = !it.wifiOnly
prefPictureUploadsOnCharging?.isChecked = it.chargingOnly
prefPictureUploadsBehaviour?.value = it.behavior.name
prefPictureUploadsLastSync?.summary = formatLastSyncSummary(it.lastSyncTimestamp)
@@ -243,6 +248,13 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
prefPictureUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue ->
newValue as Boolean
picturesViewModel.useWifiOnly(newValue)
prefPictureUploadsAllowRoaming?.isEnabled = !newValue
newValue
}
prefPictureUploadsAllowRoaming?.setOnPreferenceChangeListener { _, newValue ->
newValue as Boolean
picturesViewModel.allowRoaming(newValue)
newValue
}
@@ -277,6 +289,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
}
prefPictureUploadsPath?.isEnabled = value
prefPictureUploadsOnWifi?.isEnabled = value
prefPictureUploadsAllowRoaming?.isEnabled = value && prefPictureUploadsOnWifi?.isChecked != true
prefPictureUploadsOnCharging?.isEnabled = value
prefPictureUploadsSourcePath?.isEnabled = value
prefPictureUploadsClearSourcePaths?.isEnabled = value && picturesViewModel.getPictureUploadsSourcePaths().isNotEmpty()
@@ -291,6 +304,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
prefPictureUploadsSourcePath?.summary = getString(R.string.prefs_camera_upload_source_paths_empty)
prefPictureUploadsClearSourcePaths?.isEnabled = false
prefPictureUploadsOnWifi?.isChecked = false
prefPictureUploadsAllowRoaming?.isChecked = false
prefPictureUploadsOnCharging?.isChecked = false
prefPictureUploadsBehaviour?.value = UploadBehavior.COPY.name
prefPictureUploadsLastSync?.summary = getString(R.string.prefs_camera_upload_last_sync_never)
@@ -112,6 +112,16 @@ class SettingsPictureUploadsViewModel(
}
}
fun allowRoaming(allowRoaming: Boolean) {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
savePictureUploadsConfigurationUseCase(
SavePictureUploadsConfigurationUseCase.Params(
composePictureUploadsConfiguration(allowRoaming = allowRoaming)
)
)
}
}
fun useChargingOnly(chargingOnly: Boolean) {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
savePictureUploadsConfigurationUseCase(
@@ -211,6 +221,7 @@ class SettingsPictureUploadsViewModel(
accountName: String? = _pictureUploads.value?.accountName,
uploadPath: String? = _pictureUploads.value?.uploadPath,
wifiOnly: Boolean? = _pictureUploads.value?.wifiOnly,
allowRoaming: Boolean? = _pictureUploads.value?.allowRoaming,
chargingOnly: Boolean? = _pictureUploads.value?.chargingOnly,
sourcePath: String? = _pictureUploads.value?.sourcePath,
behavior: UploadBehavior? = _pictureUploads.value?.behavior,
@@ -222,6 +233,7 @@ class SettingsPictureUploadsViewModel(
sourcePath = sourcePath.orEmpty(),
uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH,
wifiOnly = wifiOnly ?: true,
allowRoaming = allowRoaming ?: false,
chargingOnly = chargingOnly ?: false,
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
name = _pictureUploads.value?.name ?: pictureUploadsName,
@@ -42,6 +42,7 @@ import androidx.preference.SwitchPreferenceCompat
import eu.qsfera.android.R
import eu.qsfera.android.db.PreferenceManager
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ALLOW_ROAMING
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_CHARGING_ONLY
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ENABLED
@@ -68,6 +69,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
private var prefEnableVideoUploads: SwitchPreferenceCompat? = null
private var prefVideoUploadsPath: Preference? = null
private var prefVideoUploadsOnWifi: CheckBoxPreference? = null
private var prefVideoUploadsAllowRoaming: CheckBoxPreference? = null
private var prefVideoUploadsOnCharging: CheckBoxPreference? = null
private var prefVideoUploadsSourcePath: Preference? = null
private var prefVideoUploadsClearSourcePaths: Preference? = null
@@ -100,6 +102,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
prefEnableVideoUploads = findPreference(PREF__CAMERA_VIDEO_UPLOADS_ENABLED)
prefVideoUploadsPath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH)
prefVideoUploadsOnWifi = findPreference(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY)
prefVideoUploadsAllowRoaming = findPreference(PREF__CAMERA_VIDEO_UPLOADS_ALLOW_ROAMING)
prefVideoUploadsOnCharging = findPreference(PREF__CAMERA_VIDEO_UPLOADS_CHARGING_ONLY)
prefVideoUploadsSourcePath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE)
prefVideoUploadsClearSourcePaths = findPreference(PREF_VIDEO_UPLOADS_CLEAR_SOURCE_PATHS)
@@ -155,6 +158,8 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
prefVideoUploadsSourcePath?.summary = getSourcePathsSummary(sourcePaths)
prefVideoUploadsClearSourcePaths?.isEnabled = sourcePaths.isNotEmpty()
prefVideoUploadsOnWifi?.isChecked = it.wifiOnly
prefVideoUploadsAllowRoaming?.isChecked = it.allowRoaming
prefVideoUploadsAllowRoaming?.isEnabled = !it.wifiOnly
prefVideoUploadsOnCharging?.isChecked = it.chargingOnly
prefVideoUploadsBehaviour?.value = it.behavior.name
prefVideoUploadsLastSync?.summary = formatLastSyncSummary(it.lastSyncTimestamp)
@@ -241,6 +246,13 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
prefVideoUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue ->
newValue as Boolean
videosViewModel.useWifiOnly(newValue)
prefVideoUploadsAllowRoaming?.isEnabled = !newValue
newValue
}
prefVideoUploadsAllowRoaming?.setOnPreferenceChangeListener { _, newValue ->
newValue as Boolean
videosViewModel.allowRoaming(newValue)
newValue
}
@@ -275,6 +287,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
}
prefVideoUploadsPath?.isEnabled = value
prefVideoUploadsOnWifi?.isEnabled = value
prefVideoUploadsAllowRoaming?.isEnabled = value && prefVideoUploadsOnWifi?.isChecked != true
prefVideoUploadsOnCharging?.isEnabled = value
prefVideoUploadsSourcePath?.isEnabled = value
prefVideoUploadsClearSourcePaths?.isEnabled = value && videosViewModel.getVideoUploadsSourcePaths().isNotEmpty()
@@ -289,6 +302,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
prefVideoUploadsSourcePath?.summary = getString(R.string.prefs_camera_upload_source_paths_empty)
prefVideoUploadsClearSourcePaths?.isEnabled = false
prefVideoUploadsOnWifi?.isChecked = false
prefVideoUploadsAllowRoaming?.isChecked = false
prefVideoUploadsOnCharging?.isChecked = false
prefVideoUploadsBehaviour?.value = UploadBehavior.COPY.name
prefVideoUploadsLastSync?.summary = getString(R.string.prefs_camera_upload_last_sync_never)
@@ -112,6 +112,16 @@ class SettingsVideoUploadsViewModel(
}
}
fun allowRoaming(allowRoaming: Boolean) {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
saveVideoUploadsConfigurationUseCase(
SaveVideoUploadsConfigurationUseCase.Params(
composeVideoUploadsConfiguration(allowRoaming = allowRoaming)
)
)
}
}
fun useChargingOnly(chargingOnly: Boolean) {
viewModelScope.launch(coroutinesDispatcherProvider.io) {
saveVideoUploadsConfigurationUseCase(
@@ -211,6 +221,7 @@ class SettingsVideoUploadsViewModel(
accountName: String? = _videoUploads.value?.accountName,
uploadPath: String? = _videoUploads.value?.uploadPath,
wifiOnly: Boolean? = _videoUploads.value?.wifiOnly,
allowRoaming: Boolean? = _videoUploads.value?.allowRoaming,
chargingOnly: Boolean? = _videoUploads.value?.chargingOnly,
sourcePath: String? = _videoUploads.value?.sourcePath,
behavior: UploadBehavior? = _videoUploads.value?.behavior,
@@ -223,6 +234,7 @@ class SettingsVideoUploadsViewModel(
sourcePath = sourcePath.orEmpty(),
uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH,
wifiOnly = wifiOnly ?: true,
allowRoaming = allowRoaming ?: false,
chargingOnly = chargingOnly ?: false,
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
name = _videoUploads.value?.name ?: videoUploadsName,
@@ -58,7 +58,7 @@ class UploadFileFromContentUriUseCase(
.putString(UploadFileFromContentUriWorker.KEY_PARAM_CONTENT_URI, params.contentUri.toString())
.build()
val networkRequired = if (params.wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED
val networkRequired = getRequiredNetworkType(params.wifiOnly, params.allowRoaming)
val constraints = Constraints.Builder()
.setRequiredNetworkType(networkRequired)
.setRequiresCharging(params.chargingOnly)
@@ -109,6 +109,16 @@ class UploadFileFromContentUriUseCase(
val uploadPath: String,
val uploadIdInStorageManager: Long,
val wifiOnly: Boolean,
val allowRoaming: Boolean = true,
val chargingOnly: Boolean,
)
companion object {
fun getRequiredNetworkType(wifiOnly: Boolean, allowRoaming: Boolean): NetworkType =
when {
wifiOnly -> NetworkType.UNMETERED
allowRoaming -> NetworkType.CONNECTED
else -> NetworkType.NOT_ROAMING
}
}
}
@@ -195,6 +195,7 @@ class AutomaticUploadsWorker(
accountName = folderBackUpConfiguration.accountName,
uploadId = uploadId,
wifiOnly = folderBackUpConfiguration.wifiOnly,
allowRoaming = folderBackUpConfiguration.allowRoaming,
chargingOnly = folderBackUpConfiguration.chargingOnly
)
}
@@ -309,6 +310,7 @@ class AutomaticUploadsWorker(
accountName: String,
uploadId: Long,
wifiOnly: Boolean,
allowRoaming: Boolean,
chargingOnly: Boolean
) {
val lastModifiedInSeconds = (lastModified / 1000L).toString()
@@ -322,6 +324,7 @@ class AutomaticUploadsWorker(
uploadPath = uploadPath,
uploadIdInStorageManager = uploadId,
wifiOnly = wifiOnly,
allowRoaming = allowRoaming,
chargingOnly = chargingOnly
)
)
@@ -561,6 +561,8 @@
<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_allow_roaming">Разрешить копирование в роуминге</string>
<string name="prefs_camera_upload_allow_roaming_summary">Применяется только если копирование только по Wi-Fi выключено</string>
<string name="prefs_camera_upload_behaviour_dialog_title">Исходный файл после копирования</string>
<string name="prefs_camera_upload_behaviour_title">Исходный файл после копирования</string>
<string name="prefs_camera_upload_last_sync_title">Последняя проверка</string>
@@ -599,6 +599,8 @@
<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_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_behaviour_dialog_title">Original file after backup</string>
<string name="prefs_camera_upload_behaviour_title">Original file after backup</string>
<string name="prefs_camera_upload_last_sync_title">Last backup scan</string>
@@ -64,6 +64,11 @@
app:iconSpaceReserved="false"
app:key="picture_uploads_on_wifi"
app:title="@string/prefs_camera_picture_upload_on_wifi" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="picture_uploads_allow_roaming"
app:summary="@string/prefs_camera_upload_allow_roaming_summary"
app:title="@string/prefs_camera_upload_allow_roaming" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="picture_uploads_on_charging"
@@ -62,6 +62,11 @@
app:iconSpaceReserved="false"
app:key="video_uploads_on_wifi"
app:title="@string/prefs_camera_video_upload_on_wifi" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="video_uploads_allow_roaming"
app:summary="@string/prefs_camera_upload_allow_roaming_summary"
app:title="@string/prefs_camera_upload_allow_roaming" />
<CheckBoxPreference
app:iconSpaceReserved="false"
app:key="video_uploads_on_charging"
@@ -0,0 +1,38 @@
package eu.qsfera.android.usecases.transfers.uploads
import androidx.work.NetworkType
import org.junit.Assert.assertEquals
import org.junit.Test
class UploadFileFromContentUriUseCaseTest {
@Test
fun `network type is unmetered when wifi only is enabled`() {
val networkType = UploadFileFromContentUriUseCase.getRequiredNetworkType(
wifiOnly = true,
allowRoaming = false
)
assertEquals(NetworkType.UNMETERED, networkType)
}
@Test
fun `network type is connected when mobile data and roaming are allowed`() {
val networkType = UploadFileFromContentUriUseCase.getRequiredNetworkType(
wifiOnly = false,
allowRoaming = true
)
assertEquals(NetworkType.CONNECTED, networkType)
}
@Test
fun `network type avoids roaming when mobile data is allowed without roaming`() {
val networkType = UploadFileFromContentUriUseCase.getRequiredNetworkType(
wifiOnly = false,
allowRoaming = false
)
assertEquals(NetworkType.NOT_ROAMING, networkType)
}
}
File diff suppressed because it is too large Load Diff
@@ -31,7 +31,7 @@ public class ProviderMeta {
public static final String DB_NAME = "filelist";
public static final String NEW_DB_NAME = "qsfera_database";
public static final int DB_VERSION = 49;
public static final int DB_VERSION = 50;
private ProviderMeta() {
}
@@ -52,6 +52,7 @@ import eu.qsfera.android.data.migrations.MIGRATION_41_42
import eu.qsfera.android.data.migrations.MIGRATION_42_43
import eu.qsfera.android.data.migrations.MIGRATION_47_48
import eu.qsfera.android.data.migrations.MIGRATION_48_49
import eu.qsfera.android.data.migrations.MIGRATION_49_50
import eu.qsfera.android.data.sharing.shares.db.OCShareDao
import eu.qsfera.android.data.sharing.shares.db.OCShareEntity
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity
@@ -127,7 +128,8 @@ abstract class QSferaDatabase : RoomDatabase() {
MIGRATION_41_42,
MIGRATION_42_43,
MIGRATION_47_48,
MIGRATION_48_49)
MIGRATION_48_49,
MIGRATION_49_50)
.build()
INSTANCE = instance
instance
@@ -68,6 +68,7 @@ class OCLocalFolderBackupDataSource(
sourcePath = sourcePath,
uploadPath = uploadPath,
wifiOnly = wifiOnly,
allowRoaming = allowRoaming,
chargingOnly = chargingOnly,
name = name,
lastSyncTimestamp = lastSyncTimestamp,
@@ -83,6 +84,7 @@ class OCLocalFolderBackupDataSource(
sourcePath = sourcePath,
uploadPath = uploadPath,
wifiOnly = wifiOnly,
allowRoaming = allowRoaming,
chargingOnly = chargingOnly,
lastSyncTimestamp = lastSyncTimestamp,
name = name,
@@ -29,6 +29,7 @@ data class FolderBackUpEntity(
val sourcePath: String,
val uploadPath: String,
val wifiOnly: Boolean,
val allowRoaming: Boolean,
val chargingOnly: Boolean,
val name: String,
val lastSyncTimestamp: Long,
@@ -55,6 +55,7 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
behavior = getBehaviorForPreference(PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR),
lastSyncTimestamp = timestamp,
name = pictureUploadsName,
allowRoaming = false,
chargingOnly = false,
spaceId = null,
)
@@ -71,6 +72,7 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
behavior = getBehaviorForPreference(PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR),
lastSyncTimestamp = timestamp,
name = videoUploadsName,
allowRoaming = false,
chargingOnly = false,
spaceId = null,
)
@@ -0,0 +1,11 @@
package eu.qsfera.android.data.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME
val MIGRATION_49_50 = object : Migration(49, 50) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE $FOLDER_BACKUP_TABLE_NAME ADD COLUMN allowRoaming INTEGER NOT NULL DEFAULT '0'")
}
}
@@ -25,6 +25,7 @@ data class FolderBackUpConfiguration(
val sourcePath: String,
val uploadPath: String,
val wifiOnly: Boolean,
val allowRoaming: Boolean,
val chargingOnly: Boolean,
val lastSyncTimestamp: Long,
val name: String,
@@ -31,6 +31,7 @@ val OC_BACKUP = FolderBackUpConfiguration(
sourcePath = "/Photos",
uploadPath = "/Photos",
wifiOnly = true,
allowRoaming = false,
chargingOnly = true,
lastSyncTimestamp = 1542628397,
name = "",
@@ -43,6 +44,7 @@ val OC_BACKUP_ENTITY = FolderBackUpEntity(
sourcePath = "/Photos",
uploadPath = "/Photos",
wifiOnly = true,
allowRoaming = false,
chargingOnly = true,
lastSyncTimestamp = 1542628397,
name = "",
+2 -1
View File
@@ -29,6 +29,7 @@ Date: 2026-06-08.
- Android auto-upload validates persisted read permission and `DocumentFile.canRead()` before scanning SAF folders.
- Android immediate auto-upload scheduling no longer blocks the caller while waiting for WorkManager state.
- Android new auto-upload configurations default to Wi-Fi-only transfers for photos and videos.
- 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`.
- 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.
@@ -73,7 +74,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.
- Add mobile-data controls comparable to Google Photos/Yandex Disk beyond the Wi-Fi-only default: mobile-data limit, no-video-over-mobile-data, and no-roaming options.
- Add mobile-data controls comparable to Google Photos/Yandex Disk beyond the Wi-Fi-only default: mobile-data limit and no-video-over-mobile-data options.
- Store auto-upload sync cursors per source folder rather than one timestamp for all folders.
- Add production monitoring runbook and compose verification for metrics, tracing, alerts, backups, and restore drills.
- Move Docker build cache and/or Docker data-root planning for the Pi to `/mnt/data`, or add documented retention, so future server image rebuilds cannot fill the root filesystem again.