v1.0.53: block storage-root sync paths
Android 11+ denies writes to /storage/emulated/0 directly. SyncEngine now catches this early and returns FAILED with an actionable message instead of silently logging PARTIAL. LocalBrowserDialog disables the Select button at the storage root with an inline warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,16 @@ class SyncEngine @Inject constructor(
|
|||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
) {
|
) {
|
||||||
suspend fun sync(pair: SyncPair, provider: CloudProvider): SyncResult {
|
suspend fun sync(pair: SyncPair, provider: CloudProvider): SyncResult {
|
||||||
|
if (!pair.localPath.startsWith("content://")) {
|
||||||
|
val canonical = runCatching { File(pair.localPath).canonicalPath }.getOrElse { pair.localPath }
|
||||||
|
if (canonical == "/storage/emulated/0") {
|
||||||
|
val msg = "Local folder is the storage root — Android blocks writes here. Edit the pair and select a subfolder."
|
||||||
|
syncPairDao.updateSyncResult(pair.id, Instant.now(), SyncStatus.FAILED, 0)
|
||||||
|
logEvent(pair.id, SyncEventType.SYNC_FAILED, null, msg, 0)
|
||||||
|
return SyncResult(failedFiles = 1, error = Exception(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
syncPairDao.updateStatus(pair.id, SyncStatus.SYNCING)
|
syncPairDao.updateStatus(pair.id, SyncStatus.SYNCING)
|
||||||
logEvent(pair.id, SyncEventType.SYNC_STARTED, null, null, 0)
|
logEvent(pair.id, SyncEventType.SYNC_STARTED, null, null, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -263,22 +263,32 @@ fun LocalBrowserDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Select button ────────────────────────────────────────────
|
// ── Select button ────────────────────────────────────────────
|
||||||
|
val isStorageRoot = currentPath.absolutePath == STORAGE_ROOT.absolutePath
|
||||||
Surface(tonalElevation = 4.dp, modifier = Modifier.fillMaxWidth()) {
|
Surface(tonalElevation = 4.dp, modifier = Modifier.fillMaxWidth()) {
|
||||||
Column {
|
Column {
|
||||||
Button(
|
if (isStorageRoot) {
|
||||||
onClick = { onSelect(currentPath.absolutePath) },
|
Text(
|
||||||
modifier = Modifier
|
"Android blocks writes to the storage root — please open a subfolder first",
|
||||||
.fillMaxWidth()
|
style = MaterialTheme.typography.bodySmall,
|
||||||
.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp)
|
color = MaterialTheme.colorScheme.error,
|
||||||
.height(52.dp),
|
modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp),
|
||||||
shape = RoundedCornerShape(14.dp),
|
)
|
||||||
) {
|
}
|
||||||
Icon(Icons.Default.CheckCircle, null, Modifier.size(20.dp))
|
Button(
|
||||||
Spacer(Modifier.width(8.dp))
|
onClick = { onSelect(currentPath.absolutePath) },
|
||||||
Text("Select \"$currentFolderName\"",
|
enabled = !isStorageRoot,
|
||||||
style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
modifier = Modifier
|
||||||
}
|
.fillMaxWidth()
|
||||||
Spacer(Modifier.height(bottomInset))
|
.padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 12.dp)
|
||||||
|
.height(52.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.CheckCircle, null, Modifier.size(20.dp))
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Select \"$currentFolderName\"",
|
||||||
|
style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(bottomInset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
VERSION_NAME=1.0.52
|
VERSION_NAME=1.0.53
|
||||||
VERSION_CODE=53
|
VERSION_CODE=54
|
||||||
|
|||||||
Reference in New Issue
Block a user