diff --git a/.github/workflows/android-sanity-check.yml b/.github/workflows/android-sanity-check.yml index 25a5f53..088db55 100644 --- a/.github/workflows/android-sanity-check.yml +++ b/.github/workflows/android-sanity-check.yml @@ -1,11 +1,20 @@ name: Android Sanity Check (CI) on: + # One run per PR (pull_request) and one post-merge on the integration branches. + # Previously push:["**"] also ran on every feature-branch push, so an open PR + # got two redundant concurrent runs. Feature branches are covered by their PR. push: - branches: [ "**" ] + branches: [ "main", "development" ] pull_request: branches: [ "main", "development" ] +# Supersede in-flight runs: when a newer commit lands on the same branch/PR, cancel +# the older run so a fixup doesn't leave a zombie ~50-min lint job running. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: check-syntax: name: Lint & Compile Check @@ -35,11 +44,10 @@ jobs: - name: Run Unit Tests run: ./gradlew testDebugUnitTest --stacktrace - # Lint runs for visibility only and never blocks the job (continue-on-error). - # Scoped to :app so the vendored termux-core/upstream lint backlog does not fail CI. - # Once the :app lint backlog is triaged, set abortOnError=true and drop continue-on-error. - - name: Run Android Lint (reporting, non-blocking) - continue-on-error: true + # Lint is a BLOCKING gate for :app. The existing backlog is grandfathered via the + # committed app/lint-baseline.xml; any NEW lint error fails CI. Scoped to :app so + # the vendored termux-core/upstream backlog does not block. + - name: Run Android Lint (:app, blocking) run: ./gradlew :app:lintDebug - name: Compile Test (Assemble Debug) diff --git a/controller/app/build.gradle b/controller/app/build.gradle index 9a3121d..35a5e08 100644 --- a/controller/app/build.gradle +++ b/controller/app/build.gradle @@ -102,7 +102,15 @@ android { lintOptions { checkReleaseBuilds false - abortOnError false + // Phase 0: lint is a hard gate for :app. The current backlog is grandfathered + // via the committed baseline below, so existing issues don't block the build, + // but any NEW lint error fails it. Shrink the baseline as the backlog is fixed. + abortOnError true + baseline file("lint-baseline.xml") + // Missing translations in secondary locales (fr/hi/pt/ru) should not block + // the build — en/es are primary and translations land incrementally. Still + // reported as a warning so they stay visible. + warning 'MissingTranslation' } testOptions { diff --git a/controller/app/lint-baseline.xml b/controller/app/lint-baseline.xml new file mode 100644 index 0000000..5a657ba --- /dev/null +++ b/controller/app/lint-baseline.xml @@ -0,0 +1,5133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/java/org/iiab/controller/DeployFragment.java b/controller/app/src/main/java/org/iiab/controller/DeployFragment.java index 25af525..c93dfd2 100644 --- a/controller/app/src/main/java/org/iiab/controller/DeployFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/DeployFragment.java @@ -49,6 +49,7 @@ import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; import com.google.android.material.snackbar.Snackbar; +import org.iiab.controller.util.LocalVarsYamlParser; import org.iiab.controller.rootfs.domain.RootfsAbi; import org.iiab.controller.rootfs.domain.RootfsTier; import org.iiab.controller.rootfs.presentation.RootfsUiState; @@ -1629,24 +1630,10 @@ private void fetchLocalVarsFromPRoot() { } private JSONObject parseYamlToJson(String yaml) { - JSONObject json = new JSONObject(); - String[] lines = yaml.split("\n"); - for (String line : lines) { - if (line.contains(":") && !line.trim().startsWith("#")) { - String[] parts = line.split(":", 2); - String key = parts[0].trim(); - String val = parts[1].trim().toLowerCase(); - - if (key.endsWith("_install") || key.endsWith("_enabled")) { - try { - boolean isTrue = val.equals("true") || val.equals("yes") || val.equals("1"); - json.put(key, isTrue); - } catch (Exception ignored) { - } - } - } - } - return json; + // Delegates to the pure, unit-tested util (extracted from this god class). + // The naive split-on-':' behavior is unchanged; replacing it with a real + // YAML parser is still tracked as tech-debt D14. + return LocalVarsYamlParser.parseToJson(yaml); } private void verifyInstallationState(JSONObject jsonVars) { diff --git a/controller/app/src/main/java/org/iiab/controller/util/LocalVarsYamlParser.java b/controller/app/src/main/java/org/iiab/controller/util/LocalVarsYamlParser.java new file mode 100644 index 0000000..ba6642d --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/util/LocalVarsYamlParser.java @@ -0,0 +1,52 @@ +package org.iiab.controller.util; + +import org.json.JSONObject; + +import java.util.Locale; + +/** + * Minimal reader for the slice of {@code local_vars.yml} the app cares about: + * top-level {@code _install} / {@code _enabled} boolean flags. + * + *

Pure logic (no Android, no I/O) so it is JVM-unit-testable. It is + * intentionally not a general YAML parser — it splits on the + * first {@code :} and does not handle nesting, quoting, block scalars or inline + * comments. That limitation is tracked as tech-debt D14; this + * extraction is the first step (isolate + test) toward replacing it with a real + * YAML library. + */ +public final class LocalVarsYamlParser { + + private LocalVarsYamlParser() { + } + + /** + * Parses {@code _install}/{@code _enabled} flags into a {@link JSONObject} of + * key → boolean. A value counts as {@code true} when it is {@code true}, + * {@code yes} or {@code 1} (case-insensitive). Lines that are blank, comments + * ({@code #}) or unrelated keys are ignored. Never returns {@code null}. + */ + public static JSONObject parseToJson(String yaml) { + JSONObject json = new JSONObject(); + if (yaml == null) { + return json; + } + for (String line : yaml.split("\n")) { + if (!line.contains(":") || line.trim().startsWith("#")) { + continue; + } + String[] parts = line.split(":", 2); + String key = parts[0].trim(); + String val = parts[1].trim().toLowerCase(Locale.ROOT); + if (key.endsWith("_install") || key.endsWith("_enabled")) { + boolean isTrue = val.equals("true") || val.equals("yes") || val.equals("1"); + try { + json.put(key, isTrue); + } catch (Exception ignored) { + // JSONObject.put only throws on a null key, which cannot happen here. + } + } + } + return json; + } +} diff --git a/controller/app/src/main/res/values-fr/strings.xml b/controller/app/src/main/res/values-fr/strings.xml index bd2a698..7950569 100644 --- a/controller/app/src/main/res/values-fr/strings.xml +++ b/controller/app/src/main/res/values-fr/strings.xml @@ -497,4 +497,8 @@ Arch. de l\'App : %1$d-bit Matériel compatible. Démarrage de la connexion... + Pas de connexion + Pas de connexion Internet — connectez-vous pour télécharger. + Tailles estimées (hors ligne) + Identifiants de synchronisation invalides ou non sécurisés. Veuillez scanner un QR code de synchronisation IIAB valide. \ No newline at end of file diff --git a/controller/app/src/main/res/values-hi/strings.xml b/controller/app/src/main/res/values-hi/strings.xml index 64b6f0d..eb0f935 100644 --- a/controller/app/src/main/res/values-hi/strings.xml +++ b/controller/app/src/main/res/values-hi/strings.xml @@ -497,4 +497,8 @@ App Arch: %1$d-बिट हार्डवेयर संगत है। कनेक्शन शुरू हो रहा है... + कोई कनेक्शन नहीं + इंटरनेट कनेक्शन नहीं — डाउनलोड करने के लिए कनेक्ट करें। + अनुमानित आकार (ऑफ़लाइन) + अमान्य या असुरक्षित सिंक क्रेडेंशियल। कृपया एक मान्य IIAB सिंक QR कोड स्कैन करें। \ No newline at end of file diff --git a/controller/app/src/main/res/values-pt/strings.xml b/controller/app/src/main/res/values-pt/strings.xml index 1e88929..015cf9f 100644 --- a/controller/app/src/main/res/values-pt/strings.xml +++ b/controller/app/src/main/res/values-pt/strings.xml @@ -499,4 +499,8 @@ Arq. do App: %1$d-bit Hardware compatível. Iniciando conexão... + Sem conexão + Sem conexão à internet — conecte-se para baixar. + Tamanhos estimados (offline) + Credenciais de sincronização inválidas ou inseguras. Por favor, escaneie um QR code de sincronização IIAB válido. \ No newline at end of file diff --git a/controller/app/src/main/res/values-ru-rRU/strings.xml b/controller/app/src/main/res/values-ru-rRU/strings.xml index 92b9330..a2d39ee 100644 --- a/controller/app/src/main/res/values-ru-rRU/strings.xml +++ b/controller/app/src/main/res/values-ru-rRU/strings.xml @@ -496,4 +496,8 @@ Архитектура App: %1$d-бит Оборудование совместимо. Запуск подключения... + Нет соединения + Нет подключения к интернету — подключитесь для загрузки. + Оценочные размеры (офлайн) + Недействительные или небезопасные учётные данные синхронизации. Отсканируйте действительный QR-код синхронизации IIAB. \ No newline at end of file diff --git a/controller/app/src/test/java/org/iiab/controller/util/LocalVarsYamlParserTest.java b/controller/app/src/test/java/org/iiab/controller/util/LocalVarsYamlParserTest.java new file mode 100644 index 0000000..71cea66 --- /dev/null +++ b/controller/app/src/test/java/org/iiab/controller/util/LocalVarsYamlParserTest.java @@ -0,0 +1,55 @@ +package org.iiab.controller.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.json.JSONObject; +import org.junit.Test; + +/** Pure-JVM tests for the local_vars.yml flag reader. */ +public class LocalVarsYamlParserTest { + + @Test + public void readsInstallAndEnabledTrueVariants() { + JSONObject j = LocalVarsYamlParser.parseToJson( + "kiwix_install: True\n" + + "kiwix_enabled: yes\n" + + "kolibri_install: 1\n"); + assertTrue(j.optBoolean("kiwix_install")); + assertTrue(j.optBoolean("kiwix_enabled")); + assertTrue(j.optBoolean("kolibri_install")); + } + + @Test + public void readsFalseValues() { + JSONObject j = LocalVarsYamlParser.parseToJson("kiwix_install: False\nkiwix_enabled: no\n"); + assertFalse(j.optBoolean("kiwix_install", true)); + assertFalse(j.optBoolean("kiwix_enabled", true)); + } + + @Test + public void ignoresCommentsBlanksAndUnrelatedKeys() { + JSONObject j = LocalVarsYamlParser.parseToJson( + "# kiwix_install: True\n" + + "\n" + + "some_other_key: True\n" + + "version: 3.2\n"); + assertFalse(j.has("kiwix_install")); // commented out + assertFalse(j.has("some_other_key")); // not an _install/_enabled flag + assertFalse(j.has("version")); + } + + @Test + public void handlesValueContainingColon() { + // split(":", 2) keeps everything after the first colon as the value. + JSONObject j = LocalVarsYamlParser.parseToJson("note_enabled: true # at 10:30\n"); + // value is "true # at 10:30" -> not exactly "true", so it reads false. + assertFalse(j.optBoolean("note_enabled", true)); + } + + @Test + public void handlesNullAndEmpty() { + assertFalse(LocalVarsYamlParser.parseToJson(null).keys().hasNext()); + assertFalse(LocalVarsYamlParser.parseToJson("").keys().hasNext()); + } +} diff --git a/controller/docs/TECH_DEBT_PLAN.md b/controller/docs/TECH_DEBT_PLAN.md index 28b82f7..b89c209 100644 --- a/controller/docs/TECH_DEBT_PLAN.md +++ b/controller/docs/TECH_DEBT_PLAN.md @@ -8,16 +8,17 @@ _Last updated: 2026-06-17. Tracks remediation work against the findings below. I **Phase 0 — Guardrails: DONE** (PR `chore/phase0-guardrails`, merged as #4) - Extracted `SystemStatsUtil` and added the first JVM unit tests (`SystemStatsUtilTest`, `SyncHandshakeHelperTest`); added unit-test infra (`returnDefaultValues` + real `org.json`). Addresses **M10**. -- CI gate: blocking `testDebugUnitTest`; `:app:lintDebug` runs non-blocking (scoped to `:app`). Addresses **M11**. +- CI gate: blocking `testDebugUnitTest`, blocking `:app:lintDebug` (grandfathered via a committed `lint-baseline.xml`), and the `assembleDebug` compile gate. Addresses **M11**. - Added a root `.gitignore` and `FORK_DELTA_ANALYSIS.md`. -- Remaining: flip lint `abortOnError=true` once the `:app` lint backlog is triaged; broaden tests to more pure functions (`LogManager.getFormattedSize`, `InstallationPlanner` sizing, the YAML parser). +- **Hardening (DONE):** lint is now a hard gate for `:app` — `abortOnError true` + committed `lint-baseline.xml`, and CI dropped `continue-on-error`. Existing backlog grandfathered; new lint errors fail the build. (Generate/commit the baseline once: `./gradlew :app:lintDebug`.) +- **Tests broadened (DONE):** extracted the `local_vars.yml` reader to a pure, unit-tested `util/LocalVarsYamlParser` (`LocalVarsYamlParserTest`) — the "YAML parser" item and first step on tech-debt **D14**. Notes on the other two named targets: `LogManager.getFormattedSize` is Android-coupled (`Context` + string resources) and its byte→human formatting is already covered by the tested `util/ByteFormatter`; `InstallationPlanner` OS sizing moved into the rootfs domain (covered by `GetRootfsSizeUseCaseTest` / `ByteFormatterTest`), so no separate pure sizing remains there to test. **K1 — Fork delta (Termux ExtraKeys): DONE** (PR `feat/k1-extrakeys-in-app`, merged as #5; details in `FORK_DELTA_ANALYSIS.md`) - **K1**: `loadIIABDefaultKeys()` moved out of upstream `ExtraKeysView` into app `IIABExtraKeys` (public APIs only). DONE. - **K3**: layout is now a single-source-of-truth constant. DONE. - **K4**: falls back to a minimal layout if the default fails to load. DONE. - **K5**: unit test validating the layout grid (`IIABExtraKeysTest`). DONE. -- Remaining: point the submodule to `appdevforall/termux-app` at clean upstream and commit the pointer. +- Submodule now points to `appdevforall/termux-app` with a committed SHA (`30ebb2d`, v0.117-436); pointer is in place. **K2** (squash the 8 messy K1 commits) is superseded — that history already merged to `main` via #5, so rewriting it is not worth it. **K6** is cosmetic, upstream-only (priority 10). **Reference slice — Rootfs live sizes (Clean Architecture pilot): DONE** (PR #6, merged) - First feature built across all three layers (`org.iiab.controller.rootfs.{domain,data,presentation}` + `util/ByteFormatter`); serves as the copy-paste template for future slices. See root `CLAUDE.md` (design map) and `ROOTFS_SIZE_PILOT_ANALYSIS.md`. diff --git a/controller/gradle.properties b/controller/gradle.properties index d034c3b..2dd6b92 100644 --- a/controller/gradle.properties +++ b/controller/gradle.properties @@ -1,3 +1,7 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+UseParallelGC +# Build cache + parallel modules: lets unchanged tasks (incl. lint analysis) be +# reused across runs and builds independent modules concurrently. Safe defaults. +org.gradle.caching=true +org.gradle.parallel=true