From ddb558263fc25c4ad5b1097bebe4b21c3ee86e38 Mon Sep 17 00:00:00 2001 From: Amir Date: Sun, 7 Jun 2026 13:01:05 +0000 Subject: [PATCH] v1.0.75: exclude Android app-private trees (Android/data,media,obb) Scoped storage (Android 11+) lets a SAF grant LIST another app's Android/media|data|obb dir but not OPEN the files, so every transfer there failed and the pair was stuck reporting Partial forever (65 failures under Android/media/com.whatsapp/ on Zahra). These hold app-managed data, not user content, so exclude the whole subtree on both sides via path-prefix match in isExcludedPath. ExcludePathTest covers the new prefixes + case-insensitivity. --- .../com/syncflow/domain/sync/SyncEngine.kt | 14 +++++++++++++- .../com/syncflow/domain/sync/ExcludePathTest.kt | 16 ++++++++++++++++ version.properties | 4 ++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt b/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt index c65c1b5..0cf036b 100644 --- a/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt +++ b/app/src/main/kotlin/com/syncflow/domain/sync/SyncEngine.kt @@ -433,6 +433,15 @@ private val ALWAYS_IGNORED_SEGMENTS = setOf(".thumbnails") private val ALWAYS_IGNORED_PREFIXES = listOf(".trashed-", ".pending-") private val ALWAYS_IGNORED_SUFFIXES = listOf(".sfpart") +/** + * App-private storage trees. On Android 11+ (scoped storage) another app's SAF grant can LIST + * these directories but cannot OPEN the files inside, so every transfer fails and the pair is + * stuck reporting "Partial" forever (e.g. Android/media/com.whatsapp/...). They hold app-managed + * data, not user content worth syncing, so they are excluded entirely. Matched case-insensitively + * against the full relative path so the whole subtree is ignored on both sides. + */ +private val ALWAYS_IGNORED_PATH_PREFIXES = listOf("android/data/", "android/media/", "android/obb/") + /** * True if [rel] should be excluded from sync entirely. Applied symmetrically to the local walk, * the remote listing, and known state so an excluded path is never uploaded, downloaded, or @@ -441,7 +450,10 @@ private val ALWAYS_IGNORED_SUFFIXES = listOf(".sfpart") * existing local-walk semantics in LocalAccessor. */ internal fun isExcludedPath(rel: String, pair: SyncPair): Boolean { - val segments = rel.replace('\\', '/').split('/').filter { it.isNotEmpty() } + val normalized = rel.replace('\\', '/') + val lower = normalized.lowercase() + if (ALWAYS_IGNORED_PATH_PREFIXES.any { lower.startsWith(it) }) return true + val segments = normalized.split('/').filter { it.isNotEmpty() } if (segments.isEmpty()) return false for (seg in segments) { if (seg in ALWAYS_IGNORED_SEGMENTS) return true diff --git a/app/src/test/kotlin/com/syncflow/domain/sync/ExcludePathTest.kt b/app/src/test/kotlin/com/syncflow/domain/sync/ExcludePathTest.kt index a3fe4f6..93dc661 100644 --- a/app/src/test/kotlin/com/syncflow/domain/sync/ExcludePathTest.kt +++ b/app/src/test/kotlin/com/syncflow/domain/sync/ExcludePathTest.kt @@ -57,6 +57,22 @@ class ExcludePathTest { assertTrue(isExcludedPath("a/b/.photo.jpg.sfpart", p)) } + @Test fun `android app-private trees are excluded (scoped-storage unreadable)`() { + val p = pair() + assertTrue(isExcludedPath("Android/media/com.whatsapp/WhatsApp/Media/IMG.jpg", p)) + assertTrue(isExcludedPath("Android/data/com.foo/files/x.bin", p)) + assertTrue(isExcludedPath("Android/obb/com.game/main.obb", p)) + assertTrue(isExcludedPath("android/MEDIA/com.x/y.jpg", p)) // case-insensitive + } + + @Test fun `non-private Android paths are not excluded`() { + val p = pair() + // A user folder literally named "Android" at a deeper level is fine; only the + // top-level app-private trees are blocked. + assertFalse(isExcludedPath("DCIM/Android/holiday.jpg", p)) + assertFalse(isExcludedPath("Pictures/android-wallpaper.png", p)) + } + @Test fun `normal media files are not excluded`() { val p = pair() assertFalse(isExcludedPath("DCIM/Camera/IMG_0001.jpg", p)) diff --git a/version.properties b/version.properties index c0601a3..b6ee0b7 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=1.0.74 -VERSION_CODE=74 +VERSION_NAME=1.0.75 +VERSION_CODE=75