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
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ First feature built across all three layers; use it as the copy-paste template.
(which only needs the resolved companion-data filename). Removing it once the
install flow stops depending on it is a future strangler step.

**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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ---
Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>{@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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.iiab.controller.deviceinfo.domain;

/**
* Domain port exposing the ABIs the physical device supports.
*
* <p>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.
*
* <p>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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.iiab.controller.deviceinfo.domain;

/**
* Resolves the <em>real device</em> CPU architecture, independent of the running
* app's own ABI.
*
* <p>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}.
*
* <p>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;
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
4 changes: 4 additions & 0 deletions controller/docs/TECH_DEBT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ _Last updated: 2026-06-17. Tracks remediation work against the findings below. I
- **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.

**Phases 1–4: NOT STARTED.** Next: Phase 1 security — **D2**, **D6**, **S1**, **S3**, **M4**, **D12**.

## 1. Executive summary
Expand Down
Loading