Store Android auto-upload cursors per source
This commit is contained in:
+14
-1
@@ -30,6 +30,7 @@ import eu.qsfera.android.R
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_UPLOADS_DEFAULT_PATH
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.encodeSourcePaths
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.encodeSourcePathSyncTimestamps
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.initialSyncTimestamp
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName
|
||||
import eu.qsfera.android.domain.automaticuploads.model.UploadBehavior
|
||||
@@ -196,13 +197,22 @@ class SettingsPictureUploadsViewModel(
|
||||
val previousSourcePaths = getPictureUploadsSourcePaths()
|
||||
val newSourcePath = contentUriForTree.toString()
|
||||
val updatedSourcePaths = (previousSourcePaths + newSourcePath).distinct()
|
||||
val previousSourceSyncTimestamps = _pictureUploads.value?.sourceSyncTimestamps.orEmpty()
|
||||
val fallbackLastSyncTimestamp = _pictureUploads.value?.lastSyncTimestamp ?: initialSyncTimestamp
|
||||
val updatedSourceSyncTimestamps = updatedSourcePaths.associateWith { sourcePath ->
|
||||
previousSourceSyncTimestamps[sourcePath] ?: if (sourcePath in previousSourcePaths) {
|
||||
fallbackLastSyncTimestamp
|
||||
} else {
|
||||
initialSyncTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
||||
savePictureUploadsConfigurationUseCase(
|
||||
SavePictureUploadsConfigurationUseCase.Params(
|
||||
composePictureUploadsConfiguration(
|
||||
sourcePath = encodeSourcePaths(updatedSourcePaths),
|
||||
timestamp = initialSyncTimestamp.takeIf { updatedSourcePaths != previousSourcePaths }
|
||||
sourcePathSyncTimestamps = encodeSourcePathSyncTimestamps(updatedSourceSyncTimestamps)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -215,6 +225,7 @@ class SettingsPictureUploadsViewModel(
|
||||
SavePictureUploadsConfigurationUseCase.Params(
|
||||
composePictureUploadsConfiguration(
|
||||
sourcePath = "",
|
||||
sourcePathSyncTimestamps = "",
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
@@ -237,6 +248,7 @@ class SettingsPictureUploadsViewModel(
|
||||
mobileDataBytesQueuedInPeriod: Long? = _pictureUploads.value?.mobileDataBytesQueuedInPeriod,
|
||||
chargingOnly: Boolean? = _pictureUploads.value?.chargingOnly,
|
||||
sourcePath: String? = _pictureUploads.value?.sourcePath,
|
||||
sourcePathSyncTimestamps: String? = _pictureUploads.value?.sourcePathSyncTimestamps,
|
||||
behavior: UploadBehavior? = _pictureUploads.value?.behavior,
|
||||
timestamp: Long? = _pictureUploads.value?.lastSyncTimestamp,
|
||||
spaceId: String? = _pictureUploads.value?.spaceId,
|
||||
@@ -252,6 +264,7 @@ class SettingsPictureUploadsViewModel(
|
||||
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod ?: 0,
|
||||
chargingOnly = chargingOnly == true,
|
||||
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
|
||||
sourcePathSyncTimestamps = sourcePathSyncTimestamps.orEmpty(),
|
||||
name = _pictureUploads.value?.name ?: pictureUploadsName,
|
||||
spaceId = spaceId,
|
||||
).also {
|
||||
|
||||
+14
-1
@@ -30,6 +30,7 @@ import eu.qsfera.android.R
|
||||
import eu.qsfera.android.db.PreferenceManager.PREF__CAMERA_UPLOADS_DEFAULT_PATH
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.encodeSourcePaths
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.encodeSourcePathSyncTimestamps
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.initialSyncTimestamp
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.videoUploadsName
|
||||
import eu.qsfera.android.domain.automaticuploads.model.UploadBehavior
|
||||
@@ -196,13 +197,22 @@ class SettingsVideoUploadsViewModel(
|
||||
val previousSourcePaths = getVideoUploadsSourcePaths()
|
||||
val newSourcePath = contentUriForTree.toString()
|
||||
val updatedSourcePaths = (previousSourcePaths + newSourcePath).distinct()
|
||||
val previousSourceSyncTimestamps = _videoUploads.value?.sourceSyncTimestamps.orEmpty()
|
||||
val fallbackLastSyncTimestamp = _videoUploads.value?.lastSyncTimestamp ?: initialSyncTimestamp
|
||||
val updatedSourceSyncTimestamps = updatedSourcePaths.associateWith { sourcePath ->
|
||||
previousSourceSyncTimestamps[sourcePath] ?: if (sourcePath in previousSourcePaths) {
|
||||
fallbackLastSyncTimestamp
|
||||
} else {
|
||||
initialSyncTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(coroutinesDispatcherProvider.io) {
|
||||
saveVideoUploadsConfigurationUseCase(
|
||||
SaveVideoUploadsConfigurationUseCase.Params(
|
||||
composeVideoUploadsConfiguration(
|
||||
sourcePath = encodeSourcePaths(updatedSourcePaths),
|
||||
timestamp = initialSyncTimestamp.takeIf { updatedSourcePaths != previousSourcePaths }
|
||||
sourcePathSyncTimestamps = encodeSourcePathSyncTimestamps(updatedSourceSyncTimestamps)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -215,6 +225,7 @@ class SettingsVideoUploadsViewModel(
|
||||
SaveVideoUploadsConfigurationUseCase.Params(
|
||||
composeVideoUploadsConfiguration(
|
||||
sourcePath = "",
|
||||
sourcePathSyncTimestamps = "",
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
@@ -237,6 +248,7 @@ class SettingsVideoUploadsViewModel(
|
||||
mobileDataBytesQueuedInPeriod: Long? = _videoUploads.value?.mobileDataBytesQueuedInPeriod,
|
||||
chargingOnly: Boolean? = _videoUploads.value?.chargingOnly,
|
||||
sourcePath: String? = _videoUploads.value?.sourcePath,
|
||||
sourcePathSyncTimestamps: String? = _videoUploads.value?.sourcePathSyncTimestamps,
|
||||
behavior: UploadBehavior? = _videoUploads.value?.behavior,
|
||||
timestamp: Long? = _videoUploads.value?.lastSyncTimestamp,
|
||||
spaceId: String? = _videoUploads.value?.spaceId,
|
||||
@@ -253,6 +265,7 @@ class SettingsVideoUploadsViewModel(
|
||||
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod ?: 0,
|
||||
chargingOnly = chargingOnly == true,
|
||||
lastSyncTimestamp = timestamp ?: initialSyncTimestamp,
|
||||
sourcePathSyncTimestamps = sourcePathSyncTimestamps.orEmpty(),
|
||||
name = _videoUploads.value?.name ?: videoUploadsName,
|
||||
spaceId = spaceId,
|
||||
).also {
|
||||
|
||||
+38
-3
@@ -163,11 +163,14 @@ class AutomaticUploadsWorker(
|
||||
var mobileDataQuotaState = resolveMobileDataQuotaState(folderBackUpConfiguration, currentTimestamp)
|
||||
var mobileDataLimitReached = false
|
||||
|
||||
val localPicturesDocumentFiles: List<DocumentFile> = folderBackUpConfiguration.sourcePaths.flatMap { sourcePath ->
|
||||
val sourcePaths = folderBackUpConfiguration.sourcePaths
|
||||
val currentSourceSyncTimestamps = folderBackUpConfiguration.sourceSyncTimestamps
|
||||
val localPicturesDocumentFiles: List<DocumentFile> = sourcePaths.flatMap { sourcePath ->
|
||||
val lastSyncTimestamp = currentSourceSyncTimestamps[sourcePath] ?: folderBackUpConfiguration.lastSyncTimestamp
|
||||
getFilesReadyToUpload(
|
||||
syncType = syncType,
|
||||
sourcePath = sourcePath,
|
||||
lastSyncTimestamp = folderBackUpConfiguration.lastSyncTimestamp,
|
||||
lastSyncTimestamp = lastSyncTimestamp,
|
||||
currentTimestamp = currentTimestamp,
|
||||
)
|
||||
}
|
||||
@@ -241,7 +244,20 @@ class AutomaticUploadsWorker(
|
||||
// so skipped files are re-evaluated after the quota period resets.
|
||||
val safeTimestamp = currentTimestamp - WRITE_SAFETY_BUFFER_MS
|
||||
val nextTimestamp = if (mobileDataLimitReached) folderBackUpConfiguration.lastSyncTimestamp else safeTimestamp
|
||||
updateConfigurationAfterScan(folderBackUpConfiguration, syncType, nextTimestamp, mobileDataQuotaState)
|
||||
val nextSourceSyncTimestamps = resolveSourceSyncTimestampsAfterScan(
|
||||
sourcePaths = sourcePaths,
|
||||
currentSourceSyncTimestamps = currentSourceSyncTimestamps,
|
||||
fallbackLastSyncTimestamp = folderBackUpConfiguration.lastSyncTimestamp,
|
||||
nextSyncTimestamp = nextTimestamp,
|
||||
scanCompleted = !mobileDataLimitReached,
|
||||
)
|
||||
updateConfigurationAfterScan(
|
||||
folderBackUpConfiguration = folderBackUpConfiguration,
|
||||
syncType = syncType,
|
||||
nextSyncTimestamp = nextTimestamp,
|
||||
nextSourceSyncTimestamps = nextSourceSyncTimestamps,
|
||||
mobileDataQuotaState = mobileDataQuotaState
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNotification(
|
||||
@@ -294,10 +310,12 @@ class AutomaticUploadsWorker(
|
||||
folderBackUpConfiguration: FolderBackUpConfiguration,
|
||||
syncType: SyncType,
|
||||
nextSyncTimestamp: Long,
|
||||
nextSourceSyncTimestamps: Map<String, Long>,
|
||||
mobileDataQuotaState: MobileDataQuotaState,
|
||||
) {
|
||||
val updatedConfiguration = folderBackUpConfiguration.copy(
|
||||
lastSyncTimestamp = nextSyncTimestamp,
|
||||
sourcePathSyncTimestamps = FolderBackUpConfiguration.encodeSourcePathSyncTimestamps(nextSourceSyncTimestamps),
|
||||
mobileDataPeriodStartTimestamp = mobileDataQuotaState.periodStartTimestamp,
|
||||
mobileDataBytesQueuedInPeriod = mobileDataQuotaState.bytesQueuedInPeriod
|
||||
)
|
||||
@@ -459,5 +477,22 @@ class AutomaticUploadsWorker(
|
||||
): Boolean =
|
||||
!shouldCountAgainstMobileDataLimit(wifiOnly, isActiveNetworkMetered, mobileDataLimitBytes) ||
|
||||
fileSize <= mobileDataLimitBytes - mobileDataBytesQueuedInPeriod
|
||||
|
||||
fun resolveSourceSyncTimestampsAfterScan(
|
||||
sourcePaths: List<String>,
|
||||
currentSourceSyncTimestamps: Map<String, Long>,
|
||||
fallbackLastSyncTimestamp: Long,
|
||||
nextSyncTimestamp: Long,
|
||||
scanCompleted: Boolean,
|
||||
): Map<String, Long> =
|
||||
sourcePaths
|
||||
.distinct()
|
||||
.associateWith { sourcePath ->
|
||||
if (scanCompleted) {
|
||||
nextSyncTimestamp
|
||||
} else {
|
||||
currentSourceSyncTimestamps[sourcePath] ?: fallbackLastSyncTimestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+42
@@ -121,6 +121,47 @@ class AutomaticUploadsWorkerTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `completed scan advances current source sync timestamps and drops removed sources`() {
|
||||
val timestamps = AutomaticUploadsWorker.resolveSourceSyncTimestampsAfterScan(
|
||||
sourcePaths = listOf("content://camera", "content://screenshots", "content://camera"),
|
||||
currentSourceSyncTimestamps = mapOf(
|
||||
"content://camera" to 100L,
|
||||
"content://old-folder" to 200L,
|
||||
),
|
||||
fallbackLastSyncTimestamp = 50L,
|
||||
nextSyncTimestamp = 1_000L,
|
||||
scanCompleted = true,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
mapOf(
|
||||
"content://camera" to 1_000L,
|
||||
"content://screenshots" to 1_000L,
|
||||
),
|
||||
timestamps
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `interrupted scan keeps source sync timestamps and falls back to global timestamp`() {
|
||||
val timestamps = AutomaticUploadsWorker.resolveSourceSyncTimestampsAfterScan(
|
||||
sourcePaths = listOf("content://camera", "content://screenshots"),
|
||||
currentSourceSyncTimestamps = mapOf("content://camera" to 100L),
|
||||
fallbackLastSyncTimestamp = 50L,
|
||||
nextSyncTimestamp = 1_000L,
|
||||
scanCompleted = false,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
mapOf(
|
||||
"content://camera" to 100L,
|
||||
"content://screenshots" to 50L,
|
||||
),
|
||||
timestamps
|
||||
)
|
||||
}
|
||||
|
||||
private fun backupConfiguration(
|
||||
mobileDataPeriodStartTimestamp: Long,
|
||||
mobileDataBytesQueuedInPeriod: Long,
|
||||
@@ -137,6 +178,7 @@ class AutomaticUploadsWorkerTest {
|
||||
mobileDataBytesQueuedInPeriod = mobileDataBytesQueuedInPeriod,
|
||||
chargingOnly = false,
|
||||
lastSyncTimestamp = 0L,
|
||||
sourcePathSyncTimestamps = "",
|
||||
name = FolderBackUpConfiguration.pictureUploadsName,
|
||||
spaceId = null,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user