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
9 changes: 6 additions & 3 deletions controller/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,17 @@ def hasGoogleServices = file("google-services.json").exists()
android {
// 2. Identity
namespace 'org.appdevforall.k2go'
compileSdk 34
compileSdk 35
ndkVersion "26.3.11579264"

defaultConfig {
applicationId "org.appdevforall.k2go"
minSdk 24
// VITAL FOR PROOT AND W^X! Do not go up unless strictly necessary
targetSdk 28
// targetSdk 35 for Play. proot is NOT blocked above 28: AOSP neverallows only
// execute_no_trans (direct execve) on app_data_file; K2Go runs guest binaries
// through the proot loader (mmap/execute), which AOSP allows on certified devices.
// Verified on-device at 35. See K2GO-438 and controller/docs/ARCHITECTURE.md.
targetSdk 35

// 3. Version Control (OTA)
// NOTE: With splits enabled, this number will be multiplied by 10 (e.g. 50 -> 500, 501, 502)
Expand Down
2 changes: 2 additions & 0 deletions controller/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- ADFA-4520: LocalOnlyHotspot requires location at runtime on this targetSdk -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- K2GO-438: Android 12+ lets the user grant COARSE only, so declare it with FINE (CoarseFineLocation). -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
Expand Down Expand Up @@ -143,11 +142,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
public void onStart() {
super.onStart();
IntentFilter f = new IntentFilter(DashboardRebuildService.ACTION_STATE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireContext().registerReceiver(rebuildState, f, Context.RECEIVER_NOT_EXPORTED);
} else {
requireContext().registerReceiver(rebuildState, f);
}
ContextCompat.registerReceiver(requireContext(), rebuildState, f,
ContextCompat.RECEIVER_NOT_EXPORTED);
resolveInitialUpdatingState();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import org.appdevforall.k2go.ui.dialog.BrandDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.lifecycle.ViewModelProvider;

Expand Down Expand Up @@ -79,11 +80,8 @@ public UpdateController(AppCompatActivity activity) {

public void registerDownloadReceiver() {
IntentFilter filter = new IntentFilter(android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.registerReceiver(downloadReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
activity.registerReceiver(downloadReceiver, filter);
}
ContextCompat.registerReceiver(activity, downloadReceiver, filter,
ContextCompat.RECEIVER_NOT_EXPORTED);
}

public void unregisterDownloadReceiver() {
Expand Down
2 changes: 1 addition & 1 deletion controller/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ buildscript {
*/

dependencies {
classpath 'com.android.tools.build:gradle:8.4.1'
classpath 'com.android.tools.build:gradle:8.8.0'
// ADFA-4466 Phase 1: Firebase (Google Services) Gradle plugin.
classpath 'com.google.gms:google-services:4.4.2'
}
Expand Down
2 changes: 1 addition & 1 deletion controller/docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ This shape is *why* every new feature deepens the debt: there is no seam to add

Some "smells" are load-bearing and must not be naively removed:

- **`targetSdk 28`** is intentional — it exempts the app from Android 10–14 runtime enforcement so proot's W^X memory model works. Raising it (required for Play Store) is a *project*, not a cleanup (see `TECH_DEBT_PLAN.md` §Phase 4).
- **`targetSdk 28`** was believed load-bearing for proot (W^X). That is wrong: proot is NOT blocked at targetSdk 29+. AOSP neverallows only `execute_no_trans` (direct execve) on `app_data_file`; K2Go runs binaries through the proot loader (`mmap`/`execute`), which AOSP allows on every certified device. Verified on-device at targetSdk 35 (Samsung A16 stock Android 16, OnePlus 7T LineageOS 15). It is being raised to 35 for Play (K2GO-438) plus the API-33/34/35 fixes (K2GO-439).
- **`usesCleartextTraffic="true"`** is currently required for the local rsync/APK/HTTP servers. The goal is to *scope* it via a network-security-config, not to flip it off.
- **`MANAGE_EXTERNAL_STORAGE`** and **`PURPOSE`-broad keystore keys** exist for real reasons but are over-broad; tighten, don't delete.
- The build's **SHA256 audit of native binaries at build time** is a genuine strength — preserve it.
2 changes: 1 addition & 1 deletion controller/docs/TECH_DEBT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ _Last updated: 2026-09-25. Tracks remediation work against the findings below. I

> **Current status (2026-09-25): post-redesign snapshot.** The redesign retired the original god classes: `MainActivity` and `DeployFragment` no longer exist, so the `F1` / `D1` targets and the older entries that name them are historical records, not the current tree. The current god-tier classes are `install/presentation/InstallService` (~2.1k LOC), `redesign/SetupProgressActivity` (~1.6k) and `redesign/CloneFragment` (~1.6k); the root `CLAUDE.md` design map holds the full list, the second tier, and the shared debt (public/static state, hand-rolled `HttpURLConnection` across ~27 classes, inline size formatting). The server-lifecycle redesign (`ADR-5343`) and the operation-model work shipped: 31 of 34 tracked tickets are done. The live board is `controller/docs/operation-model-roadmap.svg` and the lifecycle state map is `controller/docs/state-spine.svg`; open work is `K2GO-4` (download contract, last leg `K2GO-255`) and `K2GO-235` (module removal). Project keys moved ADFA -> K2GO on 1 Sep 2026: the `ADFA-XXXX` references in the dated logs below are historical and are kept as-is (Jira redirects each to its current `K2GO-XXXX`).

> **Current status (2026-07-15): register essentially closed at code level; v0.4.x cleanup wrapped.** Corrects stale entries below. **F1 (`MainActivity` God class) — DONE:** carved from ~2,384 to ~900 LOC via a Controller/Host seam (`TerminalController`, `ServerController`, an update controller); the ~727-LOC `addNewTerminalSession` is gone. **D3 — DONE (ADFA-4713):** the box base URL is now a single `config/BoxEndpoints.BASE`; ~10 hardcoded sites reference it (JVM-tested, no behavior change). **S18 — DONE (ADFA-4714):** cleartext scoped via `res/xml/network_security_config.xml` to loopback only (localhost:8085, 127.0.0.1:8114) and denied elsewhere; instrumentation test `NetworkSecurityConfigTest` passed on-device. **S13 — DONE (ADFA-4717):** removed the dead `TermuxCallbackReceiver` + its uncalled feeder chain (`performHeartbeat`/`sendStimulus`/`performDebugPing`) — legacy v1 external-`com.termux` IPC, superseded by the bundled termux-core — plus 7 now-orphaned strings across 34 locales. **D2 follow-up — DONE (ADFA-4718):** single-quoted the extract `xz|tar` pipe paths (the backup pipe was already quoted under D11; the bootstrap command is a static string). **D17 — reviewed, no further work:** `ApkServer` is GET-only and serves one public file (the app's own APK) over the LAN only while the user shares; auth/HTTPS not warranted (friction + self-signed warnings for no real gain). **M9 (`targetSdk 28`) — WON'T MOVE, by design:** load-bearing for PRoot + W^X (proot executes the rootfs guest binaries from writable storage, which targetSdk 29+ forbids); documented at the `targetSdk` line in `build.gradle`. The "blocks Play Store" rationale is moot — K2Go is sideloaded, never Play-distributed. **`minifyEnabled false` — reviewed, kept off:** low value for an offline/sideloaded app; enabling R8 would need ProGuard rules + on-device testing, so it gets its own ticket if ever wanted. **S11 — reviewed, acceptable residual:** the rsync secret is written plaintext to disk only while sharing, now owner-only (`SecretStore`) and deleted on stop; plaintext-at-rest is inherent to rsyncd. **ADFA-4476 — DONE** (was listed open below). **Remaining (verification / docs, no app code):** on-device 32- and 64-bit backup round-trip; the arbitrary-file attack-vector analysis; a `ROOTFS_MANIFEST.md` addendum (origin / algo:none); and the VPN dead-code removal (its own cleanup review). With the code-level register closed, the v0.4.x docs umbrella (ADFA-4452) can rotate/close.
> **Current status (2026-07-15): register essentially closed at code level; v0.4.x cleanup wrapped.** Corrects stale entries below. **F1 (`MainActivity` God class) — DONE:** carved from ~2,384 to ~900 LOC via a Controller/Host seam (`TerminalController`, `ServerController`, an update controller); the ~727-LOC `addNewTerminalSession` is gone. **D3 — DONE (ADFA-4713):** the box base URL is now a single `config/BoxEndpoints.BASE`; ~10 hardcoded sites reference it (JVM-tested, no behavior change). **S18 — DONE (ADFA-4714):** cleartext scoped via `res/xml/network_security_config.xml` to loopback only (localhost:8085, 127.0.0.1:8114) and denied elsewhere; instrumentation test `NetworkSecurityConfigTest` passed on-device. **S13 — DONE (ADFA-4717):** removed the dead `TermuxCallbackReceiver` + its uncalled feeder chain (`performHeartbeat`/`sendStimulus`/`performDebugPing`) — legacy v1 external-`com.termux` IPC, superseded by the bundled termux-core — plus 7 now-orphaned strings across 34 locales. **D2 follow-up — DONE (ADFA-4718):** single-quoted the extract `xz|tar` pipe paths (the backup pipe was already quoted under D11; the bootstrap command is a static string). **D17 — reviewed, no further work:** `ApkServer` is GET-only and serves one public file (the app's own APK) over the LAN only while the user shares; auth/HTTPS not warranted (friction + self-signed warnings for no real gain). **M9 (`targetSdk 28`): reopened for Play (K2GO-438).** The earlier WON'T-MOVE rationale was wrong: proot is NOT blocked at targetSdk 29+. AOSP neverallows only `execute_no_trans` (direct execve) on `app_data_file`; K2Go runs binaries through the proot loader (`mmap`/`execute`), which AOSP allows on every certified device. Verified on-device at targetSdk 35 (Samsung A16 stock Android 16, OnePlus 7T LineageOS 15: box boots, 0 exec denials). Raising targetSdk breaks API-33/34/35 behaviors (NEARBY_WIFI_DEVICES, edge-to-edge, typed FGS) that are fixable (K2GO-439). **`minifyEnabled false` — reviewed, kept off:** low value for an offline/sideloaded app; enabling R8 would need ProGuard rules + on-device testing, so it gets its own ticket if ever wanted. **S11 — reviewed, acceptable residual:** the rsync secret is written plaintext to disk only while sharing, now owner-only (`SecretStore`) and deleted on stop; plaintext-at-rest is inherent to rsyncd. **ADFA-4476 — DONE** (was listed open below). **Remaining (verification / docs, no app code):** on-device 32- and 64-bit backup round-trip; the arbitrary-file attack-vector analysis; a `ROOTFS_MANIFEST.md` addendum (origin / algo:none); and the VPN dead-code removal (its own cleanup review). With the code-level register closed, the v0.4.x docs umbrella (ADFA-4452) can rotate/close.

> **Current status (2026-06-30):** **Phase 3 — Share tab carved: `S14` DONE (epic ADFA-1028, umbrella ADFA-4492).** `SyncFragment`'s mechanic was extracted into a clean, exportable stack: a pure `sync/domain` (rsyncd.conf/argv builders, progress parse, exit-code→outcome, `ShareConfig`, `TransferGuard`, `SyncCredentialValidator` — no `android.*`, JVM-tested), a `TransportEngine` port with `RsyncManager` as its adapter, platform adapters (`NetworkInterfaces`/`QrCodec`/`SecretStore`), and an Activity-scoped `SyncStateViewModel` + `SyncProgressRepository` so the probe/dry-run/transfer survive a configuration-change recreation (PRs #93/#94/#96/#98/#99/#104). Folded in and closed here: `S7` (hardcoded ports → `ShareConfig`), `S8`/`S9`/`S10` (concurrency/lifecycle), `S11` (rsync secret lifecycle), `S15`/`S16` (theming / magic-strings), the `EX1–EX6` export items, and **`D17`** (`ApkServer` now GET-only). **Residual — DONE (ADFA-4506):** the `SyncFragment` view was carved into per-area collaborators via the Controller/Host seam — `ArchCheckController` (#126), `ShareController` (rsync daemon + APK server together, #127) and `ReceiveController` (scan/probe/dry-run/transfer, #128) — leaving a thin ~261 LOC view (from ~810) that only wires the mode toggle, the QR scanner, system-protection and the hosts. No separate `SyncViewModel` was needed beyond the already-carved `SyncStateViewModel`/`SyncProgressRepository`. **Separately, real-world Share robustness (bug ADFA-4496, closed):** Wi-Fi network binding for the receive (#105), IP-under-QR transparency, a phantom-process SIGKILL (exit 137) safety net (#107), and an informed pre-flight + connection-failed hint (#109); the recurring "Connection Failed" was root-caused to **AP client-isolation** (a network condition), not the app. **Phase 3 remainder:** `F1` (`MainActivity` ~2,384 LOC) not carved; `D3` (central config/endpoints) not done.

Expand Down
4 changes: 2 additions & 2 deletions controller/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading