From cf366b2699b2ea2cf0103dcbc915991c2eee9acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Mon, 8 Jun 2026 21:50:33 +0300 Subject: [PATCH] Add Android backup roaming control --- .../qsfera/android/db/PreferenceManager.java | 2 + .../SettingsPictureUploadsFragment.kt | 14 + .../SettingsPictureUploadsViewModel.kt | 12 + .../SettingsVideoUploadsFragment.kt | 14 + .../SettingsVideoUploadsViewModel.kt | 12 + .../UploadFileFromContentUriUseCase.kt | 12 +- .../android/workers/AutomaticUploadsWorker.kt | 3 + .../src/main/res/values-ru/strings.xml | 2 + .../qsferaApp/src/main/res/values/strings.xml | 2 + .../main/res/xml/settings_picture_uploads.xml | 5 + .../main/res/xml/settings_video_uploads.xml | 5 + .../UploadFileFromContentUriUseCaseTest.kt | 38 + .../50.json | 1230 +++++++++++++++++ .../eu/qsfera/android/data/ProviderMeta.java | 2 +- .../eu/qsfera/android/data/QSferaDatabase.kt | 4 +- .../OCLocalFolderBackupDataSource.kt | 2 + .../folderbackup/db/FolderBackUpEntity.kt | 1 + .../android/data/migrations/Migration_34.kt | 2 + .../android/data/migrations/Migration_50.kt | 11 + .../model/FolderBackUpConfiguration.kt | 1 + .../testutil/OCFolderBackUpConfiguration.kt | 2 + PRODUCTION_READINESS.md | 3 +- 22 files changed, 1375 insertions(+), 4 deletions(-) create mode 100644 Android/qsferaApp/src/test/java/eu/qsfera/android/usecases/transfers/uploads/UploadFileFromContentUriUseCaseTest.kt create mode 100644 Android/qsferaData/schemas/eu.qsfera.android.data.QSferaDatabase/50.json create mode 100644 Android/qsferaData/src/main/java/eu/qsfera/android/data/migrations/Migration_50.kt diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/db/PreferenceManager.java b/Android/qsferaApp/src/main/java/eu/qsfera/android/db/PreferenceManager.java index 7abd0894..67184488 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/db/PreferenceManager.java +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/db/PreferenceManager.java @@ -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"; diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsPictureUploadsFragment.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsPictureUploadsFragment.kt index 2824128e..5b74f6c6 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsPictureUploadsFragment.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsPictureUploadsFragment.kt @@ -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) diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsPictureUploadsViewModel.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsPictureUploadsViewModel.kt index b35f7f35..f21c960e 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsPictureUploadsViewModel.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsPictureUploadsViewModel.kt @@ -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, diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsVideoUploadsFragment.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsVideoUploadsFragment.kt index 6a69d08d..5414f1fa 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsVideoUploadsFragment.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsVideoUploadsFragment.kt @@ -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) diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsVideoUploadsViewModel.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsVideoUploadsViewModel.kt index 056e4bba..bc87c471 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsVideoUploadsViewModel.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/automaticuploads/SettingsVideoUploadsViewModel.kt @@ -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, diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/usecases/transfers/uploads/UploadFileFromContentUriUseCase.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/usecases/transfers/uploads/UploadFileFromContentUriUseCase.kt index 1164324c..ffc2c8b0 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/usecases/transfers/uploads/UploadFileFromContentUriUseCase.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/usecases/transfers/uploads/UploadFileFromContentUriUseCase.kt @@ -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 + } + } } diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/workers/AutomaticUploadsWorker.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/workers/AutomaticUploadsWorker.kt index 6d62b4de..27779d53 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/workers/AutomaticUploadsWorker.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/workers/AutomaticUploadsWorker.kt @@ -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 ) ) diff --git a/Android/qsferaApp/src/main/res/values-ru/strings.xml b/Android/qsferaApp/src/main/res/values-ru/strings.xml index 6d70b11d..42c59b77 100644 --- a/Android/qsferaApp/src/main/res/values-ru/strings.xml +++ b/Android/qsferaApp/src/main/res/values-ru/strings.xml @@ -561,6 +561,8 @@ Папки телефона не выбраны Убрать выбранные папки Копирование перестанет проверять папки телефона, пока вы снова не добавите папку. + Разрешить копирование в роуминге + Применяется только если копирование только по Wi-Fi выключено Исходный файл после копирования Исходный файл после копирования Последняя проверка diff --git a/Android/qsferaApp/src/main/res/values/strings.xml b/Android/qsferaApp/src/main/res/values/strings.xml index ebe7f463..09b09129 100644 --- a/Android/qsferaApp/src/main/res/values/strings.xml +++ b/Android/qsferaApp/src/main/res/values/strings.xml @@ -599,6 +599,8 @@ No phone folders selected Remove selected folders Backup will stop scanning phone folders until you add a folder again. + Allow backup while roaming + Only applies when Wi-Fi-only backup is off Original file after backup Original file after backup Last backup scan diff --git a/Android/qsferaApp/src/main/res/xml/settings_picture_uploads.xml b/Android/qsferaApp/src/main/res/xml/settings_picture_uploads.xml index 1ba11ab5..5179a1ec 100644 --- a/Android/qsferaApp/src/main/res/xml/settings_picture_uploads.xml +++ b/Android/qsferaApp/src/main/res/xml/settings_picture_uploads.xml @@ -64,6 +64,11 @@ app:iconSpaceReserved="false" app:key="picture_uploads_on_wifi" app:title="@string/prefs_camera_picture_upload_on_wifi" /> + +