Phone management — deploy services and control Samsung S23 Ultra Android device via ADB and SSH from Mac. Trigger when user mentions phone, Android, Termux, nix-on-droid, mobile device, or deploying to phone.
Never modify the phone directly. All persistent changes must go through:
nix/phone.nix + just deploy) for services and configsAd-hoc SSH/ADB commands that READ state are fine. Commands that CHANGE state require explicit user confirmation. An agent ran adb shell svc wifi disable during this project and bricked all connectivity, requiring physical phone recovery. The deny-list exists because of this.
Three access methods with different capabilities:
| Method | Address | UID | Use for |
|---|---|---|---|
| SSH (Termux) | ssh [email protected] -p 8022 |
| app (10414) |
| Service management, file deploy, scripting |
| SSH (nix-on-droid) | ssh [email protected] -p 8023 | nix-on-droid app UID | Nix operations, nix copy target, nix-on-droid switch |
| ADB wireless | adb connect 192.168.1.5:<port> | shell (2000) | Screen, input, app management, permissions, system settings |
ADB port changes on every reboot. Pairing is permanent (RSA key burned in). After reboot: user re-enables Wireless Debugging toggle and provides new port.
Critical behavioral difference: am start from SSH runs in background and cannot foreground apps. From ADB shell it can. Use ADB when you need an app to appear on screen.
Termux and nix-on-droid are separate Android apps with isolated storage:
| Termux | nix-on-droid | |
|---|---|---|
| Package | com.termux | com.termux.nix |
| HOME | /data/data/com.termux/files/home | /data/data/com.termux.nix/files/home |
| PREFIX | /data/data/com.termux/files/usr | /data/data/com.termux.nix/files/usr |
| Nix | None | 2.18.1, flakes, /nix/store via proot |
| SSH | port 8022 (working) | port 8023 (working, user nix-on-droid) |
| Services | runit (sshd, ssh-agent) | activation scripts via nix-on-droid switch |
| Boot | Termux:Boot runs ~/.termux/boot/ scripts | No boot integration yet |
| Packages | pkg (apt/dpkg) | nix-env / flakes |
| Config | stow + dotfiles | ~/.config/nix-on-droid/ (flake.nix + nix-on-droid.nix) |
They cannot access each other's private storage. File transfer between them:
adb push file /data/local/tmp/ then copy from within target app's terminal/sdcard/ works for Termux (has storage permission). nix-on-droid needs adb shell pm grant com.termux.nix android.permission.READ_EXTERNAL_STORAGE first.# Screen (wake screen first!)
adb shell input keyevent KEYCODE_WAKEUP # MUST do this before screencap or screen is black
adb exec-out screencap -p > file.png
adb shell screenrecord --time-limit 5 /sdcard/out.mp4
# Input
adb shell input tap 540 1158 # tap coordinates
adb shell input swipe 100 800 100 200 300 # swipe (x1 y1 x2 y2 duration_ms)
adb shell "input text 'hello world'" # type text (outer quotes matter for spaces)
adb shell input keyevent 66 # ENTER
adb shell input keyevent 67 # BACKSPACE
adb shell input keyevent KEYCODE_HOME
adb shell input keyevent KEYCODE_BACK
# Apps
adb shell am start -a android.intent.action.VIEW -d "https://example.com"
adb shell am start -n com.termux/.app.TermuxActivity
adb shell am force-stop org.mozilla.firefox
adb install app.apk # silent install
# Inspection
adb shell pm list packages # 814 packages on this device
adb shell dumpsys battery # battery health, temp, ASOC
adb shell dumpsys package com.termux # package details, activities
adb shell uiautomator dump /sdcard/ui.xml # full UI hierarchy as XML
adb shell logcat -d -t 100 # last 100 log lines (system-wide)
adb shell settings list global # all global settings
adb shell getprop ro.build.version.release # system properties
# Permissions & settings
adb shell pm grant pkg android.permission.READ_EXTERNAL_STORAGE
adb shell appops set com.termux SYSTEM_ALERT_WINDOW allow
adb shell settings put global verifier_verify_adb_installs 0
# File transfer
adb push local_file /data/local/tmp/ # staging area (ADB-writable)
adb forward tcp:8080 tcp:9999 # port forwarding
svc wifi disable/enable # KILLS ALL CONNECTIVITY — learned this the hard way
svc data disable # kills mobile data
pm clear <package> # permanent data wipe
am force-stop com.termux # kills SSH, runit, all services
settings put secure ... # security settings
rm (anything outside ~/.phone-services/)
wm size/density (write) # changes display
If SSH dies but ADB works:
# 1. Wake screen (screencap and input need this)
adb shell input keyevent KEYCODE_WAKEUP
# 2. Bring Termux to foreground
adb shell am start -n com.termux/.app.TermuxActivity
# 3. Wait for terminal to be ready, tap it
sleep 2 && adb shell input tap 540 1158
# 4. Type sshd and press enter
adb shell "input text 'sshd'" && adb shell input keyevent 66
# 5. Verify
sleep 2 && ssh -o ConnectTimeout=5 [email protected] -p 8022 'echo OK'
Gotcha: The terminal might have a pending prompt (e.g., package config question). Take a screenshot first to check: adb exec-out screencap -p > /tmp/check.png
sshd is normally started by ~/.termux/boot/termux-services on device boot (via Termux:Boot). It runs runsvdir which supervises sshd. This only fires on device boot, not app relaunch.
Mac: nix build .#packages.aarch64-linux.phoneProfile
-> rsync --delete to phone:~/.phone-services/
-> manifest diffing removes stale, registers new services
-> runit supervises at $PREFIX/var/service/
Edit nix/phone.nix, add to the services attrset:
services = {
my-service = mkService {
name = "my-service";
script = ''
exec some-command --flag
'';
};
};
Then: just -f termux/justfile deploy [email protected]
Scripts use Termux tools only (no Nix store paths). Shebang is auto-set to Termux bash.
nix/phone.nix — service definitions with mkService helperflake.nix — exposes packages.aarch64-linux.phoneProfile (built on darwin)termux/justfile — deploy recipe (build + rsync + converge via manifest diff)chmod 700 (matching sshd/ssh-agent)supervise/ inside symlinked dirs from Nix store — deploy copies, not symlinksrunsv start if runsvdir doesn't pick them up; restarting runsvdir: kill -HUP $(pgrep runsvdir)nix copy from Mac to phone works:
nix build .#packages.aarch64-linux.phoneProfile
nix copy --to ssh://[email protected]:8023 ./result
nix-on-droid supports flakes: nix-on-droid switch --flake ~/.config/nix-on-droid
Config lives at ~/.config/nix-on-droid/flake.nix + nix-on-droid.nix on the nix-on-droid side.
SSH PATH note: nix-on-droid's ~/.bashrc sources /etc/profile — this is required for nix copy and any SSH command that uses Nix binaries.
sshd not yet auto-starting on boot. Currently started manually via bash /sdcard/start-sshd-bg.sh from within nix-on-droid terminal. Needs to be declared in nix-on-droid.nix activation.
Target architecture (next phase):
Mac: lib.evalModules -> nix copy -> phone
Phone: nix-on-droid for config switching, deploy-rs for activation
Rollback: generational symlinks via deploy-rs
# Disable phantom process killer (fixes runit supervision)
adb shell device_config set_sync_disabled_for_tests persistent
adb shell device_config put activity_manager max_phantom_processes 2147483647
# Battery optimization whitelist
adb shell dumpsys deviceidle whitelist +com.termux
adb shell dumpsys deviceidle whitelist +com.termux.nix
adb shell dumpsys deviceidle whitelist +com.termux.boot
# Permissions
adb shell appops set com.termux SYSTEM_ALERT_WINDOW allow
adb shell appops set com.termux.widget SYSTEM_ALERT_WINDOW allow
adb shell pm grant com.termux.nix android.permission.READ_EXTERNAL_STORAGE
# APK verification (allow sideloading)
adb shell settings put global verifier_verify_adb_installs 0
# Export current state
ssh phone -p 8022 'dpkg --get-selections' > packages.txt
# Restore desired state
cat packages.txt | ssh phone -p 8022 'dpkg --set-selections && apt-get dselect-upgrade -y'
#!/data/data/com.termux/files/usr/bin/bash/nix/store — ~5-20% syscall overheadnix copy --to ssh://[email protected]:8023 works — nix-on-droid uses standard /nix/store via proot