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)
}
}