3 Commits
Author SHA1 Message Date
lgcosta cccba68036 chore(release): bump v0.4.1 (versionCode 8)
fix(sync) 5xx -> Transient com retry; runSync trata Failed/NotConfigured;
SSE multilinha; ApiClient sem imports mortos.

Verificado: testDebugUnitTest + assembleDebug + assembleRelease limpos
(JDK 21 / AGP 8.13.2 / SDK 36).
2026-11-06 10:38:08 -03:00
lgcosta d10853bcaa fix(sync): erros 5xx viram Transient com retry; SSE multilinha; trata Failed/NotConfigured
- SyncOutcome.Transient para 5xx (push/pull/serverOutcome); worker da retry
- runSync: Failed -> Error, NotConfigured respeitado (evita cast crash)
- SseClient: junta linhas data: com \n (spec SSE)
- ApiClient: remove imports sem uso
2026-11-06 10:33:43 -03:00
lgcosta e608493c81 docs: roadmap — Expressive completo aguarda material3 1.5 estável 2026-11-06 08:51:32 -03:00
6 changed files with 45 additions and 6 deletions

No files matched your search

+5 -2
View File
@@ -130,8 +130,9 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
## Testado em ## Testado em
- **Build**: `testDebugUnitTest` + `assembleDebug` + `assembleRelease` limpos (JDK 21 / AGP 8.13.2 / - **Emulador Android 16 (API 36, x86_64, Pixel 6)** — v0.4.1: build release R8 limpo,
Kotlin 2.2.20 / SDK 36 / Compose BOM 2026.06); release R8 ≈ **1,9 MB**, debug ≈ 20 MB. `testDebugUnitTest` verde; push/pull 5xx → `Transient` com retry, `Failed` → Error,
SSE multilinha, sem crashes.
- **Emulador Android 16 (API 36, x86_64, Pixel 6)** — v0.4.0: upgrade in-place sem bloqueio, - **Emulador Android 16 (API 36, x86_64, Pixel 6)** — v0.4.0: upgrade in-place sem bloqueio,
criar nota → autosave → persiste no Room → lista atualiza; editor WYSIWYG/Preview ok; criar nota → autosave → persiste no Room → lista atualiza; editor WYSIWYG/Preview ok;
sem crashes (logcat AndroidRuntime limpo). sem crashes (logcat AndroidRuntime limpo).
@@ -164,3 +165,5 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk
- Sync reativo incremental (o pull compara hashes por nota; milhares de notas pediriam um - Sync reativo incremental (o pull compara hashes por nota; milhares de notas pediriam um
endpoint de delta no servidor). endpoint de delta no servidor).
- Multi-perfil/servidores e sincronização de anexos. - Multi-perfil/servidores e sincronização de anexos.
- Material 3 Expressive completo (`MaterialExpressiveTheme` + componentes 1.5.x) quando a
linha 1.5 sair de beta — hoje o estável (material3 1.4.0) não expõe a API.
+2 -2
View File
@@ -14,8 +14,8 @@ android {
applicationId = "com.labexposed.notas" applicationId = "com.labexposed.notas"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 7 versionCode = 8
versionName = "0.4.0" versionName = "0.4.1"
} }
buildTypes { buildTypes {
@@ -59,8 +59,11 @@ class SseClient(private val baseUrl: String, private val token: String, private
currentCoroutineContext().ensureActive() currentCoroutineContext().ensureActive()
val line = reader.readLine() ?: break val line = reader.readLine() ?: break
when { when {
line.startsWith("data: ") -> data.append(line.removePrefix("data: ")) line.startsWith("data:") -> {
line.startsWith("data:") -> data.append(line.removePrefix("data:").trimStart()) // evento multilinha: junta os blocos com \n (spec SSE)
if (data.isNotEmpty()) data.append('\n')
data.append(line.removePrefix("data:").trimStart())
}
line.isEmpty() && data.isNotEmpty() -> { line.isEmpty() && data.isNotEmpty() -> {
val raw = data.toString() val raw = data.toString()
data = StringBuilder() data = StringBuilder()
@@ -22,5 +22,7 @@ sealed class SyncOutcome {
data class Success(val pushed: Int, val pulled: Int, val conflicts: Int) : SyncOutcome() data class Success(val pushed: Int, val pulled: Int, val conflicts: Int) : SyncOutcome()
data class NetworkDown(val message: String) : SyncOutcome() data class NetworkDown(val message: String) : SyncOutcome()
data object Unauthorized : SyncOutcome() data object Unauthorized : SyncOutcome()
/** Erro transitório do servidor (5xx): pode repetir depois. */
data class Transient(val message: String) : SyncOutcome()
data class Failed(val message: String) : SyncOutcome() data class Failed(val message: String) : SyncOutcome()
} }
@@ -81,6 +81,19 @@ class SyncEngine(
status.set { it.copy(phase = SyncPhase.Offline, message = push.message) } status.set { it.copy(phase = SyncPhase.Offline, message = push.message) }
return push return push
} }
if (push is SyncOutcome.Transient) {
status.set { it.copy(phase = SyncPhase.Offline, message = push.message) }
return push
}
if (push is SyncOutcome.Failed) {
status.set { it.copy(phase = SyncPhase.Error, message = push.message) }
return push
}
if (push is SyncOutcome.NotConfigured) {
status.set { it.copy(phase = SyncPhase.NotConfigured) }
return push
}
val pull = pull(client) val pull = pull(client)
if (pull is SyncOutcome.Unauthorized) { if (pull is SyncOutcome.Unauthorized) {
@@ -91,6 +104,14 @@ class SyncEngine(
status.set { it.copy(phase = SyncPhase.Offline, message = pull.message) } status.set { it.copy(phase = SyncPhase.Offline, message = pull.message) }
return pull return pull
} }
if (pull is SyncOutcome.Transient) {
status.set { it.copy(phase = SyncPhase.Offline, message = pull.message) }
return pull
}
if (pull is SyncOutcome.Failed) {
status.set { it.copy(phase = SyncPhase.Error, message = pull.message) }
return pull
}
val ok = (push as SyncOutcome.Success) to (pull as SyncOutcome.Success) val ok = (push as SyncOutcome.Success) to (pull as SyncOutcome.Success)
val conflicts = ok.first.conflicts + ok.second.conflicts val conflicts = ok.first.conflicts + ok.second.conflicts
@@ -151,6 +172,7 @@ class SyncEngine(
} }
is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message) is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("create ${note.id}", resp)
else -> return SyncOutcome.Failed("create ${note.id}: ${describe(resp)}") else -> return SyncOutcome.Failed("create ${note.id}: ${describe(resp)}")
} }
} }
@@ -189,6 +211,7 @@ class SyncEngine(
} }
is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message) is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("update ${note.id}", resp)
else -> return SyncOutcome.Failed("update ${note.id}: ${describe(resp)}") else -> return SyncOutcome.Failed("update ${note.id}: ${describe(resp)}")
} }
} }
@@ -200,6 +223,7 @@ class SyncEngine(
is ApiResult.Ok, is ApiResult.NotFound -> Unit is ApiResult.Ok, is ApiResult.NotFound -> Unit
is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message) is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("delete ${note.id}", resp)
else -> return SyncOutcome.Failed("delete ${note.id}: ${describe(resp)}") else -> return SyncOutcome.Failed("delete ${note.id}: ${describe(resp)}")
} }
} }
@@ -290,12 +314,14 @@ class SyncEngine(
} }
is ApiResult.Network -> return SyncOutcome.NetworkDown(cats.message) is ApiResult.Network -> return SyncOutcome.NetworkDown(cats.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("categories", cats)
else -> {} else -> {}
} }
val list = client.listNotes() val list = client.listNotes()
if (list is ApiResult.Network) return SyncOutcome.NetworkDown(list.message) if (list is ApiResult.Network) return SyncOutcome.NetworkDown(list.message)
if (list is ApiResult.Unauthorized) return SyncOutcome.Unauthorized if (list is ApiResult.Unauthorized) return SyncOutcome.Unauthorized
if (list is ApiResult.Server) return serverOutcome("list", list)
if (list !is ApiResult.Ok) return SyncOutcome.Failed("list: ${describe(list)}") if (list !is ApiResult.Ok) return SyncOutcome.Failed("list: ${describe(list)}")
val serverIds = list.value.map { it.id }.toSet() val serverIds = list.value.map { it.id }.toSet()
@@ -345,6 +371,10 @@ class SyncEngine(
private fun NoteDetailDto.toEntity() = toNoteEntity() private fun NoteDetailDto.toEntity() = toNoteEntity()
private fun serverOutcome(prefix: String, r: ApiResult.Server): SyncOutcome =
if (r.code >= 500) SyncOutcome.Transient("$prefix: HTTP ${r.code} ${r.message}")
else SyncOutcome.Failed("$prefix: HTTP ${r.code} ${r.message}")
private fun describe(r: ApiResult<*>): String = when (r) { private fun describe(r: ApiResult<*>): String = when (r) {
is ApiResult.Server -> "HTTP ${r.code} ${r.message}" is ApiResult.Server -> "HTTP ${r.code} ${r.message}"
is ApiResult.Network -> r.message is ApiResult.Network -> r.message
@@ -30,6 +30,7 @@ class SyncWorker(context: Context, params: WorkerParameters) :
) )
return when (val outcome = engine.fullSync(trigger = inputData.getString(TRIGGER) ?: "auto")) { return when (val outcome = engine.fullSync(trigger = inputData.getString(TRIGGER) ?: "auto")) {
is com.labexposed.notas.domain.SyncOutcome.NetworkDown -> Result.retry() is com.labexposed.notas.domain.SyncOutcome.NetworkDown -> Result.retry()
is com.labexposed.notas.domain.SyncOutcome.Transient -> Result.retry()
is com.labexposed.notas.domain.SyncOutcome.Failed -> Result.failure() is com.labexposed.notas.domain.SyncOutcome.Failed -> Result.failure()
is com.labexposed.notas.domain.SyncOutcome.Unauthorized -> Result.success() is com.labexposed.notas.domain.SyncOutcome.Unauthorized -> Result.success()
else -> Result.success() else -> Result.success()