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
- **Build**: `testDebugUnitTest` + `assembleDebug` + `assembleRelease` limpos (JDK 21 / AGP 8.13.2 /
Kotlin 2.2.20 / SDK 36 / Compose BOM 2026.06); release R8 ≈ **1,9 MB**, debug ≈ 20 MB.
- **Emulador Android 16 (API 36, x86_64, Pixel 6)** — v0.4.1: build release R8 limpo,
`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,
criar nota → autosave → persiste no Room → lista atualiza; editor WYSIWYG/Preview ok;
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
endpoint de delta no servidor).
- 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"
minSdk = 26
targetSdk = 36
versionCode = 7
versionName = "0.4.0"
versionCode = 8
versionName = "0.4.1"
}
buildTypes {
@@ -59,8 +59,11 @@ class SseClient(private val baseUrl: String, private val token: String, private
currentCoroutineContext().ensureActive()
val line = reader.readLine() ?: break
when {
line.startsWith("data: ") -> data.append(line.removePrefix("data: "))
line.startsWith("data:") -> data.append(line.removePrefix("data:").trimStart())
line.startsWith("data:") -> {
// 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() -> {
val raw = data.toString()
data = StringBuilder()
@@ -22,5 +22,7 @@ sealed class SyncOutcome {
data class Success(val pushed: Int, val pulled: Int, val conflicts: Int) : SyncOutcome()
data class NetworkDown(val message: String) : 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()
}
@@ -81,6 +81,19 @@ class SyncEngine(
status.set { it.copy(phase = SyncPhase.Offline, message = push.message) }
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)
if (pull is SyncOutcome.Unauthorized) {
@@ -91,6 +104,14 @@ class SyncEngine(
status.set { it.copy(phase = SyncPhase.Offline, message = pull.message) }
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 conflicts = ok.first.conflicts + ok.second.conflicts
@@ -151,6 +172,7 @@ class SyncEngine(
}
is ApiResult.Network -> return SyncOutcome.NetworkDown(resp.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("create ${note.id}", 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.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("update ${note.id}", 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.Network -> return SyncOutcome.NetworkDown(resp.message)
is ApiResult.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("delete ${note.id}", 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.Unauthorized -> return SyncOutcome.Unauthorized
is ApiResult.Server -> return serverOutcome("categories", cats)
else -> {}
}
val list = client.listNotes()
if (list is ApiResult.Network) return SyncOutcome.NetworkDown(list.message)
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)}")
val serverIds = list.value.map { it.id }.toSet()
@@ -345,6 +371,10 @@ class SyncEngine(
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) {
is ApiResult.Server -> "HTTP ${r.code} ${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")) {
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.Unauthorized -> Result.success()
else -> Result.success()