Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Normalize line endings to LF for text/source files so diffs stay clean
# across Windows/macOS/Linux contributors. Binary assets are left untouched.
* text=auto eol=lf

*.java text eol=lf
*.kt text eol=lf
*.gradle text eol=lf
*.xml text eol=lf
*.md text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sh text eol=lf
*.pro text eol=lf
*.properties text eol=lf

# Binary assets (never touch line endings)
*.png binary
*.jpg binary
*.jpeg binary
*.webp binary
*.gif binary
*.ico binary
*.ttf binary
*.otf binary
*.so binary
*.jar binary
*.aar binary
*.keystore binary
*.zip binary
*.tar.gz binary
*.meta4 binary
113 changes: 113 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# CLAUDE.md — Project guardrails for iiab-android

## Language

All **code, comments and documentation** are written in **English**, to
standardize across contributors and users worldwide.

Conversations / interactions with a contributor may happen in their **native
language** (e.g. Spanish). Reason in the contributor's language if helpful, but
every committed artifact ships in English.

---

## Architecture: layered design is mandatory for new code

The app started as a POC and has **no layering** today (UI, business and data
logic are mixed in large fragments/activities). We are migrating to a layered
(Clean Architecture) design: **Presentation → Domain ← Data**.

Golden rule — dependency direction always points **inward**:

- **Presentation** (Fragments/Activities, `ViewModel`, view state, formatting)
depends on Domain.
- **Domain** (entities, use cases, repository **interfaces**) depends on
**nothing**. No `android.*`, no `androidx.*`, no `java.net`, no HTTP, no JSON
framework. It must be unit-testable on a plain JVM.
- **Data** (repository **implementations**, remote/local data sources, DTOs,
catalogs, caches) depends on Domain and implements its interfaces.

Hard requirements for any new or migrated code:

1. **Do not** put Android or networking dependencies in the Domain layer.
2. New features live in their own feature package, split by layer
(`<feature>/domain`, `<feature>/data`, `<feature>/presentation`).
3. Business rules (validation, fallbacks, "what counts as valid") belong in
**use cases**, not in the UI or the data source.
4. The UI observes state from a `ViewModel`; it does not fetch or format data
itself.
5. No DI framework is required yet — wire dependencies by hand (constructors /
a small factory). Introducing Hilt/Dagger is a separate, explicit decision
(write an ADR first).

---

## Strangler-fig migration policy (do not stop feature work)

We do **not** freeze development for a big-bang rewrite. We strangle the legacy
incrementally:

- **New code** is written in the layered architecture from day one.
- **Legacy code** is migrated **when we touch it** — the boy-scout rule: leave
the code better than you found it. Each fix/feature that touches a god class
should peel a small, well-defined slice into the new structure.
- Prefer **small seams**: connect new layered code to the legacy through one or
two narrow call sites rather than rewriting a whole screen at once.
- A specific module **may** be frozen briefly while it is being migrated (to
avoid changes landing on top of a half-done migration). This is different from
a global freeze.
- Before migrating old code, confirm it is in scope for the current task. No
unsolicited mass refactors.

---

## Design map (keep this updated as we advance)

> Update this section whenever a slice is migrated, a layer is added, or a god
> class shrinks. It is the living picture of how far the layering has spread.

**Reference slice (DONE) — rootfs size (`org.iiab.controller.rootfs`)**
First feature built across all three layers; use it as the copy-paste template.

- `domain/` — `Rootfs` (entity), `RootfsTier`, `RootfsAbi`,
`RootfsRepository` (port), `GetRootfsSizeUseCase` (validation + fallback rule).
Pure JVM, unit-tested (`src/test/.../rootfs/domain`, `.../util`).
- `data/` — `RootfsRemoteDataSource` (HTTP read of the `latest_*.meta4`
`<size>`, in-memory cache, returns `-1` on failure), `RootfsCatalog` (URL
building + hardcoded fallback bytes + ABI detection), `RootfsRepositoryImpl`.
- `presentation/` — `RootfsViewModel` + `RootfsUiState` + `RootfsViewModelFactory`.
- `util/ByteFormatter` — shared, pure byte→human formatting.
- **Legacy seam:** `InstallationPlanner.resolveOsSizeGb()` routes the OS size
through the use case (live-then-fallback) instead of the old hardcoded
`OS_*_GB` constants. Migrating `DeployFragment`'s projection UI to consume
`RootfsViewModel` directly is the next strangler step for this area.

**Legacy (NOT yet layered)** — most of `org.iiab.controller` is still flat:
god classes `MainActivity` and `DeployFragment` (~2.7k LOC), shared mutable
state on public/static fields, hand-rolled `HttpURLConnection` calls duplicated
across classes, inline size formatting.

See `ROOTFS_SIZE_PILOT_ANALYSIS.md` (repo root) for the detailed change map and
live-size data behind the reference slice.

---

## Tech-debt watch list (controller) — opportunistic targets

Evident debt noticed while building the pilot. Chip away at these **only when
you are already in the file** (boy-scout), and record progress in the design map:

- **God classes:** `DeployFragment` (~2.7k LOC) and `MainActivity` mix UI, IO,
process control and networking. Extract cohesive slices into feature packages.
- **Shared mutable state:** public/`static` fields used as cross-class state
(e.g. download flags). Prefer encapsulated state in a `ViewModel`.
- **Duplicated networking:** `HttpURLConnection` is reimplemented in
`InstallationPlanner`, `DeployFragment`, `MainActivity`. Consolidate behind
data sources / a small HTTP helper as features migrate.
- **Inline formatting:** byte/size strings are formatted ad hoc in several
places. Route them through `util/ByteFormatter`.
- **Thin tests:** only pure static logic is covered. Every migrated slice must
add JVM unit tests for its domain/use-case layer (no emulator needed).
- **Connectivity gating:** live network calls on the projection path can stall
up to the timeout when offline; prefer an explicit connectivity check before
the live attempt as this path is migrated.
247 changes: 247 additions & 0 deletions ROOTFS_SIZE_PILOT_ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Rootfs-Size Pilot — Analysis & Change Map

> Companion to the refactor context document. This file is the result of the
> "analyze the requested changes" pass and is meant to run **in parallel** with
> the phased refactor (phases 0, 1, 2, 3, K…) without blocking feature work.
>
> Goal of the pilot slice: replace the **hardcoded** rootfs OS sizes with the
> **live** sizes published on the Deploy server, with a safe **fallback** to
> hardcoded values when there is no network access — built through the three
> Clean Architecture layers as the reference implementation.

---

## 1. Live sizes on the Deploy server (ground truth)

Source: the stable `latest_<tier>_<arch>.meta4` Metalink pointers under
`https://iiab.switnet.org/android/rootfs/`. The `.meta4` files carry the exact
size in **bytes** (`<size>` element) and the canonical download `<url>`, and
they do **not** embed the version/hash in the name — so they are the right
target for both the size lookup and the download.

Verified on 2026-06-17:

| Tier | ABI `arm64-v8a` | ABI `armeabi-v7a` |
|------|-----------------|-------------------|
| `basic` | 1,219,422,532 B — 1.14 GiB / 1.22 GB | 1,220,401,364 B — 1.14 GiB / 1.22 GB |
| `standard` | 1,428,970,336 B — 1.33 GiB / 1.43 GB | 1,429,892,132 B — 1.33 GiB / 1.43 GB |
| `full` | 2,926,676,923 B — 2.73 GiB / 2.93 GB | 2,917,715,443 B — 2.72 GiB / 2.92 GB |

> Latest builds at verification time: `iiab-oa_2026.158_*_arm64-v8a` and
> `iiab-oa_2026.159_*_armeabi-v7a`. Sizes drift between builds — that is exactly
> why they must be fetched live rather than hardcoded.

### Naming note: "medium" → `standard`

The reference document uses the label **medium**, but the server (and the
existing app code) uses **`standard`**. The pilot keeps the enum
`Tier.{BASIC, STANDARD, FULL}` and maps the user-facing "medium" wording to
`standard` so it matches the real Deploy artifacts.

---

## 2. Current state: where the hardcoded sizes live

The values are hardcoded in **`InstallationPlanner.java`** (lines 25–27):

```java
private static final double OS_BASIC_GB = 1.0;
private static final double OS_STANDARD_GB = 1.2;
private static final double OS_FULL_GB = 2.7;
```

They flow through `calculateProjectedSize(...)` (a `switch (tier)`,
lines 152–162) into `StorageProjection.osSize`, and are rendered in
**`DeployFragment.java`** `recalculateProjection()`:

```java
double pOs = (selectedTier == null) ? 0.0 : projection.osSize; // line 716
txtLegendIiab.setText(String.format(Locale.US, "%.1fG", pOs)); // line 729
// also folded into the gauge total at line 811
```

### Accuracy gap (why this matters)

The displayed values are stale and round-tripped through a single decimal:

| Tier | Shown today | Live (GiB) | Off by |
|------|-------------|------------|--------|
| basic | `1.0G` | 1.14 | ~0.14 |
| standard | `1.2G` | 1.33 | ~0.13 |
| full | `2.7G` | 2.73 | ~0.03 |

`basic` and `standard` are understated by ~12–14%.

### The server URL is already built in the app

`DeployFragment.java` (lines 983–988) already constructs exactly the URL the
data source needs, for the download flow:

```java
String arch = getTermuxArch(); // line 983
String archSuffix = (arch.contains("arm") && !arch.contains("64"))
? "armeabi-v7a" : "arm64-v8a"; // line 984
Tier safeTier = (selectedTier != null) ? selectedTier : Tier.BASIC;
String tierString = safeTier.name().toLowerCase(Locale.US);
String directUrl = "https://iiab.switnet.org/android/rootfs/latest_"
+ tierString + "_" + archSuffix + ".meta4"; // line 987
```

Arch detection lives in `getTermuxArch()` (lines 2645–2662; falls back to
`Build.SUPPORTED_ABIS[0]`). The RemoteDataSource should reuse this exact
URL convention and arch mapping for **both** ABIs.

### Reusable networking entry points (no new HTTP library needed)

- `InstallationPlanner.getOrFetchCatalog` (lines 74–147): the canonical
`HttpURLConnection` + 8 s timeout + disk cache + `Handler(mainLooper)` pattern.
- `DeployFragment.pingUrl` / `checkInternetAccess` (lines 2619 / 2674) and the
existing `HEAD` requests at lines 2627 / 2683 — already do
`conn.setRequestMethod("HEAD")`; ideal template for `Content-Length`.
- `Aria2Manager` (`--follow-metalink=mem`) already consumes the `.meta4` URL for
the actual download.

---

## 3. Project / architecture readiness

| Item | Value |
|------|-------|
| Language | **Pure Java** — no Kotlin plugin, no `.kt` files in our modules |
| AGP / Gradle | 8.4.1 / 8.8 |
| Java | 17 |
| compileSdk / minSdk / targetSdk | 34 / 24 / **28** (intentional, required for proot W^X) |
| Layering | **None** — flat package `org.iiab.controller`, no ViewModel / Repository / UseCase / DI |
| HTTP client | None — hand-rolled `HttpURLConnection` (no OkHttp/Retrofit) |
| Lifecycle (ViewModel/LiveData) | **Not present** |
| Byte-formatting helper | **Missing** — formatting is inlined ad hoc |
| Tests | JUnit 4 + `unitTests.returnDefaultValues = true`; two JVM-only tests of pure static methods; `androidTest` empty; no Mockito |

**Implication:** this is a *greenfield* slice — it establishes the layering
pattern rather than refactoring an existing one. That is precisely the intended
role of the pilot.

### Recommendation: write the pilot in **Java**

Match the existing codebase. Introducing Kotlin would add the Kotlin plugin +
stdlib (+ coroutines for idiomatic value) and conflate a *language migration*
with an *architecture pilot*. Clean Architecture is fully expressible in Java 17,
keeps reviewers focused on the layer boundaries, and lets the all-Java team adopt
the pattern with no language ramp. A Kotlin migration, if desired, should be its
own explicit ADR later (it interops cleanly with this slice).

---

## 4. Proposed pilot slice (layer mapping → real code)

New sub-package under `controller/app/src/main/java/org/iiab/controller/rootfs/`:

```
org.iiab.controller.rootfs
├── domain (pure JVM — NO Android, NO HTTP imports)
│ ├── Rootfs.java // entity: tier, abi, url, sizeBytes
│ ├── RootfsRepository.java // port (interface) the domain owns
│ └── GetRootfsSizeUseCase.java // business rules: reject 0 / negative / absurd
├── data (implementation details)
│ ├── RootfsCatalog.java // tier+abi -> latest_*.meta4 URL + fallback bytes
│ ├── RootfsRemoteDataSource.java// HttpURLConnection: HEAD Content-Length OR meta4 <size>
│ └── RootfsRepositoryImpl.java // implements RootfsRepository; live-then-fallback; DTO->entity
└── presentation
└── RootfsViewModel.java // exposes Loading / Success / Error state

org.iiab.controller.util
└── ByteFormatter.java // long bytes -> "1.3 GiB" (pure, unit-testable)
```

Dependency direction (enforced): `presentation → domain ← data`. `domain` imports
nothing from Android or `java.net`, so `GetRootfsSizeUseCase` and `Rootfs` are
unit-testable on the JVM with the existing JUnit setup (mirrors
`SystemStatsUtilTest`).

### Layer responsibilities

**Data**
- `RootfsRemoteDataSource`: build `latest_<tier>_<abi>.meta4`; preferred path is
a `HEAD` on the resolved `.tar.gz` reading `getContentLengthLong()`, or read the
`.meta4` and parse `<size>(\d+)</size>` (exact bytes — avoids hardcoding the
version in the URL). 8 s timeouts, background thread.
- `RootfsCatalog`: single source for the URL convention **and** the hardcoded
fallback byte values (see §5). Detects ABI via `Build.SUPPORTED_ABIS` /
`getTermuxArch()` and selects `arm64-v8a` vs `armeabi-v7a`.
- `RootfsRepositoryImpl`: try live, on any failure/offline return fallback;
map raw bytes (DTO) → `Rootfs` entity.

**Domain**
- `GetRootfsSizeUseCase`: validation rules — reject `0`, negative, or absurd
sizes (e.g. < 100 MB or > 10 GB), in which case the fallback is authoritative.
- `RootfsRepository`: `Rootfs getSize(Tier tier, Abi abi)` contract; no leakage
of HTTP/Android types.

**Presentation**
- `RootfsViewModel`: calls the use case off the main thread; emits
`Loading → Success(Rootfs) | Error(fallback)`; formats via `ByteFormatter`.

---

## 5. Fallback constants (ready to use)

Replace the three `double … _GB` constants with per-ABI **byte** fallbacks
seeded from today's live values. Bytes (not rounded GB) keep the fallback exact
and let the UI choose GiB vs GB formatting consistently.

```java
// Fallback sizes in BYTES, captured from latest_*.meta4 on 2026-06-17.
// Used only when the live HEAD/meta4 lookup fails (offline or server error).
// arm64-v8a
static final long FALLBACK_BASIC_ARM64 = 1_219_422_532L; // 1.14 GiB
static final long FALLBACK_STANDARD_ARM64 = 1_428_970_336L; // 1.33 GiB
static final long FALLBACK_FULL_ARM64 = 2_926_676_923L; // 2.73 GiB
// armeabi-v7a
static final long FALLBACK_BASIC_ARMV7 = 1_220_401_364L; // 1.14 GiB
static final long FALLBACK_STANDARD_ARMV7 = 1_429_892_132L; // 1.33 GiB
static final long FALLBACK_FULL_ARMV7 = 2_917_715_443L; // 2.72 GiB
```

> Display note: the rest of the app formats storage with binary units
> (`G` = GiB, dividing by 1024³). Convert bytes → GiB for the legend
> (`txtLegendIiab`) and gauge so the new value is consistent with free-space math.

---

## 6. Ordered change map (when implementing)

1. **`build.gradle` (app):** add `androidx.lifecycle:lifecycle-viewmodel:2.8.7`
and `:lifecycle-livedata:2.8.7` (Java artifacts, no Kotlin). Optional:
`testImplementation 'org.mockito:mockito-core:5.12.0'`. No HTTP lib needed.
2. **Create the `rootfs/` package** with the classes in §4. Domain first
(pure, test it), then data, then presentation.
3. **`ByteFormatter`** util + JVM unit test (matches `SystemStatsUtilTest` style).
4. **Wire into `InstallationPlanner`:** demote `OS_*_GB` to fallback; have
`calculateProjectedSize(...)` (already on its own `Thread`, line 150) obtain
the OS size from the repository (live-then-fallback) using the tier + ABI.
Pass `archSuffix` from the two call sites (`DeployFragment` lines 706 / 1032),
which already compute it.
5. **Display:** no markup change required — once `projection.osSize` is live,
`DeployFragment` line 729 (`txtLegendIiab`) and the gauge total (line 811)
update automatically. Optional `(live)` / `(offline)` indicator only if wanted.
6. **Gate offline:** consult `checkInternetAccess()` (line 2674) to skip the live
call and go straight to fallback without a timeout wait.
7. **Validate against the real Deploy server** for both ABIs and offline mode.

### Tests
- Domain: `GetRootfsSizeUseCaseTest` with a fake `RootfsRepository` — accepts
valid sizes, rejects 0 / negative / absurd, returns fallback on repo failure.
- Util: `ByteFormatterTest` — boundaries (B/KiB/MiB/GiB), rounding.
- Data: `RootfsRepositoryImplTest` — live success maps DTO→entity; failure →
fallback bytes per ABI.

---

## 7. Parallelism with the phased refactor

This slice is self-contained inside the new `rootfs/` package and touches the
legacy God classes only at two seams (`InstallationPlanner` OS-size source and
the two `DeployFragment` call sites). It can proceed alongside phases 0/1/2/3/K
without freezing other features — consistent with the strangler-fig strategy and
the boy-scout rule. It also doubles as the reference implementation future slices
copy.
Loading
Loading