Skip to content
View in the app

A better way to browse. Learn more.

Firmware, Software & Manuals for BYD Owners

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Vitaly

Administrators
  • Joined

  • Last visited

Everything posted by Vitaly

  1. DashCast v1.2.0-build1821.2.0 — DiLink 5 enablement (prerelease) Pre-release channel. This build lives on beta/1.2.0-dilink5 and is not intended for merge into main. Enable Settings → Include prereleases to receive it over-the-air on a DiLink 3 / DiLink 5 device. Why a major version bump DiLink 5 is a different platform from DiLink 3 — Android 12 (API 32) instead of Android 9, BYD-AUTO brand, and three new system components observed on the first field log (byd_report_20260521_212356.log): auto_container [android.os.IAutoContainer] is still present — our typed Phase4Verbs.autoContainerSendInfo path may work as-is. Display naming is the same shared_fission_bg_XDJAScreenProjection_N family as DL3 — same underlying mechanism. New gatekeepers: com.byd.containerservice v12 (BYD-rebranded XDJA), com.byd.appstartmanagement v1.5.0.0, com.byd.providers.appstartup v12 — these are the suspected behavioural divergence and need offline inspection. To triage this efficiently from the field, the diagnostic surface had to be expanded significantly. Bumping the minor version (1.1 → 1.2) reflects this scope change. What changed RE Sniffer restored (build 181 carry-over) The Sniffer system (continuous logcat + periodic dumpsys snapshots into a single BYD_RE_Sniffer_*.txt on external files dir) was wiped at v0.9.88 (commit 229059c) when DiagActivity was refactored into the tabbed layout. With Dilink 5 testers now in the loop, we need the Sniffer back to bind user-reported issues to ground truth. Restored as a dedicated Material 3 tab (8th tab in DiagActivity). State machine ported 1:1 from the pre-wipe revision, so Start / Stop / Snapshot / Export and crash-safe state restoration on Activity rebuild all behave exactly as before. DiLink 5 diagnostic suite — 9 → 20 tests D9 — reworked, now dynamic. Replaces the 7-package hardcoded whitelist with a shell scan: pm list packages -f | grep -iE 'com\.byd\.|com\.xdja\.|com\.dilink\.|cluster|automotive' | sort -u. Parses every match (package:/data/app/.../base.apk=<pkg>), reads versionName + getLongVersionCode() via PackageManager, and stores the result in a static sLastDiscovery list for the next test. Output now shows the full APK path for every match. D10 — RE-relevant APK extraction. Iterates sLastDiscovery, filters through a curated whitelist (clusterdebug, car.server, crosscontrol, containerservice, xdja.containerservice, appstartmanagement, providers.appstartup, smarttravel, commander, freedom, overdrive, windowmanagement, plus any package containing fission / projection / cluster / dilink5 / automotive.cluster), and cats each APK from /data/app/... to getExternalFilesDir(null)/extracted_apks/<pkg>_v<versionCode>.apk via the existing ADB-local shell relay. The report card prints the exact adb pull command to retrieve everything in one shot. D11 — AutoContainer sendInfo refresh probe. service call auto_container 2 i32 1000 i32 0 s16 "" (the harmless sendInfo(0) refresh already used in our restoreBydOnCluster sequence). PASS = service accepted from uid 2000 (confirms our typed Phase 4c path will work on DL5 without modification). D12 — Qt standby cycle probe. Active 3-step sequence sendInfo(16) (Qt standby) → 1.5 s → sendInfo(18) (close) → 0.5 s → sendInfo(0) (refresh). Validates the protocol cycle, not just binder reachability. No projection actually started — these three commands together form a complete null cycle. D13 — IATM.setTaskWindowingMode reflection probe. Resolves android.app.ActivityTaskManager.getService() then reflective setTaskWindowingMode(0, 4, true). Disambiguates the D6 FAIL line: SecurityException → WARN (expected, signature perm or proxy daemon needed); IllegalArgumentException → PASS (method is reachable, only rejected the bogus taskId — best case); NoSuchMethodException → FAIL. D14 — Window manager state dump. dumpsys window | grep -E 'Display #|mDisplayId|imeLayerStack|focused|mStackId|mCurrentFocus|mDisplayContent' | head -80. D15 — com.byd.appstartmanagement inspection. dumpsys package filtered to permission/receiver/service/activity/authority/provider/signature/version. This is the suspected DL5 launch gatekeeper. D16 — com.byd.containerservice inspection. Same filter for the BYD-rebranded XDJA. Will reveal whether transaction codes match the legacy com.xdja.containerservice we already have decompiled. D17 — Proxy daemon spawn capability. id + which app_process64 + dry-run app_process64 -Xnoimage-dex2oat /system/bin --nice-name=dl5_probe -version + ls /system/bin/app_process64. PASS = our Beta proxy daemon strategy is viable on DL5/API 32 (no SELinux denial). D18 — SELinux context. id + ps -Z -o LABEL,USER,PID,NAME | head -10. D19 — Full system services inventory. service list | grep -iE 'auto|byd|cluster|fission|display|window|projection|crosscontrol|xdja|task' | head -60 — broader than D3. D20 — IActivityTaskManager service presence. service list | grep -iE 'activity_task|activitytask'. Versioning versionName 1.1.9 → 1.2.0 versionCode 181 → 182 Validation plan Install the APK below (or enable Settings → Include prereleases for OTA delivery). Open Diag → DiLink 5 tab → tap Run all. Copy the report with the Copy button. From a PC: adb pull /sdcard/Android/data/com.byd.dashcast/files/extracted_apks/ — retrieves every whitelisted BYD/XDJA/DiLink APK for offline jadx work. Expected outcomes D9 → PASS with 10–20 packages discovered. D10 → PASS, all whitelisted APKs copied to extracted_apks/. D11 / D12 → key signal for whether our typed sendInfo path already works on DL5. D13–D20 → triage data for the next iteration. Files DashCast-v1.2.0-debug.apk — debug-signed APK, attaches below. Compatibility Min SDK 28, target SDK 29, compile SDK 33 — unchanged. No new runtime permissions, no new dependencies, no new resources beyond the Sniffer panel layout. Branch policy This release lives on beta/1.2.0-dilink5. It is not merged into main. The stable channel remains 1.0.1. LinksAPK download GitHub release page
  2. DashCast v1.1.9-build179DashCast v1.1.9 — build 179 (Beta Engine Phase 5) Channel: beta/1.1.0 • versionCode: 179 • versionName: 1.1.9 • target: BYD Atto3 / Seal EU, DiLink 3.0, API 29 Phase 5 — proxy daemon pre-warm at BOOT_COMPLETED Kills the cold-start cost paid by the first user action after every reboot. Build-178 device log measured the very first sendInfo of a fresh session at 1916 ms, versus 3 ms warm. That 1.9 s was entirely the BetaProxyClient.connect() bootstrap: spawning app_process with our APK on the classpath, waiting for the daemon to broadcast its live binder, running the WHOAMI handshake. Build 178 already validated that the runtime cost is paid once per daemon lifetime, so moving the bootstrap to BOOT means every subsequent in-app trigger is warm. What changed 1. BootReceiver refactor. The existing BOOT_COMPLETED receiver already handled two cases (auto-start projection ON → start ClusterService; OFF → run cleanupDisplayAffinityAtBoot via goAsync()). Phase 5 adds a third orthogonal task: if BetaConfig.isProxyDaemonEnabled(context) is true, spawn beta-daemon-prewarm thread that calls BetaProxyClient.connect(appCtx). Since BroadcastReceiver.PendingResult.finish() may only be called once, the previous single-task goAsync() pattern doesn't scale to two concurrent tasks (pre-warm + display cleanup) — refactored into a shared AtomicInteger pending + releaseOne lambda that finishes the receiver only after the last task completes. The pre-warm thread name is beta-daemon-prewarm (visible in any thread dump for diagnostics). 2. Logging on the boot path. Two new log lines surface the pre-warm outcome: beta daemon pre-warm starting... at thread start beta daemon pre-warm ok (XXms) / beta daemon pre-warm FAILED (XXms) once connect() returns Expected XX is ~1500-2000 ms on the BYD Seal EU (matches the cold cost previously paid by the user). On failure the log line is beta daemon pre-warm threw: <msg> and Phase 5 silently degrades — runtime fallback in ShellGateway / AdbLocalClient still routes through legacy shell on demand, so the user's first action just pays the cold cost as before. 3. Threading correctness. The pre-warm runs on its own background thread (not the receiver thread that delivers the broadcast — that one returns immediately so the OS can move on with system boot). BetaProxyClient.connect() is internally synchronized on LOCK and is safe to call concurrently with future in-app calls from MainActivity.onCreate etc. — if the user opens the app while pre-warm is still in flight, the in-app connect() will block on the same LOCK, see the now-cached binder on entry, and return immediately. 4. Idempotent against double-trigger. BetaProxyClient.connect()'s fast-path explicitly checks isConnected() first and skips the bootstrap if a live binder is already cached (this was the v1.1.6 anti-respawn fix), so a pre-warm followed by a MainActivity.onCreate() connect is harmless — second call is a no-op. 5. Receiver-lifetime safety. PendingResult.finish() must be called eventually or Android logs a leak. The new AtomicInteger-based coordinator guarantees finish is called exactly once, on the completion of whichever task finishes last (pre-warm + cleanup typical max ~3 s). If BetaConfig.isProxyDaemonEnabled(context) returns false AND autoStartEnabled is true, pending stays at zero and finish is invoked immediately — covers the edge case where the receiver has no asynchronous work to do. 6. Permissions / manifest. No change. RECEIVE_BOOT_COMPLETED was already granted (build-178 SigDump confirms granted=true); the <receiver> declaration with the BOOT_COMPLETED action filter is unchanged from build 178. 7. Behavioural contract preserved. When the beta proxy toggle is OFF, the new if (BetaConfig.isProxyDaemonEnabled(context)) branch is skipped entirely — no extra thread, no daemon bootstrap, identical boot behaviour to v1.0.1. The existing auto-start projection path is unchanged in terms of order: startForegroundService runs synchronously on the receiver thread as before; only the cleanup branch lost its dedicated goAsync() (it's now sharing the receiver's single PendingResult). 8. What's left. Phase 5 closes the cold-start hole exposed by Phase 4c/4d logs. Toggling the proxy ON at runtime (via Settings) still requires the user to pay the cold cost on the first call — by design, since the toggle change is rare and the UI already labels it "restart required after change". A future micro-polish could trigger BetaProxyClient.connect(ctx) in the toggle handler too, but it's a deliberate non-scope of Phase 5 (boot is the dominant cold-start surface). How to validate Install the APK over build 178. Reboot the head unit. Open the in-app Diag screen → look at the head of the captured log for: BootReceiver | beta daemon pre-warm starting... BootReceiver | beta daemon pre-warm ok (~1500–2000 ms) Trigger a normal projection start (or restoreBydOnCluster). The first BetaProxyClient.sendInfo should now be warm (~3 ms) instead of the 1916 ms measured in build 178. Build metadata versionCode 178 → 179 versionName pinned at "1.1.9" per channel policy Sole touched files outside CHANGELOG/gradle: BootReceiver.java No new strings, layouts, drawables, or dependencies Roadmap context Phase 4 (typed verbs replacing shell forks on production hot paths) closed in build 178 with all four verbs live: setOverscan (4a), getPidsByPackage (4b), autoContainerSendInfo (4c), forceStopPackage (4d). Build 178 device validation: 56/56 tests PASS, restoreBydOnCluster typed ok (1578 ms), restoreOriginCluster typed ok (12012 ms). Phase 5 builds on that by removing the only remaining latency wart — the per-reboot daemon bootstrap. LinksAPK download GitHub release page
  3. DashCast v1.1.9-build180DashCast v1.1.9 — build 180 (Phase 4d.1 — force-stop fix) Channel: beta/1.1.0 • versionCode: 180 • versionName: 1.1.9 • target: BYD Atto3 / Seal EU, DiLink 3.0, API 29 Bug fixed Build 179 field test surfaced: when the user tapped Restore BYD cluster with Waze projected, the app correctly migrated to display 0 (moveSessionAppsToMainDisplay works) but the Waze process kept running. The device log reported beta restoreBydOnCluster typed ok (Xms) — a lie: the underlying IActivityManager.forceStopPackage(pkg, -1) returned cleanly but did not actually terminate the target. Root cause BetaProxyClient.forceStopPackage(targetPackage, -1) was passing userId = -1 (UserHandle.USER_ALL). On API 29 BYD framework, the IActivityManager implementation silently no-ops on negative user IDs instead of throwing — the caller has no way to know the kill failed. Legacy shell path (am force-stop pkg without --user) targets the current user implicitly, which is why pre-Phase-4d builds did not exhibit this. What changed 1. userId fix in AdbLocalClient — both restoreBydOnCluster and restoreOriginCluster now pass userId = 0 (current user) instead of -1. The teardown report shown in the UI now reads force-stop <pkg> (typed,u=0) to make the user-id explicit. 2. Post-kill verification (verifyForceStop helper). After every typed forceStopPackage on the teardown hot path, the client now calls BetaProxyClient.getPidsByPackage(pkg) (Phase 4b verb already shipping since build 175) and: if no surviving PIDs → log nothing extra, report shows verified killed if PIDs survive → log WARN beta force-stop ineffective for <pkg> (pids=...) and append WARN: still alive, pids=… to the UI report Future force-stop failures will surface immediately in device logs instead of silently lying about success. 3. Defensive failure mode. Verification exceptions are swallowed (logged at WARN) so the teardown sequence is never broken by a diagnostic helper. Out of scope (deferred) forceStopApp(Context, String) (the in-list "✕" button) is unchanged — it's a shell-only legacy path running am force-stop directly with no userId issue, and includes its own am task remove Recents cleanup. Process.killProcess(pid) as a third escalation tier was considered and explicitly deferred. We want to see whether userId=0 alone is sufficient before adding more retry layers. Wire / protocol No change. TXN_FORCE_STOP_PACKAGE (=8) and TXN_GET_PIDS (=6) were both shipped in builds 178/175 respectively. Phase 4d.1 is a pure client-side correctness fix. PROTOCOL_VERSION stays "2". Behavioural contract preserved With the beta proxy toggle OFF, BetaConfig.isProxyDaemonEnabled(context) short-circuits before any binder touch — boot/runtime behaviour identical to v1.0.1. The legacy shell fallback inside restoreBydOnCluster / restoreOriginCluster (used when the typed path throws) is unchanged. How to validate Install over build 179, reboot. Project an app (Waze ideally) on the cluster. Tap Restore BYD cluster. In Diag → log, check for: force-stop <pkg> (typed,u=0) followed by verified killed the app is no longer running on display 0 (launcher returns to idle, not the freshly-relocated app) If WARN beta force-stop ineffective for <pkg> (pids=…) appears → capture the log; userId=0 alone wasn't enough and we need to investigate BYD framework re-launch behaviour. Build metadata versionCode 179 → 180, versionName pinned at "1.1.9" Sole touched file outside CHANGELOG/gradle: AdbLocalClient.java No new strings, layouts, drawables, dependencies, or protocol verbs Stack of fixes in this beta channel build 178 — Phase 4d (typed teardown sequence) + SDK B4/B5/B6 rewire build 179 — Phase 5 (daemon pre-warm at BOOT_COMPLETED) build 180 — Phase 4d.1 (force-stop userId fix + post-kill verify) LinksAPK download GitHub release page
  4. DashCast v1.1.9-build178v1.1.9-build178 — Phase 4d + B4/B5/B6 SDK rewire Phase 4d — typed teardown sequence (closes the Phase 4c gap) Build 177 migrated AdbLocalClient.sendInfo to the typed daemon path, but the end-of-projection teardown still forked shells: restoreBydOnCluster and restoreOriginCluster issued their own dadb.shell("am force-stop …") and dadb.shell("service call AutoContainer 2 …") calls directly, bypassing the centralised sendInfo hook. The build-177 device log (byd_log_20260521_234340.log) confirmed this — every projection stop printed restoreBydOnCluster -> OK with 3 shell forks (1 force-stop + 2 sendInfo). Build 178 closes that gap with a new typed verb and full migration of both restore flows. New typed verb: IActivityManager.forceStopPackage Phase4Verbs.forceStopPackage(String packageName, int userId) — lazy double-checked cache of IActivityManager (ActivityManager.getService() with fallback to ActivityManagerNative.getDefault()) and the forceStopPackage(String, int) Method handle. Probe P3 (build 173) had already confirmed IActivityManager.forceStopPackage(String, int) is callable from uid 2000 on the BYD Seal EU. Pass userId = -1 for UserHandle.USER_ALL — matches the legacy am force-stop default behaviour. Wire-protocol additions ProxyDaemonMain.TXN_FORCE_STOP_PACKAGE = FIRST_CALL_TRANSACTION + 7 (= 8). Wire format: String packageName, int userId in → empty reply (or writeException on failure). onTransact mirrors the Phase 4c pattern: unwraps InvocationTargetException so the client sees the real exception, wraps non-Exception throwables in RuntimeException so writeException accepts them. BetaProxyClient.forceStopPackage(String, int) throws BetaProxyException mirror on the client side. RemoteException nulls the cached binder so the next call re-bootstraps. Protocol version still "2" — every Phase 4 verb has been purely additive, so old clients keep working with the new daemon and vice-versa modulo the missing verbs. Refactored hot paths (typed-first with whole-flow fallback) AdbLocalClient.restoreBydOnCluster and AdbLocalClient.restoreOriginCluster both gained a top-of-run() block (same pattern as Phase 4c sendInfo): If BetaConfig.isProxyDaemonEnabled(context), run the whole typed sequence: restoreBydOnCluster : forceStopPackage(target, -1) → sleep 500 ms → autoContainerSendInfo(1000, 18, "") → sleep 1 s → autoContainerSendInfo(1000, 0, ""). restoreOriginCluster : forceStopPackage(target, -1) → sleep 500 ms → autoContainerSendInfo(1000, 18, "") → sleep 6 s → autoContainerSendInfo(1000, 0, "") → sleep 6 s → autoContainerSendInfo(1000, screenSizeCmd, ""). On success: log beta restoreBydOnCluster typed ok (Xms) / beta restoreOriginCluster typed ok (Xms), return. On any Throwable other than InterruptedException: log beta … typed failed after Xms, falling back to ADB shell: <msg> and fall through to the unchanged legacy try (Dadb dadb = connect(context)) block. InterruptedException short-circuits to callback.onError("interrupted") — don't re-fork shell when the caller cancelled. Why whole-flow (not per-step) fallback Both flows interleave binder calls with Thread.sleep calls whose semantics are load-bearing (the Qt cluster process needs time to release / reacquire its surface, not the shell overhead). A per-step fallback would race the daemon binder state against the legacy shell relay. By committing to the whole flow up-front (typed or shell), we never mix the two within one teardown, and either branch is a path we already shipped and validated. Sites that benefit automatically restoreBydOnCluster runs at the end of every projection (user exit button, app force-stop detection in reconcileDisplayState, BYDApp.moveSessionAppsToMainDisplay). restoreOriginCluster runs on every "reset cluster to native size" action (Settings → Cluster size → Apply). With Phase 4d, both stop forking shells entirely when the proxy is enabled. Wire-protocol summary so far TXN_PING = 1 TXN_WHOAMI = 2 TXN_EXEC = 3 TXN_PROBE_PHASE4 = 4 TXN_SET_OVERSCAN = 5 ← Phase 4a (wm overscan) TXN_GET_PIDS = 6 ← Phase 4b (pidof) TXN_AUTOCONTAINER_SEND_INFO = 7 ← Phase 4c (service call AutoContainer 2) TXN_FORCE_STOP_PACKAGE = 8 ← Phase 4d (am force-stop) PROTOCOL_VERSION still "2". The v1.0.1 wire format remains a strict subset. Bundled fix — BetaTestRunner B4/B5/B6 wired to the real SDK The mockup row "SDK BYD B4/B5/B6 — Bloqué : SDK absent du build (Phase 1 l'a retiré)" was a false trail. The SDK has never been removed from the build: app/libs/byd-auto-api-stubs.jar ships on every build. The B4/B5/B6 tests were just pointing at a phantom package name (com.byd.bydautosdk.*) that doesn't exist anywhere in this codebase. The actual classes in the stub jar live under android.hardware.bydauto.* and expose the expected getInstance(Context) static factory: B4 : com.byd.bydautosdk.BYDAutoBodyworkDevice → android.hardware.bydauto.bodywork.BYDAutoBodyworkDevice B5 : com.byd.bydautosdk.BYDAutoRadarDevice → android.hardware.bydauto.radar.BYDAutoRadarDevice B6 : same swap (compares legacy vs beta SystemContext factories). Result: B4/B5/B6 will now actually execute (PASS / FAIL with detail) instead of always returning SKIPPED: … not on classpath. Any runtime SecurityException from the SDK factory will finally surface so we can diagnose if needed. What's left after this build Every shell call on a complete projection lifecycle (start → run → stop → restore) is typed when the proxy is enabled. The remaining dadb.shell(…) calls in AdbLocalClient are all explicitly diagnostic / self-test paths: runDisplayOneLaunch — Diag "Test 4-sendInfo sequence" button sendInfoDiagDump — Diag dump for OEM compatibility checks dumpSignatureAndPermissions — one-shot debug button setAccWhitelist / grantPermission / killAndRelaunchDaemon — admin tools, fire once per install None are on a hot path. They are intentionally kept on the legacy shell relay so the Diag UI can exercise both code paths for comparison. Behavioural contract preserved No public API removed, no signature change. With the beta proxy toggle OFF both restore paths still call dadb.shell exactly as in v1.0.1 (BetaConfig.isProxyDaemonEnabled(ctx) short-circuits before any binder touch). B4/B5/B6 rewire only changes string constants used by reflection — no API surface change. Build versionCode 177 → 178 versionName pinned at "1.1.9" per channel policy No new strings, layouts, drawables, or dependencies LinksAPK download GitHub release page
  5. DashCast v1.1.9-build177DashCast 1.1.9 — build 177 (beta channel) Beta Engine Phase 4c — typed AutoContainer.transact(2) replaces the service call AutoContainer 2 … shell relay used by AdbLocalClient.sendInfo. Third and final hot-path production cash-in of the Phase 4 probe matrix. Build-176 P13 confirmed binder.transact(2, ...) direct on AutoContainer from uid 2000 with descriptor android.os.IAutoContainer, measured at 0 ms. What changed Component Before After AdbLocalClient.sendInfo() Always dadb.shell("service call AutoContainer 2 i32 … i32 … s16 \"…\"") When beta proxy ON: direct binder.transact(2) via daemon; on any failure falls back to legacy shell wrapper Phase4Verbs setOverscan, getPidsByPackage + autoContainerSendInfo(int type, int info, String str) with lazy cached binder + live descriptor ProxyDaemonMain TXN 1–6 + TXN_AUTOCONTAINER_SEND_INFO = 7 (additive; protocol version unchanged) BetaProxyClient mirror methods 1–6 + autoContainerSendInfo(int, int, String) throws BetaProxyException Integration choice — option B Rather than migrating every service call AutoContainer 2 … site to ShellGateway, the hook is placed in the single private API that centralises the call (AdbLocalClient.sendInfo). Pros: One integration point, not ten Every caller benefits automatically: ClusterService.startActivityViaIAM, projection start/stop, MainActivity.toggleClusterProjection, SysInfoActivity info dump Diag → ADB → "Tester sendInfo" button intentionally keeps calling shell directly (legacy comparison path stays exercisable) Descriptor robustness The cache reads the live descriptor at runtime via INTERFACE_TRANSACTION instead of hard-coding "android.os.IAutoContainer". If a future OEM rebrand renames the service but keeps transaction code 2, the typed verb still works — robustness specifically validated by probe P13. Cache invalidation IBinder.pingBinder() is checked on every entry. If the AutoContainer service process restarted, the cache is cleared and a fresh ServiceManager.getService resolve runs. Fallback chain typed daemon → legacy ADB shell relay. One hop simpler than the gateway chain because there is no runShell intermediate — the entire legacy try (Dadb dadb = …) block is reused as-is. BetaProxyException (transact failure, daemon dead, remote SecurityException) triggers the fall-through with a log line beta sendInfo typed failed after Xms, falling back to ADB shell: <msg>. Latency expectations Path Build 176 Build 177 service call AutoContainer 2 … ~100–300 ms (fork service, parse hex output, escape s16 arg) ~30 ms (binder round-trip only) Most user-visible win: projection start/stop sequence makes ~6 sendInfo calls back-to-back — the saving compounds. Test plan Install build 177, enable beta proxy in Settings, restart app Démarre la projection cluster avec Waze (ou n'importe quelle app) Laisse tourner ~30 s Arrête la projection (le séquentiel sendInfo(18) puis sendInfo(0) est le plus visible) Diag → Logs → Envoyer (.log) Doit apparaître : AdbLocalClient beta sendInfo typed ok (Xms): 1000,18,"" AdbLocalClient beta sendInfo typed ok (Xms): 1000,0,"" AdbLocalClient beta sendInfo typed ok (Xms): 1000,30,"" Ne doit PAS apparaître : AdbLocalClient sendInfo ADB: service call AutoContainer 2 i32 ... ← fallback bug AdbLocalClient beta sendInfo typed failed after ... ← typed call broken Wire protocol summary TXN Code Verb TXN_PING 1 health check TXN_WHOAMI 2 uid/pid/version TXN_EXEC 3 runShell TXN_PROBE_PHASE4 4 probe suite TXN_SET_OVERSCAN 5 Phase 4a TXN_GET_PIDS 6 Phase 4b TXN_AUTOCONTAINER_SEND_INFO 7 Phase 4c (this build) PROTOCOL_VERSION stays "2" — every Phase 4 verb has been purely additive. Behavioural contract No public API removed No signature change in AdbLocalClient.sendInfo With beta proxy toggle OFF: binary byte-for-byte identical to v1.0.1 (the BetaConfig.isProxyDaemonEnabled(ctx) check short-circuits before any binder touch) Build metadata versionCode: 176 → 177 versionName: 1.1.9 (pinned per channel policy) No new strings, layouts, drawables, dependencies. What's left Phase 4 is feature-complete for every production hot path: wm overscan (4a) → typed ✅ pidof (4b) → typed ✅ service call AutoContainer 2 … (4c) → typed ✅ Remaining shell uses are intentionally diagnostic/self-test (SysInfoActivity info dump, Diag → ADB button, BetaTestRunner X-family cross-checks). Probes P3/P4 (forceStopPackage / killBackgroundProcesses) remain available if a future need surfaces — Phase 4d would be a one-off then. LinksAPK download GitHub release page
  6. DashCast v1.1.9-build176DashCast 1.1.9 — build 176 (beta channel) Beta Engine — Phase 4c feasibility probe P13 (AutoContainer.transact(2) direct). Diagnostic-only build. Zero production code change vs build 175 — ShellGateway, MainActivity, ClusterService behaviour identical. Why Build-175 device validation confirmed Phase 4b (typed pidof) works end-to-end: 18/18 typed calls, 0 fallbacks, die-path semantics correct. The next remaining shell call in the projection critical path is the AutoContainer family used by AdbLocalClient.sendInfo: service call AutoContainer 2 i32 <type> i32 <info> s16 "<str>" Build-173 probe P6 only confirmed that the binder is reachable and exposes descriptor android.os.IAutoContainer via INTERFACE_TRANSACTION — a benign metadata query that does not exercise per-method permission checks. We need to know whether binder.transact(code=2, ...) itself goes through from uid 2000 before writing the production verb. P13 design Single new probe in Phase4Probes.runAll(): ServiceManager.getService("AutoContainer") → IBinder INTERFACE_TRANSACTION → re-read live descriptor (defensive against OEM rebrand) Build parcel exactly as service call would: writeInterfaceToken(descr) + writeInt(1000) + writeInt(30) + writeString("") binder.transact(2, data, reply, 0) non-oneway reply.readException() so remote SecurityException / IllegalArgumentException surfaces in the structured failure taxonomy Payload chosen — sendInfo(1000, 30, "") — is the idempotent "Seal EU screen size" notification already triggered by the Diag → ADB diagnostic dump button, so even on PASS no visible side effect beyond what the user can already trigger. Decision matrix P13 result Meaning Next step PASS Direct transact accepted Ship Phase 4c in build 177 (~80 LoC) FAIL_SECURITY OEM has uid/signature check on transaction 2 Investigate why service shell wrapper bypasses it FAIL_OTHER:no descriptor OEM renamed/replaced AutoContainer Re-discovery needed Anything else Unknown failure mode Triage from detail string Wire protocol No new transaction codes, no protocol version bump. P13 piggy-backs on the existing TXN_PROBE_PHASE4 (=4) channel from build 173 — daemon-side Phase4Probes.runAll simply appends a 13th Pn=STATUS:detail token. Diag UI Diag → Beta Engine tab now lists 28 rows (was 27). The new P13 row sits after P12. Behavioural impact None for end users. Probe only runs when the user opens Diag → Beta Engine → Run All. With beta proxy toggle OFF the entire P family is FAIL: daemon probe run failed: daemon not connected. Shipping binary is byte-for-byte identical to v1.0.1 with both switches OFF. Build metadata versionCode: 175 → 176 versionName: 1.1.9 (pinned per channel policy) No new strings, layouts, drawables, or dependencies. Test plan Install build 176 Activate beta proxy in Settings, restart app Diag → Beta Engine → Run All Send report (.log) Look for P13=PASS:… or P13=FAIL_…:… in the report LinksAPK download GitHub release page
  7. DashCast v1.1.9-build174DashCast v1.1.9 — build 174 (Phase 4a: typed setOverscan) Channel: beta/1.1.0 · versionCode: 174 · versionName: 1.1.9 First production cash-in of the build-173 probe results (P1–P12 all PASS on BYD Seal EU). Every wm overscan … command produced by ShellGateway is now intercepted client-side, parsed, and routed through a typed Binder call to IWindowManager.setOverscan(displayId, l, t, r, b) inside the daemon — bypassing the ProcessBuilder("sh", "-c", "wm overscan …") JVM fork that took 70–275 ms per call in build 172. Before / after Build 172 device log (real measurements): [22:24:58.904] ShellGateway: beta runShell ok (1687ms): wm overscan 60,52,60,52 -d 1 [22:25:03.328] ShellGateway: beta runShell ok ( 275ms): wm overscan 30,52,30,52 -d 1 [22:25:04.138] ShellGateway: beta runShell ok ( 84ms): wm overscan 30,52,30,52 -d 1 [22:25:24.449] ShellGateway: beta runShell ok ( 197ms): wm overscan 30,84,30,84 -d 1 Expected build 174 (after the first-call reflection warm-up — probe P1 measured 117 ms cold then 0 ms warm): [xx:xx:xx.xxx] ShellGateway: beta setOverscan typed ok ( 117ms): d=1 60,52,60,52 [xx:xx:xx.xxx] ShellGateway: beta setOverscan typed ok ( 0ms): d=1 30,52,30,52 [xx:xx:xx.xxx] ShellGateway: beta setOverscan typed ok ( 0ms): d=1 30,52,30,52 [xx:xx:xx.xxx] ShellGateway: beta setOverscan typed ok ( 1ms): d=1 30,84,30,84 Resize SeekBar drag becomes effectively instantaneous (30+ calls/s × ~0 ms vs 30+ calls/s × ~100 ms previously). What the build adds 1. Phase4Verbs.java (new, ~70 LoC, in com.byd.dashcast.beta.proxy) Lives in the daemon process. Holds a lazy double-checked cache of: the IWindowManager proxy (ServiceManager.getService("window") + IWindowManager$Stub.asInterface) the setOverscan(int, int, int, int, int) Method So subsequent calls skip Class.forName / getService / asInterface entirely — critical for the resize SeekBar which fires 30+ overscan changes per second while the user drags. setOverscan(displayId, l, t, r, b) throws Throwable is the single public verb in this build. More verbs land in Phase 4b/4c. 2. ProxyDaemonMain — new TXN_SET_OVERSCAN (=5) Wire format: int displayId, int l, int t, int r, int b in → empty reply (or writeException on failure) The onTransact case unwraps InvocationTargetException so the client receives the real underlying exception (typically SecurityException or IllegalArgumentException from the framework), not a generic reflection wrapper Protocol version stays at "2" — purely additive 3. BetaProxyClient.setOverscan(int displayId, int l, int t, int r, int b) New public method mirroring the established runShell(String) Parcel pattern: writeInterfaceToken → 5× writeInt → transact(TXN_SET_OVERSCAN) → readException. Daemon-side exceptions surface as BetaProxyException so ShellGateway can transparently fall back. 4. ShellGateway — pattern-matching typed-verb interception Two pre-compiled regex patterns test the command before any shell call: WM_OVERSCAN = ^\s*wm\s+overscan\s+(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s+-d\s+(\d+)\s*$ WM_OVERSCAN_RESET = ^\s*wm\s+overscan\s+reset\s+-d\s+(\d+)\s*$ On match → BetaProxyClient.setOverscan(...); on success the original callback receives onSuccess("") (overscan produces no useful stdout) and we log beta setOverscan typed ok (Xms): d=N L,T,R,B. On regex miss or typed-call exception, the code falls through to the existing runShell path (which itself falls back to legacy AdbLocalClient on its own failure). The fallback chain is now: typed Binder ──FAIL──► runShell (sh -c "wm …") ──FAIL──► legacy AdbLocalClient Anchored regex (^…$) on both ends so any compound/unusual command (e.g. wm overscan … && wm size …) cleanly falls through to the shell path. 5. Sites that benefit automatically All 7 production call sites migrated in build 172 already route through ShellGateway, so the speedup is transparent — no MainActivity / ClusterService source change in this build: MainActivity resize SeekBar (~30 calls/s while dragging) MainActivity.autoApplyInsetsIfNeeded (500 ms post-launch per-app overscan) ClusterService.startActivityViaIAM (launch-time overscan) ClusterService.stopProjectionNoAdb (wm overscan reset -d 1) ClusterService post-projection apply (INSET_H,INSET_V,INSET_H,INSET_V) The two pidof sites in reconcileDisplayState / reconcileMainDisplayState are not affected by Phase 4a (no wm overscan match) — they keep going through runShell and will be migrated in Phase 4b via IActivityManager.getRunningAppProcesses / getTasks (probe P5 PASS in build 173). Behavioural contract preserved No public API removed MainActivity / ClusterService source unchanged from build 173 With the beta proxy toggle OFF, the binary is byte-for-byte identical to v1.0.1 Order of overscan calls is still preserved (single-threaded shell-gateway executor stays as the serialisation point) Any failure on the typed path falls through to the exact same code paths used in build 172 Roadmap Phase Scope Status 4a (this build) wm overscan → typed setOverscan ✅ shipped 4b pidof / dumpsys activity recents → IActivityManager.getTasks / getRunningAppProcesses + forceStopPackage / killBackgroundProcesses next 4c AdbLocalClient.sendInfo → direct IAutoContainer transact (descriptor android.os.IAutoContainer confirmed by P6) after 4b How to test on device Install the APK (OTA auto-updater picks up versionCode 174) Settings → Beta (expérimental) → Engine + Proxy daemon both ON Restart the app Use the app normally — drag the resize SeekBar, launch apps, stop projection Diag → Beta Engine → Run all still shows 27/27 ✓ (P1 path is now the same one used in production) Send (.log) and look for ShellGateway: beta setOverscan typed ok (Xms) lines — those are the new fast path; the absence of beta runShell ok (…ms): wm overscan … lines confirms the typed path is being used everywhere Build versionCode 173 → 174 versionName pinned at "1.1.9" No new strings, layouts, drawables, dependencies AGP 7.4.2, target SDK 29, compile SDK 33 — unchanged Build artefact: MyBYDApp/app/build/outputs/apk/debug/DashCast-v1.1.9-debug.apk LinksAPK download GitHub release page
  8. DashCast v1.1.9-build175DashCast 1.1.9 — build 175 (beta channel) Beta Engine Phase 4b — typed pidof via /proc scan replaces shell fork. Second production cash-in of the build-173 probe matrix, built on top of build-174's Phase 4a infrastructure. Why this build Build-174 device log validation (file byd_log_20260521_231046.log) confirmed Phase 4a: 8 typed setOverscan calls measured at 11–61 ms (warm) vs 84–275 ms in build 172 Zero shell-fallback log lines for overscan — the typed path never failed The cold first call stayed at ~1680 ms but that is daemon bootstrap, identical to build 172, hors-scope Phase 4 But the same log surfaced the next obvious hot path: 7 beta runShell ok (Xms): pidof com.waze lines at 48–181 ms every ~5 s while the Waze cluster was alive That fork is what this build eliminates. What changed Area Before (build 174) After (build 175) pidof <pkg> call sites ProcessBuilder("sh", "-c", "pidof <pkg>") inside the daemon → ~48–181 ms In-daemon pure-Java /proc/<pid>/cmdline scan (probe P8: 0 ms) → ~0–5 ms Wire protocol TXN_GET_PIDS not present New transaction TXN_GET_PIDS = FIRST_CALL_TRANSACTION + 5 (=6), additive; protocol version stays "2" ShellGateway.tryTypedVerb boolean (success/no) String payload — needed because pidof returns a real stdout (the PID list); overscan paths continue to receive "" Regex WM_OVERSCAN, WM_OVERSCAN_RESET + PIDOF = ^\s*pidof\s+([A-Za-z0-9._:\-]+)\s*$ (anchored, single-arg only) Files touched Phase4Verbs.java — new getPidsByPackage(String) ProxyDaemonMain.java — new TXN_GET_PIDS + onTransact case BetaProxyClient.java — new getPidsByPackage(String) ShellGateway.java — tryTypedVerb refactor + new PIDOF pattern CHANGELOG.md, app/build.gradle — bookkeeping Zero production source change in MainActivity / ClusterService — the two pidof call sites already route through ShellGateway and benefit automatically. Why /proc scan and not IActivityManager.getRunningAppProcesses() Both probes PASSED in build 173, but /proc was the right pick: Immune to Android-10 getRunningAppProcesses visibility restrictions (some OEMs return an empty list to non-system callers) No system Context handle required Reproduces shell pidof's argv[0] matching semantics exactly (including pkg:subprocess Android sub-processes declared in the manifest) Probe P8 measured 0 ms on the device with 241 live processes — there is no faster option Fallback chain (unchanged) typed → shell-via-daemon → legacy ADB. If the /proc scan ever returns an unexpected empty result on a future OEM build that puts apps in invisible cgroups, the typed call succeeds with "", the caller interprets that as "process dead" (legitimate semantics), and worst case the cluster bookkeeping clears one polling cycle early. If the daemon transaction itself fails, BetaProxyException triggers fall-through to runShell → legacy ADB. Behavioural contract With beta proxy toggle OFF the binary remains byte-for-byte identical to v1.0.1 (gateway short-circuits to AdbLocalClient before any pattern matching). No public API removed, no source change in MainActivity / ClusterService since build 174. Expected log lines after install + drive ShellGateway beta pidof typed ok (Xms): com.waze → "12345" ShellGateway beta pidof typed ok (Xms): com.foo → "" Should see zero lines like beta runShell ok (...ms): pidof ... (would indicate the regex did not match → bug to investigate). Build metadata versionCode: 174 → 175 versionName: 1.1.9 (pinned per channel policy — 1.2.0 reserved for DiLink 5) No new strings, layouts, drawables, or dependencies. What's next Phase 4c (probe P6 confirmed IAutoContainer descriptor): direct typed transact for AdbLocalClient.sendInfo, bypassing the BYD shell wrapper entirely. Phase 4d (optional, probes P3/P4 to be re-confirmed against shell uid): typed forceStopPackage / killBackgroundProcesses. LinksAPK download GitHub release page
  9. DashCast v1.1.9-build173DashCast v1.1.9 — build 173 (Phase 4 feasibility probes) Channel: beta/1.1.0 · versionCode: 173 · versionName: 1.1.9 This is a diagnostic-only drop. Zero production-path changes vs build 172. Its single purpose is to answer one question before we commit to the Phase 4 rewrite (typed Binder verbs in place of sh -c "wm …" / am … forks): Which native AOSP APIs can the proxy daemon actually call from uid=2000 / SELinux domain shell on the BYD Seal EU? Each probe is a single typed Binder call that Phase 4 would use as a drop-in replacement for a shell-fork hot path. The diag UI now lists 12 new rows (P1…P12) in the Beta Engine tab. Run the suite once, send the report. What the build adds 1. Phase4Probes.java (new, ~330 LoC) Runs inside the daemon process (uid 2000) and tries every typed verb that Phase 4 would substitute for a shell fork. The harness: wraps every probe in a single try/catch unwraps InvocationTargetException so the displayed message is the real cause (not a generic reflection wrapper) buckets failures into four categories so we can triage them automatically: PASS — call succeeded FAIL_SECURITY — SecurityException → Phase 4 verb is not viable from uid 2000 (sepolicy / permission blocker) FAIL_API — NoSuchMethodException / ClassNotFoundException → API signature differs on this OEM, need device-specific reflection FAIL_OTHER — any other Throwable, detail string carries the class name and message never crashes the daemon — any exception escaping runAll is caught at the binder layer and returned as P0=FAIL_OTHER:harness … The 12 probes: # Verb tested Replaces today P1 IWindowManager.setOverscan(0,0,0,0,0) the headline wm overscan shell fork (resize SeekBar, autoApplyInsetsIfNeeded, ClusterService startup) P2 IWindowManager.getInitialDisplaySize(0, Point) read-only counterpart, alternative to wm size/dumpsys display P3 IActivityManager.forceStopPackage("…does.not.exist", -1) am force-stop P4 IActivityManager.killBackgroundProcesses("…does.not.exist", -1) am kill P5 IActivityManager.getTasks(20) (with getRunningTasks(int) fallback) the dumpsys activity recents + regex pipeline used by ClusterService.findRunningTaskId P6 INTERFACE_TRANSACTION against the AutoContainer system service first step toward replacing AdbLocalClient.sendInfo P7 SystemProperties.get(ro.build.version.release) + set(debug.dashcast.probe, "1") sanity / sepolicy probe — write almost certainly fails P8 pure-Java scan of /proc/*/cmdline for com.byd.dashcast sandbox baseline; if this fails the whole Phase 4 plan is doomed regardless of any Binder permission P9 ServiceManager.listServices() with visibility check for window, activity, package, input, display, AutoContainer, media_session, audio enumerate what the daemon can even see P10 IPackageManager.getInstallerPackageName("com.byd.dashcast") typed PM read P11 IInputManager.Stub.asInterface(…) + walk of getDeclaredMethods() sanity vs the known-working injectInputEvent path P12 DisplayManager.getDisplays() via the daemon's system Context confirms framework managers attached to the system Context (already proven by BetaSystemContext) are usable inside the daemon process too The probes never disturb a running app — P3/P4 target a guaranteed-non-existent package, P1 uses an all-zeros overscan, P6 only issues INTERFACE_TRANSACTION. Results are encoded as a pipe-separated string (P1=PASS:setOverscan(0,0,0,0,0) accepted on display 0|P2=FAIL_SECURITY:…|…) with | and newlines stripped from detail strings to keep the parser trivial. 2. ProxyDaemonMain — new TXN_PROBE_PHASE4 transaction New constant TXN_PROBE_PHASE4 = FIRST_CALL_TRANSACTION + 3 (= 4) New static volatile Context sSystemContext, set right after acquireSystemContext() so ProxyBinder.onTransact can hand the system context to Phase4Probes.runAll(...) without redoing the reflection New case in the onTransact switch; wire format: no args in → String out, with the harness-safety wrapper described above Protocol version stays at "2" — the new transaction is purely additive, older clients keep working unchanged 3. BetaProxyClient.runPhase4Probes() — new client method Mirrors the existing runShell(String) pattern: writeInterfaceToken → transact(TXN_PROBE_PHASE4) → readException → readString. Throws BetaProxyException on transact failure (same fallback semantics as every other client method). 4. BetaTestRunner — Family.P and 12 new tests Family enum extended with P (next to A, B, X) catalog() grows from 15 to 27 entries with the 12 probe rows appended after X3 runOne() switch grew a single shared case-block that dispatches every P* id to a new testProbe(ctx, r) helper To avoid hammering the daemon 12 times (once per row in the diag UI), the first P-test populates two static fields: sProbeCache — parsed Map<String, String> of probe-id → "STATUS:detail" sProbeError — set if the daemon connect or the transact itself failed Subsequent P-tests hit the cache The cache is reset at the start of each runAll() invocation, so re-running the suite re-queries the daemon Status mapping is strict: only STATUS == "PASS" reports green; everything else (FAIL_SECURITY, FAIL_API, FAIL_NULL, FAIL_OTHER) is rendered as a red FAIL with the category prepended to the message, so a row in DiagActivity reads e.g. FAIL_SECURITY: SecurityException Neither user 2000 nor current process has android.permission.WRITE_SECURE_SETTINGS 5. Diag UI wiring DiagActivity already iterates BetaTestRunner.catalog() to inflate the rows (DiagActivity.java#L187) and runAll(ctx, listener) to drive them, so the 12 new probes appear automatically in the Beta Engine tab — no Diag UI code change needed. The existing "Run all" button executes them; the existing "Send (.log)" button ships the result file. Behavioural impact Production users see nothing. ShellGateway, MainActivity and ClusterService behave byte-for-byte like build 172. The new transaction only fires if the user opens Diag → Beta Engine and runs the suite. With the beta toggle OFF, the entire P family is reported as FAIL with message daemon probe run failed: daemon not connected, which is the expected legacy-only behaviour. Public API surface preserved. Phase4Probes.runAll(Context) and BetaProxyClient.runPhase4Probes() are purely additive. Nothing is removed or renamed. What we learn from a single device run Each row maps 1:1 to a Phase 4 verb: PASS → ship this verb in Phase 4 FAIL_SECURITY → stay on the shell path (or wrap behind a different mechanism, e.g. a system-signature shim) FAIL_API → OEM divergence isolated from outright denial; just needs a device-specific reflection FAIL_OTHER → triage with the carried class name + message Expected steady-state outcome (to be confirmed by the device test): P8 always PASS (sandbox baseline) P2, P5, P9, P10, P11, P12 PASS P1, P3, P4 most likely PASS (AOSP allows these from shell on most builds, BYD overlays unknown) P6 PASS for INTERFACE_TRANSACTION only — the actual sendInfo verb still has to be triaged separately P7 read=PASS, write=FAIL (sepolicy) Build versionCode 172 → 173 versionName pinned at "1.1.9" per channel policy (1.2.0 is reserved for DiLink 5) No new strings, layouts or drawables AGP 7.4.2, target SDK 29, compile SDK 33 — unchanged How to run on device Install the APK (OTA auto-updater will pick this build up because versionCode > 172) Settings → Beta (expérimental) → make sure Engine and Proxy daemon are ON Restart the app Menu → Diag → Beta Engine tab → Run all (~1 s once the daemon is up) Tap Send (.log) and share the resulting byd_report_*.log — the report carries every probe's status + detail, that is all we need to greenlight (or rescope) Phase 4. Build artefact: MyBYDApp/app/build/outputs/apk/debug/DashCast-v1.1.9-debug.apk LinksAPK download GitHub release page
  10. DashCast v1.1.9-build172Phase 3 — Beta Engine call-site migration Build 171 made the proxy daemon production-quality (12 ✓ / 0 ✗ / 3 ⊘ on device, X1 measured at 0 ms beta vs 68 ms legacy — a ~60× shell-call latency win). This build cashes that win in: the seven hot-path callers of AdbLocalClient.executeShell / executeShellWithResult in MainActivity and ClusterService now route through the proxy when beta_proxy_enabled is ON, while staying byte-for-byte identical to v1.0.1 when the toggle is OFF. What changed 1. New class com.byd.dashcast.beta.ShellGateway (~80 LoC) Drop-in wrapper with the same signatures as the legacy methods: execShell(ctx, cmd) — fire-and-forget execShellWithResult(ctx, cmd, AdbLocalClient.Callback) — with callback Owns its own dedicated single-threaded executor (shell-gateway, daemon thread) so call order is preserved within a process. Fast path (toggle OFF): delegates synchronously to AdbLocalClient with zero overhead. Beta path (toggle ON): lazily connects to the daemon via BetaProxyClient.connect(ctx) if needed, then calls BetaProxyClient.runShell(cmd) and logs beta runShell ok (Xms): <cmd>. Fallback (beta path throws): logs a warning beta runShell failed after Xms, fallback legacy: <msg> [cmd=<cmd>], then re-runs the same call through AdbLocalClient with the original callback. Consumers never see the swap. 2. MainActivity — 4 sites migrated Site Command Frequency Resize SeekBar listener wm overscan w,h,w,h -d 1 Every SeekBar drag step autoApplyInsetsIfNeeded per-app saved overscan 500 ms after every cluster launch reconcileDisplayState pidof <clusterPkg> Every ~5 s while a cluster app is alive reconcileMainDisplayState pidof <mainPkg> Every ~5 s for the main-display marker 3. ClusterService — 3 sites migrated All three wm overscan system calls: Launch-time per-app overscan in startActivityViaIAM setup stopProjectionNoAdb reset (wm overscan reset -d 1) Post-projection-active overscan apply with INSET_H,INSET_V,INSET_H,INSET_V on the cluster display 4. Sites intentionally NOT migrated Site Reason SysInfoActivity (Test ADB) Diagnostic surface — must exercise legacy on purpose SettingsActivity (Test ADB button) Same DiLink5TestRunner Self-test; would compare beta against beta BetaTestRunner (A1–A6, X1) Same; X1 specifically measures legacy vs beta delta BetaProxyClient self (bootstrap setsid sh -c …, readDaemonLogTail) The proxy cannot route itself 5. Build versionCode 171 → 172 versionName pinned at "1.1.9" per the channel policy (1.2.0 reserved for DiLink 5) Expected device behaviour With beta_proxy_enabled ON — wm overscan and pidof calls drop from ~70 ms each to ~0 ms (single-digit ms) as measured by X1 in build 171. Most visible on: The resize SeekBar — no more lag when dragging The per-app overscan auto-apply 500 ms after launch — felt as instantaneous The pidof reconciliation timer — lower CPU per poll With the toggle OFF — the binary is functionally identical to v1.1.9 build 171. Every migrated site short-circuits to the same AdbLocalClient method it was calling before. No executor overhead, no proxy connection attempt. Regression / fallback safety Any failure on the beta path (daemon down, transact failure, exception in runShell, etc.) is caught and logged as: ShellGateway: beta runShell failed after Xms, fallback legacy: <msg> [cmd=<cmd>] Then the same callback is invoked through the legacy path with the same arguments. The user never sees a broken cluster operation because of a proxy hiccup. The dedicated shell-gateway executor uses a single thread so beta-then-legacy fallback for the same call cannot interleave with other in-flight commands. Public API surface BetaProxyClient and AdbLocalClient signatures unchanged. ShellGateway is purely additive. No layout / strings / drawable changes in this build. Testing checklist on the car Install the APK (the in-app updater on build 171+ will detect this build automatically thanks to the -buildN suffix logic). Open Settings → Beta (experimental) and toggle Beta proxy ON. Tap restart in the dialog. Exercise the cluster flows: Launch a cluster app Drag the resize SeekBar (look for instant feedback) Kill the cluster app externally and wait ~5 s (should clear the tile) Stop projection Open Diagnostics → Logs and look for ShellGateway: beta runShell ok (Xms): … lines. Expected X < 5 ms. Migration baseline From v1.1.9-build171 (release) — proxy daemon production-ready, tests passing, no production consumer yet To v1.1.9-build172 — proxy daemon in production on 7 cluster hot paths, opt-in via Settings toggle, transparent fallback safety Branch: beta/1.1.0 · Not merged to main. LinksAPK download GitHub release page
  11. DashCast v1.1.9-build171DashCast v1.1.9 build 171 — UpdateChecker honours -buildN suffix Pre-release on beta/1.1.0. Bug With the new versioning policy (versionName pinned at "1.1.9", only versionCode increments), the in-app OTA checker kept reporting: [UpdateChecker] Up to date (current=1.1.9 latest=1.1.9) …for every new v1.1.9-buildN pre-release. Root cause: parseVer() stripped everything after the first -, so 1.1.9-build170 and 1.1.9 parsed to the same [1, 1, 9] array and isNewer() returned false. Fix isNewer(latest, currentName, currentCode) now: Compares base semver components — if they differ, the higher wins (1.2.0 > 1.1.9 still works). If base versions are equal, parses an optional -buildN (or -bN) suffix from the latest tag and compares against BuildConfig.VERSION_CODE. Latest tag Current name / code Result v1.1.9-build170 1.1.9 / 169 Update ✓ v1.1.9-build170 1.1.9 / 170 Up to date v1.1.9-build171 1.1.9 / 170 Update ✓ v1.2.0 1.1.9 / 999 Update ✓ (semver wins, build ignored) v1.1.9 (no suffix) 1.1.9 / 170 Up to date (legacy behaviour preserved) Log lines also now include the build number for clarity: Update available: 1.1.9+build170 → 1.1.9-build171 Files changed File Change UpdateChecker.java isNewer() signature (String, String) → (String, String, int); new extractBuild() / stripSuffix() helpers; updated call site and log lines app/build.gradle versionCode 170 → 171 Install adb install -r DashCast-v1.1.9-debug.apk Devices on build 170 or earlier will now see this build offered as an update. LinksAPK download GitHub release page
  12. DashCast v1.1.9-build170DashCast v1.1.9 build 170 — disconnect() is now a no-op Pre-release on beta/1.1.0. Goal: make A5 pass without re-introducing the build-168 daemon-mortality bug. The "magic" fix disconnect() was clearing sBinder / sDaemonUid / sDaemonPid / sDaemonVer, which forced the next connect() into a full bootstrap cycle (kill stale + respawn). That changed the daemon PID and broke A5. But disconnect() has zero production callers — it's only invoked by the test suite (A5, A6). And architecturally it was lying: the cached binder is process-scoped (static), not Activity-scoped the daemon is a separate app_process64 process under uid 2000 that outlives every Activity destroy/recreate clearing our cached reference doesn't disconnect anything; the daemon keeps running the kernel already notifies us via DeathRecipient when the daemon actually dies So disconnect() is now a no-op (with a debug log) — no socket to close, no resource to release. The static field is process lifetime, exactly like the daemon process. Why this works Test Flow Outcome A5 Persistence across Activity destroy disconnect() no-op → connect() fast-path returns the same cached binder Same PID ✓ A6 Restart resilience disconnect() no-op → kill -9 daemon → DeathRecipient fires (or pingBinder() returns false on next check) → connect() falls through to bootstrap New PID ✓ (still validates respawn) Production disconnect() never called No behavior change No registerReceiver() from a non-app process (which killed the daemon in build 168). No extra latch waits. No bootstrap thrashing. Expected result [✓] B1 ActivityThread reflection [✓] B2 System context fetch [✓] B3 Wrapped context permissions [⊘] B4 BYDAutoBodyworkDevice — SDK not on classpath (Phase 4) [⊘] B5 BYDAutoRadarDevice — SDK not on classpath (Phase 4) [⊘] B6 Legacy vs system-context delta — SDK not on classpath (Phase 4) [✓] A1 Proxy daemon alive [✓] A2 Binder reachable [✓] A3 Round-trip ping < 500 ms [✓] A4 Daemon UID = 2000 [✓] A5 Persistence across Activity destroy ← now PASS [✓] A6 Restart resilience [✓] X1 Latency comparative [✓] X2 Permission delta [✓] X3 Restore cluster integrity 12 ✓ / 0 ✗ / 3 ⊘ — no freezes, fast latencies Files changed vs build 169 File Change BetaProxyClient.java disconnect() becomes a no-op (single AppLogger.d call) with extensive Javadoc explaining why app/build.gradle versionCode 169 → 170 (versionName stays "1.1.9" per pinning policy) Install adb install -r DashCast-v1.1.9-debug.apkLinksAPK download GitHub release page
  13. DashCast v1.1.9-build169DashCast v1.1.9 build 169 — Revert REQUEST_BINDER Pre-release on beta/1.1.0. Replaces the deleted build 168 (which was a regression). What happened with build 168 Build 168 introduced an ACTION_REQUEST_BINDER broadcast so the app could ask an existing daemon to re-emit its binder (intended fix for A5). On-device run regressed to 10 ✓ / 2 ✗ with much slower test latencies (3 s per connect instead of <100 ms). Symptoms from byd_report_20260521_214713.log: no existing daemon answered REQUEST_BINDER within 1500ms on every connect() — the daemon never responds Daemon respawned on every single test (PIDs 19216 → 19268 → 19361 → 19413 → 19464 → 19513) A1 EXEC ps failed: transact: null — daemon answers pingBinder() (kernel-level) but transact() (Looper-level) fails. Looper is dead. Root cause hypothesis: systemContext.registerReceiver() called from a non-app process (app_process64 running as shell uid 2000 with no AMS ProcessRecord) is silently killing the daemon shortly after the initial broadcast. The daemon survives just long enough to broadcast and answer one pingBinder(), then dies. transact() failures cascade through runShell → sBinder = null → next test must re-bootstrap → stale-kill of the just-spawned daemon → respawn. Each extra bootstrap also adds 1.5 s REQUEST_BINDER wait + ~1 s spawn. Fix Revert REQUEST_BINDER entirely. Back to the v1.1.8 connect() flow (fast-path + bootstrap), which was: [✓] A1 Proxy daemon alive [✓] A2 Binder reachable [✓] A3 Round-trip ping < 500 ms [✓] A4 Daemon UID = 2000 [✗] A5 Persistence across Activity destroy ← known limitation [✓] A6 Restart resilience [✓] X1, X2, X3 + B1/B2/B3 (Phase 2 system context) - B4/B5/B6 (skip, Phase 4) = 11 ✓ / 1 ✗ / 3 ⊘ — no freezes A5 is now accepted as a known limitation of the one-shot ACTION_PROXY_CONNECTED architecture. A proper fix (likely ServiceManager.addService if SELinux ever allows, or another recovery channel that doesn't trigger AMS-side process validation) is deferred. Versioning policy change Per user request: versionName is pinned at "1.1.9" for all subsequent DiLink 3.0 builds Only versionCode increments (this build: 169) "1.2.0" is reserved for the DiLink 5 port (Dilink5 Dashboard pipeline already reverse-engineered) Files changed vs build 168 File Change ProxyDaemonMain.java Remove ACTION_REQUEST_BINDER constant + dynamic BroadcastReceiver registration BetaProxyClient.java Remove tryRequestExistingBinder(), REQUEST_BINDER_WAIT_MS, restore v1.1.8 connect() app/build.gradle versionCode 168 → 169 (versionName unchanged) Install adb install -r DashCast-v1.1.9-debug.apkLinksAPK download GitHub release page
  14. DashCast v1.1.8DashCast v1.1.8 — Beta Engine Phase 2: Monitor/Latch Deadlock Fix Pre-release on beta/1.1.0. Not merged into main (which stays at v1.0.1). TL;DR The persistent A1 / A5 / A6 failures and the head-unit freezes observed in v1.1.6 and v1.1.7 had a single root cause: a textbook synchronized + CountDownLatch.await() deadlock in BetaProxyClient.connect(). v1.1.8 restructures the method so the lock is released across the wait. No other changes. The smoking gun v1.1.7 device log showed the same pattern on every single connect: 21:29:21.206 bootstrap result OK 21:29:36.207 no live binder after 15000ms (latch=timed-out) ← exactly 15 s 21:29:36.207 live binder received from daemon ← 0 ms later The broadcast wasn't arriving late — it was arriving promptly, but onReceive() was blocked behind the lock that connect() was holding while waiting on the latch. Why public static boolean connect(Context ctx) { synchronized (LOCK) { // ← acquire ... latch.await(BROADCAST_WAIT_MS, ...); // ← does NOT release LOCK ... } } @Override public void onReceive(Context c, Intent intent) { ... synchronized (LOCK) { // ← blocked for 15 s sBinder = bp.binder; latch.countDown(); // ← never reached in time } } CountDownLatch.await() is not Object.wait() — it does not drop the monitor of the enclosing synchronized. So the receiver thread was queued behind the connect thread for the entire timeout window, the latch was never counted down in time, and connect() returned false even though the daemon had broadcast its binder seconds earlier. The tablet freezes follow directly from the same deadlock: A1 + A5 + A6 each held LOCK for 15 s, and any other component (test runner, AdbLocalClient callbacks, the broadcast dispatcher itself) that needed it queued up. Combined with the heavy ActivityThread.systemMain() work happening in the daemon process, the head unit appeared to lock up. The fix connect() is split into three short synchronized sections: Acquire — check fast-path, register receiver, arm the latch, drop LOCK. Bootstrap + await — LOCK-free. The receiver can now run. Reacquire — late-arrival check, handshake, return. That's it. ~30 lines of code shuffled, no behavioural change to anything else. Expected behaviour on the same device Test v1.1.6 v1.1.7 v1.1.8 (expected) A1 Proxy daemon alive ✗ ✗ (deadlock) ✓ A2 Binder reachable ✓ ✓ ✓ A3 Round-trip ping ✓ ✓ ✓ A4 Daemon UID = 2000 ✗ ✓ ✓ A5 Persistence after destroy ✗ ✗ (deadlock) ✓ A6 Restart resilience ✗ ✗ (deadlock) ✓ Head-unit freezes several A1 + A5 (≥ 2) none expected B-tests, X-tests, and the SDK-related skips are unchanged. Files changed File Change app/src/main/java/com/byd/dashcast/beta/BetaProxyClient.java connect() split into three synchronized sections — LOCK released across the latch wait app/build.gradle versionCode 166 → 167, versionName "1.1.7" → "1.1.8" Install adb install -r DashCast-v1.1.8-debug.apk Run the Beta panel → "Run all tests". Expected: all six A-tests ✓, no freezes during A5 / A6. LinksAPK download GitHub release page
  15. DashCast v1.1.7DashCast v1.1.7 — Beta Engine Phase 2: Broadcast Race + Dead-Binder Fix Pre-release on beta/1.1.0. Not merged into main (which stays at v1.0.1). TL;DR v1.1.6's switch to Binder + broadcast IPC successfully bypassed the SELinux block (A2 ✓ isConnected=true, A3 ✓ ping avg 0 ms on the device). However the device test exposed three subtle bugs in the dispatch logic that caused most A-tests to be reported as ✗ and made the head unit freeze repeatedly during the A5/A6 resilience tests. v1.1.7 fixes all three. What the v1.1.6 device run revealed 21:21:11.194 bootstrapping daemon 21:21:19.976 no PROXY_CONNECTED broadcast within 8000ms ← timeout 21:21:19.977 binder received from daemon (alive=true) ← 1 ms too late … 21:21:37.827 binder received from daemon (alive=false) ← stale broadcast 21:21:46.492 binder received from daemon (alive=false) 21:21:55.143 binder received from daemon (alive=false) 21:21:55.165 binder received from daemon (alive=true) Test results: Total: 15 ✓ 6 ✗ 4 ⊘ 5 (A1, A4, A5, A6 ✗). Bug 1 — Latch raced the broadcast by 1 ms ActivityThread.systemMain() in the daemon brings up the entire framework runtime inside app_process and takes 5–8 s cold on a DiLink 3.0 SoC. v1.1.6's 8 s wait was finishing essentially at the same instant the broadcast arrived. A1 was reported as ✗ even though the binder showed up 1 ms after the timeout, which is why A2 / A3 immediately succeeded on the very next call. Fix: BROADCAST_WAIT_MS raised from 8 s → 15 s. After timeout, connect() re-checks sBinder.pingBinder() instead of treating the timeout as a hard failure. Bug 2 — handshake() never ran on timeout (A4 ✗ uid=-1) connect() returned false on the latch timeout and skipped handshake(), so sDaemonUid / sDaemonPid / sDaemonVer stayed at -1/-1/null. A4's "Daemon UID = 2000" check therefore failed permanently, even though A2 / A3 happily talked to the live binder. Fix: connect() always runs handshake() when sBinder is live but the WHOAMI cache is stale (sDaemonUid < 0). The cache is also invalidated on every accepted binder swap so a fresh handshake is queued. Bug 3 — Receiver overwrote a live binder with a dead one (A5, A6 ✗ + tablet freezes) Each call to connect() killed the existing daemon and spawned a new one. The AMS broadcast queue still held the previous daemon's PROXY_CONNECTED intent, which got dispatched after the kill — so the receiver overwrote the live binder reference with one whose pingBinder() returned false. Worse, every spurious re-bootstrap fired up another full ActivityThread.systemMain() inside app_process, and the cumulative load is what froze the head unit during the resilience tests. Fix: Drop incoming broadcasts whose binder.pingBinder() is already false. Drop duplicate broadcasts when a live binder is already cached. Fast-path connect() returns immediately when sBinder.pingBinder() is true — no kill, no respawn, no ActivityThread.systemMain() thrash. Files changed File Change app/src/main/java/com/byd/dashcast/beta/BetaProxyClient.java Receiver hardening, fast-path connect(), late-broadcast recovery, lazy handshake app/build.gradle versionCode 165 → 166, versionName "1.1.6" → "1.1.7" Expected v1.1.7 behaviour on the same device Test v1.1.5 v1.1.6 v1.1.7 (expected) A1 Proxy daemon alive ✗ ✗ (1 ms race) ✓ A2 Binder reachable ✗ ✓ ✓ A3 Round-trip ping ✗ ✓ ✓ A4 Daemon UID = 2000 ✗ ✗ (uid=-1) ✓ A5 Persistence after destroy ✗ ✗ (stale binder) ✓ A6 Restart resilience ✗ ✗ (4× respawn freeze) ✓ Head-unit freezes — several none expected B1–B3 (system-context probes) and X3 (cluster restore) already passed in v1.1.6 and are unaffected. B4 / B5 / B6 still skip — the real BYD SDK package is android.hardware.bydauto.*, not com.byd.bydautosdk.* (tracked for v1.2.x). Install adb install -r DashCast-v1.1.7-debug.apk Run the Beta panel → "Run all tests". On a working device you should see all six A-tests flip to ✓ and zero head-unit freezes. LinksAPK download GitHub release page
  16. DashCast v1.1.6DashCast v1.1.6 — Beta Engine Phase 2: SELinux Fix (Binder + Broadcast IPC) Pre-release on beta/1.1.0. Not merged into main (which stays at v1.0.1). TL;DR The Phase 2 proxy daemon (Component A) now works on real DiLink 3.0 devices. The abstract Unix socket used in v1.1.2–v1.1.5 was silently blocked by SELinux on Android 10+; this release replaces it with a Binder published through a one-shot broadcast — the exact same trick OpenBYD uses, and the same pattern as our own MirrorDaemon. What broke in v1.1.5 The v1.1.5 device test (Atto3, DiLink 3.0, API 29) showed the daemon spawning correctly and printing: [dashcast_proxy] listening on @dashcast_proxy uid=2000 pid=18924 ver=1 but every BetaProxyClient.connect() call from the app failed. Root cause: Android 10+ SELinux policy denies untrusted_app_29 → shell for unix_stream_socket connectto, so the app could never reach the daemon's abstract socket. The [d]ashcast_proxy regex + kill -9 stale-cleanup added in v1.1.5 worked perfectly (visible as stale_killed=<pid> in the daemon log) — the daemon side was fine, the wire was not. What changes in v1.1.6 New IPC architecture Concern v1.1.2 – v1.1.5 v1.1.6 Wire transport Abstract LocalServerSocket @dashcast_proxy IBinder published through a one-shot broadcast App → daemon discovery LocalSocket.connect(ABSTRACT) (denied by SELinux) Dynamic BroadcastReceiver listening for com.byd.dashcast.beta.PROXY_CONNECTED Request/response Line-based text protocol (PING, WHOAMI, EXEC …) Binder.transact(code, Parcel, Parcel) with three transaction codes PROTOCOL_VERSION "1" "2" Bootstrap (unchanged — works as of v1.1.5) The setsid sh -c "CLASSPATH='$APK' exec /system/bin/app_process64 -Xnoimage-dex2oat /system/bin --nice-name=dashcast_proxy …" recipe, the stale-kill guard, and the diagnostic header in /data/local/tmp/dashcast_proxy.log are all preserved exactly as in v1.1.5. The process name still shows as dashcast_proxy in ps -A, so the existing kill heuristic (ps -A | grep '[d]ashcast_proxy') keeps working. Daemon (ProxyDaemonMain.java) Looper.prepareMainLooper() + Looper.loop() — keeps the binder pool alive. ActivityThread.systemMain().getSystemContext() via reflection — gives the daemon a usable Context for AMS calls. New ProxyBinder extends Binder with DESCRIPTOR = "com.byd.dashcast.beta.proxy.IProxyDaemon" and a hand-rolled onTransact: TXN_PING (1) → long epoch_ms TXN_WHOAMI (2) → int uid, int pid, String ver TXN_EXEC (3) → int exit, String combinedOutput Broadcast com.byd.dashcast.beta.PROXY_CONNECTED targeted at com.byd.dashcast with the binder wrapped in a new BinderParcelable extra, plus FLAG_INCLUDE_STOPPED_PACKAGES. Client (BetaProxyClient.java) Dynamic BroadcastReceiver registered once per process on the application context. CountDownLatch armed before the bootstrap script is executed, so a fast broadcast cannot be missed. binder.pingBinder() used for liveness; binder.transact() for all RPCs. Public API surface preserved (connect, isConnected, disconnect, ping, runShell, getCallerUid, getDaemonPid, getProtocolVersion, readDaemonLogTail, BetaProxyException) → BetaTestRunner and its 15 tests need no change. readDaemonLogTail() still queries tail -n 20 /data/local/tmp/dashcast_proxy.log via legacy ADB, so cold-start failures remain self-diagnostic. New file app/src/main/java/com/byd/dashcast/beta/proxy/BinderParcelable.java — minimal Parcelable wrapping a single IBinder, because Intent.putExtra cannot carry a raw IBinder value. Why this works (and the old approach didn't) Mechanism Blocked by SELinux for untrusted_app_29 → shell? Abstract LocalServerSocket connectto (v1.1.2–v1.1.5) Yes — denied by stock Android 10+ sepolicy Broadcast Intent dispatched by ActivityManagerService No — capability-based, allowed IBinder.transact() on a binder ref previously received No — capability-based, allowed Same reasoning as OpenBYD's EntryPoint.main() → systemContext.sendBroadcast(intent) → app receives ProxyBinderParcelable → calls ICarControl.runShellCommand(). Our MirrorDaemon (already shipping in this app) uses an equivalent published-binder pattern. Compatibility minSdkVersion 28 (lowered in v1.1.5, kept here) — DiLink 2.0 owners can still install and inspect. targetSdkVersion 29 unchanged. The 2-arg Context.registerReceiver overload is used; on targetSdk ≥ 33 the call would need Context.RECEIVER_EXPORTED (documented inline as a future-bump TODO). Beta channel only — main remains on v1.0.1. Files changed File Change app/build.gradle versionCode 164 → 165, versionName "1.1.5" → "1.1.6" app/src/main/java/com/byd/dashcast/beta/proxy/ProxyDaemonMain.java Full rewrite — Binder + broadcast pattern, no more LocalServerSocket app/src/main/java/com/byd/dashcast/beta/BetaProxyClient.java Full rewrite — dynamic receiver + binder.transact, public API preserved app/src/main/java/com/byd/dashcast/beta/proxy/BinderParcelable.java New — Parcelable wrapper for the daemon's IBinder Known limitations (still on the roadmap) Phase 4 BYD SDK probes (B4 / B5 / B6) still skip on stock DiLink — the real package is android.hardware.bydauto.*, not com.byd.bydautosdk.*. Tracked for v1.2.x. Daemon does not auto-restart if it crashes; the client always re-bootstraps on the next connect() (handled by the existing stale-kill guard). Install adb install -r DashCast-v1.1.6-debug.apk Then run the Beta panel → "Run all tests". On a working device you should see the four A-tests (A1–A4) flip from ✗ to ✓ versus v1.1.5. LinksAPK download GitHub release page
  17. DashCast v1.1.5DashCast 1.1.5 (beta) Hotfix release for the Phase 2 / Component A proxy daemon: the @dashcast_proxy abstract socket is now reclaimed before every bootstrap, and the minimum supported Android version is lowered to Android 9 / API 28 so DiLink 2 owners can install and test the beta. Why a third hotfix in the 1.1.x line 1.1.2 introduced the Phase 2 daemon but used a broken bootstrap (bare app_process) that crashed silently. 1.1.3 switched to the proven MirrorDaemon recipe but kept a shell-quoting bug that left the JVM with an empty CLASSPATH. 1.1.4 fixed the quoting; the daemon finally started, but the log tail then revealed a new failure mode: java.io.IOException: Address already in use at new LocalServerSocket("dashcast_proxy"). The cause of that last failure was a stale daemon from a previous session. The bootstrap uses setsid, so the daemon survives both the ADB session it was launched from and the host app shutdown. Once a daemon successfully binds the abstract socket, every subsequent bootstrap loses the race to bind and SIGABRTs — and the host app, which is now talking to a daemon it cannot identify (potentially a different protocol version), cannot recover on its own. Fix — reclaim before bootstrap The bootstrap script now starts with: STALE=$(ps -A 2>/dev/null | grep '[d]ashcast_proxy' | awk '{print $2}') if [ -n "$STALE" ]; then kill -9 $STALE 2>/dev/null; sleep 0.3; fi Notes: The [d] trick prevents grep from matching its own process line while still matching daemons whose --nice-name was set to dashcast_proxy. awk '{print $2}' extracts the PID column from Android's ps -A output regardless of the active toybox version. kill -9 $STALE is unconditional. The daemon is stateless and the client always re-bootstraps as needed, so killing is always safe. A 300 ms sleep gives the kernel time to release the abstract socket before the new daemon's bind() call. The killed PID (or none) is now logged to /data/local/tmp/dashcast_proxy.log as [boot] stale_killed=<pid>, so future failures will reveal whether the reclaim step actually fired. This change is also fully compatible with A6 — Restart resilience: that test kills the daemon then expects the next connect() to re-bootstrap a fresh one. After the manual kill, the bootstrap's STALE is empty and the new daemon starts on the first try. minSdkVersion 29 → 28 — DiLink 2 owners can now install The Android 9 (API 28) device fleet is excluded from the previous builds because of minSdkVersion 29. Lowering it to 28 lets DiLink 2 owners install the APK and at least exercise the Diag suite. No 1.1.x runtime path uses an API-29-only symbol on the main flow. If a 1.1.x feature does fail on API 28 (likely candidates: the cluster mirror or the BYD SDK reflection), the in-app log will surface a clean stack trace — which is exactly the kind of feedback this release is meant to collect. What 1.1.5 does NOT change No protocol change — wire version is still 1 (PING / WHOAMI / EXEC / QUIT). No socket-name change — still @dashcast_proxy. No Diag catalog change — A1–A6, B1–B6, X1–X3 unchanged. No SDK / classpath work — B4/B5/B6 still skip because they look for com.byd.bydautosdk.*; the real classes live under android.hardware.bydauto.* and will be addressed in a separate beta. How to verify Install the new APK over 1.1.4 (versionCode bumps 163 → 164). Open Diag → Beta Engine. Expected: A1–A6 turn ✓. The first bootstrap will report [boot] stale_killed=<pid> in the daemon log if it found and killed a leftover from 1.1.4; subsequent bootstraps within the same session should report [boot] stale_killed=none. If A1 still fails, the daemon log tail now contains both the stale_killed field and the full Java stack trace, which is enough to pinpoint the remaining cause without an adb shell. Files DashCast-v1.1.5-debug.apk — signed debug APK (versionCode 164, versionName 1.1.5, minSdkVersion 28). Compatibility Tested target: BYD Atto3 / DiLink 3.0 / Android 10 (API 29) / firmware v25.02. DiLink 2 (Android 9 / API 28) — install path now open, runtime behaviour pending field reports. DiLink 5.x testing still pending external validation. Branch: beta/1.1.0 — main remains pinned to the last stable release v1.0.1 and is not merged in this prerelease. LinksAPK download GitHub release page
  18. DashCast v1.1.4DashCast 1.1.4 (beta) Hotfix release for a shell-quoting bug introduced in 1.1.3 that prevented the Phase 2 / Component A proxy daemon from ever starting on real hardware. The 1.1.3 release switched the daemon bootstrap to the proven MirrorDaemon recipe (/system/bin/app_process64 -Xnoimage-dex2oat /system/bin --nice-name=... <class>) and added a daemon log tail in test failures. The new log tail revealed a single line — Aborted — which exposed the real bug: a shell-quoting mistake that left the JVM with an empty CLASSPATH. Root cause The 1.1.3 bootstrap command was: APK=$(pm path com.byd.dashcast | head -n1 | cut -d: -f2-) setsid sh -c 'CLASSPATH="$APK" /system/bin/app_process64 \ -Xnoimage-dex2oat /system/bin \ --nice-name=dashcast_proxy \ com.byd.dashcast.beta.proxy.ProxyDaemonMain \ </dev/null >/data/local/tmp/dashcast_proxy.log 2>&1' & The sh -c '...' argument is wrapped in single quotes, so the outer shell never expands $APK. The inner shell receives the literal string $APK, treats it as a variable defined in its own context (where it is empty), and ends up exporting CLASSPATH= (empty). app_process64 then cannot load ProxyDaemonMain, ART aborts during class resolution, and the only trace left in the log is the bash runner's terse Aborted message — no stack trace, no ClassNotFoundException, because the JVM died before it could write anything useful to stderr. MirrorDaemon doesn't hit this bug because its recipe concatenates the APK path into the Java string (`"... CLASSPATH=" + apkPath " ..."`) before the single quotes, so the quoting is irrelevant. Fix The bootstrap now uses double quotes for sh -c "...", allowing the outer shell to expand $APK before handing the command to setsid: APK=$(pm path com.byd.dashcast | head -n1 | cut -d: -f2-) LOG=/data/local/tmp/dashcast_proxy.log # Instrumentation prologue (overwrites previous log on each bootstrap) { echo "[boot] $(date) apk=$APK" echo "[boot] id=$(id)" echo "[boot] getenforce=$(getenforce 2>/dev/null)" ls -la "$APK" 2>&1 echo "[boot] exec app_process64..." } > "$LOG" 2>&1 setsid sh -c "CLASSPATH='$APK' exec /system/bin/app_process64 \ -Xnoimage-dex2oat /system/bin \ --nice-name=dashcast_proxy \ com.byd.dashcast.beta.proxy.ProxyDaemonMain \ </dev/null >>'$LOG' 2>&1" & echo OK $APK Additional small improvements bundled in the same change: Instrumentation prologue — every bootstrap now writes id, getenforce, and ls -la <APK> to the daemon log before exec'ing app_process64. If a future cold-start fails for an unrelated reason (SELinux, missing APK, wrong uid…), the context required to diagnose it is already on disk by the time the daemon log tail is read. exec inside the inner shell — the wrapper sh is replaced by app_process64, so ps no longer shows an extra dangling sh -c parent for each daemon instance. APK path single-quoted inside the inner command — defensive quoting in case a future package install ends up at a path with unusual characters. What 1.1.4 does NOT change No protocol change — wire version is still 1 (PING / WHOAMI / EXEC / QUIT). No socket-name change — still @dashcast_proxy. No Diag catalog change — A1–A6, B1–B6, X1–X3 unchanged. No SDK / classpath work — B4/B5/B6 still skip because they look for com.byd.bydautosdk.*; the real classes live under android.hardware.bydauto.* and will be addressed in a separate beta. How to verify Install the new APK over 1.1.3 (versionCode bumps 162 → 163, so the in-app updater accepts it automatically). Open Diag → run Beta Engine. Expected: A1–A6 turn ✓ (or at worst show a real Java stack trace in the detail field instead of Aborted). If A1 still fails, the new instrumentation prologue means the daemon log tail now starts with [boot] ... id=uid=2000(shell) ... etc., which is enough to pinpoint the remaining issue. Files DashCast-v1.1.4-debug.apk — signed debug APK (versionCode 163, versionName 1.1.4). Compatibility Tested target: BYD Atto3 / DiLink 3.0 / Android 10 (API 29) / firmware v25.02. DiLink 5.x testing still pending external validation. Branch: beta/1.1.0 — main remains pinned to the last stable release v1.0.1 and is not merged in this prerelease. LinksAPK download GitHub release page
  19. DashCast v1.1.3DashCast 1.1.3 (beta) Hotfix release for the Phase 2 / Component A proxy daemon shipped in 1.1.2. On real DiLink 3.0 hardware (BYD Atto3, API 29) the daemon spawned in 1.1.2 reported a successful bootstrap (OK <apk-path>) but never bound its @dashcast_proxy abstract socket, so every Diag test in the A family failed with connect() returned false and there was no way to see why (stderr was silently dropped to /dev/null). This release fixes the bootstrap recipe and adds first-class observability so future cold-start failures are diagnosable directly from the in-app Diag report. What changed Bootstrap aligned with the proven MirrorDaemon recipe The 1.1.2 bootstrap was: ( CLASSPATH="$APK" nohup app_process / com.byd.dashcast.beta.proxy.ProxyDaemonMain \ >/dev/null 2>&1 </dev/null & ) It now matches the recipe that has been working for MirrorDaemon on the same device for months: setsid sh -c 'CLASSPATH="$APK" /system/bin/app_process64 \ -Xnoimage-dex2oat /system/bin \ --nice-name=dashcast_proxy \ com.byd.dashcast.beta.proxy.ProxyDaemonMain \ </dev/null >/data/local/tmp/dashcast_proxy.log 2>&1' & The differences that matter on BYD Android 10 images: Aspect 1.1.2 (broken) 1.1.3 (fixed) Detachment ( ... & ) subshell + nohup setsid sh -c '...' & (detached from ADB session group, survives SIGHUP) Binary bare app_process (symlink not always resolvable on BYD images) absolute /system/bin/app_process64 (explicit 64-bit) AOT no flag (potential dex2oat crash at startup) -Xnoimage-dex2oat Parent dir argument / /system/bin argv[0] only the daemon's own setArgV0 reflection --nice-name=dashcast_proxy set by the runtime before main() Output >/dev/null 2>&1 (blackhole) >/data/local/tmp/dashcast_proxy.log 2>&1 Daemon log tail surfaced in Diag failures New BetaProxyClient.readDaemonLogTail(Context) reads the last 20 lines of /data/local/tmp/dashcast_proxy.log via the existing legacy-ADB client. When A1 — connect() fails, the tail is attached to the result's detail field. It is visible in the in-app test report and in the exported byd_report_*.log, so the actual error (ClassNotFoundException, dex2oat failure, SELinux denial, etc.) no longer requires a host-side adb shell. No protocol or socket-name changes Abstract socket name is still @dashcast_proxy. Protocol version is still 1 (PING / WHOAMI / EXEC / QUIT). Diag catalog (A1–A6, B1–B6, X1–X3) is unchanged. Upgrade notes If you installed 1.1.2 and saw A1–A6 all marked FAIL with connect() returned false, upgrade to 1.1.3 and rerun the Diag suite. The A family should now turn green. If A1 still fails, the new detail payload contains the daemon's stderr tail; please share it in an issue. Files DashCast-v1.1.3-debug.apk — signed debug APK (versionCode 162, versionName 1.1.3). Compatibility Tested target: BYD Atto3 / DiLink 3.0 / Android 10 (API 29) / firmware v25.02. DiLink 5.x testing still pending external validation. Branch: beta/1.1.0 — main remains pinned to the last stable release v1.0.1 and is not merged in this prerelease. LinksAPK download GitHub release page
  20. DashCast v1.1.1DashCast v1.1.1 (pre-release) Channel: Beta · versionCode: 159 · Branch: beta/1.1.0 Successor to v1.1.0-beta1. Stable channel users are not affected. ✨ New features DiLink 5 dedicated diagnostic tab A 7th tab is added to the Diag screen, completely independent from the Beta Engine. It contains 9 tests targeting the DiLink 5.0 / Android 12 platform reported by Alexander: ID Test D1 Platform identity — ro.product.name, Build.MODEL/BRAND/FINGERPRINT, API level. D2 Displays inventory — every Display exposed by DisplayManager (id, state, flags, size). D3 Service binder probe — detects renamed AutoContainer services (e.g. crosscontrol, xdja). D4 wm overscan availability — confirms AOSP removed the command in Android 11+. D5 am --display flag probe — checks whether am start --display N is supported. D6 Granted permissions inventory — what the current APK actually has at runtime. D7 ADB local TCP reachability (127.0.0.1:5555). D8 Guarded cluster launch — am force-stop → am start --display N → wait → am start --display 0 (retract) → wait → am force-stop. The retract step prevents OS display affinity from sticking the app on the cluster forever. Target package is picked from a dropdown listing every launchable app. D9 BYD packages inventory — versionCode + signature digest for com.byd.* packages. The tab also displays a live Mode pill (AUTO = DiLink 5 / FORCED on/off) and a counter row (✓ PASS ✗ FAIL ! WARN ⊘ SKIP). Platform auto-detection + manual override New Platform singleton initialised once in DashCastApp.onCreate() : Reads ro.product.name via SystemProperties reflection. Detects DiLink 5 from substrings (dilink5, dilink_5) or API ≥ 31 on a BYD device. Stores user override in byd_app_prefs (AUTO / FORCE_ON / FORCE_OFF). A new switch in Settings → Beta Engine named Mode DiLink 5 (experimental) lets you override the auto value. Initial position mirrors the auto-detect; toggling it triggers the same restart required dialog as the other Beta switches. When the platform is auto-detected as DiLink 5, the Diag screen now opens on the DiLink 5 tab by default; otherwise it opens on Beta Engine as before. Settings — checkbox renamed to Beta Channel The pre-release toggle (which opens DashCast to alpha/beta/pre-release tags) is now labelled simply Beta Channel in all 12 locales — same channel concept as Chrome/Edge stable/beta/dev channels. 🐛 Fixes Beta Engine — B1/B2/B3 (ActivityThread.systemMain) no longer crash Real-device testing of 1.1.0-beta1 revealed that B1/B2/B3 always failed with: RuntimeException: Can't create handler inside thread Thread[beta-test-runner,5,main] that has not called Looper.prepare() at android.app.ActivityThread.<init> at android.app.ActivityThread.systemMain ActivityThread.systemMain() instantiates an internal Handler in its constructor, which requires a Looper on the current thread. BetaTestRunner runs on a single-threaded background Executor with no Looper, so the reflection call exploded before it could return the ActivityThread instance. Fix mirrors the BydAgent reference implementation : if (Looper.myLooper() == null) Looper.prepare(); Added in BetaSystemContext.get() (protects every future caller, not just tests) and in BetaTestRunner.testB1(). B1/B2/B3 should now turn ✓ on any device where ActivityThread is not stripped. B4/B5/B6 still skip with not on classpath until the BYD SDK is bundled — by design. 📦 Install DashCast-v1.1.1-debug.apk — installs cleanly over v1.1.0-beta1 (versionCode bumped 156 → 159). ⚠️ Phase status Beta Engine — Component A (proxy daemon) : A1–A6 remain SKIPPED. The daemon backend is Phase 2 and not yet implemented. Beta Engine — Component B (system context reflection) : B1–B3 active (fixed in this release), B4–B6 require the BYD SDK on the classpath. DiLink 5 tab : Phase 1 — pure observation. Acting on the results (auto-switching am --display vs the legacy SDK, picking the renamed binder name, etc.) lands in a later beta. LinksAPK download GitHub release page
  21. DashCast v1.1.2DashCast v1.1.2 — Beta Engine Phase 2 (re-issued) Pre-release · beta channel · do not merge to main This release ships Component A of the Beta Engine — the proxy daemon that was scaffolded but stubbed in v1.1.0 / v1.1.1. The eight diagnostic tests A1–A6, X1, X2 that previously reported ⊘ proxy daemon not implemented yet (Phase-2) are now real. Re-issue notice. The first v1.1.2 build (dc4b565) shipped with the daemon listening on @openbyd_proxy, a name borrowed from another project. To avoid collisions with anyone running OpenBYD on the same tablet, the abstract socket and setArgV0 process name have both been renamed to dashcast_proxy. The release tag was re-cut on top of the amended commit (692aad4), versionCode was bumped 160 → 161 so devices that already installed the previous build can upgrade in-place. What's new Proxy daemon (dashcast_proxy) A new long-lived helper process is now spawned from the app the first time the Beta Engine tab is exercised (or the Use proxy daemon toggle is flipped on). It is brought up by running app_process through the existing local-ADB bridge, which means it inherits the shell UID (2000) — the same identity used by every legacy adb shell call — without requiring root. The daemon: listens on the Linux abstract namespace socket @dashcast_proxy, renames itself to dashcast_proxy via setArgV0 so it is easy to spot in ps -A, serves a tiny line-based protocol — PING, WHOAMI, EXEC <command>, QUIT — with replies framed as OK …, DAT <line>, or ERR <message>, survives the app being killed or restarted (re-attached via the abstract socket on the next connect()), respawns automatically if killed (kill -9) the next time the app tries to connect. Client (BetaProxyClient) The previous Phase-1 no-op was replaced by a full implementation: connect(ctx) — fast path tries the socket; cold start issues a bootstrap script through AdbLocalClient (CLASSPATH=$(pm path com.byd.dashcast) nohup app_process / com.byd.dashcast.beta.proxy.ProxyDaemonMain &) and retries the socket connect 8× with a 300 ms backoff. WHOAMI handshake caches the daemon's UID, PID and protocol version. ping(), runShell(cmd), getCallerUid(), getDaemonPid(), getProtocolVersion() are now real RPCs. Thread-safe through a single static lock; concurrent callers queue rather than corrupting the wire format. Diagnostic tests now active The Beta Engine tab of Diagnostics now exercises all 15 tests end-to-end: Test What it checks A1 Proxy daemon alive connect + ps -A | grep dashcast_proxy A2 Binder reachable isConnected() == true after connect A3 Round-trip ping < 500 ms 5 pings; fail if worst > 500 ms A4 Daemon UID = 2000 handshake UID equals shell A5 Persistence across disconnect reconnect yields the same PID A6 Restart resilience kill -9, reconnect re-bootstraps, new PID X1 Latency comparative proxy ping vs. legacy id -u (≥ 2× faster) X2 Permission delta both paths land on shell UID 2000 B1–B6 and X3 remain as in v1.1.1; the Looper fix shipped in v1.1.0-beta2.1 is preserved. Behaviour for users on the legacy path If the Use proxy daemon toggle is left off and the Beta Engine tab is never opened, behaviour is identical to v1.1.1 — the daemon is never spawned, no extra processes appear, no extra sockets are bound. If the toggle is on, BetaEngineGateway.safeCall still falls back to AdbLocalClient on any failure, so the worst case is "behaves like legacy". First-run notes The first invocation triggers the standard local-ADB pairing popup — Allow USB debugging? Always allow from this computer. Tick the box; the RSA key is persisted and the popup never reappears unless cleared from the developer settings. Once paired, subsequent app launches re-attach to the existing daemon without going through ADB again (sub-10 ms typical). Compatibility Same minSdk 29 / targetSdk 29 / API surface as v1.1.1. DiLink 3.0 confirmed (test reports from BYD Atto3 v25.02). DiLink 5 still needs owner testing — the proxy daemon should behave identically since it relies on standard Android primitives (LocalServerSocket, app_process, ps, id). Upgrade Already on v1.1.x with Beta Channel enabled → the OTA picks v1.1.2 up automatically at next app launch. Anyone who installed the original v1.1.2 (vc 160) gets the re-issued build (vc 161) through the same OTA flow. Otherwise: Settings → tick Beta Channel. Re-open the app; OTA updater fetches v1.1.2 and installs. Files DashCast-v1.1.2-debug.apk — debug build signed with the BYD platform key, ~14.2 MB, versionCode 161. Changelog (commit) feat(beta): Phase 2 — Component A proxy daemon (v1.1.2) — see 692aad4. Reminder Branch main stays on v1.0.1 — no merge in this cycle. This is a beta intended for owners who already opted into the Beta Channel. Please share diagnostic reports through the in-app Copy report button or by attaching byd_report_*.log. LinksAPK download GitHub release page
  22. DashCast v1.1.0-beta1DashCast v1.1.0-beta1 — Beta Engine scaffolding + Diagnostics rebuild + file-based log sharing First pre-release on the new beta/1.1.0 channel. Introduces an opt-in "Beta Engine" pipeline that is wired everywhere it needs to be, but stays inert at runtime by default so the v1.0.1 stable code path remains the source of truth. ⚠️ Pre-release for testers. Do not merge to main. Install only if you understand what a beta channel implies. Both Beta toggles are OFF by default — with both OFF, the app behaves exactly like v1.0.1. What's new 1. New com.byd.dashcast.beta package (5 classes, ~654 LoC) Class LoC Role BetaConfig 67 SharedPreferences-backed (byd_app_prefs, same store as SettingsActivity) feature flags: beta_engine_enabled and beta_proxy_enabled (both default false). BetaSystemContext 119 Process-wide singleton snapshot of the beta state. Exposes isEngineEnabled() / isProxyEnabled() so hot paths never re-read prefs. BetaProxyClient 94 Stub client that will later route ADB calls through an alternate transport. Currently returns NOT_IMPLEMENTED for every probe. BetaEngineGateway 65 Single entry point used by future call-sites. Exposes a safeCall(legacy, beta) helper that auto-falls back to the legacy implementation on any exception so the user can keep using the app even if the beta path fails. BetaTestRunner 309 15-test self-check matrix — B1–B6 real beta-engine probes, A1–A6 SKIPPED placeholders for the ADB-bridge work, X1–X3 mixed cross-checks. Each test returns a Result with status (PASS / FAIL / SKIPPED / WARN), latency in ms and a short message. Granularity = option B (two independent toggles). Activation semantics = next launch (toggles read once at process start by BetaSystemContext.refresh()). Fallback = automatic on any thrown exception in a beta code path. 2. SettingsActivity (+36 LoC) — new Beta card Always-visible "Beta (expérimental)" card containing two MaterialSwitch rows: sw_beta_engine and sw_beta_proxy. Amber warning banner (bg_beta_warning drawable + bg_beta_pill for the "BETA" pill) explicitly stating the beta nature. Restart-required AlertDialog triggered whenever either switch is toggled. The card is always visible — no hidden gesture, no developer mode — because the v1.1.0 line is a beta channel and the user opted in by installing this APK. Toggle changes persist via BetaConfig.set*() but have no runtime ADB effect yet. Flipping them OFF restores the exact v1.0.1 behaviour. Flipping them ON currently only enables the diagnostics readouts and the auto-fallback path in BetaEngineGateway. 3. DiagActivity rebuilt as a 6-tab TabLayout (~257 LoC) Tab order: Beta Engine (default selected) — runs the 15-test BetaTestRunner and renders a colour-coded card list via item_beta_test.xml + include_diag_beta_engine.xml, with a Run all button and a Send (.log) button. Système Cluster ADB Logs Apps The five legacy tabs share a single include_diag_coming_soon.xml placeholder. The existing v1.0.1 diag screens remain reachable via the overflow menu and will be migrated tab by tab in subsequent beta drops. 4. AppLogger refactor — file-based sharing everywhere New writeFile(ctx, prefix, content) — generic timestamped .log writer under getExternalFilesDir("logs") with a files-path fallback. New shareFile(ctx, File, subject, chooserTitle) — generic Intent.ACTION_SEND with EXTRA_STREAM via the existing FileProvider authority (${packageName}.fileprovider; xml/file_paths.xml already covers both external-files-path and files-path) and FLAG_GRANT_READ_URI_PERMISSION. Refactored share(ctx) is now file-first (attaches byd_log_<timestamp>.log) with a plain-text fallback only when the file write fails. New shareWithReport(ctx, reportText) writes a combined byd_report_<ts>.log (header line + diagnostic report + full log buffer) and attaches it via shareFile. Net effect — every "Send logs" / "Send report" action in the app now ships a real .log file instead of a raw text body. Fixes the previous ~64 KB silent truncation on Android share-sheets and lets users attach the file to GitHub issues / e-mail / WhatsApp without copy-paste loss. 5. New layouts and drawables layout/include_diag_beta_engine.xml layout/include_diag_coming_soon.xml layout/item_beta_test.xml drawable/bg_beta_pill.xml drawable/bg_beta_warning.xml layout/activity_diag.xml (TabLayout root) layout/activity_settings.xml (Beta card insertion) 6. Strings — full i18n parity (12 locales) Added a block of 28 new keys ending with diag_beta_run_all / diag_beta_copy_report / diag_beta_report_copied. values/ (FR default) + values-en/ populated by hand. 10 remaining locales (ar, be, de, es, it, kk, ru, tr, uk, uz) populated via scripts/inject_beta_strings.py — idempotent thanks to a diag_beta_run_all marker check; re-runs are no-ops. 12 locales total, full parity with the existing UI — non-FR/EN users will see the "Send (.log)" button and the Beta Engine tab in their language. 7. Build Before (v1.0.1) After (v1.1.0-beta1) versionCode 155 156 versionName "1.0.1" "1.1.0-beta1" Compatibility & regression No business logic from v1.0.1 is modified. Cluster projection, overscan, OTA, restore-origin-cluster, app shortcuts, category filters, reconnect popup and visual mode all behave exactly as in v1.0.1 with both beta switches OFF. Installs cleanly over v1.0.1 (versionCode strictly increased). All existing widget IDs preserved. Same minSdk 29 / targetSdk 29, same theme (Material 3 DayNight), same FileProvider authority. Known limitations (intentional in this drop) A1–A6 tests show SKIPPED — they cover the ADB-bridge work scheduled for the next beta drop. BetaProxyClient is a stub — every probe returns NOT_IMPLEMENTED. The 5 legacy Diag tabs (Système / Cluster / ADB / Logs / Apps) render a "Bientôt disponible" placeholder. Use the overflow menu to reach the v1.0.1 diag screens until they are migrated. These will be filled in subsequent 1.1.0-betaN drops on the same channel. Install Sideload the attached DashCast-v1.1.0-beta1-debug.apk over your existing v1.0.1 install (no uninstall required). Branch — beta/1.1.0 Tag — v1.1.0-beta1 Channel — pre-release (will not be picked by the in-app OTA unless the "Pre-release" toggle is enabled in Settings → À propos). LinksAPK download GitHub release page
  23. DashCast v1.0.1Bug fix release — cluster screen-size selector in Settings is now clickable What was broken Reported by a user shortly after the v1.0.0 stable release: in the Settings screen, the three cluster-type options (8.8", 12.3", 10.25") inside the Cluster type card could not be selected. Tapping a row did nothing, the radio button never changed, and the previously-saved size could not be modified. Root cause The three radio rows in activity_settings.xml were structured as follows: RadioGroup (rg_cluster_type) ├── LinearLayout (no id, no click handler, no selectable background) │ └── MaterialRadioButton (rb_88, clickable=false, focusable=false) │ └── TextView (title + description) ├── LinearLayout ... (same pattern for 12.3") └── LinearLayout ... (same pattern for 10.25") Three independent layers all blocked clicks: The MaterialRadioButton widgets had android:clickable="false" and android:focusable="false", so they could not receive touch events directly. The wrapping LinearLayout rows had no id, no setOnClickListener, and no android:background="?attr/selectableItemBackground", so the rows themselves were not interactive either. The radio buttons are nested inside another LinearLayout (one level below the RadioGroup), and Android's RadioGroup only auto-checks direct child RadioButton widgets via its internal mChildOnCheckedChangeListener. Nested radio buttons never receive the auto-toggle wiring. Net effect: the entire selection UI was inert. Fix app/src/main/res/layout/activity_settings.xml — each of the three rows now has its own id (row_cluster_88, row_cluster_123, row_cluster_1025), android:clickable="true", android:focusable="true" and android:background="?attr/selectableItemBackground" for a proper Material ripple. app/src/main/java/com/byd/dashcast/SettingsActivity.java — in wireListeners(), each row delegates its click to rgClusterType.check(R.id.rb_XX). That call triggers the existing OnCheckedChangeListener which persists the corresponding sendInfo command (29 = 8.8", 30 = 12.3" Seal EU, 31 = 10.25") into the cluster_screen_size_cmd key in SharedPreferences. Downstream wiring confirmed end-to-end The fix preserves the existing flow from Settings to the runtime cluster restoration: User taps a row in Settings → rgClusterType.check(rb_id) → onCheckedChanged → prefs.putInt("cluster_screen_size_cmd", 29|30|31). User long-presses the Stop projection button in MainActivity → originCluster() reads getClusterTypeCmd() → calls AdbLocalClient.restoreOriginCluster(ctx, screenSizeCmd, pkg, cb) → the native BYD instrument cluster is restored with the user-selected screen size. The Settings preference is only consumed by the Restore Origin Cluster path. The projection activation sequence (ClusterManager.activateClusterDisplay) still hard-codes CMD_SCREEN_SIZE_SEAL_EU = 30, which is the only screen-size command that produces a correct projection on the BYD Seal EU 12.3" cluster — this is intentional and unchanged. What did not change No business logic was modified. No new permission, manifest entry, dependency, resource id, theme attribute, color token or layout outside the three rows. The full Material 3 design system, the 12-locale i18n pass, the OTA flow, the long-press "Restore Origin Cluster" bottom-sheet and every other v1.0.0 feature behave exactly as before. Version Before After versionCode 154 155 versionName 1.0.0 1.0.1 Install Sideload DashCast-v1.0.1-debug.apk over v1.0.0 — the package id and signing key are unchanged so Android will perform an in-place upgrade with no data loss. LinksAPK download GitHub release page
  24. Here is another user with the same problem
  25. @tk dilan kumara You're not the only one with this problem. I wouldn't do anything, maybe they'll fix it after a while. But you can also contact a dealer.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.