From 6c533e2eb601ee4ac55a77ba4c2b75801736f6ac Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Wed, 17 Jun 2026 23:41:25 -0600 Subject: [PATCH] fix(controller): show real device arch on the dashboard The dashboard "device architecture" field reported the app's own ABI (derived from nativeLibraryDir), so a 32-bit build running on a 64-bit device wrongly displayed 32-bit. We intentionally install the 32-bit app on 64-bit hardware to test the 32-bit path, so the device panel must report the hardware truth, not the app's ABI. Add a small layered "deviceinfo" slice (refactor-by-feature): - domain: DeviceAbiProvider port + GetDeviceArchUseCase (prefer the device's primary 64-bit ABI, else 32-bit, else generic). Pure JVM, unit-tested incl. the 32-bit-app-on-64-bit-device case. - data: BuildDeviceAbiProvider reads device-level Build.SUPPORTED_*_ABIS (populated from device properties, so correct even in a 32-bit process). - DashboardFragment uses the use case for the device panel; getTermuxArch() (app/content ABI) is unchanged for modules, termux and debian arch. Docs: CLAUDE.md design map + controller/docs/TECH_DEBT_PLAN.md progress log. --- CLAUDE.md | 13 ++++ .../iiab/controller/DashboardFragment.java | 12 +++- .../data/BuildDeviceAbiProvider.java | 32 ++++++++++ .../deviceinfo/domain/DeviceAbiProvider.java | 23 +++++++ .../domain/GetDeviceArchUseCase.java | 55 ++++++++++++++++ .../domain/GetDeviceArchUseCaseTest.java | 63 +++++++++++++++++++ controller/docs/TECH_DEBT_PLAN.md | 14 +++++ 7 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 controller/app/src/main/java/org/iiab/controller/deviceinfo/data/BuildDeviceAbiProvider.java create mode 100644 controller/app/src/main/java/org/iiab/controller/deviceinfo/domain/DeviceAbiProvider.java create mode 100644 controller/app/src/main/java/org/iiab/controller/deviceinfo/domain/GetDeviceArchUseCase.java create mode 100644 controller/app/src/test/java/org/iiab/controller/deviceinfo/domain/GetDeviceArchUseCaseTest.java diff --git a/CLAUDE.md b/CLAUDE.md index 4efa391..bc818cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,6 +82,19 @@ First feature built across all three layers; use it as the copy-paste template. `OS_*_GB` constants. Migrating `DeployFragment`'s projection UI to consume `RootfsViewModel` directly is the next strangler step for this area. +**Slice (DONE) — device architecture (`org.iiab.controller.deviceinfo`)** +Carved out while fixing the dashboard "device architecture" field (it showed the +app's ABI, so a 32-bit app on a 64-bit phone reported 32-bit). + +- `domain/` — `DeviceAbiProvider` (port) + `GetDeviceArchUseCase` (rule: + prefer the device's primary 64-bit ABI, else 32-bit, else generic). Pure JVM, + unit-tested (`GetDeviceArchUseCaseTest`). +- `data/` — `BuildDeviceAbiProvider` reads `Build.SUPPORTED_*_ABIS` (device-level, + not the app process), so it reports the real hardware arch. +- **Legacy seam:** `DashboardFragment` resolves the device-panel arch through this + use case; `getTermuxArch()` (app/content ABI) is unchanged for modules, termux + and debian arch. + **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 diff --git a/controller/app/src/main/java/org/iiab/controller/DashboardFragment.java b/controller/app/src/main/java/org/iiab/controller/DashboardFragment.java index 980bf5c..17fa200 100644 --- a/controller/app/src/main/java/org/iiab/controller/DashboardFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/DashboardFragment.java @@ -27,6 +27,9 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import org.iiab.controller.deviceinfo.data.BuildDeviceAbiProvider; +import org.iiab.controller.deviceinfo.domain.GetDeviceArchUseCase; + import java.io.BufferedReader; import java.io.File; import java.io.FileReader; @@ -220,9 +223,14 @@ private void updateSystemStats() { int sdkVersion = android.os.Build.VERSION.SDK_INT; txtAndroidVersion.setText(getString(R.string.dash_android_version_value, "v" + androidRelease, String.valueOf(sdkVersion))); - // --- FETCH AND DISPLAY HOST ARCHITECTURE --- + // --- FETCH AND DISPLAY HOST (DEVICE) ARCHITECTURE --- + // This must be the REAL device arch, not the app's ABI: a 32-bit app can + // run on a 64-bit device (used for testing the 32-bit path), and the + // device panel must still report 64-bit. App/content arch keeps using + // getTermuxArch() elsewhere (modules, termux, debian). if (txtHostArch != null) { - txtHostArch.setText(getTermuxArch()); + String deviceArch = new GetDeviceArchUseCase(new BuildDeviceAbiProvider()).execute(); + txtHostArch.setText(deviceArch); } // --- CALCULATE SERVER UPTIME --- diff --git a/controller/app/src/main/java/org/iiab/controller/deviceinfo/data/BuildDeviceAbiProvider.java b/controller/app/src/main/java/org/iiab/controller/deviceinfo/data/BuildDeviceAbiProvider.java new file mode 100644 index 0000000..affe0d2 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/deviceinfo/data/BuildDeviceAbiProvider.java @@ -0,0 +1,32 @@ +package org.iiab.controller.deviceinfo.data; + +import android.os.Build; + +import org.iiab.controller.deviceinfo.domain.DeviceAbiProvider; + +/** + * Data-layer {@link DeviceAbiProvider} backed by {@link android.os.Build}. + * + *

{@code Build.SUPPORTED_*_ABIS} are populated from device system properties + * (the {@code ro.product.cpu.abilist*} family) at runtime init, so they reflect + * the hardware's capabilities even when the current process is 32-bit. That is + * exactly what lets us report the real device architecture for a 32-bit app + * running on a 64-bit device. + */ +public final class BuildDeviceAbiProvider implements DeviceAbiProvider { + + @Override + public String[] supported64BitAbis() { + return Build.SUPPORTED_64_BIT_ABIS; + } + + @Override + public String[] supported32BitAbis() { + return Build.SUPPORTED_32_BIT_ABIS; + } + + @Override + public String[] allSupportedAbis() { + return Build.SUPPORTED_ABIS; + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/deviceinfo/domain/DeviceAbiProvider.java b/controller/app/src/main/java/org/iiab/controller/deviceinfo/domain/DeviceAbiProvider.java new file mode 100644 index 0000000..2e96f67 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/deviceinfo/domain/DeviceAbiProvider.java @@ -0,0 +1,23 @@ +package org.iiab.controller.deviceinfo.domain; + +/** + * Domain port exposing the ABIs the physical device supports. + * + *

These lists are device-level (what the hardware/OS can run), independent of + * the current app's own ABI. The Data layer implements this by reading + * {@code android.os.Build}; the domain never touches Android types. + * + *

Each method returns the ABI identifiers (e.g. {@code "arm64-v8a"}, + * {@code "armeabi-v7a"}) most-preferred first, or an empty array if none. + */ +public interface DeviceAbiProvider { + + /** 64-bit ABIs the device supports (empty on a 32-bit-only device). */ + String[] supported64BitAbis(); + + /** 32-bit ABIs the device supports. */ + String[] supported32BitAbis(); + + /** All ABIs the device supports, most-preferred first. */ + String[] allSupportedAbis(); +} diff --git a/controller/app/src/main/java/org/iiab/controller/deviceinfo/domain/GetDeviceArchUseCase.java b/controller/app/src/main/java/org/iiab/controller/deviceinfo/domain/GetDeviceArchUseCase.java new file mode 100644 index 0000000..81ba1ea --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/deviceinfo/domain/GetDeviceArchUseCase.java @@ -0,0 +1,55 @@ +package org.iiab.controller.deviceinfo.domain; + +/** + * Resolves the real device CPU architecture, independent of the running + * app's own ABI. + * + *

Why this exists: a 32-bit build of the app can be installed on a 64-bit + * device (a common way to test the 32-bit path without 32-bit hardware). In that + * case the app's own ABI is 32-bit, but the device is still 64-bit. Device-facing + * UI (the dashboard "device architecture") must report the hardware truth, so we + * derive it from the device's supported-ABI lists rather than the app's + * {@code nativeLibraryDir}. + * + *

Rule: prefer the primary 64-bit ABI when the device supports any; otherwise + * the primary 32-bit ABI; otherwise the first of the generic list. Pure domain + * logic — no Android dependencies, fully unit-testable on the JVM. + */ +public final class GetDeviceArchUseCase { + + private final DeviceAbiProvider provider; + + public GetDeviceArchUseCase(DeviceAbiProvider provider) { + this.provider = provider; + } + + /** Returns the device's primary ABI (e.g. {@code "arm64-v8a"}), or {@code "unknown"}. */ + public String execute() { + String abi = first(provider.supported64BitAbis()); + if (abi != null) { + return abi; + } + abi = first(provider.supported32BitAbis()); + if (abi != null) { + return abi; + } + abi = first(provider.allSupportedAbis()); + if (abi != null) { + return abi; + } + return "unknown"; + } + + /** First non-empty entry of an ABI array, or {@code null}. */ + private static String first(String[] abis) { + if (abis == null) { + return null; + } + for (String abi : abis) { + if (abi != null && !abi.isEmpty()) { + return abi; + } + } + return null; + } +} diff --git a/controller/app/src/test/java/org/iiab/controller/deviceinfo/domain/GetDeviceArchUseCaseTest.java b/controller/app/src/test/java/org/iiab/controller/deviceinfo/domain/GetDeviceArchUseCaseTest.java new file mode 100644 index 0000000..21d68dc --- /dev/null +++ b/controller/app/src/test/java/org/iiab/controller/deviceinfo/domain/GetDeviceArchUseCaseTest.java @@ -0,0 +1,63 @@ +package org.iiab.controller.deviceinfo.domain; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Pure-JVM tests for the device-arch rule. No Android — uses a fake provider. + */ +public class GetDeviceArchUseCaseTest { + + private static final class FakeProvider implements DeviceAbiProvider { + private final String[] a64; + private final String[] a32; + private final String[] all; + FakeProvider(String[] a64, String[] a32, String[] all) { + this.a64 = a64; + this.a32 = a32; + this.all = all; + } + @Override public String[] supported64BitAbis() { return a64; } + @Override public String[] supported32BitAbis() { return a32; } + @Override public String[] allSupportedAbis() { return all; } + } + + private static String run(String[] a64, String[] a32, String[] all) { + return new GetDeviceArchUseCase(new FakeProvider(a64, a32, all)).execute(); + } + + @Test + public void reports64BitDeviceEvenWhenAppIs32Bit() { + // The bug scenario: a 32-bit app on a 64-bit device. The device IS arm64, + // so the device panel must report arm64-v8a, not the app's 32-bit ABI. + assertEquals("arm64-v8a", run( + new String[]{"arm64-v8a"}, + new String[]{"armeabi-v7a", "armeabi"}, + new String[]{"arm64-v8a", "armeabi-v7a", "armeabi"})); + } + + @Test + public void reports32BitOnlyDevice() { + assertEquals("armeabi-v7a", run( + new String[]{}, + new String[]{"armeabi-v7a", "armeabi"}, + new String[]{"armeabi-v7a", "armeabi"})); + } + + @Test + public void fallsBackToGenericList() { + assertEquals("x86", run(new String[]{}, new String[]{}, new String[]{"x86"})); + } + + @Test + public void unknownWhenNothingReported() { + assertEquals("unknown", run(new String[]{}, new String[]{}, new String[]{})); + assertEquals("unknown", run(null, null, null)); + } + + @Test + public void skipsEmptyLeadingEntries() { + assertEquals("arm64-v8a", run(new String[]{"", "arm64-v8a"}, null, null)); + } +} diff --git a/controller/docs/TECH_DEBT_PLAN.md b/controller/docs/TECH_DEBT_PLAN.md index 3c5b1a3..20ee4c0 100644 --- a/controller/docs/TECH_DEBT_PLAN.md +++ b/controller/docs/TECH_DEBT_PLAN.md @@ -19,6 +19,20 @@ _Last updated: 2026-06-16. Tracks remediation work against the findings below. I - **K5**: unit test validating the layout grid (`IIABExtraKeysTest`). DONE. - Remaining: point the submodule to `appdevforall/termux-app` at clean upstream and commit the pointer. +<<<<<<< Updated upstream +======= +**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`. +- Replaced the hardcoded `OS_*_GB` constants with live sizes from the Deploy server (`latest_*.meta4`), with per-ABI byte fallbacks for offline/error. Domain is pure JVM and unit-tested (`GetRootfsSizeUseCaseTest`, `ByteFormatterTest`). +- **Strangler step (DONE):** `DeployFragment`'s projection UI now consumes `RootfsViewModel` directly (observes live/fallback OS size) instead of going through `InstallationPlanner.resolveOsSizeGb()`. `InstallationPlanner.calculateProjectedSize` gained a `osSizeGb` overload so the OS-size resolution leaves the planner; the legacy overload (still resolving internally) is retained only for the non-UI install flow. +- **Offline UX (DONE):** addresses the "connectivity gating" watch item. `checkInternetAccess()` now stores a `hasInternet` flag; `updateDynamicButtons()` disables the install button (label "No connection") and the click listener shows a snackbar instead of starting a doomed download; an "Estimated sizes (offline)" caption shows whenever the size is a fallback (`RootfsUiState.live == false`); and offline we skip the live fetch (new `attemptLive` flag on the use case / ViewModel) to avoid the ~6 s timeout. The gauge itself was intentionally left untouched. +- Remaining (optional): later remove the legacy `resolveOsSizeGb` path once the install flow no longer needs it. + +**Slice — Device architecture (`org.iiab.controller.deviceinfo`): DONE** (refactor-by-feature while fixing a dashboard bug) +- Bug: the dashboard "device architecture" field reported the *app's* ABI (via `nativeLibraryDir`), so a 32-bit build on a 64-bit device wrongly showed 32-bit (we install the 32-bit app on 64-bit hardware to test the 32-bit path). +- Fix: new layered slice — `domain` (`DeviceAbiProvider` port + `GetDeviceArchUseCase`, prefer-64-bit rule, pure JVM) and `data` (`BuildDeviceAbiProvider` reading device-level `Build.SUPPORTED_*_ABIS`). `DashboardFragment` now shows the real device arch; `getTermuxArch()` stays for app/content arch (modules, termux, debian). Unit test `GetDeviceArchUseCaseTest` covers the 32-bit-app-on-64-bit-device case. + +>>>>>>> Stashed changes **Phases 1–4: NOT STARTED.** Next: Phase 1 security — **D2**, **D6**, **S1**, **S3**, **M4**, **D12**. ## 1. Executive summary