Current version: v0.16.0 (stable) Last updated: 2026-04-29
pm install via Shizuku for silent self-update.RECORD_AUDIO and SYSTEM_ALERT_WINDOW permissions. Accessibility service no longer listens for typeAllMask events (only uses dispatchGesture).GridCells.Adaptive → GridCells.Fixed with calculated columns. Per-tile lazy bitmap decode with inSampleSize=2 + RGB_565.NetworkCallback now filtered to TRANSPORT_WIFI only, ignoring 3G/4G mobile data fluctuations.ConnectionService auto-starts when the phone app is opened (e.g. via car USB ADB), removing the need to manually press Start. ✅ Done--activity-clear-task from car’s USB ADB phone launch. If the phone app is already open, the car moves forward without disrupting it. ✅ Done*.log files from /sdcard/DiLinkAuto/ and shares via Android share sheet. FileLog.zipLogs() creates a dilinkauto-logs.zip. ✅ Donebuild.yml, build-develop.yml), pre-release on -dev tags (build-pre-release.yml), release on vX.Y.Z tags (build-release.yml), main→develop back-sync (sync-main-to-develop.yml), and autonomous issue-agent (issue-agent.yml). ✅ DonePowerManager.SCREEN_BRIGHT_WAKE_LOCK with ACQUIRE_CAUSES_WAKEUP restores display after USB disconnect, even when VD server dies without cleanup.FLAG_KEEP_SCREEN_ON prevents screen lock during streaming. Touch input confirmed working on POCO F5 with Xiaomi 17 Pro Max.repeat-previous-frame-after=500ms for static content.Root cause found and fixed: Signature.getInstance("SHA1withRSA") double-hashes the ADB AUTH_TOKEN. ADB’s 20-byte token is a pre-hashed value — AOSP’s RSA_sign(NID_sha1) treats it as already hashed. Now uses NONEwithRSA with manually prepended SHA-1 DigestInfo ASN.1 prefix (prehashed signing). “Always allow” now persists correctly — AUTH_SIGNATURE accepted on reconnect without dialog.
DisplayControl from /system/framework/services.jar via ClassLoaderFactory.createClassLoader() + android_servers native library. Falls back to cmd display power-off/on if reflection fails.VirtualDisplayClient.disconnect() runs cmd display power-on 0 + KEYCODE_WAKEUP as safety net when VD server process is killed before cleanup.encodePublicKey() matching AOSP reference exactly — fixed constants, explicit bigIntToLEPadded(), struct header logging.100ms * TARGET_FPS / 1000 frames (6 at 60fps), skips every other non-keyframe. Image still moves at 2x speed, gradually catches up without jumping.onLost now checks if the lost network is the one carrying the connection. Ignores unrelated drops (mobile data cycling). Previously, any network loss killed the streaming session.handleInputFrame dispatched on Dispatchers.IO (was Main, caused NetworkOnMainThreadException on localhost socket write)break inside switch only broke out of switch, not the parse loop. Now uses break parseLoop; labeled break.--activity-clear-task from am start. Existing apps resume instead of restarting.targetFps field to HandshakeRequest. Car requests 60fps. VD server accepts FPS as command-line arg, uses it for encoder KEY_FRAME_RATE and FRAME_INTERVAL_MS.windowSoftInputMode="adjustNothing" + imePadding() on TextField. Keyboard doesn’t push the activity, only the search bar moves.pruneUnavailable() removes apps no longer on phone when app list updates./sdcard/DiLinkAuto/ → getExternalFilesDir → getFilesDir. Migration searches all locations.UPDATING_CAR message. Car shows “Updating car app…” status and doesn’t reconnect.updatingFromPhone flag is set.TARGET_FPS and FRAME_INTERVAL_MS constants. All video-path waits/polls capped at frame interval.glDrawArrays + eglSwapBuffers every frame interval, even when no new frame from VD. Only calls updateTexImage when a new frame is available. Feeds encoder on static content.DataOutputStream/DataInputStream with NIO write queue (ConcurrentLinkedQueue<ByteBuffer>) + Selector-based command reader. No blocking I/O anywhere in the pipeline.dequeueOutputBuffer timeout reduced from 100ms to FRAME_INTERVAL_MS (16ms at 60fps).FRAME_INTERVAL_MS.FRAME_INTERVAL_MS (configurable via constructor param).FRAME_INTERVAL_MS.FRAME_INTERVAL_MS.start() is called./sdcard/DiLinkAuto/client.log) bypasses HyperOS logcat filtering. Rotation: archives as client-YYYYMMDD-HHmmss.log, keeps 10 max.Split single multiplexed TCP connection into 3 dedicated connections to eliminate cross-channel interference causing video stalls:
Each connection has its own Connection instance with independent SocketChannel, NioReader, and write queue. Heartbeat/watchdog on control only.
Comprehensive logging for investigating video frame stall after ~420 frames:
Thread.yield() → delay(1) in writeBuffersToChannel — releases IO thread back to coroutine pool instead of busy-waiting (investigation finding: Thread.yield starved the video relay coroutine)scope.launch) so heavy processing (app list decode) doesn’t block the reader from draining TCPInvestigating root cause of video stall. Added TCP buffer size logging, write stall diagnostics.
Log.* calls routed through carLogSend() which sends via DATA channel CAR_LOG to phone. Phone logs with tag CarLog in logcat. Buffer up to 200 messages before connection is established.shellNoWait + exec app_process (v0.6.2 approach). The setsid/nohup detachment broke localhost connectivity. VD server dies on USB disconnect but recovers on re-plug.startListening() opens synchronously, waitForVDServer() skips if already waitinggetExternalFilesDir with writability check + migration from getFilesDir + fingerprint loggingselector.isOpen checks in writer and NioReaderFinal milestone + bug fixes:
waitForVDServer skips if already waiting — prevents closing/reopening ServerSocket when car sends multiple handshakes during reconnect (root cause of VD server unable to connect, confirmed via phone logcat showing 4x startListening in 45s)LOADED/GENERATED + fingerprint + path) to /data/local/tmp/car-adb-key.log on the phone after USB ADB connect. Enables diagnosing whether auth repeats because key changes or phone doesn’t persist “Always allow.”TOUCH_MOVE_BATCH (0x04) message type carries all pointers in one frame. MOVE events batched on car side, unbatched on phone side. Reduces syscalls from N*60/sec to 60/sec for N-finger gestures.6 polish fixes + 2 critical fixes:
startListening() (synchronous bind) + acceptConnection() (async wait). ServerSocket opens BEFORE handshake response — fixes VD server unable to connect on localhost:19637.getExternalFilesDir with writability check + migration from getFilesDir. Fingerprint logging on each connect to diagnose whether key changes between connections.checkStackEmpty uses simpler grep -E instead of fragile sed section parsingSystem.nanoTime()/1000 for timestamps (was fixed 33ms increment)UsbAdbConnection.readFile() uses read loop + try-with-resources4 performance optimizations + 2 crash fixes:
checkStackEmpty() runs on background thread — command reader no longer blocked for 300ms+ after Back pressPointerProperties[10] + PointerCoords[10] pools in VD server — eliminates per-touch GC pressurecmd display power-off 0 runs on fire-and-forget thread — removed jitter from touch injection pathClosedSelectorException in Connection writer + NioReader — added selector.isOpen checks and catch block. Race: disconnect() closes selectors while reader/writer coroutines still executing.usbConnecting reset in startConnection() now also guarded by usbAdb == null (second location of USB auth race, first was fixed in v0.7.3)3 I/O performance optimizations + hotfix:
channel.write(ByteBuffer[]) coalesces header+payload into single syscall/TCP segmentByteArray(size) directly — removes intermediate relayBuf + copyOfBufferedOutputStream(65536) — coalesces small localhost writeswaitForVDServer() called BEFORE sending handshake response — ensures ServerSocket on :19637 is open before car deploys VD server (v0.7.4 regression: sequencing put VD wait after response, causing VD server to fail connecting)Write architecture change + flow improvements:
synchronized(outputLock) with lock-free ConcurrentLinkedQueue + dedicated writer coroutine. Writer uses delay(1) when TCP send buffer full (releases IO thread to pool). No more blocking other coroutines during writes.usbConnecting only resets when usbAdb == null — prevents duplicate USB-ADB auth dialogsNetwork resilience + critical HyperOS fix discovered via logcat evidence:
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS — prevents HyperOS “greeze” from freezing the client app while screen is off. Root cause of “frames only during touch” bug: VD server (shell process) produced 960+ frames but the client app’s NioReader was frozen by OS power management. Prompt shown on first launch.ConnectivityManager.NetworkCallback on car that re-triggers WiFi track when WiFi becomes available. Handles hotspot enabled after USB plug.onLost → cleanupSession() → listen loop restarts. onAvailable only resets when WAITING (no disruption to active connections).am start --display <id> HOME) — ensures encoder has content immediately.usbConnecting only resets when usbAdb == null — prevents duplicate USB-ADB auth dialogs.5 stability fixes + critical NioReader fix:
delay(1) polling with Selector in NioReader — selector.select(100) wakes instantly via epoll when data arrives. Fixes video not streaming (frames only flowed during touch on Android 10 car due to delay(1) taking 10-16ms). Added wakeup()/close() for clean shutdown from disconnect.@Volatile on wifiReady, usbReady, vdServerStarted, usbConnecting — prevents stuck CONNECTING stateVideoDecoder.stop() joins feed thread (2s timeout) before codec.stop() — prevents native crashcleanup() in VD server — prevents IllegalStateException on double releasecleanupSession() resets _serviceState to WAITING — prevents stale UI during reconnect delayonCreate() clears static activeConnection and _serviceState — prevents stale state on service restart6 critical/high fixes from comprehensive review:
writeAll() 5s write timeout — prevents system freeze on full send buffer@Volatile on VirtualDisplayClient channel/reader + timeout-protected writesConnection.connect() try/catch — closes SocketChannel on cancellationAll socket operations converted to non-blocking NIO. mDNS registration no longer blocks listen loop. Version code read at runtime via PackageManager.
Changes:
readFrame(NioReader) and writeFrameToChannel(SocketChannel, Frame) NIO methodsAPP_VERSION_CODE constant — both apps read versionCode at runtime via PackageManager.getPackageInfo()Major architecture rewrite: parallel WiFi + USB tracks, NIO non-blocking sockets, phone-driven auto-update, multi-touch via IInputManager.
Working:
/sdcard/DiLinkAuto/vd-server.jar on launch (CRC32 checked)ServerSocketChannel/SocketChannel — instant cancellation, no EADDRINUSEServiceManager → IInputManager (supports tap, swipe, pinch)cmd display power-off 0 during streaming, proximity/lift wake disabled, throttled re-power-off after touch injectionconnectionScope cancels all coroutines on disconnect, exponential backoff reconnectappVersionCode field in HandshakeRequest, vdServerJarPath in HandshakeResponseArchitecture changes from v0.5.0:
Major architecture change: the car deploys the VD server to the phone via USB ADB. Wireless Debugging eliminated.
Apps render at phone’s native DPI (480dpi), GPU downscales to car viewport. SurfaceScaler EGL/GLES pipeline.
Car UI with always-visible left nav bar, TextureView, real app icons.
VD creation, self-ADB, resilient server, multi-app support.
Project created. Screen mirroring on emulators.
Comprehensive review performed 2026-04-23 covering performance, stability, and flow-continuity.
| ID | Category | Finding | Status |
|---|---|---|---|
| C1 | Stability/Perf | writeAll() spins indefinitely on full send buffer — no timeout, holds outputLock, blocks all senders. System freeze risk |
v0.7.1 |
| C2 | Flow | VD server dies on USB disconnect (shellNoWait ties process to ADB stream). Full reconnect 5-15s |
REVERTED — setsid/nohup broke localhost. Using shellNoWait+exec (v0.6.2 approach). Reconnects on re-plug. |
| C3 | Flow | Auto-update has no loop-break — if pm install silently fails, infinite restart cycle |
v0.7.1 |
| C4 | Flow | Car WiFi track runs once and gives up — hotspot enabled after USB plug → stuck forever | v0.7.3 |
| C5 | Stability | WakeLock acquired without timeout — battery drain if service killed without onDestroy() |
v0.7.1 |
| C6 | Stability | VirtualDisplayClient.touch() non-blocking write spin + channel field not volatile — data race |
v0.7.1 |
| ID | Category | Finding | Status |
|---|---|---|---|
| H1 | Perf | NIO delay(1) polling adds 1-4ms latency floor per read + 1000 wake-ups/sec idle. Use Selector or runInterruptible |
v0.7.2 |
| H2 | Perf | Two syscalls per frame write (6-byte header + payload). Use GatheringByteChannel.write(ByteBuffer[]) |
v0.8.0 |
| H3 | Perf | Per-frame ByteArray.copyOf() in video relay (~30 allocs/sec of 10-100KB). Pass offset+length |
v0.8.0 |
| H4 | Perf | synchronized(outputLock) serializes video+touch+heartbeat. Keyframe write blocks touch ~200ms |
v0.7.4 |
| H5 | Stability | Connection.connect() leaks SocketChannel on cancellation — no try/finally |
v0.7.1 |
| H6 | Stability | disconnectListener invoked synchronously in CAS — potential deadlock |
v0.7.1 |
| H7 | Stability | Car state flags (wifiReady, usbReady) not volatile — can get stuck in CONNECTING |
v0.7.2 |
| H8 | Stability | VideoDecoder.stop() doesn’t join feed thread before codec.stop() — native crash risk |
v0.7.2 |
| H9 | Flow | Phone network callback ignores CONNECTED/STREAMING — hotspot toggle causes 10s frozen frame | v0.7.3 |
| H10 | Flow | Handshake + auto-update + VD deploy all race — deployAssets may not be done, concurrent ADB ops | v0.7.4 |
| H11 | Flow | No user feedback during 5-12s VD server startup — car shows static spinner | v0.7.4 |
| H12 | Flow | First-time USB ADB auth dialog on phone with no guidance on car screen — 30s timeout | v0.7.4 |
| ID | Category | Finding | Status |
|---|---|---|---|
| M1 | Perf | VD server flushes after every frame on localhost — unnecessary syscall | v0.8.0 |
| M2 | Perf | checkStackEmpty() blocks command reader 500ms — touch blackout after Back |
v0.8.1 |
| M3 | Perf | Multi-touch sends N separate frames per MOVE — should batch all pointers | v0.8.3 |
| M4 | Perf | MotionEvent PointerProperties/Coords allocated per injection — pool these | v0.8.1 |
| M5 | Perf | Decoder frame queue 6 deep (200ms) — reduce to 2-3 for lower latency | v0.8.1 |
| M6 | Perf | execFast("cmd display power-off 0") on touch thread — move to timer |
v0.8.1 |
| M7 | Stability | Double cleanup() in VD server — handleClient finally + run both call it |
v0.7.2 |
| M8 | Stability | cleanupSession() doesn’t reset _serviceState — stale UI during delay |
v0.7.2 |
| M9 | Stability | Static MutableStateFlow in companion survives service restarts — stale activeConnection | v0.7.2 |
| M10 | Flow | User disconnect (eject) not persisted — car reconnects after process kill | v0.8.3 |
| M11 | Flow | Car mDNS + gateway IP probe one-shot — need periodic retry | v0.7.3 |
| M12 | Flow | dumpsys activity parsing in checkStackEmpty fragile across Android versions |
v0.8.2 |
| ID | Category | Finding | Status |
|---|---|---|---|
| L1 | Perf | TouchEvent.encode() allocates 25-byte ByteArray per event — can’t pool with async write queue |
WONTFIX |
| L2 | Perf | VD server localhost socket missing send/receive buffer size config | v0.8.2 |
| L3 | Perf | Video decoder uses fixed 33,333us timestamp — should use wall clock | v0.8.2 |
| L4 | Stability | NioReader direct ByteBuffer not freed deterministically | v0.8.3 |
| L5 | Stability | UsbAdbConnection.readFile() doesn’t guarantee full read |
v0.8.2 |
| L6 | Stability | SurfaceScaler HandlerThread never quit | v0.8.2 |
| L7 | Flow | App icons 48x48px — blurry on car displays, need 96-128px | v0.8.2 |
| Issue | Impact | Status |
|---|---|---|
| USB ADB auth dialog on replug | Phone asked “Allow USB debugging?” each time | FIXED v0.13.1 — was double-hashing AUTH_TOKEN with SHA1withRSA. Now uses NONEwithRSA + prehashed SHA-1 DigestInfo. “Always allow” persists. |
| VD server dies on USB disconnect | Stream stops if USB unplugged | Accepted — setsid/nohup detachment broke localhost connectivity. Car re-deploys on reconnect. |
| Touch injection wakes physical display | Screen turns on briefly during interaction | Mitigated with throttled re-power-off (1s, on background thread) |
| Portrait apps letterboxed on landscape VD | Petal Maps home screen narrow | Android limitation |
| Hotspot must be enabled manually | User enables before plugging in | Android 16 limitation |
Phone (Xiaomi 17 Pro Max, HyperOS 3, Android 16)
├── DiLink Auto Client App
│ ├── ConnectionService (3-port accept: 9637/9638/9639)
│ │ ├── Control (9637): handshake, heartbeat, commands, data, car logs
│ │ ├── Video (9638): H.264 relay from VD server to car
│ │ ├── Input (9639): touch events from car, dispatched on Dispatchers.IO
│ │ ├── VD JAR deploy to /sdcard/DiLinkAuto/ (CRC32 checked)
│ │ ├── Car auto-update: sends UPDATING_CAR, then dadb push+install
│ │ ├── Smart network callback (ignores unrelated network drops)
│ │ ├── Battery exemption (REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
│ │ └── FileLog: /sdcard/DiLinkAuto/client.log (rotation, 10 max)
│ ├── VirtualDisplayClient (videoConnection + controlConnection)
│ │ ├── startListening() — synchronous ServerSocket on localhost:19637
│ │ ├── acceptConnection() — NIO non-blocking accept
│ │ ├── NioReader (Selector-based, FRAME_INTERVAL_MS timeout)
│ │ └── Video relay via videoConnection, stack empty via controlConnection
│ └── NotificationService (captures phone notifications with progress)
│
├── VD Server (app_process, shell UID 2000)
│ ├── NIO write queue (ConcurrentLinkedQueue) + Selector-based command reader
│ ├── IInputManager injection (ServiceManager → injectInputEvent)
│ ├── Multi-touch: pre-allocated PointerProperties/Coords pools (10 slots)
│ ├── VirtualDisplay (TRUSTED + OWN_DISPLAY_GROUP + OWN_FOCUS)
│ ├── SurfaceScaler (EGL/GLES GPU downscale, skips GL work on idle, encoder repeat-previous-frame)
│ ├── H.264 encoder (8Mbps CBR, Main profile, configurable FPS, backpressure at 6 frames)
│ ├── Screen power-off (background thread, proximity/lift disabled)
│ └── Reverse NIO connection to phone on localhost:19637
│
Car (BYD DiLink 3.0, Android 10)
├── DiLink Auto Server App
│ ├── CarConnectionService — 3 connections + parallel USB track
│ │ ├── controlConnection (9637): heartbeat, commands, data
│ │ ├── videoConnection (9638): video frames → VideoDecoder
│ │ ├── inputConnection (9639): touch events from MirrorScreen
│ │ ├── Track B (USB): UsbAdbConnection with logSink → carLogSend
│ │ ├── UPDATING_CAR handling: shows status, skips reconnect
│ │ ├── Early decoder start: offscreen surface on first CONFIG
│ │ ├── carLogSend() + logSink callbacks → phone FileLog
│ │ └── Eject state persisted to SharedPreferences
│ ├── VideoDecoder (queue=15, early start, logSink, 4-zone catchup)
│ ├── PersistentNavBar (76dp, 40dp icons, 14sp text, recent apps pruned)
│ ├── LauncherScreen (64dp icons, 160dp grid, imePadding search)
│ ├── NotificationScreen (progress bars, tap-to-launch, dedup by ID)
│ └── MirrorScreen (TextureView + touch forwarding, decoder restart)
1. Phone app starts → deploys VD JAR, rotates FileLog, requests battery exemption
2. Phone plugged into car USB → car detects USB_DEVICE_ATTACHED
3. Track A (WiFi): car discovers phone via gateway IP (3s retry) or mDNS
4. Track B (USB): car connects USB ADB (logSink for diagnostics), launches phone app
5. Control (9637): TCP connect → handshake (viewport + DPI + version + targetFps)
6. Phone: checks version → if mismatch, sends UPDATING_CAR → auto-updates via dadb
7. Video (9638) + Input (9639): car connects in parallel after handshake
8. Phone: accepts both, opens VD ServerSocket on localhost:19637
9. USB: car starts VD server (shellNoWait + exec app_process, FPS as arg)
10. VD server: VD + SurfaceScaler (periodic re-draw) + encoder → NIO connect localhost:19637
11. Car: starts VideoDecoder on offscreen surface on first CONFIG frame
12. MirrorScreen shows → decoder restarts with real TextureView surface
13. Video: VD → SurfaceScaler → encoder → NIO write queue → localhost → phone NioReader → videoConnection → WiFi TCP → car NioReader → VideoDecoder → TextureView
14. Touch: car TextureView → inputConnection → WiFi TCP → phone (Dispatchers.IO) → VD server NIO Selector → IInputManager injection
15. Car logs: carLogSend() + logSink callbacks → DATA CAR_LOG → phone FileLog