Add Android auto-upload mobile data limit
This commit is contained in:
@@ -49,9 +49,11 @@ public abstract class PreferenceManager {
|
||||
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_PICTURE_UPLOADS_MOBILE_DATA_LIMIT = "picture_uploads_mobile_data_limit";
|
||||
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_VIDEO_UPLOADS_MOBILE_DATA_LIMIT = "video_uploads_mobile_data_limit";
|
||||
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";
|
||||
|
||||
+32
-16
@@ -46,6 +46,7 @@ import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_BEHAV
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_CHARGING_ONLY
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ENABLED
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_LAST_SYNC
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_MOBILE_DATA_LIMIT
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_PATH
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_SOURCE
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY
|
||||
@@ -70,6 +71,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
private var prefPictureUploadsPath: Preference? = null
|
||||
private var prefPictureUploadsOnWifi: CheckBoxPreference? = null
|
||||
private var prefPictureUploadsAllowRoaming: CheckBoxPreference? = null
|
||||
private var prefPictureUploadsMobileDataLimit: ListPreference? = null
|
||||
private var prefPictureUploadsOnCharging: CheckBoxPreference? = null
|
||||
private var prefPictureUploadsSourcePath: Preference? = null
|
||||
private var prefPictureUploadsClearSourcePaths: Preference? = null
|
||||
@@ -103,6 +105,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefPictureUploadsPath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH)
|
||||
prefPictureUploadsOnWifi = findPreference(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY)
|
||||
prefPictureUploadsAllowRoaming = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ALLOW_ROAMING)
|
||||
prefPictureUploadsMobileDataLimit = findPreference(PREF__CAMERA_PICTURE_UPLOADS_MOBILE_DATA_LIMIT)
|
||||
prefPictureUploadsOnCharging = findPreference(PREF__CAMERA_PICTURE_UPLOADS_CHARGING_ONLY)
|
||||
prefPictureUploadsSourcePath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE)
|
||||
prefPictureUploadsClearSourcePaths = findPreference(PREF_PICTURE_UPLOADS_CLEAR_SOURCE_PATHS)
|
||||
@@ -117,7 +120,9 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefPictureUploadsAccount = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME)
|
||||
|
||||
val comment = getString(R.string.prefs_camera_upload_source_path_title_required)
|
||||
prefPictureUploadsSourcePath?.title = String.format(prefPictureUploadsSourcePath?.title.toString(), comment)
|
||||
prefPictureUploadsSourcePath?.let {
|
||||
it.title = String.format(it.title?.toString().orEmpty(), comment)
|
||||
}
|
||||
|
||||
initPreferenceListeners()
|
||||
}
|
||||
@@ -143,7 +148,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
showMessageInSnackbar(getString(R.string.prefs_automatic_uploads_not_available_users_light))
|
||||
} else {
|
||||
val currentAccount = manageAccountsViewModel.getCurrentAccount()?.name
|
||||
currentAccount?.let {
|
||||
if (currentAccount != null) {
|
||||
selectedAccount = if (manageAccountsViewModel.checkUserLight(currentAccount)) {
|
||||
availableAccounts.first().accountName
|
||||
} else {
|
||||
@@ -162,6 +167,8 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefPictureUploadsOnWifi?.isChecked = it.wifiOnly
|
||||
prefPictureUploadsAllowRoaming?.isChecked = it.allowRoaming
|
||||
prefPictureUploadsAllowRoaming?.isEnabled = !it.wifiOnly
|
||||
prefPictureUploadsMobileDataLimit?.value = it.mobileDataLimitBytes.toString()
|
||||
prefPictureUploadsMobileDataLimit?.isEnabled = !it.wifiOnly
|
||||
prefPictureUploadsOnCharging?.isChecked = it.chargingOnly
|
||||
prefPictureUploadsBehaviour?.value = it.behavior.name
|
||||
prefPictureUploadsLastSync?.summary = formatLastSyncSummary(it.lastSyncTimestamp)
|
||||
@@ -246,33 +253,40 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
prefPictureUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as Boolean
|
||||
picturesViewModel.useWifiOnly(newValue)
|
||||
prefPictureUploadsAllowRoaming?.isEnabled = !newValue
|
||||
newValue
|
||||
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||
picturesViewModel.useWifiOnly(value)
|
||||
prefPictureUploadsAllowRoaming?.isEnabled = !value
|
||||
prefPictureUploadsMobileDataLimit?.isEnabled = !value
|
||||
value
|
||||
}
|
||||
|
||||
prefPictureUploadsAllowRoaming?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as Boolean
|
||||
picturesViewModel.allowRoaming(newValue)
|
||||
newValue
|
||||
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||
picturesViewModel.allowRoaming(value)
|
||||
value
|
||||
}
|
||||
|
||||
prefPictureUploadsMobileDataLimit?.setOnPreferenceChangeListener { _, newValue ->
|
||||
val value = (newValue as? String)?.toLongOrNull() ?: return@setOnPreferenceChangeListener false
|
||||
picturesViewModel.setMobileDataLimitBytes(value)
|
||||
true
|
||||
}
|
||||
|
||||
prefPictureUploadsOnCharging?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as Boolean
|
||||
picturesViewModel.useChargingOnly(newValue)
|
||||
newValue
|
||||
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||
picturesViewModel.useChargingOnly(value)
|
||||
value
|
||||
}
|
||||
|
||||
prefPictureUploadsAccount?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as String
|
||||
picturesViewModel.handleSelectAccount(newValue)
|
||||
val value = newValue as? String ?: return@setOnPreferenceChangeListener false
|
||||
picturesViewModel.handleSelectAccount(value)
|
||||
true
|
||||
}
|
||||
|
||||
prefPictureUploadsBehaviour?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as String
|
||||
picturesViewModel.handleSelectBehaviour(newValue)
|
||||
val value = newValue as? String ?: return@setOnPreferenceChangeListener false
|
||||
picturesViewModel.handleSelectBehaviour(value)
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -290,6 +304,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefPictureUploadsPath?.isEnabled = value
|
||||
prefPictureUploadsOnWifi?.isEnabled = value
|
||||
prefPictureUploadsAllowRoaming?.isEnabled = value && prefPictureUploadsOnWifi?.isChecked != true
|
||||
prefPictureUploadsMobileDataLimit?.isEnabled = value && prefPictureUploadsOnWifi?.isChecked != true
|
||||
prefPictureUploadsOnCharging?.isEnabled = value
|
||||
prefPictureUploadsSourcePath?.isEnabled = value
|
||||
prefPictureUploadsClearSourcePaths?.isEnabled = value && picturesViewModel.getPictureUploadsSourcePaths().isNotEmpty()
|
||||
@@ -305,6 +320,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefPictureUploadsClearSourcePaths?.isEnabled = false
|
||||
prefPictureUploadsOnWifi?.isChecked = false
|
||||
prefPictureUploadsAllowRoaming?.isChecked = false
|
||||
prefPictureUploadsMobileDataLimit?.value = "0"
|
||||
prefPictureUploadsOnCharging?.isChecked = false
|
||||
prefPictureUploadsBehaviour?.value = UploadBehavior.COPY.name
|
||||
prefPictureUploadsLastSync?.summary = getString(R.string.prefs_camera_upload_last_sync_never)
|
||||
|
||||
+20
-4
@@ -122,6 +122,16 @@ class SettingsPictureUploadsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setMobileDataLimitBytes(mobileDataLimitBytes: Long) {
|
||||
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
||||
savePictureUploadsConfigurationUseCase(
|
||||
SavePictureUploadsConfigurationUseCase.Params(
|
||||
composePictureUploadsConfiguration(mobileDataLimitBytes = mobileDataLimitBytes)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun useChargingOnly(chargingOnly: Boolean) {
|
||||
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
||||
savePictureUploadsConfigurationUseCase(
|
||||
@@ -222,6 +232,9 @@ class SettingsPictureUploadsViewModel(
|
||||
uploadPath: String? = _pictureUploads.value?.uploadPath,
|
||||
wifiOnly: Boolean? = _pictureUploads.value?.wifiOnly,
|
||||
allowRoaming: Boolean? = _pictureUploads.value?.allowRoaming,
|
||||
mobileDataLimitBytes: Long? = _pictureUploads.value?.mobileDataLimitBytes,
|
||||
mobileDataPeriodStartTimestamp: Long? = _pictureUploads.value?.mobileDataPeriodStartTimestamp,
|
||||
mobileDataBytesQueuedInPeriod: Long? = _pictureUploads.value?.mobileDataBytesQueuedInPeriod,
|
||||
chargingOnly: Boolean? = _pictureUploads.value?.chargingOnly,
|
||||
sourcePath: String? = _pictureUploads.value?.sourcePath,
|
||||
behavior: UploadBehavior? = _pictureUploads.value?.behavior,
|
||||
@@ -232,9 +245,12 @@ class SettingsPictureUploadsViewModel(
|
||||
behavior = behavior ?: UploadBehavior.COPY,
|
||||
sourcePath = sourcePath.orEmpty(),
|
||||
uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH,
|
||||
wifiOnly = wifiOnly ?: true,
|
||||
allowRoaming = allowRoaming ?: false,
|
||||
chargingOnly = chargingOnly ?: false,
|
||||
wifiOnly = wifiOnly != false,
|
||||
allowRoaming = allowRoaming == true,
|
||||
mobileDataLimitBytes = mobileDataLimitBytes ?: 0,
|
||||
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp ?: 0,
|
||||
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod ?: 0,
|
||||
chargingOnly = chargingOnly == true,
|
||||
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
|
||||
name = _pictureUploads.value?.name ?: pictureUploadsName,
|
||||
spaceId = spaceId,
|
||||
@@ -251,7 +267,7 @@ class SettingsPictureUploadsViewModel(
|
||||
|
||||
fun getUploadPathString(): String {
|
||||
|
||||
val spaceName = handleSpaceName(pictureUploadsSpace?.name)
|
||||
val spaceName = handleSpaceName(pictureUploadsSpace?.name).orEmpty()
|
||||
val uploadPath = pictureUploads.value?.uploadPath
|
||||
val spaceId = pictureUploads.value?.spaceId
|
||||
|
||||
|
||||
+32
-16
@@ -46,6 +46,7 @@ import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_ALLOW_R
|
||||
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
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_MOBILE_DATA_LIMIT
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_PATH
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_SOURCE
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY
|
||||
@@ -70,6 +71,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
private var prefVideoUploadsPath: Preference? = null
|
||||
private var prefVideoUploadsOnWifi: CheckBoxPreference? = null
|
||||
private var prefVideoUploadsAllowRoaming: CheckBoxPreference? = null
|
||||
private var prefVideoUploadsMobileDataLimit: ListPreference? = null
|
||||
private var prefVideoUploadsOnCharging: CheckBoxPreference? = null
|
||||
private var prefVideoUploadsSourcePath: Preference? = null
|
||||
private var prefVideoUploadsClearSourcePaths: Preference? = null
|
||||
@@ -103,6 +105,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefVideoUploadsPath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH)
|
||||
prefVideoUploadsOnWifi = findPreference(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY)
|
||||
prefVideoUploadsAllowRoaming = findPreference(PREF__CAMERA_VIDEO_UPLOADS_ALLOW_ROAMING)
|
||||
prefVideoUploadsMobileDataLimit = findPreference(PREF__CAMERA_VIDEO_UPLOADS_MOBILE_DATA_LIMIT)
|
||||
prefVideoUploadsOnCharging = findPreference(PREF__CAMERA_VIDEO_UPLOADS_CHARGING_ONLY)
|
||||
prefVideoUploadsSourcePath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE)
|
||||
prefVideoUploadsClearSourcePaths = findPreference(PREF_VIDEO_UPLOADS_CLEAR_SOURCE_PATHS)
|
||||
@@ -115,7 +118,9 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefVideoUploadsAccount = findPreference<ListPreference>(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME)
|
||||
|
||||
val comment = getString(R.string.prefs_camera_upload_source_path_title_required)
|
||||
prefVideoUploadsSourcePath?.title = String.format(prefVideoUploadsSourcePath?.title.toString(), comment)
|
||||
prefVideoUploadsSourcePath?.let {
|
||||
it.title = String.format(it.title?.toString().orEmpty(), comment)
|
||||
}
|
||||
|
||||
initPreferenceListeners()
|
||||
}
|
||||
@@ -141,7 +146,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
showMessageInSnackbar(getString(R.string.prefs_automatic_uploads_not_available_users_light))
|
||||
} else {
|
||||
val currentAccount = manageAccountsViewModel.getCurrentAccount()?.name
|
||||
currentAccount?.let {
|
||||
if (currentAccount != null) {
|
||||
selectedAccount = if (manageAccountsViewModel.checkUserLight(currentAccount)) {
|
||||
availableAccounts.first().accountName
|
||||
} else {
|
||||
@@ -160,6 +165,8 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefVideoUploadsOnWifi?.isChecked = it.wifiOnly
|
||||
prefVideoUploadsAllowRoaming?.isChecked = it.allowRoaming
|
||||
prefVideoUploadsAllowRoaming?.isEnabled = !it.wifiOnly
|
||||
prefVideoUploadsMobileDataLimit?.value = it.mobileDataLimitBytes.toString()
|
||||
prefVideoUploadsMobileDataLimit?.isEnabled = !it.wifiOnly
|
||||
prefVideoUploadsOnCharging?.isChecked = it.chargingOnly
|
||||
prefVideoUploadsBehaviour?.value = it.behavior.name
|
||||
prefVideoUploadsLastSync?.summary = formatLastSyncSummary(it.lastSyncTimestamp)
|
||||
@@ -244,33 +251,40 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
prefVideoUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as Boolean
|
||||
videosViewModel.useWifiOnly(newValue)
|
||||
prefVideoUploadsAllowRoaming?.isEnabled = !newValue
|
||||
newValue
|
||||
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||
videosViewModel.useWifiOnly(value)
|
||||
prefVideoUploadsAllowRoaming?.isEnabled = !value
|
||||
prefVideoUploadsMobileDataLimit?.isEnabled = !value
|
||||
value
|
||||
}
|
||||
|
||||
prefVideoUploadsAllowRoaming?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as Boolean
|
||||
videosViewModel.allowRoaming(newValue)
|
||||
newValue
|
||||
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||
videosViewModel.allowRoaming(value)
|
||||
value
|
||||
}
|
||||
|
||||
prefVideoUploadsMobileDataLimit?.setOnPreferenceChangeListener { _, newValue ->
|
||||
val value = (newValue as? String)?.toLongOrNull() ?: return@setOnPreferenceChangeListener false
|
||||
videosViewModel.setMobileDataLimitBytes(value)
|
||||
true
|
||||
}
|
||||
|
||||
prefVideoUploadsOnCharging?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as Boolean
|
||||
videosViewModel.useChargingOnly(newValue)
|
||||
newValue
|
||||
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||
videosViewModel.useChargingOnly(value)
|
||||
value
|
||||
}
|
||||
|
||||
prefVideoUploadsAccount?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as String
|
||||
videosViewModel.handleSelectAccount(newValue)
|
||||
val value = newValue as? String ?: return@setOnPreferenceChangeListener false
|
||||
videosViewModel.handleSelectAccount(value)
|
||||
true
|
||||
}
|
||||
|
||||
prefVideoUploadsBehaviour?.setOnPreferenceChangeListener { _, newValue ->
|
||||
newValue as String
|
||||
videosViewModel.handleSelectBehaviour(newValue)
|
||||
val value = newValue as? String ?: return@setOnPreferenceChangeListener false
|
||||
videosViewModel.handleSelectBehaviour(value)
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -288,6 +302,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefVideoUploadsPath?.isEnabled = value
|
||||
prefVideoUploadsOnWifi?.isEnabled = value
|
||||
prefVideoUploadsAllowRoaming?.isEnabled = value && prefVideoUploadsOnWifi?.isChecked != true
|
||||
prefVideoUploadsMobileDataLimit?.isEnabled = value && prefVideoUploadsOnWifi?.isChecked != true
|
||||
prefVideoUploadsOnCharging?.isEnabled = value
|
||||
prefVideoUploadsSourcePath?.isEnabled = value
|
||||
prefVideoUploadsClearSourcePaths?.isEnabled = value && videosViewModel.getVideoUploadsSourcePaths().isNotEmpty()
|
||||
@@ -303,6 +318,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
||||
prefVideoUploadsClearSourcePaths?.isEnabled = false
|
||||
prefVideoUploadsOnWifi?.isChecked = false
|
||||
prefVideoUploadsAllowRoaming?.isChecked = false
|
||||
prefVideoUploadsMobileDataLimit?.value = "0"
|
||||
prefVideoUploadsOnCharging?.isChecked = false
|
||||
prefVideoUploadsBehaviour?.value = UploadBehavior.COPY.name
|
||||
prefVideoUploadsLastSync?.summary = getString(R.string.prefs_camera_upload_last_sync_never)
|
||||
|
||||
+20
-4
@@ -122,6 +122,16 @@ class SettingsVideoUploadsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setMobileDataLimitBytes(mobileDataLimitBytes: Long) {
|
||||
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
||||
saveVideoUploadsConfigurationUseCase(
|
||||
SaveVideoUploadsConfigurationUseCase.Params(
|
||||
composeVideoUploadsConfiguration(mobileDataLimitBytes = mobileDataLimitBytes)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun useChargingOnly(chargingOnly: Boolean) {
|
||||
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
||||
saveVideoUploadsConfigurationUseCase(
|
||||
@@ -222,6 +232,9 @@ class SettingsVideoUploadsViewModel(
|
||||
uploadPath: String? = _videoUploads.value?.uploadPath,
|
||||
wifiOnly: Boolean? = _videoUploads.value?.wifiOnly,
|
||||
allowRoaming: Boolean? = _videoUploads.value?.allowRoaming,
|
||||
mobileDataLimitBytes: Long? = _videoUploads.value?.mobileDataLimitBytes,
|
||||
mobileDataPeriodStartTimestamp: Long? = _videoUploads.value?.mobileDataPeriodStartTimestamp,
|
||||
mobileDataBytesQueuedInPeriod: Long? = _videoUploads.value?.mobileDataBytesQueuedInPeriod,
|
||||
chargingOnly: Boolean? = _videoUploads.value?.chargingOnly,
|
||||
sourcePath: String? = _videoUploads.value?.sourcePath,
|
||||
behavior: UploadBehavior? = _videoUploads.value?.behavior,
|
||||
@@ -233,9 +246,12 @@ class SettingsVideoUploadsViewModel(
|
||||
behavior = behavior ?: UploadBehavior.COPY,
|
||||
sourcePath = sourcePath.orEmpty(),
|
||||
uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH,
|
||||
wifiOnly = wifiOnly ?: true,
|
||||
allowRoaming = allowRoaming ?: false,
|
||||
chargingOnly = chargingOnly ?: false,
|
||||
wifiOnly = wifiOnly != false,
|
||||
allowRoaming = allowRoaming == true,
|
||||
mobileDataLimitBytes = mobileDataLimitBytes ?: 0,
|
||||
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp ?: 0,
|
||||
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod ?: 0,
|
||||
chargingOnly = chargingOnly == true,
|
||||
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
|
||||
name = _videoUploads.value?.name ?: videoUploadsName,
|
||||
spaceId = spaceId,
|
||||
@@ -252,7 +268,7 @@ class SettingsVideoUploadsViewModel(
|
||||
|
||||
fun getUploadPathString(): String {
|
||||
|
||||
val spaceName = handleSpaceName(videoUploadsSpace?.name)
|
||||
val spaceName = handleSpaceName(videoUploadsSpace?.name).orEmpty()
|
||||
val uploadPath = videoUploads.value?.uploadPath
|
||||
val spaceId = videoUploads.value?.spaceId
|
||||
|
||||
|
||||
+106
-12
@@ -22,6 +22,7 @@
|
||||
package eu.qsfera.android.workers
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
@@ -51,7 +52,9 @@ import org.koin.core.component.inject
|
||||
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AutomaticUploadsWorker(
|
||||
@@ -142,9 +145,12 @@ class AutomaticUploadsWorker(
|
||||
WorkManager.getInstance(appContext).cancelUniqueWork(AUTOMATIC_UPLOADS_WORKER)
|
||||
}
|
||||
|
||||
private fun syncFolder(folderBackUpConfiguration: FolderBackUpConfiguration?) {
|
||||
if (folderBackUpConfiguration == null) return
|
||||
private fun isActiveNetworkMetered(): Boolean {
|
||||
val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||
return connectivityManager?.isActiveNetworkMetered != false
|
||||
}
|
||||
|
||||
private fun syncFolder(folderBackUpConfiguration: FolderBackUpConfiguration) {
|
||||
val syncType = when {
|
||||
folderBackUpConfiguration.isPictureUploads -> SyncType.PICTURE_UPLOADS
|
||||
folderBackUpConfiguration.isVideoUploads -> SyncType.VIDEO_UPLOADS
|
||||
@@ -153,6 +159,9 @@ class AutomaticUploadsWorker(
|
||||
}
|
||||
|
||||
val currentTimestamp = System.currentTimeMillis()
|
||||
val activeNetworkMetered = isActiveNetworkMetered()
|
||||
var mobileDataQuotaState = resolveMobileDataQuotaState(folderBackUpConfiguration, currentTimestamp)
|
||||
var mobileDataLimitReached = false
|
||||
|
||||
val localPicturesDocumentFiles: List<DocumentFile> = folderBackUpConfiguration.sourcePaths.flatMap { sourcePath ->
|
||||
getFilesReadyToUpload(
|
||||
@@ -163,19 +172,34 @@ class AutomaticUploadsWorker(
|
||||
)
|
||||
}
|
||||
|
||||
showNotification(syncType, localPicturesDocumentFiles.size)
|
||||
|
||||
var queuedUploadsCount = 0
|
||||
for (documentFile in localPicturesDocumentFiles) {
|
||||
// Dedup: if this content URI already has a queued, in-progress, or succeeded transfer,
|
||||
// skip it. Without this, a worker killed mid-loop (before updateTimestamp) or a
|
||||
// file whose lastModified changed (e.g. media scanner) would be re-discovered and
|
||||
// enqueued with a new upload ID — leading to duplicate uploads or 0-byte files
|
||||
// enqueued with a new upload ID - leading to duplicate uploads or 0-byte files
|
||||
// when two workers race on the same cache path.
|
||||
val contentUri = documentFile.uri.toString()
|
||||
if (transferRepository.existsNonFailedTransferForUri(contentUri)) {
|
||||
Timber.d("Skipping already-tracked file: %s", documentFile.name)
|
||||
continue
|
||||
}
|
||||
val documentFileSize = documentFile.length()
|
||||
if (!canQueueMobileDataUpload(
|
||||
wifiOnly = folderBackUpConfiguration.wifiOnly,
|
||||
isActiveNetworkMetered = activeNetworkMetered,
|
||||
mobileDataLimitBytes = folderBackUpConfiguration.mobileDataLimitBytes,
|
||||
mobileDataBytesQueuedInPeriod = mobileDataQuotaState.bytesQueuedInPeriod,
|
||||
fileSize = documentFileSize
|
||||
)
|
||||
) {
|
||||
mobileDataLimitReached = true
|
||||
Timber.i(
|
||||
"Skipping automatic upload %s because the daily mobile data queue limit has been reached",
|
||||
documentFile.name
|
||||
)
|
||||
continue
|
||||
}
|
||||
val uploadId = storeInUploadsDatabase(
|
||||
documentFile = documentFile,
|
||||
uploadPath = folderBackUpConfiguration.uploadPath.plus(File.separator).plus(documentFile.name),
|
||||
@@ -198,11 +222,26 @@ class AutomaticUploadsWorker(
|
||||
allowRoaming = folderBackUpConfiguration.allowRoaming,
|
||||
chargingOnly = folderBackUpConfiguration.chargingOnly
|
||||
)
|
||||
if (shouldCountAgainstMobileDataLimit(
|
||||
wifiOnly = folderBackUpConfiguration.wifiOnly,
|
||||
isActiveNetworkMetered = activeNetworkMetered,
|
||||
mobileDataLimitBytes = folderBackUpConfiguration.mobileDataLimitBytes,
|
||||
)
|
||||
) {
|
||||
mobileDataQuotaState = mobileDataQuotaState.copy(
|
||||
bytesQueuedInPeriod = mobileDataQuotaState.bytesQueuedInPeriod + documentFileSize
|
||||
)
|
||||
}
|
||||
queuedUploadsCount += 1
|
||||
}
|
||||
showNotification(syncType, queuedUploadsCount)
|
||||
// Save safeTimestamp (not currentTimestamp) so that files skipped by the
|
||||
// write-safety buffer are re-evaluated on the next run instead of being lost.
|
||||
// Keep the previous timestamp when the mobile-data daily limit stops the scan,
|
||||
// so skipped files are re-evaluated after the quota period resets.
|
||||
val safeTimestamp = currentTimestamp - WRITE_SAFETY_BUFFER_MS
|
||||
updateTimestamp(folderBackUpConfiguration, syncType, safeTimestamp)
|
||||
val nextTimestamp = if (mobileDataLimitReached) folderBackUpConfiguration.lastSyncTimestamp else safeTimestamp
|
||||
updateConfigurationAfterScan(folderBackUpConfiguration, syncType, nextTimestamp, mobileDataQuotaState)
|
||||
}
|
||||
|
||||
private fun showNotification(
|
||||
@@ -251,23 +290,29 @@ class AutomaticUploadsWorker(
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateTimestamp(
|
||||
private fun updateConfigurationAfterScan(
|
||||
folderBackUpConfiguration: FolderBackUpConfiguration,
|
||||
syncType: SyncType,
|
||||
currentTimestamp: Long,
|
||||
nextSyncTimestamp: Long,
|
||||
mobileDataQuotaState: MobileDataQuotaState,
|
||||
) {
|
||||
val updatedConfiguration = folderBackUpConfiguration.copy(
|
||||
lastSyncTimestamp = nextSyncTimestamp,
|
||||
mobileDataPeriodStartTimestamp = mobileDataQuotaState.periodStartTimestamp,
|
||||
mobileDataBytesQueuedInPeriod = mobileDataQuotaState.bytesQueuedInPeriod
|
||||
)
|
||||
|
||||
when (syncType) {
|
||||
SyncType.PICTURE_UPLOADS -> {
|
||||
val savePictureUploadsConfigurationUseCase: SavePictureUploadsConfigurationUseCase by inject()
|
||||
savePictureUploadsConfigurationUseCase(
|
||||
SavePictureUploadsConfigurationUseCase.Params(folderBackUpConfiguration.copy(lastSyncTimestamp = currentTimestamp))
|
||||
SavePictureUploadsConfigurationUseCase.Params(updatedConfiguration)
|
||||
)
|
||||
}
|
||||
SyncType.VIDEO_UPLOADS -> {
|
||||
val saveVideoUploadsConfigurationUseCase: SaveVideoUploadsConfigurationUseCase by inject()
|
||||
saveVideoUploadsConfigurationUseCase(
|
||||
SaveVideoUploadsConfigurationUseCase.Params(folderBackUpConfiguration.copy(lastSyncTimestamp = currentTimestamp))
|
||||
SaveVideoUploadsConfigurationUseCase.Params(updatedConfiguration)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -281,7 +326,7 @@ class AutomaticUploadsWorker(
|
||||
): List<DocumentFile> {
|
||||
val sourceUri: Uri = sourcePath.toUri()
|
||||
val documentTree = DocumentFile.fromTreeUri(applicationContext, sourceUri)
|
||||
val arrayOfLocalFiles = documentTree?.listFiles() ?: arrayOf()
|
||||
val arrayOfLocalFiles = documentTree?.listFiles().orEmpty()
|
||||
|
||||
// Exclude files modified within the last few seconds. Camera apps may still be
|
||||
// writing the file (not all apps use atomic rename), so picking it up too early
|
||||
@@ -296,7 +341,7 @@ class AutomaticUploadsWorker(
|
||||
|
||||
Timber.i("Last sync ${syncType.name}: ${Date(lastSyncTimestamp)}")
|
||||
Timber.i("CurrentTimestamp ${Date(currentTimestamp)}")
|
||||
Timber.i("${arrayOfLocalFiles.size} files found in folder: ${sourceUri.path}")
|
||||
Timber.i("${arrayOfLocalFiles.size} files found in folder: ${sourceUri.path.orEmpty()}")
|
||||
Timber.i("${filteredList.size} files are ${syncType.name} and were taken after last sync")
|
||||
|
||||
return filteredList
|
||||
@@ -355,6 +400,11 @@ class AutomaticUploadsWorker(
|
||||
}
|
||||
|
||||
companion object {
|
||||
data class MobileDataQuotaState(
|
||||
val periodStartTimestamp: Long,
|
||||
val bytesQueuedInPeriod: Long,
|
||||
)
|
||||
|
||||
const val AUTOMATIC_UPLOADS_WORKER = "AUTOMATIC_UPLOADS_WORKER"
|
||||
const val IMMEDIATE_UPLOADS_WORKER = "IMMEDIATE_AUTOMATIC_UPLOADS_WORKER"
|
||||
const val MEDIA_STORE_UPLOADS_WORKER = "MEDIA_STORE_AUTOMATIC_UPLOADS_WORKER"
|
||||
@@ -365,5 +415,49 @@ class AutomaticUploadsWorker(
|
||||
private const val videoUploadsNotificationId = 102
|
||||
const val WRITE_SAFETY_BUFFER_MS = 10_000L
|
||||
const val MEDIA_STORE_TRIGGER_MAX_DELAY_MS = 60_000L
|
||||
|
||||
fun resolveMobileDataQuotaState(
|
||||
folderBackUpConfiguration: FolderBackUpConfiguration,
|
||||
currentTimestamp: Long,
|
||||
timeZone: TimeZone = TimeZone.getDefault(),
|
||||
): MobileDataQuotaState {
|
||||
val currentPeriodStart = getMobileDataLimitPeriodStart(currentTimestamp, timeZone)
|
||||
val bytesQueuedInPeriod =
|
||||
if (folderBackUpConfiguration.mobileDataPeriodStartTimestamp == currentPeriodStart) {
|
||||
folderBackUpConfiguration.mobileDataBytesQueuedInPeriod
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
return MobileDataQuotaState(
|
||||
periodStartTimestamp = currentPeriodStart,
|
||||
bytesQueuedInPeriod = bytesQueuedInPeriod,
|
||||
)
|
||||
}
|
||||
|
||||
fun getMobileDataLimitPeriodStart(timestamp: Long, timeZone: TimeZone = TimeZone.getDefault()): Long =
|
||||
Calendar.getInstance(timeZone).apply {
|
||||
timeInMillis = timestamp
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
|
||||
fun shouldCountAgainstMobileDataLimit(
|
||||
wifiOnly: Boolean,
|
||||
isActiveNetworkMetered: Boolean,
|
||||
mobileDataLimitBytes: Long,
|
||||
): Boolean =
|
||||
!wifiOnly && isActiveNetworkMetered && mobileDataLimitBytes > 0
|
||||
|
||||
fun canQueueMobileDataUpload(
|
||||
wifiOnly: Boolean,
|
||||
isActiveNetworkMetered: Boolean,
|
||||
mobileDataLimitBytes: Long,
|
||||
mobileDataBytesQueuedInPeriod: Long,
|
||||
fileSize: Long,
|
||||
): Boolean =
|
||||
!shouldCountAgainstMobileDataLimit(wifiOnly, isActiveNetworkMetered, mobileDataLimitBytes) ||
|
||||
fileSize <= mobileDataLimitBytes - mobileDataBytesQueuedInPeriod
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,6 +601,22 @@
|
||||
<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_mobile_data_limit_title">Mobile data daily limit</string>
|
||||
<string name="prefs_camera_upload_mobile_data_limit_summary">Only applies when Wi-Fi-only backup is off and the active network is metered</string>
|
||||
<string-array name="prefs_camera_upload_mobile_data_limit_entries">
|
||||
<item>Unlimited</item>
|
||||
<item>10 MB per day</item>
|
||||
<item>50 MB per day</item>
|
||||
<item>200 MB per day</item>
|
||||
<item>1 GB per day</item>
|
||||
</string-array>
|
||||
<string-array name="prefs_camera_upload_mobile_data_limit_values">
|
||||
<item>0</item>
|
||||
<item>10485760</item>
|
||||
<item>52428800</item>
|
||||
<item>209715200</item>
|
||||
<item>1073741824</item>
|
||||
</string-array>
|
||||
<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>
|
||||
|
||||
@@ -69,6 +69,15 @@
|
||||
app:key="picture_uploads_allow_roaming"
|
||||
app:summary="@string/prefs_camera_upload_allow_roaming_summary"
|
||||
app:title="@string/prefs_camera_upload_allow_roaming" />
|
||||
<ListPreference
|
||||
app:entries="@array/prefs_camera_upload_mobile_data_limit_entries"
|
||||
app:entryValues="@array/prefs_camera_upload_mobile_data_limit_values"
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="picture_uploads_mobile_data_limit"
|
||||
app:negativeButtonText=""
|
||||
app:summary="@string/prefs_camera_upload_mobile_data_limit_summary"
|
||||
app:title="@string/prefs_camera_upload_mobile_data_limit_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="picture_uploads_on_charging"
|
||||
|
||||
@@ -67,6 +67,15 @@
|
||||
app:key="video_uploads_allow_roaming"
|
||||
app:summary="@string/prefs_camera_upload_allow_roaming_summary"
|
||||
app:title="@string/prefs_camera_upload_allow_roaming" />
|
||||
<ListPreference
|
||||
app:entries="@array/prefs_camera_upload_mobile_data_limit_entries"
|
||||
app:entryValues="@array/prefs_camera_upload_mobile_data_limit_values"
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="video_uploads_mobile_data_limit"
|
||||
app:negativeButtonText=""
|
||||
app:summary="@string/prefs_camera_upload_mobile_data_limit_summary"
|
||||
app:title="@string/prefs_camera_upload_mobile_data_limit_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<CheckBoxPreference
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="video_uploads_on_charging"
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package eu.qsfera.android.workers
|
||||
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
|
||||
import eu.qsfera.android.domain.automaticuploads.model.UploadBehavior
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.util.TimeZone
|
||||
|
||||
class AutomaticUploadsWorkerTest {
|
||||
|
||||
@Test
|
||||
fun `mobile data quota period starts at local midnight`() {
|
||||
val timeZone = TimeZone.getTimeZone("UTC")
|
||||
val timestamp = 1_717_411_245_000L // 2024-06-03T10:40:45Z
|
||||
|
||||
val periodStart = AutomaticUploadsWorker.getMobileDataLimitPeriodStart(timestamp, timeZone)
|
||||
|
||||
assertEquals(1_717_372_800_000L, periodStart) // 2024-06-03T00:00:00Z
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mobile data quota resets when period changes`() {
|
||||
val timeZone = TimeZone.getTimeZone("UTC")
|
||||
val configuration = backupConfiguration(
|
||||
mobileDataPeriodStartTimestamp = 1_717_286_400_000L,
|
||||
mobileDataBytesQueuedInPeriod = 10_000L
|
||||
)
|
||||
|
||||
val state = AutomaticUploadsWorker.resolveMobileDataQuotaState(
|
||||
folderBackUpConfiguration = configuration,
|
||||
currentTimestamp = 1_717_372_800_000L,
|
||||
timeZone = timeZone
|
||||
)
|
||||
|
||||
assertEquals(1_717_372_800_000L, state.periodStartTimestamp)
|
||||
assertEquals(0L, state.bytesQueuedInPeriod)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `mobile data quota keeps bytes when period matches`() {
|
||||
val periodStart = 1_717_372_800_000L
|
||||
val configuration = backupConfiguration(
|
||||
mobileDataPeriodStartTimestamp = periodStart,
|
||||
mobileDataBytesQueuedInPeriod = 10_000L
|
||||
)
|
||||
|
||||
val state = AutomaticUploadsWorker.resolveMobileDataQuotaState(
|
||||
folderBackUpConfiguration = configuration,
|
||||
currentTimestamp = periodStart + 60_000L,
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
)
|
||||
|
||||
assertEquals(periodStart, state.periodStartTimestamp)
|
||||
assertEquals(10_000L, state.bytesQueuedInPeriod)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wifi only uploads ignore mobile data quota`() {
|
||||
assertTrue(
|
||||
AutomaticUploadsWorker.canQueueMobileDataUpload(
|
||||
wifiOnly = true,
|
||||
isActiveNetworkMetered = true,
|
||||
mobileDataLimitBytes = 10L,
|
||||
mobileDataBytesQueuedInPeriod = 10L,
|
||||
fileSize = 1_000L
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unlimited mobile data quota allows upload`() {
|
||||
assertTrue(
|
||||
AutomaticUploadsWorker.canQueueMobileDataUpload(
|
||||
wifiOnly = false,
|
||||
isActiveNetworkMetered = true,
|
||||
mobileDataLimitBytes = 0L,
|
||||
mobileDataBytesQueuedInPeriod = Long.MAX_VALUE,
|
||||
fileSize = 1_000L
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unmetered active network ignores mobile data quota`() {
|
||||
assertTrue(
|
||||
AutomaticUploadsWorker.canQueueMobileDataUpload(
|
||||
wifiOnly = false,
|
||||
isActiveNetworkMetered = false,
|
||||
mobileDataLimitBytes = 10L,
|
||||
mobileDataBytesQueuedInPeriod = 10L,
|
||||
fileSize = 1_000L
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `limited mobile data quota blocks upload that would exceed limit`() {
|
||||
assertFalse(
|
||||
AutomaticUploadsWorker.canQueueMobileDataUpload(
|
||||
wifiOnly = false,
|
||||
isActiveNetworkMetered = true,
|
||||
mobileDataLimitBytes = 1_000L,
|
||||
mobileDataBytesQueuedInPeriod = 800L,
|
||||
fileSize = 201L
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `limited mobile data quota allows upload at exact limit`() {
|
||||
assertTrue(
|
||||
AutomaticUploadsWorker.canQueueMobileDataUpload(
|
||||
wifiOnly = false,
|
||||
isActiveNetworkMetered = true,
|
||||
mobileDataLimitBytes = 1_000L,
|
||||
mobileDataBytesQueuedInPeriod = 800L,
|
||||
fileSize = 200L
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun backupConfiguration(
|
||||
mobileDataPeriodStartTimestamp: Long,
|
||||
mobileDataBytesQueuedInPeriod: Long,
|
||||
): FolderBackUpConfiguration =
|
||||
FolderBackUpConfiguration(
|
||||
accountName = "user@example.com",
|
||||
behavior = UploadBehavior.COPY,
|
||||
sourcePath = "content://source",
|
||||
uploadPath = "/CameraUpload",
|
||||
wifiOnly = false,
|
||||
allowRoaming = false,
|
||||
mobileDataLimitBytes = 100_000L,
|
||||
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp,
|
||||
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod,
|
||||
chargingOnly = false,
|
||||
lastSyncTimestamp = 0L,
|
||||
name = FolderBackUpConfiguration.pictureUploadsName,
|
||||
spaceId = null,
|
||||
)
|
||||
}
|
||||
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 = 50;
|
||||
public static final int DB_VERSION = 51;
|
||||
|
||||
private ProviderMeta() {
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ 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.migrations.MIGRATION_50_51
|
||||
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
|
||||
@@ -129,7 +130,8 @@ abstract class QSferaDatabase : RoomDatabase() {
|
||||
MIGRATION_42_43,
|
||||
MIGRATION_47_48,
|
||||
MIGRATION_48_49,
|
||||
MIGRATION_49_50)
|
||||
MIGRATION_49_50,
|
||||
MIGRATION_50_51)
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
|
||||
+6
@@ -69,6 +69,9 @@ class OCLocalFolderBackupDataSource(
|
||||
uploadPath = uploadPath,
|
||||
wifiOnly = wifiOnly,
|
||||
allowRoaming = allowRoaming,
|
||||
mobileDataLimitBytes = mobileDataLimitBytes,
|
||||
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp,
|
||||
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod,
|
||||
chargingOnly = chargingOnly,
|
||||
name = name,
|
||||
lastSyncTimestamp = lastSyncTimestamp,
|
||||
@@ -85,6 +88,9 @@ class OCLocalFolderBackupDataSource(
|
||||
uploadPath = uploadPath,
|
||||
wifiOnly = wifiOnly,
|
||||
allowRoaming = allowRoaming,
|
||||
mobileDataLimitBytes = mobileDataLimitBytes,
|
||||
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp,
|
||||
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod,
|
||||
chargingOnly = chargingOnly,
|
||||
lastSyncTimestamp = lastSyncTimestamp,
|
||||
name = name,
|
||||
|
||||
+3
@@ -30,6 +30,9 @@ data class FolderBackUpEntity(
|
||||
val uploadPath: String,
|
||||
val wifiOnly: Boolean,
|
||||
val allowRoaming: Boolean,
|
||||
val mobileDataLimitBytes: Long,
|
||||
val mobileDataPeriodStartTimestamp: Long,
|
||||
val mobileDataBytesQueuedInPeriod: Long,
|
||||
val chargingOnly: Boolean,
|
||||
val name: String,
|
||||
val lastSyncTimestamp: Long,
|
||||
|
||||
@@ -48,7 +48,7 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
||||
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false)) return null
|
||||
|
||||
return FolderBackUpConfiguration(
|
||||
accountName = sharedPreferencesProvider.getString(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, null) ?: "",
|
||||
accountName = sharedPreferencesProvider.getString(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, null).orEmpty(),
|
||||
wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY, false),
|
||||
uploadPath = getUploadPathForPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH),
|
||||
sourcePath = getSourcePathForPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE),
|
||||
@@ -56,6 +56,9 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
||||
lastSyncTimestamp = timestamp,
|
||||
name = pictureUploadsName,
|
||||
allowRoaming = false,
|
||||
mobileDataLimitBytes = 0,
|
||||
mobileDataPeriodStartTimestamp = 0,
|
||||
mobileDataBytesQueuedInPeriod = 0,
|
||||
chargingOnly = false,
|
||||
spaceId = null,
|
||||
)
|
||||
@@ -65,7 +68,7 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
||||
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false)) return null
|
||||
|
||||
return FolderBackUpConfiguration(
|
||||
accountName = sharedPreferencesProvider.getString(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, null) ?: "",
|
||||
accountName = sharedPreferencesProvider.getString(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, null).orEmpty(),
|
||||
wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY, false),
|
||||
uploadPath = getUploadPathForPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH),
|
||||
sourcePath = getSourcePathForPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE),
|
||||
@@ -73,6 +76,9 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
||||
lastSyncTimestamp = timestamp,
|
||||
name = videoUploadsName,
|
||||
allowRoaming = false,
|
||||
mobileDataLimitBytes = 0,
|
||||
mobileDataPeriodStartTimestamp = 0,
|
||||
mobileDataBytesQueuedInPeriod = 0,
|
||||
chargingOnly = false,
|
||||
spaceId = null,
|
||||
)
|
||||
@@ -87,7 +93,7 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
||||
}
|
||||
|
||||
private fun getSourcePathForPreference(keyPreference: String): String =
|
||||
sharedPreferencesProvider.getString(keyPreference, null) ?: ""
|
||||
sharedPreferencesProvider.getString(keyPreference, null).orEmpty()
|
||||
|
||||
private fun getBehaviorForPreference(keyPreference: String): UploadBehavior {
|
||||
val storedBehaviour = sharedPreferencesProvider.getString(keyPreference, null) ?: return UploadBehavior.COPY
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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_50_51 = object : Migration(50, 51) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE $FOLDER_BACKUP_TABLE_NAME ADD COLUMN mobileDataLimitBytes INTEGER NOT NULL DEFAULT '0'")
|
||||
database.execSQL("ALTER TABLE $FOLDER_BACKUP_TABLE_NAME ADD COLUMN mobileDataPeriodStartTimestamp INTEGER NOT NULL DEFAULT '0'")
|
||||
database.execSQL("ALTER TABLE $FOLDER_BACKUP_TABLE_NAME ADD COLUMN mobileDataBytesQueuedInPeriod INTEGER NOT NULL DEFAULT '0'")
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -26,6 +26,9 @@ data class FolderBackUpConfiguration(
|
||||
val uploadPath: String,
|
||||
val wifiOnly: Boolean,
|
||||
val allowRoaming: Boolean,
|
||||
val mobileDataLimitBytes: Long,
|
||||
val mobileDataPeriodStartTimestamp: Long,
|
||||
val mobileDataBytesQueuedInPeriod: Long,
|
||||
val chargingOnly: Boolean,
|
||||
val lastSyncTimestamp: Long,
|
||||
val name: String,
|
||||
@@ -40,6 +43,7 @@ data class FolderBackUpConfiguration(
|
||||
const val pictureUploadsName = "Picture uploads"
|
||||
const val videoUploadsName = "Video uploads"
|
||||
const val initialSyncTimestamp = 0L
|
||||
private const val SOURCE_PATH_SEPARATOR = "\n"
|
||||
|
||||
fun parseSourcePaths(sourcePath: String): List<String> =
|
||||
sourcePath
|
||||
@@ -54,8 +58,6 @@ data class FolderBackUpConfiguration(
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinct()
|
||||
.joinToString(SOURCE_PATH_SEPARATOR)
|
||||
|
||||
private const val SOURCE_PATH_SEPARATOR = "\n"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -32,6 +32,9 @@ val OC_BACKUP = FolderBackUpConfiguration(
|
||||
uploadPath = "/Photos",
|
||||
wifiOnly = true,
|
||||
allowRoaming = false,
|
||||
mobileDataLimitBytes = 0,
|
||||
mobileDataPeriodStartTimestamp = 0,
|
||||
mobileDataBytesQueuedInPeriod = 0,
|
||||
chargingOnly = true,
|
||||
lastSyncTimestamp = 1542628397,
|
||||
name = "",
|
||||
@@ -45,6 +48,9 @@ val OC_BACKUP_ENTITY = FolderBackUpEntity(
|
||||
uploadPath = "/Photos",
|
||||
wifiOnly = true,
|
||||
allowRoaming = false,
|
||||
mobileDataLimitBytes = 0,
|
||||
mobileDataPeriodStartTimestamp = 0,
|
||||
mobileDataBytesQueuedInPeriod = 0,
|
||||
chargingOnly = true,
|
||||
lastSyncTimestamp = 1542628397,
|
||||
name = "",
|
||||
|
||||
@@ -30,6 +30,7 @@ Date: 2026-06-08.
|
||||
- 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`.
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -79,7 +80,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 and no-video-over-mobile-data options.
|
||||
- Add a clearer video-specific "never use mobile data for videos" shortcut in the Android automatic-upload UI; videos are still Wi-Fi-only by default, and the new daily mobile-data limit applies to metered networks after Wi-Fi-only is turned off.
|
||||
- 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