From 731bcf63f46d5b6050b838c8522b3ed8d42b9d03 Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Thu, 1 Oct 2026 09:59:37 -0600 Subject: [PATCH 1/2] K2GO-438 chore(build): raise targetSdk to 35 for Play (AGP 8.8, Gradle 8.10.2) compileSdk 34->35, targetSdk 28->35. proot is not blocked above 28: AOSP neverallows only execute_no_trans; K2Go runs guest binaries through the proot loader (mmap/execute). Verified on-device at 35. API 33/34/35 behavior fixes tracked in K2GO-439. --- controller/app/build.gradle | 9 ++++++--- controller/build.gradle | 2 +- controller/docs/ARCHITECTURE.md | 2 +- controller/docs/TECH_DEBT_PLAN.md | 2 +- controller/gradle/wrapper/gradle-wrapper.properties | 4 ++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/controller/app/build.gradle b/controller/app/build.gradle index 334cd918e..a82743766 100644 --- a/controller/app/build.gradle +++ b/controller/app/build.gradle @@ -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) diff --git a/controller/build.gradle b/controller/build.gradle index b7f32e3d0..bd8b08762 100644 --- a/controller/build.gradle +++ b/controller/build.gradle @@ -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' } diff --git a/controller/docs/ARCHITECTURE.md b/controller/docs/ARCHITECTURE.md index 5438c11b8..eb9d6ac8a 100644 --- a/controller/docs/ARCHITECTURE.md +++ b/controller/docs/ARCHITECTURE.md @@ -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. diff --git a/controller/docs/TECH_DEBT_PLAN.md b/controller/docs/TECH_DEBT_PLAN.md index 41f3aa756..c1ce199b9 100644 --- a/controller/docs/TECH_DEBT_PLAN.md +++ b/controller/docs/TECH_DEBT_PLAN.md @@ -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. diff --git a/controller/gradle/wrapper/gradle-wrapper.properties b/controller/gradle/wrapper/gradle-wrapper.properties index 58899002c..7859569fd 100644 --- a/controller/gradle/wrapper/gradle-wrapper.properties +++ b/controller/gradle/wrapper/gradle-wrapper.properties @@ -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 From 73f85db7af09f358ec01fcab262ccce70a8cc78e Mon Sep 17 00:00:00 2001 From: Luis Guzman Date: Thu, 1 Oct 2026 11:38:43 -0600 Subject: [PATCH 2/2] K2GO-438 chore(lint): resolve 3 new lint errors from the compileSdk 35 / AGP 8.8 bump Add ACCESS_COARSE_LOCATION alongside FINE (CoarseFineLocation), and route two registerReceiver sites through ContextCompat.registerReceiver (UnspecifiedRegisterReceiverFlag). --- controller/app/src/main/AndroidManifest.xml | 2 ++ .../k2go/redesign/DashboardDetailFragment.java | 8 ++------ .../k2go/update/presentation/UpdateController.java | 8 +++----- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/controller/app/src/main/AndroidManifest.xml b/controller/app/src/main/AndroidManifest.xml index 2399a0a37..6c3bd9c3e 100644 --- a/controller/app/src/main/AndroidManifest.xml +++ b/controller/app/src/main/AndroidManifest.xml @@ -8,6 +8,8 @@ + + diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardDetailFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardDetailFragment.java index 0e5cdbdd4..db51d7daa 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardDetailFragment.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/DashboardDetailFragment.java @@ -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; @@ -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(); } diff --git a/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java b/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java index 06c1b7d85..1da0d56db 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/update/presentation/UpdateController.java @@ -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; @@ -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() {