# Notas — cliente Android (offline-first) Cliente mobile da plataforma [notas-online](https://git.labexposed.com/lgcosta/notas-online) (editor de notas estilo Mousepad com versionamento Git-like). **Kotlin + Jetpack Compose**, arquitetura offline-first: tudo funciona sem rede, com sincronização bidirecional em segundo plano (WorkManager) e resolução determinística de conflitos. ## Stack (e por que é a mais leve das opções) | Requisito | Escolha | Peso | |---|---|---| | UI | Jetpack Compose (Material 3, dynamic color) | nativo, sem engine extra | | Banco local | **Room** (SQLite) + WAL | ~0 APK extra relevante vs Flutter/RN | | Fila offline | tabela `outbox` (FIFO por `seq`) | — | | Sync em background | **WorkManager** (periódico 1h + `runNow` por gatilhos) | — | | Rede | OkHttp (timeout 10/20s, retry 2x com backoff em `IOException`) | — | | JSON | kotlinx.serialization (compile-time, sem reflection) | — | | Config | DataStore Preferences | — | | Conflitos | `base_hash` da API (409) + política *servidor vence + cópia local* | — | Flutter (engine ~8 MB) e React Native (bridge/Hermes) dariam mais runtime pelo mesmo trabalho; Android já entrega exatamente as primitivas que offline-first pede (Room, WorkManager, ConnectivityManager). ## Preparação do ambiente — Fedora 44 ```bash # 1) Java 21 (AGP 8.7 trabalha com 17/21) + utilitários sudo dnf install -y java-21-openjdk-devel git unzip curl findutils # 2a) Caminho recomendado: Android Studio (SDK + emulador inclusos) # baixe o .tar.gz em https://developer.android.com/studio e: mkdir -p ~/opt && tar -xzf ~/Downloads/android-studio-*.tar.gz -C ~/opt ~/opt/android-studio/bin/studio.sh # primeiro assistente instala o SDK em ~/Android/Sdk # 2b) Caminho só-CLI (sem Studio): mkdir -p ~/Android/Sdk/cmdline-tools && cd /tmp curl -LO https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip unzip -q commandlinetools-linux-*.zip -d ~/Android/Sdk/cmdline-tools mv ~/Android/Sdk/cmdline-tools/cmdline-tools ~/Android/Sdk/cmdline-tools/latest yes | ~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager --licenses ~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager \ "platform-tools" "platforms;android-35" "build-tools;35.0.0" # 3) ~/.bashrc (ou ~/.zshrc) export JAVA_HOME=/usr/lib/jvm/java-21-openjdk export ANDROID_HOME=$HOME/Android/Sdk export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools ``` O projeto usa o **Gradle Wrapper** (8.11.1) — não é preciso instalar Gradle. Se clonar sem os jars do wrapper, gere-os uma vez: `gradle wrapper --gradle-version 8.11.1` (ou abra no Android Studio, que regenera automaticamente). ## Estrutura do repositório ``` notas-online-android/ ├── settings.gradle.kts / build.gradle.kts / gradle.properties ├── gradle/wrapper/ # Gradle 8.11.1 ├── app/ │ ├── build.gradle.kts # AGP 8.7.3 · Kotlin 2.0.21 · Compose BOM 2024.12 │ ├── proguard-rules.pro │ └── src/main/ │ ├── AndroidManifest.xml # INTERNET + ACCESS_NETWORK_STATE; cleartext p/ LAN │ ├── res/ # tema, ícone adaptativo │ └── java/com/labexposed/notas/ │ ├── NotasApp.kt # Application: periodic work + gatilho de rede │ ├── MainActivity.kt # NavHost + SyncBanner (estado da sincronização) │ ├── data/ │ │ ├── local/ # Room: notes, outbox, categories, meta │ │ └── remote/ # ApiClient (OkHttp+DTOs), ConnectivityObserver │ ├── domain/ # SyncEngine, SettingsRepository, modelos de estado │ ├── sync/ # SyncWorker + SyncScheduler (WorkManager) │ └── ui/ # ViewModel, telas (Settings/Lista/Editor), tema ``` ## Como funciona a sincronização 1. **Tudo é local primeiro**: editar grava no Room e marca a nota `dirty`; a mutação entra na `outbox` (fila FIFO). Notas criadas offline ganham id `L…` temporário. 2. **Push**: para cada item da fila — `create` (POST, renomeia o id local para o id do servidor), `update` (PUT com `base_hash` = último hash conhecido) e `delete` (DELETE; nota local-only só sai do banco). 3. **Conflito (HTTP 409)**: o servidor mudou desde a sua base. Política determinística: **servidor vence a nota + texto local preservado** numa cópia *"Título (conflito)"* (fila como `create`). A versão antiga do servidor também está no histórico de revisões. Nada é perdido e não depende de relógios. 4. **Pull**: categorias + lista de notas; notas locais **limpas** com hash divergente são rebaixadas (GET completo); notas que sumiram do servidor (e não estão na fila) são removidas localmente. 5. **Gatilhos**: WorkManager periódico (1h, exige rede, backoff exponencial) · rede restabelecida (`ConnectivityManager` callback) · **a cada edição com autosave (10 s de ociosidade), incluindo mudanças só de título/formato/categorias** · **ao voltar para o app** (`ON_RESUME`) · botão **Sincronizar** · troca de configuração. Uma auto-reconciliação a cada rodada enfileira qualquer nota suja sem entrada na fila (recupera estados presos). 6. **Tempo real via SSE** (`GET /api/events`, OkHttp, reconexão com backoff, só em primeiro plano): eventos `saved` atualizam a UI na hora — nota limpa aberta no editor é recarregada ao vivo; nota suja mostra banner "Mudou no servidor — Recarregar/Manter"; snackbar na lista. Eventos `presence` desenham **👥N** na nota quando outra sessão (app ou navegador web) está com ela aberta. 7. **WYSIWYG e Preview** (WebView nativo, sem dependência extra): modo WYSIWYG renderiza o Markdown (via `/api/render` do servidor) num `contenteditable`; cada edição é convertida de volta para Markdown por um serializador JS embutido (títulos, negrito/itálico/tachado, listas, citação, código, links, tabelas) e sincronizada — notas `.html` editam o HTML preservando tags/atributos (scripts/`on*` sanitizados). Preview é o mesmo WebView não-editável. Barra de ferramentas aplica `execCommand` (B I S H1 H2 listas ❝ 🔗 código). ## UI (Material 3, dynamic color) - **Lista**: `MediumTopAppBar`, banner de status colorido (Offline/Sincronizando/Sincronizado± pendências/Erro), chips de filtro (Todas / 📁 categoria com caminho da árvore / #tag), cards com título+extensão, tempo relativo, tags, chip de categoria(s) e ícone de status por nota (☁✓ sincronizada, ● âmbar não enviada, 🗑 exclusão na fila, ⚠ conflito/mudança remota, 👥N presença), **botão de excluir por card** (🗑) com diálogo de confirmação, FAB estendido "Nova". - **Editor**: campo de título na top bar, seletor de modo (Editar / WYSIWYG / Preview), chips de formato (TXT/MD/HTML) e de categorias (multi-seleção), corpo monoespaçado com toolbar de Markdown (B I S H1 H2 • ❝ 🔗 ` { }) que envolve/prefixa a seleção, selo de presença 👥, horário do último salvamento e **botão de excluir** (com confirmação; volta para a lista). - **Exclusões sincronizam**: apagar marca `pendingDelete`, entra `delete` na fila e sobe na próxima sync (nota local-only some direto); outras sessões recebem o evento SSE `deleted` e a nota some delas também se estiver limpa. Se a nota já sumiu do servidor (404), a fila limpa sozinha. - **Servidor**: URL + token + autor com *Testar conexão* e validação de esquema http(s). ## Compilar e instalar ```bash # CLI (debug APK assinado com debug key): ./gradlew assembleDebug # saída: app/build/outputs/apk/debug/app-debug.apk adb install -r app/build/outputs/apk/debug/app-debug.apk # release (R8/minify): ./gradlew assembleRelease # Android Studio: abrir a pasta, sincronizar, Run ▶ (ou Build > Build APK(s)). ``` ## Testado em - **Build**: `assembleRelease` limpo (JDK 21 / AGP 8.7.3 / Kotlin 2.0.21 / SDK 35), APK assinado com R8 ≈ **1,6 MB** (debug ≈ 17 MB). - **Emulador Android 15 (x86_64, Pixel 6)** contra o servidor notas-online real, cenários: - configuração de servidor com *Testar conexão* → OK; - criar nota offline → autosave por ociosidade (10 s) → `POST` único (sem duplicatas); - editar offline → fila resiste à queda de rede → push automático ao restabelecer (`ConnectivityManager`); - nota criada na Web aparece no device via pull; - **mudança só de título sincroniza** (bug reportado em celular real, corrigido: metadados entram na fila + auto-reconciliação + sync no `ON_RESUME`); - **SSE ao vivo**: PUT vindo da Web atualiza o editor aberto em ~2 s, sem toque; snackbar na lista; **👥 presença** aparece quando outra sessão abre a mesma nota; - **WYSIWYG**: digitar no `contenteditable` → serialização html→md na ponte JS → autosave → servidor recebe o texto; Preview renderiza o Markdown via `/api/render`; - **conflito real**: device e Web editam a mesma nota → HTTP 409 (`base_hash`) → nota principal adota o servidor e o texto do Android vira exatamente **uma** cópia "… (conflito)" (idempotente; sincronizações serializadas por mutex + índices únicos na outbox). - Integração com a API: mesmos contratos do web client (`base_hash`/409, categorias, revisões, `/api/events`, `/api/presence`). O servidor normaliza `categories`/`tags` vazios como `[]` (null quebrava clientes com tipos não-nuláveis, ex. kotlinx.serialization). ## Roadmap honesto (não implementado aqui) - Token em Keystore/EncryptedSharedPreferences (hoje: sandbox do app via DataStore). - 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.