Store Android auto-upload cursors per source
This commit is contained in:
+50
@@ -19,6 +19,9 @@
|
||||
|
||||
package eu.qsfera.android.domain.automaticuploads.model
|
||||
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
|
||||
data class FolderBackUpConfiguration(
|
||||
val accountName: String,
|
||||
val behavior: UploadBehavior,
|
||||
@@ -31,6 +34,7 @@ data class FolderBackUpConfiguration(
|
||||
val mobileDataBytesQueuedInPeriod: Long,
|
||||
val chargingOnly: Boolean,
|
||||
val lastSyncTimestamp: Long,
|
||||
val sourcePathSyncTimestamps: String,
|
||||
val name: String,
|
||||
val spaceId: String?,
|
||||
) {
|
||||
@@ -38,12 +42,15 @@ data class FolderBackUpConfiguration(
|
||||
val isPictureUploads get() = name == pictureUploadsName
|
||||
val isVideoUploads get() = name == videoUploadsName
|
||||
val sourcePaths get() = parseSourcePaths(sourcePath)
|
||||
val sourceSyncTimestamps get() = parseSourcePathSyncTimestamps(sourcePathSyncTimestamps)
|
||||
|
||||
companion object {
|
||||
const val pictureUploadsName = "Picture uploads"
|
||||
const val videoUploadsName = "Video uploads"
|
||||
const val initialSyncTimestamp = 0L
|
||||
private const val SOURCE_PATH_SEPARATOR = "\n"
|
||||
private const val SOURCE_PATH_TIMESTAMP_SEPARATOR = "\t"
|
||||
private const val UTF_8 = "UTF-8"
|
||||
|
||||
fun parseSourcePaths(sourcePath: String): List<String> =
|
||||
sourcePath
|
||||
@@ -58,6 +65,49 @@ data class FolderBackUpConfiguration(
|
||||
.filter { it.isNotEmpty() }
|
||||
.distinct()
|
||||
.joinToString(SOURCE_PATH_SEPARATOR)
|
||||
|
||||
fun parseSourcePathSyncTimestamps(sourcePathSyncTimestamps: String): Map<String, Long> =
|
||||
sourcePathSyncTimestamps
|
||||
.split(SOURCE_PATH_SEPARATOR)
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.fold(LinkedHashMap<String, Long>()) { acc, encodedEntry ->
|
||||
val separatorIndex = encodedEntry.indexOf(SOURCE_PATH_TIMESTAMP_SEPARATOR)
|
||||
if (separatorIndex <= 0) {
|
||||
return@fold acc
|
||||
}
|
||||
val sourcePath = decodeSourcePath(encodedEntry.substring(0, separatorIndex)) ?: return@fold acc
|
||||
val timestamp = encodedEntry.substring(separatorIndex + 1).toLongOrNull() ?: return@fold acc
|
||||
if (sourcePath.isNotBlank() && timestamp >= initialSyncTimestamp) {
|
||||
acc[sourcePath] = timestamp
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
fun encodeSourcePathSyncTimestamps(sourceSyncTimestamps: Map<String, Long>): String =
|
||||
sourceSyncTimestamps
|
||||
.entries
|
||||
.fold(LinkedHashMap<String, Long>()) { acc, (sourcePath, timestamp) ->
|
||||
val normalizedSourcePath = sourcePath.trim()
|
||||
if (normalizedSourcePath.isNotEmpty() && timestamp >= initialSyncTimestamp) {
|
||||
acc[normalizedSourcePath] = timestamp
|
||||
}
|
||||
acc
|
||||
}
|
||||
.map { (sourcePath, timestamp) ->
|
||||
"${encodeSourcePath(sourcePath)}$SOURCE_PATH_TIMESTAMP_SEPARATOR$timestamp"
|
||||
}
|
||||
.joinToString(SOURCE_PATH_SEPARATOR)
|
||||
|
||||
private fun encodeSourcePath(sourcePath: String): String =
|
||||
URLEncoder.encode(sourcePath, UTF_8)
|
||||
|
||||
private fun decodeSourcePath(encodedSourcePath: String): String? =
|
||||
try {
|
||||
URLDecoder.decode(encodedSourcePath, UTF_8)
|
||||
} catch (illegalArgumentException: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
@@ -11,7 +11,9 @@
|
||||
package eu.qsfera.android.domain.automaticuploads.model
|
||||
|
||||
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.parseSourcePaths
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.parseSourcePathSyncTimestamps
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
@@ -59,4 +61,33 @@ class FolderBackUpConfigurationTest {
|
||||
|
||||
assertEquals("$firstSourcePath\n$secondSourcePath", encodedSourcePaths)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `source path sync timestamps round trip encoded source paths`() {
|
||||
val firstSourcePath = "content://com.android.externalstorage.documents/tree/primary%3ADCIM"
|
||||
val secondSourcePath = "content://com.android.externalstorage.documents/tree/primary%3AScreenshots"
|
||||
val sourceSyncTimestamps = linkedMapOf(
|
||||
firstSourcePath to 100L,
|
||||
secondSourcePath to 200L,
|
||||
)
|
||||
|
||||
val encodedSourceSyncTimestamps = encodeSourcePathSyncTimestamps(sourceSyncTimestamps)
|
||||
val parsedSourceSyncTimestamps = parseSourcePathSyncTimestamps(encodedSourceSyncTimestamps)
|
||||
|
||||
assertEquals(sourceSyncTimestamps, parsedSourceSyncTimestamps)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseSourcePathSyncTimestamps ignores malformed entries`() {
|
||||
val sourcePath = "content://com.android.externalstorage.documents/tree/primary%3ADCIM"
|
||||
val encodedSourceSyncTimestamps = """
|
||||
malformed
|
||||
${java.net.URLEncoder.encode(sourcePath, "UTF-8")} not-a-number
|
||||
${java.net.URLEncoder.encode(sourcePath, "UTF-8")} 123
|
||||
""".trimIndent()
|
||||
|
||||
val parsedSourceSyncTimestamps = parseSourcePathSyncTimestamps(encodedSourceSyncTimestamps)
|
||||
|
||||
assertEquals(mapOf(sourcePath to 123L), parsedSourceSyncTimestamps)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user