Project: VSCodroid Version: 1.0-draft Date: 2026-02-10
Historical document — describes the plan as of 2026-02-10, not the code. The server is built from the MIT Code - OSS source by
.github/workflows/build-vscode-oss.yml, with unified diffs inpatches/applied before the build; app builds fetch the result withscripts/fetch-vscode-oss.sh. Anything here about downloading a pre-built server from Microsoft's CDN, or about inline regex patches indownload-vscode-server.sh, describes a path that was removed on 2026-08-12. For what the code actually does, read the code —scripts/build-vscode-oss.sh,patches/, and the Kotlin underandroid/app/src/main/kotlin/com/vscodroid/.CONTRIBUTING.mdis the prose kept current alongside it.Note also that the
docker build ... toolchains/step below cannot run: that directory's Dockerfile and build scripts were removed on 2026-08-14, andtoolchains/is now only a gitignored download work dir. Bundled binaries come from Termux packages instead, viascripts/download-*.sh.Two M2/M4 capabilities below were built and then withdrawn, so treat them as plan rather than description:
- "Open with VSCodroid" file-type intent filters (M2-T6). Removed. They advertised the app for about twenty extensions and opened none of them;
AndroidManifest.xmlcarries the reason where the filters used to be, and re-implementing is blocked oncontent://URIs having no POSIX path rather than on wiring.- A GitHub OAuth flow owned by Kotlin (
vscodroid://oauth/github,startGitHubOAuth). Never shipped in that shape. What exists is one opaque relay onvscodroid://callback, gated by whether this app itself opened a browser in the last ten minutes;docs/05-API_SPEC.md§2.5 describes the shipped flow.
This document translates the VSCodroid documentation suite (PRD, SRS, Architecture, Technical Spec, Milestones) into an actionable, week-by-week implementation plan with explicit task dependencies, file inventories, and validation checkpoints.
| Principle | Implication |
|---|---|
| Fail fast on highest risk | M0 exists solely to validate Node.js on Android — if this fails, everything stops |
| Dependency-driven ordering | Tasks are sequenced so no task starts before its inputs are ready |
| Parallelizable where possible | Independent work streams identified within each milestone |
| Risk checkpoints embedded | Go/no-go gates at week boundaries, not just milestone boundaries |
| Files-first specification | Every task lists the exact files to create or modify |
Reading this chart: Task IDs match the Mx-Ty scheme from the task breakdowns below. "Duration" = calendar time end-to-end (accounts for parallelism); "Effort" = person-days sum.
gantt
title VSCodroid Implementation Timeline
dateFormat YYYY-MM-DD
axisFormat %b %d
tickInterval 1month
excludes weekends
section M0 - POC
Android project scaffold :done, m0t1, 2025-09-08, 2d
Cross-compilation Docker env :done, m0t3, 2025-09-08, 1d
Cross-compile Node.js :done, m0t2, after m0t3, 4d
Launch Node.js from Kotlin :done, m0t4, after m0t1 m0t2, 2d
WebView + localhost :done, m0t5, after m0t4, 2d
Foreground Service :done, m0t6, after m0t5, 2d
WebSocket validation :done, m0t7, after m0t6, 1d
M0 gate :milestone, m0gate, after m0t7, 0d
section M1 - VS Code Core
Fork code-server + patches :done, m1t1, after m0gate, 5d
VS Code in WebView :done, m1t2, after m1t1, 3d
node-pty cross-compile :done, m1t3, after m0gate, 4d
Git cross-compile :done, m1t4, after m0gate, 3d
bash + tmux cross-compile :done, m1t5, after m0gate, 3d
Terminal + tmux :done, m1t6, after m1t2 m1t3 m1t5, 4d
Git bundling + SCM :done, m1t7, after m1t2 m1t4, 2d
Extensions (Open VSX) :done, m1t8, after m1t2, 3d
File system + integration :done, m1t9, after m1t6 m1t7 m1t8, 3d
M1 gate :milestone, m1gate, after m1t9, 0d
section M2 - Mobile UX
Extra Key Row + Trackpad :done, m2t1, after m1gate, 5d
Keyboard handling :done, m2t2, after m1gate, 3d
Touch + clipboard + back :done, m2t3, after m2t1 m2t2, 3d
Orientation + split-screen :done, m2t4, after m2t3, 2d
Accessibility baseline :done, m2t5, after m2t3, 2d
Intent + crash recovery :done, m2t6, after m2t4 m2t5, 3d
M2 gate :milestone, m2gate, after m2t6, 0d
section M3 - Dev Environment
Python + pip (Termux) :done, m3t1, after m2gate, 3d
make cross-compile :done, m3t2, after m2gate, 2d
npm bash functions :done, m3t3, after m3t1 m3t2, 2d
Pre-bundled extensions :done, m3t4, after m2gate, 3d
First-run experience :done, m3t5, after m3t3 m3t4, 4d
M3 gate :milestone, m3gate, after m3t5, 0d
section M4 - Polish
Error handling + logging :done, m4t1, after m3gate, 3d
Memory optimization :done, m4t2, after m3gate, 4d
Startup optimization :done, m4t3, after m4t1 m4t2, 3d
Phantom process monitoring :done, m4t4, after m4t3, 3d
Storage management :done, m4t5, after m4t4, 3d
GitHub OAuth :done, m4t6, after m4t5, 3d
External storage (SAF) :done, m4t7, after m4t6, 5d
M4 gate :milestone, m4gate, after m4t7, 0d
section M5 - Toolchain Ecosystem
worker_thread ExtHost+ptyHost :done, m5t1, after m4gate, 6d
AssetPackManager :done, m5t2, after m4gate, 4d
On-demand toolchains :done, m5t3, after m5t2, 5d
Language Picker UI :done, m5t4, after m5t3 m5t1, 5d
M5 gate :milestone, m5gate, after m5t4, 0d
section M6 - Release
Stability + auth fixes :done, m6t1, after m5gate, 3d
SSH key management :done, m6t2, after m6t1, 3d
App upgrade handling :done, m6t3, after m6t2, 4d
Device testing + verify :done, m6t4, after m6t3, 7d
Security review :done, m6t5, after m6t4, 3d
Release build + signing :done, m6t6, after m6t5, 3d
AAB build + size audit :done, m6t7, after m6t6, 3d
Branding :done, m6t8, after m5gate, 5d
Legal compliance :done, m6t9, after m6t8, 2d
Documentation :done, m6t10, after m6t9, 3d
CI/CD pipeline :done, m6t11, after m5gate, 10d
Play Store listing :done, m6t12, after m6t7 m6t10 m6t11, 3d
Closed testing (14 days) :active, m6t13, after m6t12, 10d
Launch :milestone, launch, after m6t13, 0d
Note: Task-level dependency diagrams for each milestone are in the flowcharts below (§3-9). Durations shown are critical path working days (weekends excluded). See each milestone header for total effort.
Three largely independent work streams allow parallelism within milestones:
flowchart LR
subgraph WS1["Stream A: Native Binaries"]
A1["Node.js"] --> A2["node-pty"] --> A3["Python"]
A1b["Git"] --> A3
A1c["bash + tmux"] --> A3
A3 --> A4["M5: Toolchain asset packs"]
end
subgraph WS2["Stream B: VS Code Integration"]
B1["code-server fork"] --> B2["Patches"] --> B3["vscode-web + vscode-reh"]
B3 --> B4["Extensions"] --> B5["M5: worker_thread patch"]
end
subgraph WS3["Stream C: Android Shell"]
C1["Project scaffold"] --> C2["WebView"] --> C3["Extra Key Row + GestureTrackpad"]
C3 --> C4["Clipboard + touch"] --> C5["M5: Language Picker"]
C5 --> C6["M6: Play Store"]
end
A1 --> B3
B3 --> C2
Duration: 1-2 days (one-time setup)
| Step | Command / Action | Validation |
|---|---|---|
| Install Android Studio | Latest stable with API 36 support | sdkmanager --list shows API 33-36 |
| Install NDK r27 | Via SDK Manager → NDK (Side by side) | $ANDROID_NDK_HOME/ndk-build --version |
| Install JDK 17 | System package manager or Android Studio | java --version → 17.x |
| Install Node.js 20 LTS | brew install node@20 (macOS) |
node --version → v20.x |
| Install Yarn Classic | npm install -g yarn |
yarn --version → 1.x |
| Install Python 3.11+ | brew install python |
python3 --version → 3.11+ |
| Install Docker (optional) | For reproducible cross-compilation | docker --version |
Files to create:
├── .gitignore
├── CLAUDE.md ← already exists
├── MILESTONES.md ← already exists
├── README.md ← already exists
├── LICENSE ← already exists
├── docs/ ← already exists (01-12)
├── android/ ← M0 creates this
├── server/ ← M1 creates this (code-server submodule)
├── patches/
│ ├── code-server/ ← M1 populates
│ └── vscodroid/ ← M1 populates
├── toolchains/
│ ├── Dockerfile ← M0 creates
│ ├── build-node.sh ← M0 creates
│ ├── build-python.sh ← M3 creates
│ ├── build-node-pty.sh ← M1 creates
│ ├── build-git.sh ← M1 creates
│ ├── build-bash.sh ← M1 creates
│ └── build-tmux.sh ← M1 creates
├── scripts/
│ ├── setup.sh ← M0 creates
│ ├── build-all.sh ← M1 creates
│ ├── apply-patches.sh ← M1 creates
│ ├── package-assets.sh ← M1 creates
│ └── deploy.sh ← M0 creates
└── test/
├── projects/ ← M1 creates (test fixtures)
└── extensions/ ← M1 creates (test fixtures)
- ARM64 physical Android device (API 33+) available and USB-debuggable
- ADB connection verified:
adb devicesshows device - Developer options + USB debugging enabled on device
- Device has ≥ 2 GB free storage
Goal: Validate that Node.js runs on Android and WebView can connect to it via localhost. Duration: 2-3 weeks (11 working days critical path, 13 person-days total effort) Risk level: HIGH — this is the "can we even do this?" milestone
flowchart TD
M0T1["M0-T1: Android project scaffold<br/>2 days"] --> M0T4["M0-T4: Launch Node.js from Kotlin<br/>2 days"]
M0T2["M0-T2: Cross-compile Node.js<br/>4 days"] --> M0T4
M0T3["M0-T3: Cross-compilation Docker env<br/>1 day"] --> M0T2
M0T4 --> M0T5["M0-T5: WebView + localhost<br/>1.5 days"]
M0T5 --> M0T6["M0-T6: Foreground Service<br/>1.5 days"]
M0T6 --> M0T7["M0-T7: WebSocket validation<br/>1 day"]
M0T7 --> M0GATE["M0 GATE"]
Effort: 2 days | Dependencies: Prerequisites complete | Parallel with: M0-T2, M0-T3
Files to create:
android/
├── build.gradle.kts (root build file)
├── settings.gradle.kts (project settings)
├── gradle.properties (JVM args, Android config)
├── gradle/
│ └── libs.versions.toml (version catalog)
├── app/
│ ├── build.gradle.kts (app module — minSdk 33, targetSdk 36)
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml (permissions, services, activities)
│ ├── kotlin/com/vscodroid/
│ │ ├── VSCodroidApp.kt (Application class)
│ │ └── MainActivity.kt (WebView host — placeholder)
│ ├── res/
│ │ ├── layout/
│ │ │ └── activity_main.xml (WebView container)
│ │ ├── values/
│ │ │ └── strings.xml
│ │ └── drawable/
│ │ └── ic_launcher.xml (placeholder icon)
│ └── jniLibs/
│ └── arm64-v8a/ (empty — M0-T2 populates)
Implementation steps:
- Create new Gradle project with Kotlin DSL
- Configure
app/build.gradle.kts:android { namespace = "com.vscodroid" compileSdk = 36 defaultConfig { applicationId = "com.vscodroid" minSdk = 33 targetSdk = 36 versionCode = 1 versionName = "0.0.1-m0" ndk { abiFilters += "arm64-v8a" } } packaging { jniLibs { useLegacyPackaging = true } // Critical for .so trick } } - Configure
AndroidManifest.xmlwith:INTERNET,FOREGROUND_SERVICE,FOREGROUND_SERVICE_SPECIAL_USE,POST_NOTIFICATIONSpermissionsMainActivitydeclarationNodeServicedeclaration (placeholder)
- Create
VSCodroidApp.ktwithWebView.setDataDirectorySuffix("vscodroid")inonCreate() - Create
MainActivity.ktwith basic WebView setup - Create
activity_main.xmlwith WebView filling the layout
Acceptance criteria:
-
./gradlew assembleDebugsucceeds - APK installs on ARM64 device
- App launches and shows blank WebView
- Logcat shows "VSCodroid" tag
Effort: 4 days | Dependencies: M0-T3 | Critical path item
Files to create:
toolchains/
├── build-node.sh (main build script)
└── patches/
└── node/ (Termux patches for Node.js)
├── 001-android-fixes.patch
└── ...
Output: android/app/src/main/jniLibs/arm64-v8a/libnode.so
Implementation steps:
-
Day 1 — Setup and research:
- Study Termux's
packages/nodejs/build.shfor patch inventory - Download Node.js LTS source (v20.x)
- Identify required Termux patches (typically 5-15 patches)
- Create
build-node.shscript skeleton
- Study Termux's
-
Day 2 — Cross-compilation attempt:
- Set NDK toolchain variables:
export CC=$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang export CXX=${CC}++
- Apply Termux patches to Node.js source
- Run configure:
./configure \ --dest-cpu=arm64 --dest-os=android --cross-compiling \ --partly-static --with-intl=small-icu --openssl-no-asm \ --without-inspector --without-node-snapshot --shared-zlib \ --prefix=/data/data/com.vscodroid/files/usr
- Start build:
make -j$(nproc)
- Set NDK toolchain variables:
-
Day 3 — Debug build failures:
- Fix compilation errors (expect 3-5 issues)
- Common issues: missing headers, linker flags, V8 assembly
- Apply 16KB page alignment:
LDFLAGS="-Wl,-z,max-page-size=16384" - Successful build → strip:
aarch64-linux-android-strip --strip-unneeded out/Release/node
-
Day 4 — Validate and package:
- Copy to jniLibs:
cp out/Release/node android/app/src/main/jniLibs/arm64-v8a/libnode.so - Verify:
file libnode.so→ELF 64-bit LSB pie executable, ARM aarch64 - Push to device via adb, test execution:
adb shell /data/local/tmp/libnode.so --version - Create minimal HTTP server test script
- Copy to jniLibs:
Risk checkpoint (end of Day 3):
If Node.js compilation fails after 3 days of attempts, activate contingency: extract pre-built Node.js binary from Termux .deb package. See Risk Matrix T02.
Acceptance criteria:
-
libnode.sois a valid ARM64 ELF binary - Binary runs on physical device:
node --versionoutputs version string - Binary can execute a simple
http.createServerscript - Binary size < 60 MB after stripping
Effort: 1 day | Dependencies: None | Parallel with: M0-T1
Files to create:
toolchains/
└── Dockerfile
Implementation steps:
- Create Dockerfile based on Ubuntu 22.04 with:
- Android NDK r27 pre-installed
- Build dependencies (python3, make, gcc, etc.)
- Environment variables for NDK paths
- Build and test:
docker build -t vscodroid/build-env toolchains/ - Verify NDK compiler works inside container:
docker run vscodroid/build-env aarch64-linux-android28-clang --version
Acceptance criteria:
- Docker image builds successfully
- NDK compiler accessible inside container
- Can cross-compile a trivial C program for ARM64 Android
Effort: 2 days | Dependencies: M0-T1, M0-T2
Files to create / modify:
android/app/src/main/kotlin/com/vscodroid/
├── service/
│ └── ProcessManager.kt (Node.js process lifecycle)
├── util/
│ ├── Environment.kt (env var construction)
│ └── PortFinder.kt (find available localhost port)
└── assets/
└── test-server.js (minimal Express-like HTTP server)
Implementation steps:
-
PortFinder.kt — find available localhost port:
- Bind to port 0, read assigned port, close socket
- Return port number
-
Environment.kt — construct environment variables:
fun buildEnvironment(context: Context, port: Int): Map<String, String> { val nativeLibDir = context.applicationInfo.nativeLibraryDir val filesDir = context.filesDir.absolutePath return mapOf( "HOME" to "$filesDir/home", "TMPDIR" to "${context.cacheDir}/tmp", "PATH" to "$nativeLibDir:/system/bin", "LD_LIBRARY_PATH" to nativeLibDir, "NODE_PATH" to "$filesDir/node_modules", "LANG" to "en_US.UTF-8", "TERM" to "xterm-256color", ) }
-
ProcessManager.kt — launch and monitor Node.js:
startServer(): ProcessBuilder withlibnode.sopath +--max-old-space-size=512+ server scriptwaitForReady(): Pollhttp://localhost:PORT/healthzevery 200ms, timeout 30sstopServer(): SIGTERM → wait 5s → SIGKILLisServerHealthy(): Single HTTP GET to/healthz
-
test-server.js — minimal HTTP server:
const http = require("http"); const port = process.argv[2] || 8080; http .createServer((req, res) => { if (req.url === "/healthz") { res.writeHead(200); res.end("OK"); return; } res.writeHead(200, { "Content-Type": "text/html" }); res.end("<h1>Hello from Node.js on Android!</h1>"); }) .listen(port, "127.0.0.1", () => console.log(`Listening on :${port}`));
-
Wire up in
MainActivity.kt: start ProcessManager → wait for ready → log success
Acceptance criteria:
- Node.js process starts from Kotlin via ProcessBuilder
- Health check returns 200 within 30 seconds
-
curl http://localhost:PORT/from adb shell returns HTML - Process PID is trackable
Effort: 1.5 days | Dependencies: M0-T4
Files to modify:
android/app/src/main/kotlin/com/vscodroid/
├── MainActivity.kt (add WebView configuration + loadUrl)
└── webview/
├── VSCodroidWebView.kt (WebView settings configuration)
└── VSCodroidWebViewClient.kt (URL loading, error handling)
Implementation steps:
-
VSCodroidWebView.kt — configure WebView settings:
fun configure(webView: WebView) { webView.settings.apply { javaScriptEnabled = true domStorageEnabled = true databaseEnabled = true setSupportZoom(false) builtInZoomControls = false textZoom = 100 mixedContentMode = MIXED_CONTENT_ALWAYS_ALLOW cacheMode = LOAD_DEFAULT } }
-
VSCodroidWebViewClient.kt — handle page load errors:
onReceivedError(): show retry UIonReceivedHttpError(): log
-
MainActivity.kt — orchestrate:
- Start ProcessManager
- Wait for server ready
webView.loadUrl("http://localhost:$port/")- Verify "Hello from Node.js on Android!" displays
Acceptance criteria:
- WebView displays "Hello from Node.js on Android!" served by Node.js
- Page load completes within 5 seconds of server ready
- No WebView errors in Logcat
Effort: 1.5 days | Dependencies: M0-T5
Files to create / modify:
android/app/src/main/kotlin/com/vscodroid/
├── service/
│ └── NodeService.kt (Foreground Service)
├── MainActivity.kt (bind to service)
└── AndroidManifest.xml (service declaration)
android/app/src/main/res/
└── values/
└── strings.xml (notification text)
Implementation steps:
-
NodeService.kt — Foreground Service with
specialUsetype:onCreate(): create notification channel, start foreground- Move ProcessManager logic into service
- Node.js watchdog thread:
process.waitFor()→ auto-restart if exit != 0 onDestroy(): graceful shutdown of Node.js
-
AndroidManifest.xml — declare service:
<service android:name=".service.NodeService" android:foregroundServiceType="specialUse" android:exported="false"> <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Local development server for code editor" /> </service>
-
MainActivity.kt — bind to NodeService, receive server-ready callback
Acceptance criteria:
- Foreground Service starts with persistent notification
- Node.js survives Activity going to background (Home button)
- Return to app after 5 minutes → WebView still connected
- Kill Node.js via
adb shell kill→ auto-restarts within 5 seconds
Effort: 1 day | Dependencies: M0-T6
Files to modify:
android/app/src/main/assets/
└── test-server.js (add WebSocket support)
Implementation steps:
- Add WebSocket support to test server (using
wsnpm package or raw HTTP upgrade) - WebView JavaScript: open
ws://localhost:PORT/ws, send message, receive echo - Validate bidirectional communication works through WebView
Acceptance criteria:
- WebSocket connection established from WebView to localhost
- Bidirectional message exchange works
- Connection survives for 10+ minutes without dropping
All criteria must pass before proceeding to M1:
| # | Criterion | Test Method |
|---|---|---|
| 1 | Node.js ARM64 binary runs on physical device | adb shell execution |
| 2 | Express-style HTTP server responds on localhost | curl from adb shell |
| 3 | WebView renders content from localhost | Visual inspection |
| 4 | WebSocket bidirectional communication works | JS console test |
| 5 | Process survives app backgrounding via Foreground Service | Home → 5 min → return |
| 6 | Auto-restart works after process kill | adb shell kill → observe restart |
Risk checkpoint: If any criterion fails, review Risk Matrix T02 and T06 before attempting fixes. Consider Termux binary fallback.
Goal: VS Code Workbench running in WebView with extensions, terminal, and Git. Duration: 3 weeks (15 working days critical path, 30 person-days across 3 parallel streams) Entry criteria: All M0 gate criteria passed
flowchart TD
M1T1["M1-T1: Fork code-server + patches<br/>5 days"] --> M1T2["M1-T2: VS Code in WebView<br/>3 days"]
M1T3["M1-T3: node-pty cross-compile<br/>4 days"] --> M1T6["M1-T6: Terminal + tmux<br/>4 days"]
M1T4["M1-T4: Git cross-compile<br/>3 days"] --> M1T7["M1-T7: Git bundling + SCM<br/>2 days"]
M1T5["M1-T5: bash + tmux cross-compile<br/>3 days"] --> M1T6
M1T2 --> M1T6
M1T2 --> M1T7
M1T2 --> M1T8["M1-T8: Extensions (Open VSX)<br/>3 days"]
M1T6 --> M1T9["M1-T9: File system + integration<br/>3 days"]
M1T7 --> M1T9
M1T8 --> M1T9
M1T9 --> M1GATE["M1 GATE"]
Parallelization: M1-T3, M1-T4, M1-T5 can all run in parallel with M1-T1 (different work streams).
Effort: 5 days | Dependencies: M0 gate passed | Critical path
Files to create:
server/ (git submodule → code-server fork)
├── lib/vscode/ (VS Code source, managed by code-server)
├── patches/ (code-server's patches)
└── ci/dev/patch-vscode.sh (code-server's patch script)
patches/
├── code-server/
│ ├── product.diff (branding → "VSCodroid", Open VSX URLs)
│ ├── disable-telemetry.diff
│ ├── disable-update-check.diff
│ ├── marketplace.diff (Open VSX integration)
│ ├── serve-web.diff
│ ├── webview.diff
│ ├── browser-command.diff
│ ├── github-auth.diff
│ └── local-storage.diff
└── vscodroid/ (empty for M1 — populated M2+)
scripts/
├── apply-patches.sh
├── build-vscode.sh
├── build-all.sh
└── package-assets.sh
Implementation steps:
-
Day 1 — Fork and understand code-server:
- Fork
coder/code-servertormyndharis/code-server - Clone as git submodule:
git submodule add <url> server - Study code-server build system:
ci/dev/,ci/build/, patch mechanism - Read each existing code-server patch to understand what it does
- Fork
-
Day 2 — Apply and customize patches:
- Run
./ci/dev/patch-vscode.shto apply code-server patches - Create
patches/code-server/product.diffwith VSCodroid branding:{ "nameShort": "VSCodroid", "nameLong": "VSCodroid", "applicationName": "vscodroid", "dataFolderName": ".vscodroid", "extensionsGallery": { "serviceUrl": "https://open-vsx.org/vscode/gallery", "itemUrl": "https://open-vsx.org/vscode/item", "resourceUrlTemplate": "https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}" }, "telemetryOptIn": false } - Create
scripts/apply-patches.sh(as specified in Tech Spec §6.2)
- Run
-
Day 3 — Build vscode-web:
- Install VS Code dependencies:
cd server/lib/vscode && yarn - Build web client:
yarn gulp vscode-web-min - Verify output in
out-vscode-web/
- Install VS Code dependencies:
-
Day 4 — Build vscode-reh (server):
- Build server:
yarn gulp vscode-reh-min - Verify output in
out-vscode-reh/ - Verify
@vscode/ripgrepbinary is included innode_modules - Bundle ripgrep as
libripgrep.soinjniLibs/arm64-v8a/(symlink created byFirstRunSetup.setupToolSymlinks())
- Build server:
-
Day 5 — Package and scripts:
- Create
scripts/build-vscode.sh(automates steps 2-4) - Create
scripts/package-assets.sh:- Copy
out-vscode-web/→android/app/src/main/assets/vscode-web/ - Copy
out-vscode-reh/→android/app/src/main/assets/vscode-reh/
- Copy
- Create
android/app/src/main/assets/server.js(bootstrap script) - Test: full build from clean state
- Create
Acceptance criteria:
-
apply-patches.shapplies all patches without conflict -
yarn gulp vscode-web-minsucceeds -
yarn gulp vscode-reh-minsucceeds -
out-vscode-web/contains index.html and static assets -
out-vscode-reh/contains server entry point and node_modules
Effort: 3 days | Dependencies: M1-T1
Files to create / modify:
android/app/src/main/assets/
├── vscode-web/ (from M1-T1 build output)
├── vscode-reh/ (from M1-T1 build output)
└── server.js (bootstrap script — replaces test-server.js)
android/app/src/main/kotlin/com/vscodroid/
├── service/
│ ├── NodeService.kt (update to launch VS Code server)
│ └── ProcessManager.kt (update server launch command)
├── setup/
│ └── FirstRunSetup.kt (asset extraction on first run)
└── MainActivity.kt (update to load VS Code)
Implementation steps:
-
FirstRunSetup.kt — extract VS Code assets:
- Check if
files/server/vscode-reh/exists - If not, extract from APK assets to app-private storage
- Create required directories:
home/,home/.vscodroid/,home/.vscodroid/extensions/ - Track extraction version for future upgrades
- Configure VS Code settings (
settings.json):{ "terminal.integrated.profiles.linux": { "bash": { "path": "/data/data/com.vscodroid/files/usr/bin/bash" } }, "terminal.integrated.defaultProfile.linux": "bash", "git.path": "/data/data/com.vscodroid/files/usr/bin/git" }
- Check if
-
server.js — bootstrap script (see Tech Spec §3.1):
- Parse command-line arguments
- Set up VS Code product.json overrides
- Launch vscode-reh server entry point
- Expose
/healthzendpoint - Serve vscode-web static files
-
ProcessManager.kt — update server launch:
val command = listOf( "$nativeLibDir/libnode.so", "--max-old-space-size=512", "$filesDir/server/server.js", "--host=127.0.0.1", "--port=$port", "--extensions-dir=$home/.vscodroid/extensions", "--user-data-dir=$home/.vscodroid", "--server-data-dir=$home/.vscodroid", "--log=info" )
-
MainActivity.kt — load VS Code:
- Wait for server ready
webView.loadUrl("http://localhost:$port/")
Acceptance criteria:
- VS Code Workbench UI renders in WebView
- Monaco Editor loads and is interactive (typing works)
- Command Palette opens (F1 or via menu)
- File Explorer panel visible
- No critical JS errors in WebView console
Effort: 4 days | Dependencies: M0 gate | Parallel with M1-T1
Files to create:
toolchains/
└── build-node-pty.sh
Output: android/app/src/main/jniLibs/arm64-v8a/libnode_pty.so
Implementation steps:
- Set up node-gyp cross-compilation environment
- Build with NDK toolchain:
node-gyp rebuild \ --target=20.0.0 --arch=arm64 \ --nodedir=/path/to/node-source \ CC=$NDK_CC CXX=$NDK_CXX
- Copy output:
build/Release/pty.node→libnode_pty.so - Test PTY creation on device
Acceptance criteria:
-
libnode_pty.sois valid ARM64 ELF - PTY allocation works on physical device
- Can spawn a shell process through PTY
Effort: 3 days | Dependencies: M0 gate | Parallel with M1-T1
Files to create:
toolchains/
└── build-git.sh
Output: android/app/src/main/jniLibs/arm64-v8a/libgit.so + assets/usr/lib/git-core/
Implementation steps (see Tech Spec §1.6):
- Download Git 2.40+ source
- Apply Termux patches
- Configure and build:
./configure --host=aarch64-linux-android \ --prefix=/data/data/com.vscodroid/files/usr \ ac_cv_fread_reads_directories=no ac_cv_snprintf_returns_bogus=no make -j$(nproc) NO_GETTEXT=YesPlease NO_TCLTK=YesPlease - Package: main binary as
libgit.so, helper binaries in assets
Acceptance criteria:
-
git --versionruns on device -
git init,git add,git commitwork -
git statusproduces correct output
Effort: 3 days | Dependencies: M0 gate | Parallel with M1-T1
Files to create:
toolchains/
├── build-bash.sh
└── build-tmux.sh
Output:
android/app/src/main/jniLibs/arm64-v8a/libbash.soandroid/app/src/main/jniLibs/arm64-v8a/libtmux.soassets/usr/share/terminfo/(terminfo database)
Implementation steps (see Tech Spec §1.7, §1.8):
-
bash (1 day):
- Configure:
--host=aarch64-linux-android --without-bash-malloc - Build and strip
- Package as
libbash.so
- Configure:
-
tmux (2 days):
- Cross-compile dependency:
libevent - Cross-compile dependency:
ncurses(for terminfo) - Configure tmux with cross-compiled deps
- Build and strip
- Package as
libtmux.so+ terminfo data
- Cross-compile dependency:
Acceptance criteria:
-
libbash.sostarts an interactive shell on device -
libtmux.sostarts tmux on device - tmux can create multiple sessions
- terminfo works (colors, cursor movement)
Effort: 4 days | Dependencies: M1-T2, M1-T3, M1-T5
Files to modify:
android/app/src/main/kotlin/com/vscodroid/
└── util/
└── Environment.kt (add SHELL, TERMINFO, etc.)
patches/vscodroid/
└── terminal-tmux.diff (VS Code terminal → tmux integration)
Implementation steps:
-
Day 1 — Verify node-pty works with bash:
- Load
libnode_pty.soin VS Code server - Configure terminal to use
libbash.soas shell - Test: open terminal in VS Code → bash prompt appears
- Load
-
Day 2 — tmux integration:
- Configure tmux as terminal multiplexer layer
- VS Code terminal tabs → tmux sessions
- Single tmux process manages all terminal sessions
-
Day 3 — Create
terminal-tmux.diffpatch:- Modify VS Code's terminal service to route through tmux
- Handle session create/destroy/resize
-
Day 4 — Test and verify phantom process count:
- Open 3 terminal tabs
adb shell ps -A | grep vscodroid→ verify only 1 tmux process- Test: terminal I/O, colors, interactive programs (vim, top)
Acceptance criteria:
- Terminal opens in VS Code with bash shell
- Multiple terminal tabs work via tmux
- Only 1 tmux process regardless of terminal tab count
- Terminal supports colors, cursor movement, interactive programs
- Phantom process count stays under 5 with 3 terminal tabs open
Effort: 2 days | Dependencies: M1-T2, M1-T4
Files to modify:
android/app/src/main/kotlin/com/vscodroid/
└── util/
└── Environment.kt (add GIT_EXEC_PATH)
Implementation steps:
- Configure
GIT_EXEC_PATHenvironment variable to point to extracted git-core helpers FirstRunSetup.setupGitCore()— create symlinks for git-core helper binaries:- Extract
assets/usr/lib/git-core/to app-private storage on first run - Create symlinks:
git→libgit.so, plus helper binaries (git-remote-https,git-upload-pack, etc.)
- Extract
- Verify Git works in VS Code terminal
- Verify VS Code SCM panel detects Git and shows status
- Test:
git init→ create file →git add→git commit→ SCM panel reflects
Acceptance criteria:
-
git --versionworks in VS Code terminal - VS Code SCM panel shows repository status
-
git init,git add,git commitwork from terminal - SCM panel updates after git operations
Effort: 3 days | Dependencies: M1-T2
Files to modify:
patches/code-server/
└── marketplace.diff (verify Open VSX URLs work)
Implementation steps:
- Verify Open VSX search/browse works in Extensions panel
- Install a theme extension (e.g., One Dark Pro)
- Install a functional extension (e.g., Material Icon Theme)
- Verify extension activation and function
- Test extension uninstall/disable
Acceptance criteria:
- Extensions panel shows search results from Open VSX
- Can install, activate, and use a theme extension
- Can install, activate, and use Material Icon Theme
- Extensions persist across app restart
- Extension uninstall works
Effort: 3 days | Dependencies: M1-T6, M1-T7, M1-T8
Files to create:
test/
├── projects/
│ ├── test-minimal/ (1 file)
│ ├── test-small/ (10 files, HTML/JS)
│ └── test-medium/ (100 files, Node.js project)
└── extensions/ (test VSIX files)
android/app/src/androidTest/ (instrumented tests)
└── kotlin/com/vscodroid/
├── NodeLaunchTest.kt
├── WebViewLoadTest.kt
├── TerminalTest.kt
└── ExtensionTest.kt
Implementation steps:
- Day 1 — Create test fixtures and instrumented test scaffolding
- Day 2 — Integration testing:
- File explorer shows files, create/edit/save works
- Open test-small project → verify file tree
- Edit file → Ctrl+S → verify saved content
- Open 10K-line file → verify no crash
- Day 3 — Full integration validation:
- Run all E2E tests from Testing Strategy §3.3 (E2E-01 through E2E-06)
- Fix any integration issues found
- Document known issues
Acceptance criteria:
- File explorer shows correct directory tree
- Create, edit, save files works
- Monaco editor: typing, syntax highlighting, multi-cursor all work
- Terminal + Git + Extensions all function together
- 30-minute stability test passes (no crash)
| # | Criterion | Test Method |
|---|---|---|
| 1 | VS Code Workbench UI renders correctly | Visual inspection |
| 2 | Monaco Editor works (typing, highlighting, multi-cursor) | Manual editing test |
| 3 | Terminal opens with bash shell | Open terminal, type commands |
| 4 | tmux multiplexing works | Open 3+ terminal tabs, verify 1 tmux process |
| 5 | Extensions install from Open VSX | Install theme + icon pack |
| 6 | Extensions activate and function | Verify theme applies, icons show |
| 7 | File explorer shows files, CRUD works | Create/edit/save test |
| 8 | Git works in terminal | git init, git add, git commit |
| 9 | Git SCM panel works | Verify status updates |
| 10 | Phantom processes ≤ 5 | adb shell ps -A | grep vscodroid |
| 11 | Node.js startup + health check stable 30 min | Automated stability test |
Goal: Make VS Code usable on a touchscreen device. Duration: 3 weeks (13 working days critical path, 18 person-days total effort) Entry criteria: All M1 gate criteria passed, no open P0 defects
flowchart TD
M2T1["M2-T1: Extra Key Row + GestureTrackpad<br/>5 days"] --> M2T3["M2-T3: Touch + clipboard + back<br/>3 days"]
M2T2["M2-T2: Keyboard handling<br/>3 days"] --> M2T3
M2T3 --> M2T4["M2-T4: Orientation + split-screen<br/>2 days"]
M2T3 --> M2T5["M2-T5: Accessibility baseline<br/>2 days"]
M2T4 --> M2T6["M2-T6: Intent + crash recovery<br/>3 days"]
M2T5 --> M2T6
M2T6 --> M2GATE["M2 GATE"]
Effort: 5 days | Dependencies: M1 gate
Files to create:
android/app/src/main/kotlin/com/vscodroid/
└── keyboard/
├── ExtraKeyRow.kt (ViewPager2-based multi-page key row)
├── ExtraKeyButton.kt (individual key button with long-press popup)
├── KeyPageConfig.kt (multi-page key layout configuration)
├── KeyPageAdapter.kt (RecyclerView.Adapter for ViewPager2 pages)
├── KeyInjector.kt (JS key event injection)
└── GestureTrackpad.kt (3-speed drag-to-navigate trackpad)
android/app/src/main/res/
├── layout/
│ └── view_extra_key_row.xml (planned; never built this way, now deleted --
│ ExtraKeyRow builds its views in code, and the
│ layout that did exist was a HorizontalScrollView
│ of Buttons, not ViewPager2)
└── values/
├── dimens.xml (key sizes, 48dp minimum)
└── colors.xml (key colors)
Implementation steps:
-
Day 1 — Multi-page key row with ViewPager2:
KeyPageConfig.ktdefines page layouts:- Page 1:
[Tab] [Esc] [Ctrl] [Alt] [Shift] [GestureTrackpad] [{}] [()] - Page 2:
[;] [:] ["] [/] [|] [] [&] [_]` - Page 3:
[[] []] [<] [>] [=] [!] [#] [@]
- Page 1:
KeyPageAdapter.kt—RecyclerView.AdapterforViewPager2pages- Dot indicators below the key row to show current page
-
Day 2 — Key injection + modifiers:
KeyInjector.kt: construct JavaScriptKeyboardEventand dispatch viaevaluateJavascript- Handle modifier state (Ctrl held + S = Ctrl+S)
- Map key names to
keyCode,code,keyvalues setupModifierInterceptor()— intercept physical keyboard modifier keys (Ctrl, Alt) and sync state with Extra Key Row toggle buttons
-
Day 3 — Long-press popup + alternate keys:
ExtraKeyButton.showLongPressPopup()— long-press on a key shows alternate keys- Example: long-press
{shows[,<,( - Example: long-press
CtrlshowsMeta,Fn
- Example: long-press
- Each button ≥ 48dp × 48dp (accessibility requirement)
- Ctrl and Alt are sticky toggle buttons (highlight when active)
-
Day 4 — GestureTrackpad:
GestureTrackpad.kt— swipeable area (rightmost zone or dedicated area)- 3-speed drag-to-navigate: slow drag = character-by-character, medium = word, fast = line
- Injected as arrow key events via
KeyInjector - Horizontal swipe = left/right cursor, vertical swipe = up/down cursor
-
Day 5 — Visibility + integration:
- Use
WindowInsetsCompat.Type.ime()to detect keyboard visibility - Show Extra Key Row + GestureTrackpad when keyboard appears, hide when it disappears
- Position above soft keyboard using
WindowInsetspadding - Wire into
activity_main.xmllayout - Test all key combinations: Ctrl+S, Ctrl+P, Ctrl+Shift+P, Ctrl+Z, etc.
- Test with different keyboard apps (GBoard, Samsung Keyboard)
- Use
Acceptance criteria:
- Multi-page Extra Key Row with ViewPager2 + dot indicators
- All key buttons work: Tab, Esc, Ctrl, Alt, arrows, brackets, semicolons
- Long-press on keys shows alternate key popup
- GestureTrackpad navigates with 3-speed sensitivity
- Modifier interceptor syncs physical keyboard state with toggle buttons
- Ctrl+S saves file, Ctrl+P opens Quick Open, Ctrl+Shift+P opens Command Palette
- Touch targets ≥ 48dp × 48dp
Effort: 3 days | Dependencies: M1 gate | Parallel with M2-T1
Files to modify:
android/app/src/main/kotlin/com/vscodroid/
├── MainActivity.kt (soft input mode)
└── webview/
└── VSCodroidWebView.kt (viewport handling)
patches/vscodroid/
└── keyboard-aware.diff (VS Code viewport fixes)
Implementation steps:
windowSoftInputMode = adjustResize— WebView shrinks when keyboard appears- Handle viewport resize: ensure cursor scrolls into view
- Fix
position:fixedelements (command palette, suggest widget) via patch - Test with different screen sizes and keyboard heights
Acceptance criteria:
- Editor viewport resizes correctly when keyboard opens
- Cursor always visible when typing
- Command Palette and suggest widget position correctly above keyboard
- Keystroke latency < 50ms on reference device
Effort: 3 days | Dependencies: M2-T1, M2-T2
Files to create / modify:
android/app/src/main/kotlin/com/vscodroid/
├── bridge/
│ ├── AndroidBridge.kt (bridge with security model)
│ ├── ClipboardBridge.kt (clipboard methods)
│ └── IntentBridge.kt (external URL, back button)
└── MainActivity.kt (back button handling)
patches/vscodroid/
└── touch-ui.diff (touch target adjustments)
Implementation steps:
-
AndroidBridge.kt — implement security model (API Spec §2.2):
- Origin check: only
http://127.0.0.1:PORT/orhttp://localhost:PORT/ - Per-session capability token
- Sensitive method gating via
authTokenparameter
- Origin check: only
-
ClipboardBridge.kt:
copyToClipboard(text),readFromClipboard(authToken),hasClipboardText()- Test: copy in Chrome → paste in VSCodroid and vice versa
-
IntentBridge.kt:
openExternalUrl(url, authToken)— planned as a scheme allowlist (https, mailto only); shipped without one. The token is checked, the URL is notonBackPressed()— close panels/dialogs first, then minimizeminimizeApp()
-
touch-ui.diff — larger touch targets for mobile:
- Disable WebView zoom conflicts
- Handle long-press: prefer Monaco selection over WebView default
- Context menu: use Monaco's
Acceptance criteria:
- Copy/paste works between VSCodroid and other apps
- Back button closes panels/dialogs before minimizing
- External URLs open in system browser (https only)
- Long-press selects text in editor (not WebView default)
- Bridge rejects calls from untrusted origins
Effort: 2 days | Dependencies: M2-T3
Files to create / modify:
patches/vscodroid/
└── orientation.diff (rotation handling)
android/app/src/main/
└── AndroidManifest.xml (configChanges handling)
Implementation steps:
- Support portrait, landscape, and reverse orientations
- Handle configuration changes without losing editor state
- Split-screen support: test multi-window mode
- Foldable awareness (if testing device available)
Acceptance criteria:
- Rotation preserves editor content and cursor position
- Layout adapts correctly in landscape
- Split-screen mode works without crash
- No data loss on any orientation change
Effort: 2 days | Dependencies: M2-T3 | Parallel with M2-T4
Files to modify:
android/app/src/main/kotlin/com/vscodroid/
└── keyboard/
├── ExtraKeyRow.kt (add contentDescription)
└── ExtraKeyButton.kt (add contentDescription)
android/app/src/main/res/
└── values/
└── strings.xml (accessibility strings)
Implementation steps:
- Add
contentDescriptionto all Extra Key Row buttons - Ensure all interactive elements ≥ 48dp × 48dp
- Verify native UI respects system font scaling
- Test with TalkBack enabled
Acceptance criteria:
- TalkBack navigates Extra Key Row buttons with meaningful descriptions
- All native interactive elements ≥ 48dp
- Native UI scales with system font size
- Color contrast meets WCAG AA (4.5:1) for native elements
Effort: 3 days | Dependencies: M2-T4, M2-T5
Files to create / modify:
android/app/src/main/
├── AndroidManifest.xml (intent filter for code files)
└── kotlin/com/vscodroid/
├── MainActivity.kt (intent handling, crash recovery)
└── webview/
└── VSCodroidWebViewClient.kt (onRenderProcessGone)
patches/vscodroid/
└── android-intent.diff ("Open with VSCodroid")
Implementation steps:
- Intent filter for common code file types:
.js,.ts,.py,.html,.css,.json,.md,.go,.rs,.java,.kt,.rb,.c,.cpp,.h - Intent handler: receive file URI → copy to app-private workspace → open in editor
- WebView crash recovery (Tech Spec §4.2):
onRenderProcessGone(): destroy WebView, recreate, reload
- Node.js death recovery: auto-restart + reconnect WebView
- Low memory handling:
onTrimMemory()→ notify VS Code
Acceptance criteria:
- "Open with VSCodroid" appears in Android share/open menus for code files
- File opens in editor after intent
- WebView crash → recovery → editor reloads (no data loss)
- Node.js kill → auto-restart → WebView reconnects
- Low memory signal handled without crash
| # | Criterion | Test Method |
|---|---|---|
| 1 | Comfortable typing with soft keyboard + Extra Key Row | Manual coding session |
| 2 | Multi-page keys (ViewPager2) + GestureTrackpad work | Swipe page + drag cursor |
| 3 | Ctrl+S, Ctrl+P, Ctrl+Shift+P work | Key combination test |
| 4 | Copy/paste works with other apps | Cross-app clipboard test |
| 5 | Keystroke latency < 50ms | Instrumented measurement |
| 6 | Portrait, landscape, split-screen work | Rotation test |
| 7 | TalkBack works for native controls | TalkBack navigation test |
| 8 | Touch targets ≥ 48dp | Layout Inspector measurement |
| 9 | "Open with VSCodroid" works | Share from file manager |
| 10 | Crash recovery works (WebView + Node.js) | Kill process tests |
| 11 | Validated on at least 2 device models | Test on Pixel + Samsung |
Goal: Bundle Python, npm, and essential tools for out-of-the-box development. Duration: 2 weeks (9 working days critical path, 14 person-days total effort) Entry criteria: All M2 gate passed, validated on 2 devices, no P0/P1 regressions
flowchart TD
M3T1["M3-T1: Python + pip from Termux<br/>3 days"] --> M3T3["M3-T3: npm bash functions<br/>2 days"]
M3T2["M3-T2: make cross-compile<br/>2 days"] --> M3T3
M3T4["M3-T4: Pre-bundled extensions<br/>3 days"] --> M3T5["M3-T5: First-run experience<br/>4 days"]
M3T3 --> M3T5
M3T5 --> M3GATE["M3 GATE"]
Effort: 3 days | Dependencies: M2 gate
Files to create:
scripts/
└── download-python.sh (download from Termux APT)
android/app/src/main/
├── jniLibs/arm64-v8a/
│ └── libpython.so (Python binary)
└── assets/
└── usr/lib/python3.12/ (stdlib + pip site-packages)
Implementation steps:
-
Day 1 — Download pre-compiled Python from Termux APT:
- Download
pythonandpython-pippackages from Termux APT repo - Extract
.debpackages, place binary aslibpython.soin jniLibs - Place stdlib in
assets/usr/lib/python3.12/ - Place pip site-packages alongside stdlib
- Download
-
Day 2 — Setup and integration:
- Create symlinks:
python3andpython→libpython.soviasetupToolSymlinks() - Configure
PYTHONPATHandPYTHONHOMEenvironment variables - Test:
python3 --version,python3 -c "import json; print('ok')"
- Create symlinks:
-
Day 3 — pip validation:
- Verify pip works:
pip install requests - Strip
.pycfiles for size reduction - Test: import installed packages
- Verify pip works:
Note: Using pre-compiled Termux packages instead of cross-compiling from source significantly reduces build complexity (3 days vs 5 days). The Termux packages are already patched and tested for ARM64 Android.
Acceptance criteria:
-
python3 --versionworks on device -
pip install requestssucceeds - Python stdlib modules work (os, sys, json, http, etc.)
- Binary size < 35 MB
Effort: 2 days | Dependencies: M2 gate | Parallel with M3-T1
Files to create:
scripts/
└── download-termux-tools.sh
Output: android/app/src/main/jniLibs/arm64-v8a/libmake.so
Implementation steps (see Tech Spec §1.9): Download from Termux packages, similar to Python approach.
Acceptance criteria:
-
make --versionworks on device - Can execute a simple Makefile
Effort: 2 days | Dependencies: M3-T1, M3-T2
Files to modify:
android/app/src/main/kotlin/com/vscodroid/
└── setup/
└── FirstRunSetup.kt (createNpmWrappers function)
Implementation steps:
-
Day 1 — Create npm bash functions:
- Define
npmandnpxas bash functions in.bashrc(not script wrappers) - Functions invoke Node.js with
npm-cli.jsentry point fromusr/lib/node_modules/npm/ - Bash functions required because Android's noexec restriction prevents running script files from app storage
- Create
.npmrcwithscript-shellpointing tolibbash.so
- Define
-
Day 2 — Validation:
- Test:
npm --version,npm init -y,npm install express - Test:
npx create-vite-app test-app - Verify npm cache directory is properly configured
- Test:
Android noexec note: Android mounts app data partitions with
noexec, so traditional npm shim scripts (#!/usr/bin/env node) cannot be executed directly. Bash functions defined in.bashrcbypass this by invoking Node.js with the npm CLI entry point as an argument.
Acceptance criteria:
-
npm --versionworks -
npm init -ycreates package.json -
npm install expressinstalls successfully -
npxworks for running packages
Effort: 3 days | Dependencies: M2 gate | Parallel with M3-T1
Files to create:
android/app/src/main/assets/
└── extensions/
├── pkief.material-icon-theme/
├── esbenp.prettier-vscode/
├── dbaeumer.vscode-eslint/
├── ms-python.python/
├── eamodio.gitlens/
├── bradlc.vscode-tailwindcss/
├── vscodroid.vscodroid-welcome-1.0.0/
├── vscodroid.vscodroid-saf-bridge-1.0.0/
└── vscodroid.vscodroid-process-monitor-1.0.0/
Implementation steps:
-
Day 1 — Download and bundle marketplace extensions:
- Download 6 VSIX files from Open VSX: Material Icon Theme, Prettier, ESLint, Python, GitLens, Tailwind CSS
- Extract to
assets/extensions/directory
-
Day 2 — Develop 3 custom VSCodroid extensions:
vscodroid-welcome: Welcome tab with quick actions ("Open Folder", "Clone Repo", "New File", tool versions)vscodroid-saf-bridge: VS Code UI for SAF folder management (used in M4)vscodroid-process-monitor: Phantom process monitoring UI (used in M4)
-
Day 3 — Integration:
FirstRunSetup.extractBundledExtensions()extracts all 9 extensionsextensions.jsonmanifest auto-generated on first run- Verify extensions load without internet (airplane mode)
Acceptance criteria:
- All 9 extensions load on first launch (airplane mode)
- Theme + icon theme apply correctly
- ESLint activates on JS files
- Custom extensions provide their respective UI
Effort: 4 days | Dependencies: M3-T3, M3-T4
Files to create / modify:
android/app/src/main/kotlin/com/vscodroid/
├── SplashActivity.kt (first-run progress screen)
└── setup/
└── FirstRunSetup.kt (update: extraction + welcome project)
android/app/src/main/res/
└── layout/
└── activity_splash.xml (extraction progress UI)
Implementation steps:
-
Day 1 —
SplashActivity:- Check if first run (shared preferences flag)
- Show progress: percentage-based progress bar with step labels
- Steps flow: create dirs → extract vscode-reh → extract vscode-web → extract tools → setup git → setup symlinks → extract extensions → configure settings
-
Day 2 —
FirstRunSetup.runSetup():- Percentage-based progress reporting via callback
- Create welcome project (
createWelcomeProject) - Configure VS Code settings (terminal profile, git path, etc.)
- Create npm bash function wrappers (
createNpmWrappers)
-
Day 3-4 — Welcome extension + polish:
- Welcome extension provides quick-start tab with tool versions
- Subsequent launches skip extraction (< 5 sec to editor)
- Edge cases: interrupted extraction, reinstall, app update
Acceptance criteria:
- First-run extraction completes with progress UI
- All pre-installed tools work:
node,python3,pip,npm,git,make - Welcome tab appears after first-run setup
- Subsequent launches skip extraction (< 5 sec to editor)
| # | Criterion | Test Method |
|---|---|---|
| 1 | python3 and pip work out of the box | Terminal commands |
| 2 | node and npm work out of the box (npm via bash functions) | Terminal commands |
| 3 | git works out of the box | Terminal commands |
| 4 | make works out of the box | Terminal commands |
| 5 | Pre-bundled extensions load offline (9 extensions) | Airplane mode test |
| 6 | First-run extraction completes with progress UI | Visual inspection |
| 7 | Welcome project and extension provide onboarding | First-run flow test |
| 8 | RAM usage stable after tool usage | dumpsys meminfo |
Goal: Production-quality stability and performance. Duration: 4-5 weeks (21 working days critical path, 24 person-days total effort) Entry criteria: All M3 gate passed, no P0 defects
flowchart TD
M4T1["M4-T1: Error handling + logging<br/>3 days"] --> M4T3["M4-T3: Startup optimization<br/>3 days"]
M4T2["M4-T2: Memory optimization<br/>4 days"] --> M4T3
M4T3 --> M4T4["M4-T4: Phantom process monitoring<br/>3 days"]
M4T4 --> M4T5["M4-T5: Storage management<br/>3 days"]
M4T5 --> M4T6["M4-T6: GitHub OAuth<br/>3 days"]
M4T6 --> M4T7["M4-T7: External storage (SAF)<br/>5 days"]
M4T7 --> M4GATE["M4 GATE"]
Key change from original IP: Error handling/logging is now first (foundational), worker_thread patch moved to M5, extensive testing moved to M6.
Effort: 3 days | Dependencies: M3 gate | Foundational task
Files to create:
android/app/src/main/kotlin/com/vscodroid/
└── util/
├── CrashReporter.kt (in-app crash capture)
└── Logger.kt (debug logging with tag filtering)
Why first? Error handling and logging are foundational — all subsequent M4 tasks benefit from having proper crash capture and logging in place for debugging.
Implementation steps:
-
CrashReporter.kt:
- Initialize in
VSCodroidApp.onCreate()(app-level, catches all crashes) - Local-only, privacy-respecting (no remote telemetry)
- Capture uncaught exceptions + ANR detection
- Store crash logs in app-private directory
- Initialize in
-
Logger.kt:
- Tag-based logging with level filtering (DEBUG, INFO, WARN, ERROR)
- Debug logging toggle in settings
- "Report a Bug" option in settings → generates log bundle
- All VSCodroid components use Logger instead of raw
Log.d()
-
User-facing error messages:
- Map common error codes (E001-E305 from API Spec §7) to human-readable messages
- User sees helpful messages (not stack traces)
Acceptance criteria:
-
CrashReporter.init(this)in Application.onCreate catches crashes - Logger provides tag-filtered logging
- Debug logging toggle works in settings
- Bug report generates useful log bundle
- User-facing error messages for common failures
Effort: 4 days | Dependencies: M3 gate | Parallel with M4-T1
Files to modify:
# Note: VSCodroid patches (extension host, ptyHost, IPC bridge) are applied
# inline in scripts/download-vscode-server.sh, not as .diff files.
android/app/src/main/kotlin/com/vscodroid/
└── MainActivity.kt (onTrimMemory handling)
Implementation steps:
- V8
--max-old-space-sizetuning (512 MB baseline, test 384 MB on 4GB device) - Lazy extension loading: defer non-essential extensions until first use
onTrimMemory()→ signal VS Code to reduce memory (close unused tabs, GC)- Profile WebView heap usage with Chrome DevTools
- Test on 4GB RAM device
Acceptance criteria:
- Idle memory < 300 MB
- Active memory < 700 MB
- Works on 4GB RAM device without OOM
-
onTrimMemoryreduces memory usage measurably
Effort: 3 days | Dependencies: M4-T1, M4-T2
Implementation steps:
- Pre-warm WebView in
Application.onCreate()(throwaway WebView destroy pattern):// Pre-warm: loading the Chromium library is ~200-400ms. // Creating and immediately destroying a throwaway WebView // triggers the library load so the real WebView starts faster. WebView(this).destroy()
- Start Node.js server early (before Activity visible)
- HTTP caching for static assets (
Cache-Control: immutable) - Measure and optimize: target < 5 seconds to editor ready
Acceptance criteria:
- Cold start < 5 seconds on mid-range device
- Warm start < 2 seconds
Effort: 3 days | Dependencies: M4-T3
Files to create / modify:
android/app/src/main/kotlin/com/vscodroid/
└── service/
└── PhantomProcessMonitor.kt (process count tracking)
Implementation steps:
- Implement process counting and monitoring
- Track child processes spawned by Node.js (extension host, terminal sessions)
- Enforce budget: total child processes ≤ 5
- Integrate with
vscodroid-process-monitorextension (from M3) - Aggressive cleanup: kill idle processes when budget exceeded
Acceptance criteria:
- Process monitor tracks all child processes
- Process count stays ≤ 5 in typical use (editor + 3 terminals)
- Process monitor extension shows real-time count
-
adb shell ps -A | grep vscodroidconfirms count
Effort: 3 days | Dependencies: M4-T4
Files to create:
android/app/src/main/kotlin/com/vscodroid/
└── setup/
└── StorageManager.kt (track storage per component)
Implementation steps:
- Track storage usage per component (VS Code, extensions, toolchains, workspace)
- Settings UI: clear caches, show per-component sizes
StorageManager.isStorageLow()→ Toast warning when storage < threshold- Handle low storage gracefully (prevent new installs, suggest cleanup)
Acceptance criteria:
- Storage usage visible in settings per component
- Can clear caches from settings
- Low storage warning appears at threshold
- App doesn't crash on low storage
Effort: 3 days | Dependencies: M4-T5
Files to create / modify:
android/app/src/main/kotlin/com/vscodroid/
└── bridge/
└── AndroidBridge.kt (add startGitHubOAuth)
patches/vscodroid/
└── github-auth-android.diff (OAuth via Chrome Custom Tabs)
Implementation steps:
startGitHubOAuth(scopes, authToken)opens Chrome Custom Tabs- Handle deep link callback:
vscodroid://oauth/github?code=...&state=... - Forward OAuth token to WebView via
window.vscodroid.onOAuthCallback() - VS Code's built-in GitHub auth uses the callback
- Test: login → push/pull to GitHub
Acceptance criteria:
- GitHub OAuth login works via Chrome Custom Tabs
- Deep link callback properly handled
- Git push/pull to GitHub works after auth
- Token persists across app restarts
Effort: 5 days | Dependencies: M4-T6
Files to create:
android/app/src/main/kotlin/com/vscodroid/
└── storage/
├── SafStorageManager.kt (SAF document tree handling)
└── SafSyncEngine.kt (bidirectional sync)
patches/vscodroid/
└── android-fs.diff (SAF bridge for external storage)
Implementation steps:
-
Day 1-2 —
SafStorageManager.kt:- SAF (Storage Access Framework) bridge for files outside app directory
ACTION_OPEN_DOCUMENT_TREEfor folder selection- Persist URI permissions across restarts
- Map SAF URIs to virtual file paths for VS Code
-
Day 3-4 —
SafSyncEngine.kt:- Bidirectional sync between SAF documents and app-private mirror
FileObserverwatches local mirror for changes → write back to SAF- Content provider queries for SAF → local mirror sync
- Handle conflicts gracefully
-
Day 5 — Integration:
vscodroid-saf-bridgeextension (from M3) provides UI- "Open External Folder" option in Welcome tab
- Permission request UX with clear explanation
Acceptance criteria:
- Can open projects from external storage via SAF
- File operations (read/write/create/delete) work on external files
- Changes sync bidirectionally (SAF ↔ local mirror)
- Permission request UX is clear
- URI permissions persist across restarts
| # | Criterion | Test Method |
|---|---|---|
| 1 | Error handling captures crashes locally | Trigger crash → verify log |
| 2 | Startup < 5 seconds on mid-range device | Instrumented timing |
| 3 | Idle memory < 300 MB, active < 700 MB | dumpsys meminfo |
| 4 | Works on 4GB RAM device without OOM | Budget device test |
| 5 | Phantom processes ≤ 5 in typical use | adb shell ps count |
| 6 | GitHub push/pull works | OAuth + git push test |
| 7 | External storage access works via SAF | Open project from /sdcard/ |
| 8 | Storage management shows per-component usage | Settings UI check |
Goal: On-demand toolchain delivery so users can install additional languages beyond the bundled core. Deliverable: Play Asset Delivery integration with Go, Ruby, and Java toolchains, plus a Language Picker UI. Duration: 3 weeks (14 working days critical path, 20 person-days total effort) Entry criteria: All M4 gate passed, no P0 defects
flowchart TD
M5T1["M5-T1: worker_thread ExtHost + ptyHost<br/>6 days"] --> M5T4["M5-T4: Language Picker UI<br/>5 days"]
M5T2["M5-T2: AssetPackManager<br/>4 days"] --> M5T3["M5-T3: On-demand toolchains (Go/Ruby/Java)<br/>5 days"]
M5T3 --> M5T4
M5T4 --> M5GATE["M5 GATE"]
Note
Package Manager (vscodroid pkg) was originally scoped for M5 but has been deferred to the Post-Release Roadmap. Similarly, Rust and C/C++ (Clang) toolchains are deferred to Post-Release — M5 focuses on Go, Ruby, and Java only. Toolchain compatibility verification and APK size audit have been moved to M6 (Release) where they logically belong as verification/release activities.
Effort: 6 days | Dependencies: M4 gate | High complexity
Target VS Code files (see Tech Spec §6.3):
src/vs/workbench/api/node/extensionHostProcess.tssrc/vs/server/node/remoteExtensionHostAgentServer.tssrc/vs/workbench/services/extensions/common/extensionHostEnv.tssrc/vs/platform/extensions/common/extensionHostStarter.ts- ptyHost process launcher (equivalent fork → worker mapping)
Implementation steps:
-
Day 1-2 — Research and prototype:
- Study Extension Host startup code path in VS Code source
- Map
child_process.fork()calls →worker_threads.Workerequivalents - Prototype: replace fork with Worker for Extension Host in isolation
-
Day 3-4 — Implement Extension Host + ptyHost patches:
- Fork → Worker mapping (see Tech Spec §6.3 table)
- Handle message passing:
child.send()→worker.postMessage() - Handle stdio: explicit log forwarding via worker message channel
- Implement crash supervisor: restart with exponential backoff
- Apply same pattern to ptyHost: patch to run as
worker_threads.Worker()with graceful disconnect
-
Day 5 — Crash isolation:
- Worker crash → supervisor restart (max 3 crashes in 60s)
- Budget exceeded → degraded mode + user prompt to disable extensions
- Main server process only restarts if worker recovery fails
-
Day 6 — Validation:
- Extension activation/deactivation parity test vs M4 baseline
- Extensions load correctly under worker_thread mode
- Fault injection: throw error in worker → verify restart
- 2-hour stability test with extensions + terminal + SCM
- Phantom process count: verify -2 vs M4 (ExtHost + ptyHost invisible in
/proc)
Contingency: If patch proves too complex after Day 4, fall back to child_process.fork() with more aggressive process management (see Risk Matrix T08).
Acceptance criteria:
- Extension Host runs as
worker_threads.Worker()instead ofchild_process.fork() - ptyHost runs as
worker_threads.Worker()with graceful disconnect - Extensions activate and deactivate correctly under worker_thread mode
- Worker crash → supervisor restart works
- Phantom process count reduced by 2 vs M4 (ExtHost + ptyHost)
- 2-hour stability test passes
Effort: 4 days | Dependencies: M4 gate | Parallel with M5-T1
Files to create:
android/app/src/main/kotlin/com/vscodroid/
└── setup/
└── ToolchainManager.kt (asset pack management)
Implementation steps:
- Integrate Play Asset Delivery (PAD) SDK
- Implement
listAvailable(),install(),uninstall(),isInstalled() - Handle download progress, error, retry
- Asset pack extraction to
$PREFIX/lib/<toolchain>/ - Sideload detection: if not from Play Store, use bundled assets or package manager
Acceptance criteria:
- AssetPackManager integration works
- Download progress tracking works
- Sideload path works (direct asset extraction)
Effort: 5 days | Dependencies: M5-T2
Files to create:
toolchain_go/
├── build.gradle.kts (asset pack module)
└── src/main/assets/ (Go ARM64 binaries, ~179 MB)
toolchain_ruby/
├── build.gradle.kts
└── src/main/assets/ (Ruby + libgmp + libyaml, ~34 MB)
toolchain_java/
├── build.gradle.kts
└── src/main/assets/ (OpenJDK 17 + shmem + spawn, ~146 MB)
scripts/
├── download-go.sh (download from Termux golang package)
├── download-ruby.sh (download from Termux ruby + deps)
└── download-java.sh (download from Termux openjdk-17 + deps)
android/settings.gradle.kts (register asset pack modules)
Note
M5 focuses on Go, Ruby, and Java only. Additional toolchains (Rust ~100 MB, C/C++ Clang ~84 MB) are deferred to the Post-Release Roadmap and will use the same Play Asset Delivery pipeline.
Implementation steps:
-
Day 1-2 — Prepare toolchain asset packs:
- Go from Termux
golangpackage (179 MB asset pack,CGO_ENABLED=0) - Ruby from Termux
ruby+ libgmp + libyaml (34 MB asset pack) - Java from Termux
openjdk-17+ libandroid-shmem + libandroid-spawn (146 MB asset pack) - Each script: download .deb → extract → place in asset pack module → strip → write manifest
- Package each as Android App Bundle asset pack module
- Configure
build.gradle.ktswithassetPacktype
- Go from Termux
-
Day 3 — Asset pack extraction:
- Extract toolchain to
$PREFIX/lib/<toolchain>/ - Configure PATH and environment variables
- Create symlinks in
$PREFIX/bin/
- Extract toolchain to
-
Day 4-5 — Integration:
- Wire up
AndroidBridge.ktJS bridge for install/uninstall/query from extensions - Configure
.bashrcto sourcetoolchain-env.shfor terminal PATH/env updates Environment.kt: dynamic toolchain env vars merged into server processToolchainRegistry: catalog of available toolchains with sizes
- Wire up
Acceptance criteria:
- Toolchain asset pack modules build correctly (Go, Ruby, Java)
- ToolchainManager handles full lifecycle (install, uninstall, env vars, symlinks)
- AndroidBridge JS bridge for toolchains works
-
.bashrcsourcestoolchain-env.shfor PATH updates
Note
Actual device-level toolchain verification (go version, ruby --version, java -version) is performed in M6 Phase 2 (Device Testing), not here.
Effort: 5 days | Dependencies: M5-T3, M5-T1
Files to create:
android/app/src/main/kotlin/com/vscodroid/
├── ToolchainActivity.kt (settings screen for toolchain management)
└── setup/
└── ToolchainPickerAdapter.kt (shared adapter: PICKER + MANAGER modes)
android/app/src/main/res/layout/
├── layout_toolchain_picker.xml (first-run picker grid)
├── layout_toolchain_progress.xml (download progress view)
├── item_toolchain_card.xml (card for each toolchain)
└── activity_toolchain.xml (settings screen layout)
Implementation steps:
-
Day 1-2 — First-run Toolchain Picker (integrated in
SplashActivity):showToolchainPicker()displays grid of available toolchains after first-run setupToolchainPickerAdapter(ToolchainCardMode.PICKER): tap toggles checkmark, shows size per toolchain- "What do you code in?" title with Continue + Skip buttons
shouldShowPicker()/markPickerShown()via SharedPreferences
-
Day 3 — Download Progress Phase:
startDownloads()— sequential per-pack download with progress rows- Per-pack progress bar, status text, cancel button
- Handles
AssetPackStatus.DOWNLOADING,COMPLETED,FAILED,REQUIRES_USER_CONFIRMATION - Failed packs skip to next; all done → launch
MainActivity
-
Day 4 — Settings > Toolchains (
ToolchainActivity):ToolchainPickerAdapter(ToolchainCardMode.MANAGER): shows installed/available/downloading state- Action buttons: Install, Remove (with confirmation dialog), Cancel, Retry
- Opened from
AndroidBridge.openToolchainSettings() - Refreshes installed state on
onStart()
-
Day 5 — Polish:
- Toolchain cards with MaterialCardView, checkmarks, progress bars, status badges
- Handle edge cases: partial download, interrupted install, remove confirmation
- String resources for all UI text
Acceptance criteria:
- First-run picker shows "What do you code in?" with language grid
- Can select and install toolchains on-demand with progress UI
- Settings > Toolchains allows installing/removing with confirmation
- Size display per toolchain before download
- Error handling (retry, cancel, skip failed) works correctly
| # | Criterion | Test Method |
|---|---|---|
| 1 | Extension Host runs as worker_thread (phantom process count reduced) | Extension activation test + adb shell ps |
| 2 | ptyHost runs as worker_thread (additional phantom process saved) | adb shell ps comparison |
| 3 | On-demand toolchains delivered via Play Asset Delivery (Go, Ruby, Java) | Install Go or Ruby |
| 4 | ToolchainManager handles full lifecycle (install, uninstall, env vars, symlinks) | Lifecycle test |
| 5 | Language Picker UI works during first-run and from Settings | SplashActivity + ToolchainActivity test |
| 6 | 2-hour stability test passes with worker_thread | Endurance test |
Goal: Launch on Google Play Store. Deliverable: Published app with proper branding, legal compliance, and documentation. Duration: 6 weeks working effort (29 working days critical path, 49 person-days total) + 2 weeks closed testing (14 calendar days per Google Play policy) Entry criteria: All M5 gate passed, release candidate build completed with signed AAB, store listing/privacy policy/compliance artifacts ready
Ordered by dependency: fix bugs → verify features → harden → brand → ship.
flowchart TD
subgraph P1["Phase 1 — Bug Fixes & Feature Completion"]
M6T1["M6-T1: Stability & auth fixes<br/>3 days"] --> M6T2["M6-T2: SSH key management<br/>3 days"]
M6T2 --> M6T3["M6-T3: App upgrade handling<br/>4 days"]
end
subgraph P2["Phase 2 — Testing & Hardening"]
M6T4["M6-T4: Device testing + toolchain verify<br/>7 days"]
M6T5["M6-T5: Security review<br/>3 days"]
M6T6["M6-T6: Release build & signing<br/>3 days"]
M6T7["M6-T7: AAB build + size audit<br/>3 days"]
M6T4 --> M6T5
M6T5 --> M6T6
M6T6 --> M6T7
end
subgraph P3["Phase 3 — Branding & Store Presence"]
M6T8["M6-T8: Branding<br/>5 days"]
M6T9["M6-T9: Legal compliance<br/>2 days"]
M6T10["M6-T10: Documentation<br/>3 days"]
M6T11["M6-T11: CI/CD pipeline<br/>10 days"]
M6T8 --> M6T9
M6T9 --> M6T10
end
subgraph P4["Phase 4 — Ship"]
M6T12["M6-T12: Play Store listing<br/>3 days"]
M6T13["M6-T13: Launch<br/>37 days"]
M6T12 --> M6T13
end
P1 --> P2
P2 --> P4
P3 --> P4
M6T11 --> P4
M6T13 --> M6GATE["M6 GATE"]
Effort: 3 days | Dependencies: M5 gate | (discovered during device testing)
Implementation steps:
- Extension OAuth callback relay: Chrome Custom Tabs → Android Intent → WebView (
vscodroid://callback) - Persist extension secrets across app restarts: patch
isEncryptionAvailable()→truein workbench.js soSecretStorageServiceuses IndexedDB instead of in-memory Map - White screen on app reopen:
isServerHealthy()(synchronous HTTP) threwNetworkOnMainThreadExceptionon the main thread when reconnecting to an already-running server. What shipped isNodeService.isServerReady(), which reports what the health probe already found and costs no I/O. Not a process-liveness check:Process.isAliveis true from the moment the process is spawned and for the whole of a post-crash restart, so navigating on it points the WebView at a port with nothing listening. The liveness wrapper has been removed fromNodeServiceandServerReadinessCallSiteTestfails the build if it comes back - Mobile menu CSS: touch-friendly hamburger dropdown (44px touch targets, 14px font, 280px min-width) appended to workbench.css
- Keyboard/ExtraKeyRow positioning: fix double-compensation (adjustResize + bottomMargin). Switch to edge-to-edge (
setDecorFitsSystemWindows=false) with manual insets padding for consistent behavior on Android 13-16
Acceptance criteria:
- Extension OAuth callback relay works end-to-end
- Extension secrets persist across app restarts (OAuth tokens, API keys)
- App recovers cleanly from close/reopen (no white screen)
- Mobile menu dropdown is touch-friendly (44px targets)
- Extra Key Row positions correctly with keyboard on Android 13-16
Effort: 3 days | Dependencies: M6-T1
Files to create / modify:
android/app/src/main/kotlin/com/vscodroid/
└── bridge/
└── SshKeyBridge.kt (SSH key generation + management)
Implementation steps:
- Bundle OpenSSH client (
libssh.so,libssh-keygen.so) with all deps (ldns, krb5, libdb, libresolv-wrapper) - SSH config with absolute paths (Termux openssh resolves
~to compiled-in prefix, not$HOME) GIT_SSH_COMMANDenv var configured inEnvironment.kt- Generate SSH key pair from within app (command palette + AndroidBridge)
- UI to copy public key (command palette → clipboard)
- SSH connectivity to GitHub verified (key auth handshake works)
Acceptance criteria:
- SSH key generation works (
ssh-keygen -t ed2519) - SSH config uses absolute paths for Termux compatibility
-
GIT_SSH_COMMANDconfigured in environment - Public key viewable and copyable to clipboard
- Git clone/push/pull via SSH works
- SSH connectivity to GitHub verified
Effort: 4 days | Dependencies: M6-T2
Implementation steps:
- Detect app version change on launch (compare stored version vs current)
- Re-extract updated assets on upgrade (vscode-reh, usr/lib, extensions) without losing user data
- Migrate SSH config, .bashrc additions, settings.json across upgrades
- Handle stale symlinks after APK reinstall (nativeLibraryDir path changes)
Acceptance criteria:
- App upgrade preserves user data (settings, extensions, SSH keys, projects)
- Updated assets re-extracted correctly on version change
- No stale symlinks after upgrade
Effort: 7 days | Dependencies: M6-T3
Implementation steps:
- Device matrix: OnePlus CPH2791 (flagship, 16 GB), POCO 22071219CG (budget, 4 GB), Redmi 2201117TY (mid-range, 6 GB)
- Android version matrix: Android 13 (API 33, Redmi) + Android 14 (API 34, POCO) + Android 16 (API 36, OnePlus)
- Stress tests: 12,720-line JS file opens with syntax highlighting; 1,100-file project loads in explorer (lazy virtualized); memory stable at 143-145 MB PSS; zero crashes
- Extension tests: 10 bundled extensions activate, Welcome walkthrough renders, theme picker works
- Lifecycle tests: background/foreground (server survives), rotation (adapts), force-stop + cold restart (recovers)
- Run all E2E tests (E2E-01 through E2E-14)
- Run all performance tests (Testing Strategy §3.4)
- Run backup & restore tests (Testing Strategy §3.8)
- SSH key and GitHub OAuth flow testing
- Worker_thread verification (validates M5 task 1):
- Extension Host runs as worker_thread — only 1 phantom (server-main), no ExtHost in
ps - ptyHost runs as worker_thread — not visible in process list, baseline 1 phantom process
- Extensions activate correctly under worker_thread mode — 10 extensions loaded
- Extension Host runs as worker_thread — only 1 phantom (server-main), no ExtHost in
- Toolchain compatibility verification (OnePlus CPH2791, Android 16):
-
go version→ Go 1.25.6 android/arm64; hello world compile+run ✓ -
ruby --version→ Ruby 3.4.1;irbeval ✓;gem --version3.6.2 ✓ -
java -version→ OpenJDK 17.0.18;javac✓; hello world compile+run ✓ - Verify toolchains persist across app restarts — all 3 survive force-stop+restart
- Verify uninstall cleans up correctly — symlinks removed, installRoots deleted, libs cleaned, core tools intact
- Issues found and fixed: Go tool binaries need chmod +x (added to manifest binaries); Ruby needs
libandroid-execinfo.sodep,RUBYLIBenv var, versioned soname symlink (libruby.so.3.4), and bash wrapper functions for scripts (noexec /data)
-
- Memory: OnePlus 131 MB PSS, POCO 167 MB PSS, Redmi 142 MB PSS at idle (app + Node.js server)
Acceptance criteria:
-
Tested on 3 device models across Android 13-16
-
Performance targets met (startup < 5s, keystroke < 50ms, memory < 700MB)
-
Phantom processes ≤ 5 in typical use
-
No crash in 2 hours continuous use
-
Go/Ruby/Java verified working on physical device after asset pack install
Effort: 3 days | Dependencies: M6-T4
Implementation steps:
- Audit WebView security: CSP headers, JS bridge exposure, localhost-only binding
- Verify no secrets in APK (no API keys, tokens, or private keys bundled)
- Review all workbench.js patches for unintended side effects
- Confirm Android app sandbox isolation (no world-readable files)
- Validate
SecurityManagerURL allowlist (only localhost + known CDN patterns) — not what shipped; the allowlist was removed rather than relaxed, and no destination filter replaced it - Fix: restrict cleartext HTTP to localhost only (
network_security_config.xml) — reversed deliberately. The shipped config is<base-config cleartextTrafficPermitted="true" />: the format matches<domain>entries by hostname and understands neither CIDR nor ranges, so "any private address" cannot be written, and the addresses change with the network the device joins - Fix: use
Uri.parse()for exact localhost host matching (prevents domain spoofing) — shipped, and still in force. It decides which URLs the WebView keeps rather than gating an allow-list - Fix: owner-only execute permissions on extracted binaries
Acceptance criteria:
- Security review completed (no exposed secrets, sandbox intact)
- WebView CSP headers — not ours to configure. The workbench ships its own policy and nothing in
this app's Kotlin or assets sets one. What this project does do is widen it: patch 0005
adds
'unsafe-inline'toscript-srcbecausescript-srcpins the document's inline script by sha256, patching that script changes its digest, and the build does not recompute the hash.default-srcstays'none'andframe-srcstays'self'. Recorded here because it is a deliberate relaxation and this list is where someone would look for it - No URL allowlist in
SecurityManager— there are no entries to validate. A development environment has to reach a LAN dev server, a private registry and a staging host, so the destination is deliberately unjudged; the session token is what is checked - Cleartext HTTP is permitted app-wide, not restricted to localhost — a dev server is served over plain HTTP at the device's own address, and this file cannot express an address range
- Localhost matching uses
Uri.parse()for exact host comparison - Extracted binaries have owner-only execute permissions — all three sites that grant execute
(
FirstRunSetupgit-core,ToolchainManagerinstall and its repair pass) callsetExecutable(true, true); there is no plainsetExecutable(true)anywhere in the tree, and the onlyOs.chmodcalls are0700on.sshand0600on the ssh config.⚠️ The bit is not what constrains execution here. SELinux refusesexecveon anything underfilesDirwhatever its mode —markExecutablesIn's own documentation says so, and the tree records the measurements: a device reportingcannot exec 'git-remote-https': Permission denied, and a valid ELF at0755failingEACCES, identically through a symlink, because the check is on the resolved inode's label rather than the path. That is why binaries ship innativeLibraryDirand whynpm/npxare shell functions. Ticking this line without that leaves a reader right about the fact and wrong about the world. Not measured either way: whether every extracted binary passes through one of the three setters, or whether a mode carried in from a ZIP can reach disk untouched
Effort: 3 days | Dependencies: M6-T5
Implementation steps:
- Generate release signing keystore (store securely, NOT in repo)
- Configure
signingConfigs.releasein build.gradle.kts - Enable R8/ProGuard minification for Kotlin code
- Test release build on device (ProGuard can break reflection-based code)
- Verify
useLegacyPackaging = truepreserved in release build
Acceptance criteria:
- Release build signed and runs correctly on device
- R8/ProGuard minification doesn't break functionality
-
useLegacyPackaging = trueconfirmed in release build
Effort: 3 days | Dependencies: M6-T6
Implementation steps:
- Build release AAB (signed) — 253 MB total (includes on-demand packs)
- Measure base APK size (without toolchains) — 133 MB download, 138 MB APK (< 150 MB target)
- Verify per-device delivery sizes via bundletool — 133 MB for ARM64 SDK 33+
- Document per-toolchain on-demand sizes — Go 163 MB, Ruby 29 MB, Java 146 MB
- Test HTTP fallback download flow end-to-end — all 3 toolchains verified on OnePlus CPH2791:
- Ruby: 9 MB ZIP, 3.9s total (download 2.9s + extract 0.5s + install 0.3s)
- Go: 53 MB ZIP, ~10s total (download 6.3s + extract 2.3s + install 1.1s)
- Java: 55 MB ZIP, ~11s total (download 9.2s + extract 1.6s + install 0.3s)
- Updated manifests with RUBYLIB, scriptWrappers, libSymlinks all functioning
- Test Play Asset Delivery flow on internal testing track
Acceptance criteria:
- Release AAB signed and < 150 MB (base: 133 MB download, 138 MB APK)
- Per-device delivery sizes verified via bundletool
- HTTP fallback download works for all 3 toolchains
- Per-toolchain sizes documented
Effort: 5 days | Dependencies: M5 gate | Parallel with Phase 1
Files to create:
android/app/src/main/res/
├── drawable/
│ ├── ic_launcher.xml (original icon, NOT VS Code's)
│ ├── ic_launcher_round.xml
│ └── ic_launcher_foreground.xml
├── mipmap-*/ (icon sizes)
docs/
└── screenshots/ (Play Store screenshots)
Implementation steps:
- Design VSCodroid icon/logo (original, not VS Code's)
- Adaptive icon for Android 13+ (foreground + background layers)
- App screenshots for Play Store (phone + tablet)
- Feature graphic (1024×500)
Acceptance criteria:
- Original VSCodroid icon designed (not VS Code's blue icon)
- Adaptive icon layers created
- Phone + tablet screenshots generated
- Feature graphic ready
Effort: 2 days | Dependencies: M6-T8
Files to create:
docs/
└── PRIVACY_POLICY.md
android/app/src/main/res/
└── values/
└── strings.xml (legal disclaimers)
Implementation steps:
- Disclaimer in app About screen
- Privacy policy (required for Play Store)
- MIT license notice for VS Code source
- Trademark disclaimers:
Built from MIT-licensed Code-OSS source code. Not affiliated with or endorsed by Microsoft Corporation. "Visual Studio Code" and "VS Code" are trademarks of Microsoft. Uses Open VSX extension registry, not Microsoft Marketplace.
Effort: 3 days | Dependencies: M6-T9
Implementation steps:
- README.md with project overview
- CONTRIBUTING.md for contributors
- User guide: first-run, keyboard shortcuts, extensions
- Known limitations and FAQ
Effort: 10 days | Dependencies: M5 gate | Parallel with Phase 1
Files to create:
.github/workflows/
├── build.yml (debug APK on PR/push + unit tests)
├── lint.yml (Android lint on PR with baseline)
├── release.yml (tag → sign APK → toolchain ZIPs → GitHub Release)
└── pages.yml (docs site deployment)
Implementation steps (see Release Plan §2):
- build.yml: build debug APK on PR/push + unit tests with minimal stubs + download caching
- lint.yml: Android lint on PR with baseline, upload lint report as artifact
- release.yml: tag → sign APK → package toolchain ZIPs (
scripts/package-toolchains.sh) → GitHub Release with checksums - pages.yml: docs site deployment to GitHub Pages
libnode.sofetching:LIBNODE_URLsecret → current tag release → latest release → stub fallback- Caching strategy for Node.js binary, VS Code build, Gradle, tarballs + generated assets
- Secrets management: keystore env vars (
VSCODROID_KEYSTORE_*),LIBNODE_URL - CI fix: node-pty subshell path resolved with
ROOT_DIR— Build + Unit Tests green - CI fix: release workflow — remove AAB build (needs toolchain asset packs), fix libnode.so download (try current tag first), remove deprecated
api-levelparam — Release workflow green - Build toolchain ZIPs and upload as GitHub Release assets — fallback download URL served from GitHub Releases
- Automated testing on Firebase Test Lab (physical ARM64 devices)
Effort: 3 days | Dependencies: M6-T7, M6-T10, M6-T11 | (requires Google Play Developer account)
Implementation steps:
- Title: "VSCodroid"
- Short description + full description with feature list
- Screenshots: phone + tablet (from M6-T8)
- Category: Developer Tools
- Content rating questionnaire
- Foreground service permission declaration (video demo + written justification)
- Prepare for binary execution policy review (explain .so trick, local-only execution)
Effort: 14 calendar days | Dependencies: M6-T12
Internal testing track:
- AAB uploaded (versionCode 2)
- 100% M5 exit criteria pass
- Zero open S1/S2 bugs
- Crash-free rate ≥ 95% for 7 consecutive days
Closed testing track:
- AAB v0.2.2-m6 (versionCode 4) uploaded, 12 testers added, 14-day countdown started
- No new S1 bugs in last 7 days
- ANR rate < 0.5%
- Extension install success ≥ 90%
Open beta (after 14-day closed testing period):
- At least 500 beta testers
- Crash-free rate ≥ 95% for 14 days
- ANR rate < 0.5%
- Play policy checklist complete
Production launch (3 days):
- Staged rollout: 5% → 10% → 25% → 50% → 100% (see Release Plan §6.2)
- Halt conditions: crash-free < 90%, S1 reported by multiple users, ANR > 1%
- Monitor crash reports (CrashReporter) and user feedback
- Hotfix pipeline for critical bugs
- Post-launch: respond to Play Store reviews
| # | Criterion | Test Method |
|---|---|---|
| 1 | Extension secrets persist across app restarts (OAuth tokens, API keys) | Token persistence test |
| 2 | App recovers cleanly from close/reopen (no white screen) | Close/reopen cycle |
| 3 | SSH push/pull to GitHub works end-to-end | SSH key + git test |
| 4 | App upgrade preserves user data (settings, extensions, SSH keys, projects) | Upgrade simulation |
| 5 | Security review completed (no exposed secrets, sandbox intact) | Security checklist |
| 6 | Tested on 3 device models across Android 13-16 | Device matrix |
| 7 | Go/Ruby/Java verified working on physical device after asset pack install | go version, ruby --version, java -version |
| 8 | Release AAB signed and < 150 MB (base, without toolchains) | APK/AAB file size check |
| 9 | App published on Play Store | Play Console |
| 10 | Passes Play Store review (no policy violations) | Play Console |
| 11 | No critical bugs in first 48 hours | Crash monitoring |
| 12 | CI pipeline green (Build + Unit Tests + Release workflows) | GitHub Actions |
| 13 | At least 500 beta testers before production launch | Play Console stats |
Tests are written alongside implementation, not as a separate phase.
| Milestone | Tests to Write | Framework |
|---|---|---|
| M0 | ProcessManager, Environment, PortFinder | JUnit 5 + MockK |
| M1 | FirstRunSetup, server bootstrap (Jest) | JUnit 5 + Jest |
| M2 | ExtraKeyRow, KeyInjector, ClipboardBridge, AndroidBridge security | JUnit 5 + Espresso |
| M3 | npm bash functions, extension extraction | JUnit 5 |
| M4 | StorageManager, CrashReporter, SafSyncEngine | JUnit 5 |
| M5 | ToolchainManager, ToolchainPickerAdapter, worker_thread supervisor | JUnit 5 + Jest |
| M6 | CI pipeline validation, E2E test suite | Espresso + Firebase Test Lab |
Coverage targets: Kotlin ≥ 80%, JS ≥ 70%
| Milestone | Documentation Task |
|---|---|
| M0 | Update Dev Guide §3 with actual build steps |
| M1 | Document patch application process, known issues |
| M2 | User guide: keyboard shortcuts, Extra Key Row, GestureTrackpad |
| M3 | User guide: first-run setup, npm usage, extension management |
| M4 | Performance tuning guide, SAF troubleshooting |
| M5 | Toolchain management guide |
| M6 | README final update, CHANGELOG, release notes, user guide |
| Milestone | Security Task |
|---|---|
| M1 | Verify server binds localhost only, no telemetry requests |
| M2 | Verify AndroidBridge origin + token enforcement |
| M3 | Verify extension extraction integrity |
| M4 | SAF permission scoping, OAuth token security, backup exclusion test |
| M5 | Verify toolchain integrity (signed asset packs), worker_thread isolation |
| M6 | Full security testing checklist (Security §7), APK signature, dependency scan |
Embedded go/no-go decisions throughout the timeline:
| Week | Checkpoint | Go Condition | No-Go Action |
|---|---|---|---|
| M0 Week 1 Day 3 | Node.js compiles? | ARM64 ELF produced | Activate Termux binary fallback (T02) |
| M0 Week 2 | Node.js runs on device? | HTTP server responds | Re-evaluate architecture |
| M1 Week 1 | code-server patches apply? | All patches apply cleanly | Pin older VS Code version (T03) |
| M1 Week 2 | VS Code renders in WebView? | Workbench loads | Debug WebView/server communication |
| M1 Week 3 | node-pty works? | PTY creation succeeds | Fallback terminal mode (T06) |
| M2 Week 1 | Keystroke latency OK? | < 50ms | Profile and optimize key injection path |
| M3 Week 1 | Python from Termux works? | Runs on device | Cross-compile from source (T10) |
| M4 Week 2 | SAF sync engine stable? | Bidirectional sync works | Fall back to MANAGE_EXTERNAL_STORAGE |
| M5 Week 1 | worker_thread patch works? | Extensions activate | Fall back to child_process.fork (T08) |
| M6 Week 3 | Play Store accepts? | No rejection | Prepare appeal + APK distribution (P01) |
Total new files created across all milestones:
| Category | Approx Count | Key Files |
|---|---|---|
| Kotlin sources | ~30 files | MainActivity, NodeService, ProcessManager, AndroidBridge, ExtraKeyRow, GestureTrackpad, ToolchainManager, SafStorageManager, SshKeyBridge, CrashReporter, etc. |
| Layouts (XML) | ~8 files | activity_main, activity_splash, activity_toolchain, layout_toolchain_picker, layout_toolchain_progress, item_toolchain_card, |
| Build/download scripts | ~13 files | download-node.sh, download-python.sh, download-go.sh, download-ruby.sh, download-java.sh, build-git.sh, build-bash.sh, build-tmux.sh, build-node-pty.sh, download-termux-tools.sh, build-all.sh, apply-patches.sh, deploy.sh |
| VS Code patches | ~19 files | 9 code-server patches + 10 VSCodroid patches (incl. ext-host-worker, pty-host-worker, android-fs) |
| Gradle configs | ~6 files | root + app + 3 asset pack modules (Go, Ruby, Java) + settings |
| CI workflows | 4 files | build.yml, lint.yml, release.yml, pages.yml |
| Test files | ~15+ files | Unit tests (JUnit), integration tests (Espresso), E2E tests, test fixtures |
| Assets | ~6 dirs | vscode-web/, vscode-reh/, usr/lib/python3.12/, extensions/ (9 extensions), server.js |
| Native binaries (.so) | 8 files | libnode, libpython, libgit, libbash, libtmux, libmake, libc++_shared, libnode_pty |
| Custom extensions | 3 dirs | vscodroid-welcome, vscodroid-saf-bridge, vscodroid-process-monitor |
Every task is "done" when:
- Code complete: all listed files created/modified
- Tests written: unit tests for new Kotlin/JS code
- Tests pass: all existing tests still pass (no regression)
- Runs on device: verified on at least 1 physical ARM64 device
- No S1/S2 bugs: no critical or major bugs introduced
- Documented: any deviation from spec documented
- Committed: code committed with conventional commit message
| Topic | Primary Document | Section |
|---|---|---|
| Product requirements | 01-PRD | Features §4, Priorities §5 |
| Functional requirements | 02-SRS | FR §3, NFR §4 |
| Architecture decisions | 03-ARCHITECTURE | ADRs §4 |
| Build system details | 04-TECHNICAL_SPEC | §1-§3 |
| API interfaces | 05-API_SPEC | Bridge §2, Process §4 |
| Security controls | 06-SECURITY | §3 |
| Test plans | 07-TESTING_STRATEGY | §3 |
| Risk mitigations | 08-RISK_MATRIX | §3 |
| Dev environment | CONTRIBUTING | Setup, Assets, Building |
| Release process | 10-RELEASE_PLAN | §1-§6 |
| Milestone criteria | MILESTONES | All |