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_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_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_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_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_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_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_PICTURE_UPLOADS_PATH = "picture_uploads_path";
|
||||||
public static final String PREF__CAMERA_VIDEO_UPLOADS_PATH = "video_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";
|
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_CHARGING_ONLY
|
||||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_ENABLED
|
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_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_PATH
|
||||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_SOURCE
|
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_SOURCE
|
||||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY
|
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 prefPictureUploadsPath: Preference? = null
|
||||||
private var prefPictureUploadsOnWifi: CheckBoxPreference? = null
|
private var prefPictureUploadsOnWifi: CheckBoxPreference? = null
|
||||||
private var prefPictureUploadsAllowRoaming: CheckBoxPreference? = null
|
private var prefPictureUploadsAllowRoaming: CheckBoxPreference? = null
|
||||||
|
private var prefPictureUploadsMobileDataLimit: ListPreference? = null
|
||||||
private var prefPictureUploadsOnCharging: CheckBoxPreference? = null
|
private var prefPictureUploadsOnCharging: CheckBoxPreference? = null
|
||||||
private var prefPictureUploadsSourcePath: Preference? = null
|
private var prefPictureUploadsSourcePath: Preference? = null
|
||||||
private var prefPictureUploadsClearSourcePaths: Preference? = null
|
private var prefPictureUploadsClearSourcePaths: Preference? = null
|
||||||
@@ -103,6 +105,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefPictureUploadsPath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH)
|
prefPictureUploadsPath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH)
|
||||||
prefPictureUploadsOnWifi = findPreference(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY)
|
prefPictureUploadsOnWifi = findPreference(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY)
|
||||||
prefPictureUploadsAllowRoaming = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ALLOW_ROAMING)
|
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)
|
prefPictureUploadsOnCharging = findPreference(PREF__CAMERA_PICTURE_UPLOADS_CHARGING_ONLY)
|
||||||
prefPictureUploadsSourcePath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE)
|
prefPictureUploadsSourcePath = findPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE)
|
||||||
prefPictureUploadsClearSourcePaths = findPreference(PREF_PICTURE_UPLOADS_CLEAR_SOURCE_PATHS)
|
prefPictureUploadsClearSourcePaths = findPreference(PREF_PICTURE_UPLOADS_CLEAR_SOURCE_PATHS)
|
||||||
@@ -117,7 +120,9 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefPictureUploadsAccount = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME)
|
prefPictureUploadsAccount = findPreference(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME)
|
||||||
|
|
||||||
val comment = getString(R.string.prefs_camera_upload_source_path_title_required)
|
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()
|
initPreferenceListeners()
|
||||||
}
|
}
|
||||||
@@ -143,7 +148,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
showMessageInSnackbar(getString(R.string.prefs_automatic_uploads_not_available_users_light))
|
showMessageInSnackbar(getString(R.string.prefs_automatic_uploads_not_available_users_light))
|
||||||
} else {
|
} else {
|
||||||
val currentAccount = manageAccountsViewModel.getCurrentAccount()?.name
|
val currentAccount = manageAccountsViewModel.getCurrentAccount()?.name
|
||||||
currentAccount?.let {
|
if (currentAccount != null) {
|
||||||
selectedAccount = if (manageAccountsViewModel.checkUserLight(currentAccount)) {
|
selectedAccount = if (manageAccountsViewModel.checkUserLight(currentAccount)) {
|
||||||
availableAccounts.first().accountName
|
availableAccounts.first().accountName
|
||||||
} else {
|
} else {
|
||||||
@@ -162,6 +167,8 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefPictureUploadsOnWifi?.isChecked = it.wifiOnly
|
prefPictureUploadsOnWifi?.isChecked = it.wifiOnly
|
||||||
prefPictureUploadsAllowRoaming?.isChecked = it.allowRoaming
|
prefPictureUploadsAllowRoaming?.isChecked = it.allowRoaming
|
||||||
prefPictureUploadsAllowRoaming?.isEnabled = !it.wifiOnly
|
prefPictureUploadsAllowRoaming?.isEnabled = !it.wifiOnly
|
||||||
|
prefPictureUploadsMobileDataLimit?.value = it.mobileDataLimitBytes.toString()
|
||||||
|
prefPictureUploadsMobileDataLimit?.isEnabled = !it.wifiOnly
|
||||||
prefPictureUploadsOnCharging?.isChecked = it.chargingOnly
|
prefPictureUploadsOnCharging?.isChecked = it.chargingOnly
|
||||||
prefPictureUploadsBehaviour?.value = it.behavior.name
|
prefPictureUploadsBehaviour?.value = it.behavior.name
|
||||||
prefPictureUploadsLastSync?.summary = formatLastSyncSummary(it.lastSyncTimestamp)
|
prefPictureUploadsLastSync?.summary = formatLastSyncSummary(it.lastSyncTimestamp)
|
||||||
@@ -246,33 +253,40 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prefPictureUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue ->
|
prefPictureUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as Boolean
|
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||||
picturesViewModel.useWifiOnly(newValue)
|
picturesViewModel.useWifiOnly(value)
|
||||||
prefPictureUploadsAllowRoaming?.isEnabled = !newValue
|
prefPictureUploadsAllowRoaming?.isEnabled = !value
|
||||||
newValue
|
prefPictureUploadsMobileDataLimit?.isEnabled = !value
|
||||||
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
prefPictureUploadsAllowRoaming?.setOnPreferenceChangeListener { _, newValue ->
|
prefPictureUploadsAllowRoaming?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as Boolean
|
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||||
picturesViewModel.allowRoaming(newValue)
|
picturesViewModel.allowRoaming(value)
|
||||||
newValue
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
prefPictureUploadsMobileDataLimit?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val value = (newValue as? String)?.toLongOrNull() ?: return@setOnPreferenceChangeListener false
|
||||||
|
picturesViewModel.setMobileDataLimitBytes(value)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
prefPictureUploadsOnCharging?.setOnPreferenceChangeListener { _, newValue ->
|
prefPictureUploadsOnCharging?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as Boolean
|
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||||
picturesViewModel.useChargingOnly(newValue)
|
picturesViewModel.useChargingOnly(value)
|
||||||
newValue
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
prefPictureUploadsAccount?.setOnPreferenceChangeListener { _, newValue ->
|
prefPictureUploadsAccount?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as String
|
val value = newValue as? String ?: return@setOnPreferenceChangeListener false
|
||||||
picturesViewModel.handleSelectAccount(newValue)
|
picturesViewModel.handleSelectAccount(value)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
prefPictureUploadsBehaviour?.setOnPreferenceChangeListener { _, newValue ->
|
prefPictureUploadsBehaviour?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as String
|
val value = newValue as? String ?: return@setOnPreferenceChangeListener false
|
||||||
picturesViewModel.handleSelectBehaviour(newValue)
|
picturesViewModel.handleSelectBehaviour(value)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,6 +304,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefPictureUploadsPath?.isEnabled = value
|
prefPictureUploadsPath?.isEnabled = value
|
||||||
prefPictureUploadsOnWifi?.isEnabled = value
|
prefPictureUploadsOnWifi?.isEnabled = value
|
||||||
prefPictureUploadsAllowRoaming?.isEnabled = value && prefPictureUploadsOnWifi?.isChecked != true
|
prefPictureUploadsAllowRoaming?.isEnabled = value && prefPictureUploadsOnWifi?.isChecked != true
|
||||||
|
prefPictureUploadsMobileDataLimit?.isEnabled = value && prefPictureUploadsOnWifi?.isChecked != true
|
||||||
prefPictureUploadsOnCharging?.isEnabled = value
|
prefPictureUploadsOnCharging?.isEnabled = value
|
||||||
prefPictureUploadsSourcePath?.isEnabled = value
|
prefPictureUploadsSourcePath?.isEnabled = value
|
||||||
prefPictureUploadsClearSourcePaths?.isEnabled = value && picturesViewModel.getPictureUploadsSourcePaths().isNotEmpty()
|
prefPictureUploadsClearSourcePaths?.isEnabled = value && picturesViewModel.getPictureUploadsSourcePaths().isNotEmpty()
|
||||||
@@ -305,6 +320,7 @@ class SettingsPictureUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefPictureUploadsClearSourcePaths?.isEnabled = false
|
prefPictureUploadsClearSourcePaths?.isEnabled = false
|
||||||
prefPictureUploadsOnWifi?.isChecked = false
|
prefPictureUploadsOnWifi?.isChecked = false
|
||||||
prefPictureUploadsAllowRoaming?.isChecked = false
|
prefPictureUploadsAllowRoaming?.isChecked = false
|
||||||
|
prefPictureUploadsMobileDataLimit?.value = "0"
|
||||||
prefPictureUploadsOnCharging?.isChecked = false
|
prefPictureUploadsOnCharging?.isChecked = false
|
||||||
prefPictureUploadsBehaviour?.value = UploadBehavior.COPY.name
|
prefPictureUploadsBehaviour?.value = UploadBehavior.COPY.name
|
||||||
prefPictureUploadsLastSync?.summary = getString(R.string.prefs_camera_upload_last_sync_never)
|
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) {
|
fun useChargingOnly(chargingOnly: Boolean) {
|
||||||
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
||||||
savePictureUploadsConfigurationUseCase(
|
savePictureUploadsConfigurationUseCase(
|
||||||
@@ -222,6 +232,9 @@ class SettingsPictureUploadsViewModel(
|
|||||||
uploadPath: String? = _pictureUploads.value?.uploadPath,
|
uploadPath: String? = _pictureUploads.value?.uploadPath,
|
||||||
wifiOnly: Boolean? = _pictureUploads.value?.wifiOnly,
|
wifiOnly: Boolean? = _pictureUploads.value?.wifiOnly,
|
||||||
allowRoaming: Boolean? = _pictureUploads.value?.allowRoaming,
|
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,
|
chargingOnly: Boolean? = _pictureUploads.value?.chargingOnly,
|
||||||
sourcePath: String? = _pictureUploads.value?.sourcePath,
|
sourcePath: String? = _pictureUploads.value?.sourcePath,
|
||||||
behavior: UploadBehavior? = _pictureUploads.value?.behavior,
|
behavior: UploadBehavior? = _pictureUploads.value?.behavior,
|
||||||
@@ -232,9 +245,12 @@ class SettingsPictureUploadsViewModel(
|
|||||||
behavior = behavior ?: UploadBehavior.COPY,
|
behavior = behavior ?: UploadBehavior.COPY,
|
||||||
sourcePath = sourcePath.orEmpty(),
|
sourcePath = sourcePath.orEmpty(),
|
||||||
uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH,
|
uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH,
|
||||||
wifiOnly = wifiOnly ?: true,
|
wifiOnly = wifiOnly != false,
|
||||||
allowRoaming = allowRoaming ?: false,
|
allowRoaming = allowRoaming == true,
|
||||||
chargingOnly = chargingOnly ?: false,
|
mobileDataLimitBytes = mobileDataLimitBytes ?: 0,
|
||||||
|
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp ?: 0,
|
||||||
|
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod ?: 0,
|
||||||
|
chargingOnly = chargingOnly == true,
|
||||||
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
|
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
|
||||||
name = _pictureUploads.value?.name ?: pictureUploadsName,
|
name = _pictureUploads.value?.name ?: pictureUploadsName,
|
||||||
spaceId = spaceId,
|
spaceId = spaceId,
|
||||||
@@ -251,7 +267,7 @@ class SettingsPictureUploadsViewModel(
|
|||||||
|
|
||||||
fun getUploadPathString(): String {
|
fun getUploadPathString(): String {
|
||||||
|
|
||||||
val spaceName = handleSpaceName(pictureUploadsSpace?.name)
|
val spaceName = handleSpaceName(pictureUploadsSpace?.name).orEmpty()
|
||||||
val uploadPath = pictureUploads.value?.uploadPath
|
val uploadPath = pictureUploads.value?.uploadPath
|
||||||
val spaceId = pictureUploads.value?.spaceId
|
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_BEHAVIOUR
|
||||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_CHARGING_ONLY
|
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_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_PATH
|
||||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_SOURCE
|
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_SOURCE
|
||||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY
|
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 prefVideoUploadsPath: Preference? = null
|
||||||
private var prefVideoUploadsOnWifi: CheckBoxPreference? = null
|
private var prefVideoUploadsOnWifi: CheckBoxPreference? = null
|
||||||
private var prefVideoUploadsAllowRoaming: CheckBoxPreference? = null
|
private var prefVideoUploadsAllowRoaming: CheckBoxPreference? = null
|
||||||
|
private var prefVideoUploadsMobileDataLimit: ListPreference? = null
|
||||||
private var prefVideoUploadsOnCharging: CheckBoxPreference? = null
|
private var prefVideoUploadsOnCharging: CheckBoxPreference? = null
|
||||||
private var prefVideoUploadsSourcePath: Preference? = null
|
private var prefVideoUploadsSourcePath: Preference? = null
|
||||||
private var prefVideoUploadsClearSourcePaths: Preference? = null
|
private var prefVideoUploadsClearSourcePaths: Preference? = null
|
||||||
@@ -103,6 +105,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefVideoUploadsPath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH)
|
prefVideoUploadsPath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH)
|
||||||
prefVideoUploadsOnWifi = findPreference(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY)
|
prefVideoUploadsOnWifi = findPreference(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY)
|
||||||
prefVideoUploadsAllowRoaming = findPreference(PREF__CAMERA_VIDEO_UPLOADS_ALLOW_ROAMING)
|
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)
|
prefVideoUploadsOnCharging = findPreference(PREF__CAMERA_VIDEO_UPLOADS_CHARGING_ONLY)
|
||||||
prefVideoUploadsSourcePath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE)
|
prefVideoUploadsSourcePath = findPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE)
|
||||||
prefVideoUploadsClearSourcePaths = findPreference(PREF_VIDEO_UPLOADS_CLEAR_SOURCE_PATHS)
|
prefVideoUploadsClearSourcePaths = findPreference(PREF_VIDEO_UPLOADS_CLEAR_SOURCE_PATHS)
|
||||||
@@ -115,7 +118,9 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefVideoUploadsAccount = findPreference<ListPreference>(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME)
|
prefVideoUploadsAccount = findPreference<ListPreference>(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME)
|
||||||
|
|
||||||
val comment = getString(R.string.prefs_camera_upload_source_path_title_required)
|
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()
|
initPreferenceListeners()
|
||||||
}
|
}
|
||||||
@@ -141,7 +146,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
showMessageInSnackbar(getString(R.string.prefs_automatic_uploads_not_available_users_light))
|
showMessageInSnackbar(getString(R.string.prefs_automatic_uploads_not_available_users_light))
|
||||||
} else {
|
} else {
|
||||||
val currentAccount = manageAccountsViewModel.getCurrentAccount()?.name
|
val currentAccount = manageAccountsViewModel.getCurrentAccount()?.name
|
||||||
currentAccount?.let {
|
if (currentAccount != null) {
|
||||||
selectedAccount = if (manageAccountsViewModel.checkUserLight(currentAccount)) {
|
selectedAccount = if (manageAccountsViewModel.checkUserLight(currentAccount)) {
|
||||||
availableAccounts.first().accountName
|
availableAccounts.first().accountName
|
||||||
} else {
|
} else {
|
||||||
@@ -160,6 +165,8 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefVideoUploadsOnWifi?.isChecked = it.wifiOnly
|
prefVideoUploadsOnWifi?.isChecked = it.wifiOnly
|
||||||
prefVideoUploadsAllowRoaming?.isChecked = it.allowRoaming
|
prefVideoUploadsAllowRoaming?.isChecked = it.allowRoaming
|
||||||
prefVideoUploadsAllowRoaming?.isEnabled = !it.wifiOnly
|
prefVideoUploadsAllowRoaming?.isEnabled = !it.wifiOnly
|
||||||
|
prefVideoUploadsMobileDataLimit?.value = it.mobileDataLimitBytes.toString()
|
||||||
|
prefVideoUploadsMobileDataLimit?.isEnabled = !it.wifiOnly
|
||||||
prefVideoUploadsOnCharging?.isChecked = it.chargingOnly
|
prefVideoUploadsOnCharging?.isChecked = it.chargingOnly
|
||||||
prefVideoUploadsBehaviour?.value = it.behavior.name
|
prefVideoUploadsBehaviour?.value = it.behavior.name
|
||||||
prefVideoUploadsLastSync?.summary = formatLastSyncSummary(it.lastSyncTimestamp)
|
prefVideoUploadsLastSync?.summary = formatLastSyncSummary(it.lastSyncTimestamp)
|
||||||
@@ -244,33 +251,40 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prefVideoUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue ->
|
prefVideoUploadsOnWifi?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as Boolean
|
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||||
videosViewModel.useWifiOnly(newValue)
|
videosViewModel.useWifiOnly(value)
|
||||||
prefVideoUploadsAllowRoaming?.isEnabled = !newValue
|
prefVideoUploadsAllowRoaming?.isEnabled = !value
|
||||||
newValue
|
prefVideoUploadsMobileDataLimit?.isEnabled = !value
|
||||||
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
prefVideoUploadsAllowRoaming?.setOnPreferenceChangeListener { _, newValue ->
|
prefVideoUploadsAllowRoaming?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as Boolean
|
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||||
videosViewModel.allowRoaming(newValue)
|
videosViewModel.allowRoaming(value)
|
||||||
newValue
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
prefVideoUploadsMobileDataLimit?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val value = (newValue as? String)?.toLongOrNull() ?: return@setOnPreferenceChangeListener false
|
||||||
|
videosViewModel.setMobileDataLimitBytes(value)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
prefVideoUploadsOnCharging?.setOnPreferenceChangeListener { _, newValue ->
|
prefVideoUploadsOnCharging?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as Boolean
|
val value = newValue as? Boolean ?: return@setOnPreferenceChangeListener false
|
||||||
videosViewModel.useChargingOnly(newValue)
|
videosViewModel.useChargingOnly(value)
|
||||||
newValue
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
prefVideoUploadsAccount?.setOnPreferenceChangeListener { _, newValue ->
|
prefVideoUploadsAccount?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as String
|
val value = newValue as? String ?: return@setOnPreferenceChangeListener false
|
||||||
videosViewModel.handleSelectAccount(newValue)
|
videosViewModel.handleSelectAccount(value)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
prefVideoUploadsBehaviour?.setOnPreferenceChangeListener { _, newValue ->
|
prefVideoUploadsBehaviour?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
newValue as String
|
val value = newValue as? String ?: return@setOnPreferenceChangeListener false
|
||||||
videosViewModel.handleSelectBehaviour(newValue)
|
videosViewModel.handleSelectBehaviour(value)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,6 +302,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefVideoUploadsPath?.isEnabled = value
|
prefVideoUploadsPath?.isEnabled = value
|
||||||
prefVideoUploadsOnWifi?.isEnabled = value
|
prefVideoUploadsOnWifi?.isEnabled = value
|
||||||
prefVideoUploadsAllowRoaming?.isEnabled = value && prefVideoUploadsOnWifi?.isChecked != true
|
prefVideoUploadsAllowRoaming?.isEnabled = value && prefVideoUploadsOnWifi?.isChecked != true
|
||||||
|
prefVideoUploadsMobileDataLimit?.isEnabled = value && prefVideoUploadsOnWifi?.isChecked != true
|
||||||
prefVideoUploadsOnCharging?.isEnabled = value
|
prefVideoUploadsOnCharging?.isEnabled = value
|
||||||
prefVideoUploadsSourcePath?.isEnabled = value
|
prefVideoUploadsSourcePath?.isEnabled = value
|
||||||
prefVideoUploadsClearSourcePaths?.isEnabled = value && videosViewModel.getVideoUploadsSourcePaths().isNotEmpty()
|
prefVideoUploadsClearSourcePaths?.isEnabled = value && videosViewModel.getVideoUploadsSourcePaths().isNotEmpty()
|
||||||
@@ -303,6 +318,7 @@ class SettingsVideoUploadsFragment : PreferenceFragmentCompat() {
|
|||||||
prefVideoUploadsClearSourcePaths?.isEnabled = false
|
prefVideoUploadsClearSourcePaths?.isEnabled = false
|
||||||
prefVideoUploadsOnWifi?.isChecked = false
|
prefVideoUploadsOnWifi?.isChecked = false
|
||||||
prefVideoUploadsAllowRoaming?.isChecked = false
|
prefVideoUploadsAllowRoaming?.isChecked = false
|
||||||
|
prefVideoUploadsMobileDataLimit?.value = "0"
|
||||||
prefVideoUploadsOnCharging?.isChecked = false
|
prefVideoUploadsOnCharging?.isChecked = false
|
||||||
prefVideoUploadsBehaviour?.value = UploadBehavior.COPY.name
|
prefVideoUploadsBehaviour?.value = UploadBehavior.COPY.name
|
||||||
prefVideoUploadsLastSync?.summary = getString(R.string.prefs_camera_upload_last_sync_never)
|
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) {
|
fun useChargingOnly(chargingOnly: Boolean) {
|
||||||
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
||||||
saveVideoUploadsConfigurationUseCase(
|
saveVideoUploadsConfigurationUseCase(
|
||||||
@@ -222,6 +232,9 @@ class SettingsVideoUploadsViewModel(
|
|||||||
uploadPath: String? = _videoUploads.value?.uploadPath,
|
uploadPath: String? = _videoUploads.value?.uploadPath,
|
||||||
wifiOnly: Boolean? = _videoUploads.value?.wifiOnly,
|
wifiOnly: Boolean? = _videoUploads.value?.wifiOnly,
|
||||||
allowRoaming: Boolean? = _videoUploads.value?.allowRoaming,
|
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,
|
chargingOnly: Boolean? = _videoUploads.value?.chargingOnly,
|
||||||
sourcePath: String? = _videoUploads.value?.sourcePath,
|
sourcePath: String? = _videoUploads.value?.sourcePath,
|
||||||
behavior: UploadBehavior? = _videoUploads.value?.behavior,
|
behavior: UploadBehavior? = _videoUploads.value?.behavior,
|
||||||
@@ -233,9 +246,12 @@ class SettingsVideoUploadsViewModel(
|
|||||||
behavior = behavior ?: UploadBehavior.COPY,
|
behavior = behavior ?: UploadBehavior.COPY,
|
||||||
sourcePath = sourcePath.orEmpty(),
|
sourcePath = sourcePath.orEmpty(),
|
||||||
uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH,
|
uploadPath = uploadPath ?: PREF__CAMERA_UPLOADS_DEFAULT_PATH,
|
||||||
wifiOnly = wifiOnly ?: true,
|
wifiOnly = wifiOnly != false,
|
||||||
allowRoaming = allowRoaming ?: false,
|
allowRoaming = allowRoaming == true,
|
||||||
chargingOnly = chargingOnly ?: false,
|
mobileDataLimitBytes = mobileDataLimitBytes ?: 0,
|
||||||
|
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp ?: 0,
|
||||||
|
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod ?: 0,
|
||||||
|
chargingOnly = chargingOnly == true,
|
||||||
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
|
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
|
||||||
name = _videoUploads.value?.name ?: videoUploadsName,
|
name = _videoUploads.value?.name ?: videoUploadsName,
|
||||||
spaceId = spaceId,
|
spaceId = spaceId,
|
||||||
@@ -252,7 +268,7 @@ class SettingsVideoUploadsViewModel(
|
|||||||
|
|
||||||
fun getUploadPathString(): String {
|
fun getUploadPathString(): String {
|
||||||
|
|
||||||
val spaceName = handleSpaceName(videoUploadsSpace?.name)
|
val spaceName = handleSpaceName(videoUploadsSpace?.name).orEmpty()
|
||||||
val uploadPath = videoUploads.value?.uploadPath
|
val uploadPath = videoUploads.value?.uploadPath
|
||||||
val spaceId = videoUploads.value?.spaceId
|
val spaceId = videoUploads.value?.spaceId
|
||||||
|
|
||||||
|
|||||||
+106
-12
@@ -22,6 +22,7 @@
|
|||||||
package eu.qsfera.android.workers
|
package eu.qsfera.android.workers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
@@ -51,7 +52,9 @@ import org.koin.core.component.inject
|
|||||||
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.TimeZone
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class AutomaticUploadsWorker(
|
class AutomaticUploadsWorker(
|
||||||
@@ -142,9 +145,12 @@ class AutomaticUploadsWorker(
|
|||||||
WorkManager.getInstance(appContext).cancelUniqueWork(AUTOMATIC_UPLOADS_WORKER)
|
WorkManager.getInstance(appContext).cancelUniqueWork(AUTOMATIC_UPLOADS_WORKER)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun syncFolder(folderBackUpConfiguration: FolderBackUpConfiguration?) {
|
private fun isActiveNetworkMetered(): Boolean {
|
||||||
if (folderBackUpConfiguration == null) return
|
val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||||
|
return connectivityManager?.isActiveNetworkMetered != false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncFolder(folderBackUpConfiguration: FolderBackUpConfiguration) {
|
||||||
val syncType = when {
|
val syncType = when {
|
||||||
folderBackUpConfiguration.isPictureUploads -> SyncType.PICTURE_UPLOADS
|
folderBackUpConfiguration.isPictureUploads -> SyncType.PICTURE_UPLOADS
|
||||||
folderBackUpConfiguration.isVideoUploads -> SyncType.VIDEO_UPLOADS
|
folderBackUpConfiguration.isVideoUploads -> SyncType.VIDEO_UPLOADS
|
||||||
@@ -153,6 +159,9 @@ class AutomaticUploadsWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val currentTimestamp = System.currentTimeMillis()
|
val currentTimestamp = System.currentTimeMillis()
|
||||||
|
val activeNetworkMetered = isActiveNetworkMetered()
|
||||||
|
var mobileDataQuotaState = resolveMobileDataQuotaState(folderBackUpConfiguration, currentTimestamp)
|
||||||
|
var mobileDataLimitReached = false
|
||||||
|
|
||||||
val localPicturesDocumentFiles: List<DocumentFile> = folderBackUpConfiguration.sourcePaths.flatMap { sourcePath ->
|
val localPicturesDocumentFiles: List<DocumentFile> = folderBackUpConfiguration.sourcePaths.flatMap { sourcePath ->
|
||||||
getFilesReadyToUpload(
|
getFilesReadyToUpload(
|
||||||
@@ -163,19 +172,34 @@ class AutomaticUploadsWorker(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
showNotification(syncType, localPicturesDocumentFiles.size)
|
var queuedUploadsCount = 0
|
||||||
|
|
||||||
for (documentFile in localPicturesDocumentFiles) {
|
for (documentFile in localPicturesDocumentFiles) {
|
||||||
// Dedup: if this content URI already has a queued, in-progress, or succeeded transfer,
|
// 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
|
// 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
|
// 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.
|
// when two workers race on the same cache path.
|
||||||
val contentUri = documentFile.uri.toString()
|
val contentUri = documentFile.uri.toString()
|
||||||
if (transferRepository.existsNonFailedTransferForUri(contentUri)) {
|
if (transferRepository.existsNonFailedTransferForUri(contentUri)) {
|
||||||
Timber.d("Skipping already-tracked file: %s", documentFile.name)
|
Timber.d("Skipping already-tracked file: %s", documentFile.name)
|
||||||
continue
|
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(
|
val uploadId = storeInUploadsDatabase(
|
||||||
documentFile = documentFile,
|
documentFile = documentFile,
|
||||||
uploadPath = folderBackUpConfiguration.uploadPath.plus(File.separator).plus(documentFile.name),
|
uploadPath = folderBackUpConfiguration.uploadPath.plus(File.separator).plus(documentFile.name),
|
||||||
@@ -198,11 +222,26 @@ class AutomaticUploadsWorker(
|
|||||||
allowRoaming = folderBackUpConfiguration.allowRoaming,
|
allowRoaming = folderBackUpConfiguration.allowRoaming,
|
||||||
chargingOnly = folderBackUpConfiguration.chargingOnly
|
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
|
// Save safeTimestamp (not currentTimestamp) so that files skipped by the
|
||||||
// write-safety buffer are re-evaluated on the next run instead of being lost.
|
// 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
|
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(
|
private fun showNotification(
|
||||||
@@ -251,23 +290,29 @@ class AutomaticUploadsWorker(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTimestamp(
|
private fun updateConfigurationAfterScan(
|
||||||
folderBackUpConfiguration: FolderBackUpConfiguration,
|
folderBackUpConfiguration: FolderBackUpConfiguration,
|
||||||
syncType: SyncType,
|
syncType: SyncType,
|
||||||
currentTimestamp: Long,
|
nextSyncTimestamp: Long,
|
||||||
|
mobileDataQuotaState: MobileDataQuotaState,
|
||||||
) {
|
) {
|
||||||
|
val updatedConfiguration = folderBackUpConfiguration.copy(
|
||||||
|
lastSyncTimestamp = nextSyncTimestamp,
|
||||||
|
mobileDataPeriodStartTimestamp = mobileDataQuotaState.periodStartTimestamp,
|
||||||
|
mobileDataBytesQueuedInPeriod = mobileDataQuotaState.bytesQueuedInPeriod
|
||||||
|
)
|
||||||
|
|
||||||
when (syncType) {
|
when (syncType) {
|
||||||
SyncType.PICTURE_UPLOADS -> {
|
SyncType.PICTURE_UPLOADS -> {
|
||||||
val savePictureUploadsConfigurationUseCase: SavePictureUploadsConfigurationUseCase by inject()
|
val savePictureUploadsConfigurationUseCase: SavePictureUploadsConfigurationUseCase by inject()
|
||||||
savePictureUploadsConfigurationUseCase(
|
savePictureUploadsConfigurationUseCase(
|
||||||
SavePictureUploadsConfigurationUseCase.Params(folderBackUpConfiguration.copy(lastSyncTimestamp = currentTimestamp))
|
SavePictureUploadsConfigurationUseCase.Params(updatedConfiguration)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SyncType.VIDEO_UPLOADS -> {
|
SyncType.VIDEO_UPLOADS -> {
|
||||||
val saveVideoUploadsConfigurationUseCase: SaveVideoUploadsConfigurationUseCase by inject()
|
val saveVideoUploadsConfigurationUseCase: SaveVideoUploadsConfigurationUseCase by inject()
|
||||||
saveVideoUploadsConfigurationUseCase(
|
saveVideoUploadsConfigurationUseCase(
|
||||||
SaveVideoUploadsConfigurationUseCase.Params(folderBackUpConfiguration.copy(lastSyncTimestamp = currentTimestamp))
|
SaveVideoUploadsConfigurationUseCase.Params(updatedConfiguration)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,7 +326,7 @@ class AutomaticUploadsWorker(
|
|||||||
): List<DocumentFile> {
|
): List<DocumentFile> {
|
||||||
val sourceUri: Uri = sourcePath.toUri()
|
val sourceUri: Uri = sourcePath.toUri()
|
||||||
val documentTree = DocumentFile.fromTreeUri(applicationContext, sourceUri)
|
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
|
// 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
|
// 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("Last sync ${syncType.name}: ${Date(lastSyncTimestamp)}")
|
||||||
Timber.i("CurrentTimestamp ${Date(currentTimestamp)}")
|
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")
|
Timber.i("${filteredList.size} files are ${syncType.name} and were taken after last sync")
|
||||||
|
|
||||||
return filteredList
|
return filteredList
|
||||||
@@ -355,6 +400,11 @@ class AutomaticUploadsWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
data class MobileDataQuotaState(
|
||||||
|
val periodStartTimestamp: Long,
|
||||||
|
val bytesQueuedInPeriod: Long,
|
||||||
|
)
|
||||||
|
|
||||||
const val AUTOMATIC_UPLOADS_WORKER = "AUTOMATIC_UPLOADS_WORKER"
|
const val AUTOMATIC_UPLOADS_WORKER = "AUTOMATIC_UPLOADS_WORKER"
|
||||||
const val IMMEDIATE_UPLOADS_WORKER = "IMMEDIATE_AUTOMATIC_UPLOADS_WORKER"
|
const val IMMEDIATE_UPLOADS_WORKER = "IMMEDIATE_AUTOMATIC_UPLOADS_WORKER"
|
||||||
const val MEDIA_STORE_UPLOADS_WORKER = "MEDIA_STORE_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
|
private const val videoUploadsNotificationId = 102
|
||||||
const val WRITE_SAFETY_BUFFER_MS = 10_000L
|
const val WRITE_SAFETY_BUFFER_MS = 10_000L
|
||||||
const val MEDIA_STORE_TRIGGER_MAX_DELAY_MS = 60_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="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">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_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_dialog_title">Original file after backup</string>
|
||||||
<string name="prefs_camera_upload_behaviour_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>
|
<string name="prefs_camera_upload_last_sync_title">Last backup scan</string>
|
||||||
|
|||||||
@@ -69,6 +69,15 @@
|
|||||||
app:key="picture_uploads_allow_roaming"
|
app:key="picture_uploads_allow_roaming"
|
||||||
app:summary="@string/prefs_camera_upload_allow_roaming_summary"
|
app:summary="@string/prefs_camera_upload_allow_roaming_summary"
|
||||||
app:title="@string/prefs_camera_upload_allow_roaming" />
|
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
|
<CheckBoxPreference
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:key="picture_uploads_on_charging"
|
app:key="picture_uploads_on_charging"
|
||||||
|
|||||||
@@ -67,6 +67,15 @@
|
|||||||
app:key="video_uploads_allow_roaming"
|
app:key="video_uploads_allow_roaming"
|
||||||
app:summary="@string/prefs_camera_upload_allow_roaming_summary"
|
app:summary="@string/prefs_camera_upload_allow_roaming_summary"
|
||||||
app:title="@string/prefs_camera_upload_allow_roaming" />
|
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
|
<CheckBoxPreference
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
app:key="video_uploads_on_charging"
|
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 DB_NAME = "filelist";
|
||||||
public static final String NEW_DB_NAME = "qsfera_database";
|
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() {
|
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_47_48
|
||||||
import eu.qsfera.android.data.migrations.MIGRATION_48_49
|
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_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.OCShareDao
|
||||||
import eu.qsfera.android.data.sharing.shares.db.OCShareEntity
|
import eu.qsfera.android.data.sharing.shares.db.OCShareEntity
|
||||||
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity
|
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity
|
||||||
@@ -129,7 +130,8 @@ abstract class QSferaDatabase : RoomDatabase() {
|
|||||||
MIGRATION_42_43,
|
MIGRATION_42_43,
|
||||||
MIGRATION_47_48,
|
MIGRATION_47_48,
|
||||||
MIGRATION_48_49,
|
MIGRATION_48_49,
|
||||||
MIGRATION_49_50)
|
MIGRATION_49_50,
|
||||||
|
MIGRATION_50_51)
|
||||||
.build()
|
.build()
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
instance
|
instance
|
||||||
|
|||||||
+6
@@ -69,6 +69,9 @@ class OCLocalFolderBackupDataSource(
|
|||||||
uploadPath = uploadPath,
|
uploadPath = uploadPath,
|
||||||
wifiOnly = wifiOnly,
|
wifiOnly = wifiOnly,
|
||||||
allowRoaming = allowRoaming,
|
allowRoaming = allowRoaming,
|
||||||
|
mobileDataLimitBytes = mobileDataLimitBytes,
|
||||||
|
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp,
|
||||||
|
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod,
|
||||||
chargingOnly = chargingOnly,
|
chargingOnly = chargingOnly,
|
||||||
name = name,
|
name = name,
|
||||||
lastSyncTimestamp = lastSyncTimestamp,
|
lastSyncTimestamp = lastSyncTimestamp,
|
||||||
@@ -85,6 +88,9 @@ class OCLocalFolderBackupDataSource(
|
|||||||
uploadPath = uploadPath,
|
uploadPath = uploadPath,
|
||||||
wifiOnly = wifiOnly,
|
wifiOnly = wifiOnly,
|
||||||
allowRoaming = allowRoaming,
|
allowRoaming = allowRoaming,
|
||||||
|
mobileDataLimitBytes = mobileDataLimitBytes,
|
||||||
|
mobileDataPeriodStartTimestamp = mobileDataPeriodStartTimestamp,
|
||||||
|
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod,
|
||||||
chargingOnly = chargingOnly,
|
chargingOnly = chargingOnly,
|
||||||
lastSyncTimestamp = lastSyncTimestamp,
|
lastSyncTimestamp = lastSyncTimestamp,
|
||||||
name = name,
|
name = name,
|
||||||
|
|||||||
+3
@@ -30,6 +30,9 @@ data class FolderBackUpEntity(
|
|||||||
val uploadPath: String,
|
val uploadPath: String,
|
||||||
val wifiOnly: Boolean,
|
val wifiOnly: Boolean,
|
||||||
val allowRoaming: Boolean,
|
val allowRoaming: Boolean,
|
||||||
|
val mobileDataLimitBytes: Long,
|
||||||
|
val mobileDataPeriodStartTimestamp: Long,
|
||||||
|
val mobileDataBytesQueuedInPeriod: Long,
|
||||||
val chargingOnly: Boolean,
|
val chargingOnly: Boolean,
|
||||||
val name: String,
|
val name: String,
|
||||||
val lastSyncTimestamp: Long,
|
val lastSyncTimestamp: Long,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
|||||||
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false)) return null
|
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false)) return null
|
||||||
|
|
||||||
return FolderBackUpConfiguration(
|
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),
|
wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY, false),
|
||||||
uploadPath = getUploadPathForPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH),
|
uploadPath = getUploadPathForPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH),
|
||||||
sourcePath = getSourcePathForPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE),
|
sourcePath = getSourcePathForPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE),
|
||||||
@@ -56,6 +56,9 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
|||||||
lastSyncTimestamp = timestamp,
|
lastSyncTimestamp = timestamp,
|
||||||
name = pictureUploadsName,
|
name = pictureUploadsName,
|
||||||
allowRoaming = false,
|
allowRoaming = false,
|
||||||
|
mobileDataLimitBytes = 0,
|
||||||
|
mobileDataPeriodStartTimestamp = 0,
|
||||||
|
mobileDataBytesQueuedInPeriod = 0,
|
||||||
chargingOnly = false,
|
chargingOnly = false,
|
||||||
spaceId = null,
|
spaceId = null,
|
||||||
)
|
)
|
||||||
@@ -65,7 +68,7 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
|||||||
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false)) return null
|
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false)) return null
|
||||||
|
|
||||||
return FolderBackUpConfiguration(
|
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),
|
wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY, false),
|
||||||
uploadPath = getUploadPathForPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH),
|
uploadPath = getUploadPathForPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH),
|
||||||
sourcePath = getSourcePathForPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE),
|
sourcePath = getSourcePathForPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE),
|
||||||
@@ -73,6 +76,9 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
|||||||
lastSyncTimestamp = timestamp,
|
lastSyncTimestamp = timestamp,
|
||||||
name = videoUploadsName,
|
name = videoUploadsName,
|
||||||
allowRoaming = false,
|
allowRoaming = false,
|
||||||
|
mobileDataLimitBytes = 0,
|
||||||
|
mobileDataPeriodStartTimestamp = 0,
|
||||||
|
mobileDataBytesQueuedInPeriod = 0,
|
||||||
chargingOnly = false,
|
chargingOnly = false,
|
||||||
spaceId = null,
|
spaceId = null,
|
||||||
)
|
)
|
||||||
@@ -87,7 +93,7 @@ class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferen
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getSourcePathForPreference(keyPreference: String): String =
|
private fun getSourcePathForPreference(keyPreference: String): String =
|
||||||
sharedPreferencesProvider.getString(keyPreference, null) ?: ""
|
sharedPreferencesProvider.getString(keyPreference, null).orEmpty()
|
||||||
|
|
||||||
private fun getBehaviorForPreference(keyPreference: String): UploadBehavior {
|
private fun getBehaviorForPreference(keyPreference: String): UploadBehavior {
|
||||||
val storedBehaviour = sharedPreferencesProvider.getString(keyPreference, null) ?: return UploadBehavior.COPY
|
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 uploadPath: String,
|
||||||
val wifiOnly: Boolean,
|
val wifiOnly: Boolean,
|
||||||
val allowRoaming: Boolean,
|
val allowRoaming: Boolean,
|
||||||
|
val mobileDataLimitBytes: Long,
|
||||||
|
val mobileDataPeriodStartTimestamp: Long,
|
||||||
|
val mobileDataBytesQueuedInPeriod: Long,
|
||||||
val chargingOnly: Boolean,
|
val chargingOnly: Boolean,
|
||||||
val lastSyncTimestamp: Long,
|
val lastSyncTimestamp: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -40,6 +43,7 @@ data class FolderBackUpConfiguration(
|
|||||||
const val pictureUploadsName = "Picture uploads"
|
const val pictureUploadsName = "Picture uploads"
|
||||||
const val videoUploadsName = "Video uploads"
|
const val videoUploadsName = "Video uploads"
|
||||||
const val initialSyncTimestamp = 0L
|
const val initialSyncTimestamp = 0L
|
||||||
|
private const val SOURCE_PATH_SEPARATOR = "\n"
|
||||||
|
|
||||||
fun parseSourcePaths(sourcePath: String): List<String> =
|
fun parseSourcePaths(sourcePath: String): List<String> =
|
||||||
sourcePath
|
sourcePath
|
||||||
@@ -54,8 +58,6 @@ data class FolderBackUpConfiguration(
|
|||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.distinct()
|
.distinct()
|
||||||
.joinToString(SOURCE_PATH_SEPARATOR)
|
.joinToString(SOURCE_PATH_SEPARATOR)
|
||||||
|
|
||||||
private const val SOURCE_PATH_SEPARATOR = "\n"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
@@ -32,6 +32,9 @@ val OC_BACKUP = FolderBackUpConfiguration(
|
|||||||
uploadPath = "/Photos",
|
uploadPath = "/Photos",
|
||||||
wifiOnly = true,
|
wifiOnly = true,
|
||||||
allowRoaming = false,
|
allowRoaming = false,
|
||||||
|
mobileDataLimitBytes = 0,
|
||||||
|
mobileDataPeriodStartTimestamp = 0,
|
||||||
|
mobileDataBytesQueuedInPeriod = 0,
|
||||||
chargingOnly = true,
|
chargingOnly = true,
|
||||||
lastSyncTimestamp = 1542628397,
|
lastSyncTimestamp = 1542628397,
|
||||||
name = "",
|
name = "",
|
||||||
@@ -45,6 +48,9 @@ val OC_BACKUP_ENTITY = FolderBackUpEntity(
|
|||||||
uploadPath = "/Photos",
|
uploadPath = "/Photos",
|
||||||
wifiOnly = true,
|
wifiOnly = true,
|
||||||
allowRoaming = false,
|
allowRoaming = false,
|
||||||
|
mobileDataLimitBytes = 0,
|
||||||
|
mobileDataPeriodStartTimestamp = 0,
|
||||||
|
mobileDataBytesQueuedInPeriod = 0,
|
||||||
chargingOnly = true,
|
chargingOnly = true,
|
||||||
lastSyncTimestamp = 1542628397,
|
lastSyncTimestamp = 1542628397,
|
||||||
name = "",
|
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 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 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 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` 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/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.
|
- 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
|
## Remaining P1
|
||||||
|
|
||||||
- Add Google Photos-style backup status UX: overall status, per-item status, queued/paused/permanent-failure states, and low-storage messaging.
|
- 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.
|
- 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.
|
- 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.
|
- 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