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.

DashCast — BYD Cluster Launcher & Mirror

DashCast — BYD Cluster Launcher & Mirror

Android application for BYD vehicles with DiLink 3.0 (Android 10) to push any installed app onto the instrument cluster display, control it via a real-time touch mirror, and diagnose BYD APIs.

изображение.png

Links:

Apk's:

Version

Link

v1.3.25-beta

Details

v1.3.24-beta

Details

v1.3.23-beta

Details

v1.3.22-beta

Details

v1.3.21-beta

Details

v1.3.20-beta

Details

v1.3.19-beta

Details

v1.3.18-beta

Details

v1.3.17-beta

Details

v1.3.16-beta

Details

v1.3.15-beta

Details

v1.3.14-beta

Details

v1.3.13-beta

Details

v1.3.12-beta

Details

v1.3.11-beta

Details

v1.3.10-beta

Details

v1.3.9-beta

Details

v1.3.8-beta

Details

v1.3.7-beta

Details

v1.3.6-beta

Details

v1.3.5-beta

Details

v1.3.4

Details

v1.3.3

Details

v1.3.3-beta

Details

v1.3.2

Details

v1.3.1

Details

v1.3.0

Details

v1.2.82-beta

Details

v1.2.81-beta

Details

v1.2.80-beta

Details

v1.2.79-beta

Details

v1.2.78-beta

Details

v1.2.77-beta

Details

v1.2.76-beta

Details

v1.2.75-beta

Details

v1.2.74-beta

Details

v1.2.73-beta

Details

v1.2.72-beta

Details

v1.2.71-beta

Details

v1.2.70-beta

Details

v1.2.69-beta

Details

v1.2.68-beta

Details

v1.2.67-beta

Details

v1.2.66-beta

Details

v1.2.65-beta

Details

v1.2.64-beta

Details

v1.2.63-beta

Details

v1.2.62-beta

Details

v1.2.61-beta

Details

v1.2.60-beta

Details

v1.2.58-beta-phase-A-step1

Details

v1.2.57-beta

Details

v1.2.56-beta

Details

v1.2.55-beta

Details

v1.2.54-beta

Details

v1.2.51-beta

Details

v1.2.49-beta

Details

v1.2.48-beta

Details

v1.2.47-beta

Details

v1.2.46-beta

Details

v1.2.45-beta

Details

v1.2.44-beta

Details

v1.2.43-beta

Details

v1.2.42-beta

Details

v1.2.40-beta

Details

v1.2.39-beta

Details

v1.2.38-beta

Details

v1.2.37-beta

Details

v1.2.36-beta

Details

v1.2.35-beta

Details

v1.2.34-beta

Details

v1.2.33-beta

Details

v1.2.32-beta

Details

v1.2.31-beta

Details

v1.2.28

Details

v1.2.27

Details

v1.2.26

Details

v1.2.25

Details

v1.2.24

Details

v1.2.23

Details

v1.2.22

Details

v1.2.21

Details

v1.2.20

Details

v1.2.19

Details

v1.2.18

Details

v1.2.17

Details

v1.2.16

Details

v1.2.15

Details

v1.2.14

Details

v1.2.13

Details

v1.2.12

Details

v1.2.11

Details

v1.2.10

Details

v1.2.9

Details

v1.2.8

Details

v1.2.7

Details

v1.2.6

Details

v1.2.5

Details

v1.2.4

Details

v1.2.3

Details

v1.2.2

Details

v1.2.1

Details

v1.2.0-build186

Details

v1.2.0-build185

Details

v1.2.0-build184

Details

v1.2.0-build183

Details

v1.2.0-build182

Details

v1.1.9-build180

Details

v1.1.9-build179

Details

v1.1.9-build178

Details

v1.1.9-build177

Details

v1.1.9-build176

Details

v1.1.9-build175

Details

v1.1.9-build174

Details

v1.1.9-build173

Details

v1.1.9-build172

Details

v1.1.9-build171

Details

v1.1.9-build170

Details

v1.1.9-build169

Details

v1.1.8

Details

v1.1.7

Details

v1.1.6

Details

v1.1.5

Details

v1.1.4

Details

v1.1.3

Details

v1.1.2

Details

v1.1.1

Details

v1.1.0-beta1

Details

v1.0.1

Details

v1.0.0

Details

v0.9.94

Details

v0.9.0

Details

v0.8.7

Details

v0.8.6

Details

v0.8.5

Details

v0.8.2

Details

v0.8.1

Details

v0.8.0

Details

v0.6.8-beta

Details

0.5.1

Details

v0.5.0-beta

Details

v0.2.1

Details

v0.2.0-beta

Details

  • Replies 143
  • Views 454
  • Created
  • Last Reply

Top Posters In This Topic

Posted Images

Featured Replies

  • Author

DashCast v1.1.9-build170


DashCast 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 daemonDeathRecipient 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.apk

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build171


DashCast 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:

  1. Compares base semver components — if they differ, the higher wins (1.2.0 > 1.1.9 still works).
  2. 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.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build172


Phase 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 ONwm 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

  1. Install the APK (the in-app updater on build 171+ will detect this build automatically thanks to the -buildN suffix logic).
  2. Open Settings → Beta (experimental) and toggle Beta proxy ON.
  3. Tap restart in the dialog.
  4. 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
  5. 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.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build173


DashCast 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
(P1P12) 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_SECURITYSecurityException → Phase 4 verb is not viable
      from uid 2000 (sepolicy / permission blocker)
    • FAIL_APINoSuchMethodException / 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:
writeInterfaceTokentransact(TXN_PROBE_PHASE4)readException
readString. Throws BetaProxyException on transact failure (same fallback
semantics as every other client method).

4. BetaTestRunnerFamily.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

  1. Install the APK (OTA auto-updater will pick this build up because
    versionCode > 172)
  2. Settings → Beta (expérimental) → make sure Engine and
    Proxy daemon are ON
  3. Restart the app
  4. Menu → DiagBeta Engine tab → Run all (~1 s once the
    daemon is up)
  5. 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

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build174


DashCast 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× writeInttransact(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 recentsIActivityManager.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

  1. Install the APK (OTA auto-updater picks up versionCode 174)
  2. Settings → Beta (expérimental) → Engine + Proxy daemon both ON
  3. Restart the app
  4. Use the app normally — drag the resize SeekBar, launch apps, stop
    projection
  5. Diag → Beta Engine → Run all still shows 27/27 ✓ (P1 path is now
    the same one used in production)
  6. 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

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build175


DashCast 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.javatryTypedVerb 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.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build176


DashCast 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():

  1. ServiceManager.getService("AutoContainer") → IBinder
  2. INTERFACE_TRANSACTION → re-read live descriptor (defensive against OEM rebrand)
  3. Build parcel exactly as service call would: writeInterfaceToken(descr) + writeInt(1000) + writeInt(30) + writeString("")
  4. binder.transact(2, data, reply, 0) non-oneway
  5. 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

  1. Install build 176
  2. Activate beta proxy in Settings, restart app
  3. Diag → Beta Engine → Run All
  4. Send report (.log)
  5. Look for P13=PASS:… or P13=FAIL_…:… in the report

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build177


DashCast 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

  1. Install build 177, enable beta proxy in Settings, restart app
  2. Démarre la projection cluster avec Waze (ou n'importe quelle app)
  3. Laisse tourner ~30 s
  4. Arrête la projection (le séquentiel sendInfo(18) puis sendInfo(0) est le plus visible)
  5. 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.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build178


v1.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):

  1. 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, "").
  2. On success: log
    beta restoreBydOnCluster typed ok (Xms) /
    beta restoreOriginCluster typed ok (Xms), return.
  3. 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.
  4. 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

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build179


DashCast 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

  1. Install the APK over build 178.
  2. Reboot the head unit.
  3. 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)
  4. Trigger a normal projection start (or restoreBydOnCluster).
  5. 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.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build180


DashCast 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

  1. Install over build 179, reboot.
  2. Project an app (Waze ideally) on the cluster.
  3. Tap Restore BYD cluster.
  4. 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)
  5. 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)

Links

APK download

GitHub release page

  • Author

DashCast v1.2.0-build182


1.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):

  1. auto_container [android.os.IAutoContainer] is still present — our typed Phase4Verbs.autoContainerSendInfo path may work as-is.
  2. Display naming is the same shared_fission_bg_XDJAScreenProjection_N family as DL3 — same underlying mechanism.
  3. 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

  1. Install the APK below (or enable Settings → Include prereleases for OTA delivery).
  2. Open Diag → DiLink 5 tab → tap Run all.
  3. Copy the report with the Copy button.
  4. 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.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.0-build183


1.2.0-build183 — DiLink 5 reconnaissance probes (D21–D30)

Branch: beta/1.2.0-dilink5 · NOT for merge to main.
Prerelease intended for DiLink 5 field testers only.

Why this build

The first DL5 field log (build 182) revealed three open questions blocking projection:

  1. Service name mismatch. auto_container is reachable on DL5 (D11/D12 PASS via explicit snake_case) but our production code path hardcodes AutoContainer (PascalCase, DL3 convention) → all 17+ service call sites fail with Service AutoContainer does not exist. Fix is intentionally DEFERRED until the BYD APKs extracted by D10 are reverse-engineered offline (clusterdebug, containerservice, appstartmanagement, smarttravel, crosscontrol, providers.appstartup, car.server).
  2. Display #2 mystery. DL5 has 4 displays vs DL3's 3. Display #2 is not PRESENTATION-typed, holds a single window from PID 1326 / uid 10054 since boot. Is it the native cluster surface, the HUD, the mirror service?
  3. Windowing API absent. IATM.setTaskWindowingMode(int,int,boolean) doesn't exist on DL5's API 32 build — BYD either stripped it or renamed it.

Build 183 ships 10 new diagnostic probes (D21–D30) designed to answer those questions when combined with the offline APK analysis. No production-path changes — read-only reconnaissance.

New tests

ID What Why
D21 All displays full inventory (DisplayManager + reflective DisplayInfo) Captures non-PRESENTATION displays previously hidden by D2's filter (incl. mystery #2)
D22 Display #2 owner identification (dumpsys display + SurfaceFlinger --display-id + ps -A) Identify what PID 1326 actually is
D23 auto_container transaction codes scan (codes 0..8) Map the binder API surface beyond sendInfo (code 2) — find hidden capabilities
D24 BYD-specific services probe (11 services: magicwindow, crossservice, mirror, BYDMgmt, etc.) Capture each interface descriptor — magicwindow is prime suspect for dual-screen API
D25 IActivityTaskManager methods enumeration (reflection, HOT bucket = windowing/display/task) Find the actual replacement for the absent setTaskWindowingMode
D26 am start --display N probe on each non-main display (force-includes id 2) Which displays accept arbitrary apps without sendInfo preparation?
D27 BYD clusterdebug.MainActivity launch + logcat trace Watch BYD's own cluster diagnostic tool exercise the legitimate projection flow
D28 Live logcat capture during sendInfo(30) → sendInfo(16) cycle Capture every BYD log line during a real projection-start attempt (with cleanup)
D29 Projection-related intent filters discovery (9 candidate actions + containerservice/appstartmanagement intent-filter dump) Find the official entry-point intent for projection
D30 SurfaceFlinger display topology (--display-id + filtered Layer/Display dump) Ground-truth physical/virtual display mapping (bypasses WindowManager filtering)

What testers should do

  1. Install the OTA or sideload DashCast-v1.2.0-debug.apk manually.
  2. Reboot (ensures fresh state — D28 logcat capture is cleaner).
  3. Open DashCast → Diag → DiLink 5 → tap Run all → wait for the 30-test pass (~60 s).
  4. Tap Copy report → share the .log via Telegram.

If you already extracted the 7 APKs from build 182, no need to re-extract — D9/D10 will just re-confirm them. The interesting new data is in D21–D30.

What's still pinned

  • versionName stays "1.2.0" (channel policy).
  • Branch stays beta/1.2.0-dilink5NOT to merge to main until DL5 projection actually works on a real device.
  • auto_container service-name fix and windowing API fix both intentionally deferred until APK RE + D21–D30 results are combined.

Files

  • app/build/outputs/apk/debug/DashCast-v1.2.0-debug.apk (~14 MB)

Validation

Pure reconnaissance build — no production behaviour change. If everything is broken on your DL3, that's a build regression — please report.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.0-build184


1.2.0-build184 — DiLink 5 prerelease

Continuation of the beta/1.2.0-dilink5 line. Built on top of build 183, focused on shipping the actionable findings from the first DL5 field log (byd_report_20260522_092905.log).

New feature — ADAS quick-toggle diagnostic tab

The previously unused Stress test tab is replaced by a dedicated ADAS tab in Diag (tab position #5).

  • Two MaterialButtons wire the universal AutoContainer codes confirmed in clusterdebug v1.6.1.4:
    • Show ADASsendInfo(12) (显示Adas)
    • Hide ADASsendInfo(13) (关闭Adas)
  • Runtime status pill shows the resolved service name: Auto_container (PascalCase) on DL3 / Di4, auto_container (snake_case) on DL5+.
  • Service-name dispatch resolved per press via Platform.get().isDiLink5(ctx) — toggling the DL5 override in Settings is picked up without restarting Diag.
  • Raw shell response (Parcel hex or error) is displayed in a monospace result card; error path prefixes with ERREUR:.
  • Underlying command: service call <svc> 2 i32 1000 i32 <12|13> s16 "".

DiLink 5 test suite polish (based on field-log feedback)

  • D10 — APK extraction cache. Filename already embeds versionCode, so an existing non-empty destination file is now skipped (no cat redirect). New cached counter, log line ↻ <pkg> (cached, NN KB). Summary message reads N APK(s) ready (X cached), Y skipped — adb pull the dir. Second run on the same device produces zero shell calls when no APK version changed.
  • D23 — rewritten as visual warning-lamp cycle. Build 183 looped over binder transaction codes 0..8 for API surface enumeration (redundant after the clusterdebug enumeration). D23 now sends sendInfo(2) (All warning lamps ON), waits 3 s, then sendInfo(3) (All warning lamps OFF). Status WARN with a message asking the user to visually confirm the lamps lit up for ~3 s. End-to-end validation of: (a) snake-case auto_container reaches the cluster on DL5, (b) sendInfo path is functional, (c) cluster Qt honours the published codes.
  • D28 — dropped useless sendInfo(30) step. clusterdebug _Di5 variant has an empty slot at code 30 (was Set screen size on DL3 / Di4, dropped on DL5). New flow: logcat -csendInfo(16) (Qt standby ON) → 3 s → dumpsys display → filtered logcat (80 lines) → cleanup sendInfo(18) + sendInfo(0).

Documentation

  • New reference file doc_api/APK_com.byd.clusterdebug_CODES.md: full enumeration of com.byd.clusterdebug v1.6.1.4 (versionCode 10601004) sendInfo codes across all three infoListInit* variants (generic for DL3 / Di4, _Di5 for DL5, _R for R-series), side-by-side zh / EN translation for every populated slot. Canonical reference for ADAS panel codes (12/13) and D23 warning-lamp cycle (2/3); audit matrix for platform-stable vs platform-stripped codes.

Behavioural contract

  • Production hot path (ClusterManager.sendActivationSequence and full cluster lifecycle) unchanged.
  • ADAS panel uses the existing AdbLocalClient.executeShellWithResult background path — no new thread, no new permission, no new system Context, no new protocol verb.
  • Panel is read-only / write-only with respect to projection state: showing ADAS while projection is running just overlays the warning lamps; hiding ADAS does not affect projection.
  • auto_container production fix (snake_case dispatch on DL5) still deferred pending full clusterdebug + containerservice APK reverse-engineering.

Build

  • versionCode 183 → 184, versionName pinned "1.2.0".
  • No new dependencies, drawables (reuses ic_stethoscope), permissions, or protocol verbs.
  • Touched files: DiagActivity.java, dilink5/DiLink5TestRunner.java, res/layout/activity_diag.xml, res/layout/include_diag_adas.xml (new), res/values/strings.xml, app/build.gradle, CHANGELOG.md.

Validation plan

  1. On a DL3 device: open Diag → ADAS → confirm pill reads Auto_container, press Show ADAS (12) → expect warning lamps lit + Parcel response, press Hide ADAS (13) → expect lamps off.
  2. On a DL5 device: confirm pill reads auto_container (snake_case), repeat the cycle.
  3. On a DL5 device: Diag → DiLink 5 → run D10 twice → second pass shows all rows as cached; run D23 → confirm visual warning-lamp cycle WARN message; run D28 → confirm the report no longer contains a sendInfo(30) section.
  4. Toggle DL5 override in Settings on a DL3 device → reopen Diag → ADAS → confirm pill switches between Auto_container and auto_container and that subsequent button presses use the corresponding service name in the displayed $ command.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.0-build185


DashCast v1.2.0-build185 — DiLink 2 reconnaissance + horizontal swipe in Diag

Pre-release on branch beta/1.2.0-dilink5. No merge to main.

What's new

1. New DiLink 2 reconnaissance diagnostic tab

A complete recon test suite (L1–L15, 15 tests) dedicated to the DiLink 2 / Android 9 platform (alps / k65v1_64_bsp / MediaTek MT6765 / API 28). The runner is built on a completely different foundation than the DL5 suite because the DL2 environment has no working ADB-over-TCP on 127.0.0.1:5555 (confirmed ECONNREFUSED in two field reports), so the entire AdbLocalClient shell stack is unreachable.

Every probe in this suite uses Java reflection or pure-Java APIs callable from the app process — no shell at all:

ID Test Mechanism
L1 Platform fingerprint Build.* + 26 selected ro.* / persist.sys.* / sys.* props via reflected SystemProperties.get
L2 ADB local TCP ports probe Raw Socket.connect(127.0.0.1, port, 200ms) on 5037 / 5554 / 5555 / 5556 / 4444
L3 Multi-display reflective scan DisplayManager.getDisplays() + reflective DisplayManagerGlobal.getDisplayIds() (may surface IDs hidden from PRESENTATION enumeration)
L4 SurfaceFlinger physical displays via IBinder ServiceManager.getService("SurfaceFlinger") + getInterfaceDescriptor() — deliberately no custom transact to avoid AOSP-fork SIGSEGV
L5 DRM / framebuffer inventory /sys/class/drm, /dev/graphics, /sys/class/graphics via java.io.File (fb1/fb2 or multiple DRM connectors = hardware second display)
L6 ServiceManager full inventory + filter Reflective ServiceManager.listServices() filtered on cluster|display|secondary|byd|alps|auto|mirror|magic|window|fission|xdja|projection|cross
L7 Cluster service brute probe ServiceManager.getService() on 33 candidate names spanning DL3/DL5/MTK/alps conventions (cluster, byd_cluster, Auto_container, auto_container, mtk_cluster, alps_cluster, secondary_display, magicwindow, crosscontrol, xdja_container, byd_carapi, etc.) — for each present binder also dumps the AIDL interface descriptor
L8 IActivityManager method enumeration Reflection on android.app.IActivityManager (NOT IATM — API 28 has IAM only) filtered on movetask|setlaunch|startactivity|moveactivitytask|settaskwindowing|getfocusedstack|attachapplication with full signatures
L9 ActivityOptions.setLaunchDisplayId smoke Pure availability check (already confirmed present on DL2)
L10 BYD packages dynamic scan PackageManager.getInstalledPackages(0) filtered on com.byd.* / com.alps.* / com.xdja.* / *cluster* / *dilink* with version + APK path
L11 com.byd.cluster manifest deep dive Activities, services, receivers, providers, declared and requested permissions
L12 com.byd.appstartmanagement manifest Same as L11 (DL2 has v1.0 vs DL5 v1.5+)
L13 BYD SDK classpath probe Class.forName + reflective getInstance() on 8 BYDAuto*Device classes (Speed / Energy / Gearbox / AC / AirConditioner / Door / Light / Wiper)
L14 /proc cluster process scan new File("/proc").listFiles() then <pid>/cmdline filtered on cluster|fission|projection|secondary|surface|display
L15 Hidden-API reachability sanity ServiceManager.getService + descriptor on 6 core binders (window, activity, package, display, input, power)

All exceptions are caught and reported as FAIL with <ExceptionClass>: <message>; a Throwable catch in the dispatcher ensures one test crashing never breaks the suite. Each test is sub-second on average — the whole 15-test pass completes in ~3 s.

2. Horizontal swipe navigation in Diag

With 9 tabs (Cluster / Display / ADB local / Système / ADAS / Beta Engine / DiLink 5 / DiLink 2 / Sniffer) the tab bar is getting long. Build 185 adds a GestureDetector.SimpleOnGestureListener on the diag content frame:

  • Clear horizontal fling (|dx| ≥ 120 px AND |dx| ≥ 1.5×|dy| AND |vx| ≥ 220 px/s) selects the next or previous tab
  • Swipe left → next tab, swipe right → previous tab, clamped to [0, 9)
  • The touch listener returns false so all child scrollers (NestedScrollView, RecyclerView test lists, scrollable SysInfo report) keep working untouched
  • Reuses the existing OnTabSelectedListener (tabs.getTabAt(next).select()) so the same showPanelForTab code path runs — zero regression risk on the existing tab-click flow
  • No animated slide between panels for now (the bascule is dry, like the current tab click); a proper ViewPager2 + FragmentStateAdapter refactor with animated slide is planned for a later build

Wiring summary

  • New file dilink2/DiLink2TestRunner.java (~580 LoC, self-contained — no shared types with DL5 / Beta runners)
  • New layout res/layout/include_diag_dilink2.xml
  • New tab TAB_DILINK2 = 7 in DiagActivity.java (Sniffer shifted from 7 → 8); new panelDl2 field; new bindDl2Panel() / prepareDl2TestRows() / bindDl2Row() / runDl2AllTests() / updateDl2Counters() / copyDl2Report() mirror the DL5 lifecycle with their own state lists (dl2RowViews, dl2LastResults) — no state collision with DL5 or Beta
  • New attachSwipeNavigation(View) method called once in bindTabs()
  • New <TabItem> and <include> in res/layout/activity_diag.xml
  • 9 new FR strings in res/values/strings.xml (diag_tab_dilink2, diag_dl2_header_*, diag_dl2_pill_*, diag_dl2_counters_fmt) — the 12 locale files are intentionally NOT updated yet; will batch with the next translation pass once the DL2 panel labels stabilize after field testing

Zero production-path changes

  • No changes to ClusterManager, AdbLocalClient, Platform.java, BetaTestRunner, DiLink5TestRunner
  • No new permissions, no new dependencies, no new protocol verbs
  • DL3 and DL5 behaviour is bit-for-bit identical to build 184
  • DL2 signature detection in bindDl2Panel() is a UI-side hint only (reads Build.BRAND + Build.PRODUCT), no Platform.java extension

Build metadata

  • versionCode 184 → 185
  • versionName pinned "1.2.0" per channel policy
  • Branch: beta/1.2.0-dilink5 (no merge to main)
  • Tag: v1.2.0-build185
  • APK: DashCast-v1.2.0-debug.apk (14 MB)

Validation plan

  1. DL2 device — open Diag, swipe (or tap) to the new DiLink 2 tab, confirm the signature pill reads Signature : DL2 détectée, press Lancer tous, wait ~3 s for the 15-test pass, press Envoyer (.log), share the report via Telegram.
  2. DL3 / DL5 device (regression check) — open Diag, swipe through all 9 tabs, confirm:
    • No layout glitch, all panels render correctly
    • DiLink 5 panel still functional (run a D1 / D11 / D23 quick sanity)
    • ADAS panel still functional (Show / Hide buttons)
    • Beta Engine panel still functional
    • Sniffer panel still functional (start / snapshot / stop)
    • The DL2 tab is visible too (we did NOT gate it behind a platform detection) — pressing Lancer tous on DL3/DL5 produces a report where L1 reports WARN ("not a DL2 signature") and the other tests still return useful data, confirming the suite is non-destructive on non-DL2 platforms.

What this build will tell us about DL2

For the first time, the field report will answer:

  1. Whether any non-DisplayManager surface (SurfaceFlinger binder, DRM connectors, /sys/class/graphics) exposes the physically-present cluster screen
  2. Which binder services BYD or alps registered on this platform for cluster control
  3. What com.byd.cluster and com.byd.appstartmanagement actually contain and expose (entry points, permissions)
  4. Which IActivityManager methods are available for typed display routing on API 28 without IActivityTaskManager
  5. Whether the ADB-TCP situation can be unblocked from the device side

Based on these answers, build 186 will design the actual DL2 cluster projection strategy.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.0-build186


DashCast 1.2.0 — build 186 (DiLink 5 enablement)

Pre-release on the beta/1.2.0-dilink5 channel. Two scope items on top of build 185, both rooted in the 22/05/2026 DL5 field log (byd_report_20260522_115956.log) and the user's live testing on the DL5 vehicle: D10 APK extraction relocated to a user-visible destination, and a new D31 end-to-end cluster projection probe driven by com.byd.clusterdebug.

This build is detected by the in-car OTA channel as an update over build 185.

No production hot path was changed — projection lifecycle, ClusterManager, AdbLocalClient, daemon code are bit-for-bit identical to build 185. Everything in this build is opt-in via Diag → DiLink 5.


D10 — APK extraction relocated to Download/dashcast_apks/

Field-tester feedback on build 185. The previous output dir was effectively invisible:

  • getExternalFilesDir(null)/extracted_apks/ — app-private external storage, not surfaced by the BYD file manager without root.
  • User saw "PASS, 7 APKs ready" in the report but had no way to grab the files without adb pull from a PC.
  • Worse: every re-run added duplicate-or-cached copies into a dir the user could never empty by hand → slowly ate internal storage build after build.

Fix.

  • All filesystem ops in runD10 now go through the ADB shell (uid 2000, which on Android 10+ has unrestricted write access to /sdcard/Download/).
  • mkdir -p /storage/emulated/0/Download/dashcast_apks at start-up.
  • Cache check via stat -c %s '<dst>' (avoids relying on the app uid being able to read() the destination on A10+ scoped storage).
  • Copy via cat '<src>' > '<dst>' && stat -c %s '<dst>'.
  • At the very start of each runD10, the legacy getExternalFilesDir(null)/extracted_apks/ is wiped via rm -rf so the next install no longer accumulates dead bytes.
  • PASS message now reads N APK(s) in Download/dashcast_apks/ (X cached), Y skipped.

User-visible result.

  • Destination is browsable by the stock BYD file manager (visible under Downloaddashcast_apks).
  • Removes the adb pull requirement entirely for users without a PC.
  • Re-runs are zero-shell for already-extracted APKs (cache hit on versionCode embedded in the filename).

D31 (new) — Live cluster projection end-to-end probe

First test that drives the full visual cycle the user has been doing by hand to confirm DL5 projection works. For each DISPLAY_CATEGORY_PRESENTATION display (DL5 exposes #3 and #4 as XDJAScreenProjection_0/1):

Step Action Why
a sendInfo(1000, 16) Opens the projection tunnel; the shared_fission_bg_XDJAScreenProjection_* layerStack becomes visible (initially black — this is what the user confirmed seeing during D28 of build 185).
b wait 1.5 s Lets Qt reposition the overlay.
c am start --display N -n <pkg>/<launcher> Pushes a real activity onto the projection layerStack so the cluster shows the app, not just a black window.
d wait 3 s User observes the cluster — this is the user-visible window.
e dumpsys activity activities filtered on the package Snapshot of where the activity landed; correlates the visual with the stack state.
f am force-stop <pkg> Clean release of the activity.
g sendInfo(1000, 18) Closes the projection tunnel.
h sendInfo(1000, 0) Restores native cluster video flow.
i wait 1.5 s Visual separation between iterations.

Target package — clusterdebug by default

D31 targets com.byd.clusterdebug by default — BYD's own cluster diagnostic app, guaranteed to be installed on every DL5 head unit and designed to render on the cluster.

  • The D8 dropdown at the top of the DL5 panel is an optional override: pick another installed package and D31 will use that one instead.
  • PackageManager.getPackageInfo(pkg, 0) is called up front and FAILs cleanly with a clear message before any cluster touch if the package isn't installed.
  • The report header now shows (default clusterdebug) or (D8 dropdown override) so you know exactly which path ran.

Launcher resolution

Reuses the existing resolveLauncherComponent(ctx, pkg) helper so am start --display N -n <ComponentName> always targets the package's declared LAUNCHER activity, whatever its concrete class name is. FAILs with Could not resolve a launcher activity for '<pkg>' if the package declares no launcher (rare — typically a service-only package).

Status semantics

  • PASS — every am start --display returned without error / SecurityException / Permission Denial. Message reads Cycle run on N display(s) — user to confirm visual rendering to keep the final go/no-go in the user's hands (this is a visual test; the shell can only say the API accepted the call, not that pixels actually reached the cluster).
  • WARN — partial success.
  • FAIL — every launch was refused.

The full per-display sequence (commands + outputs + activity-stack snapshot) is dumped to r.detail so the user can read the report to understand which display did or did not light up.

Cleanup guarantee

D31 leaves the cluster in a clean state regardless of failure: am force-stop + sendInfo(18) + sendInfo(0) are always issued at the end of each per-display iteration, including on partial failure.


Behavioural contract

  • Production hot path (projection lifecycle, ClusterManager, AdbLocalClient, daemon code) is unchanged.
  • The new D31 sequence is opt-in (Diag → DiLink 5 → tap D31 or Run all).
  • DL5 test catalog grows from 30 to 31 entries — surfaced automatically by the existing prepareDl5TestRows() / bindDl5Row() UI code; no layout, strings, drawables, dependencies, or permissions added.
  • DL3 / DL5 / DL2 production paths are bit-for-bit identical to build 185.

Validation plan

  1. Sideload or OTA install on a DL5 vehicle.
  2. Open Diag → DiLink 5.
  3. Run D9 (discovery) then D10 → verify the 7 BYD APKs land in Download/dashcast_apks/ and that the BYD file manager sees them.
  4. Run D31 → the cluster should:
    • Black out (sendInfo 16) for ~1.5 s.
    • Display the clusterdebug UI for ~3 s on each PRESENTATION display.
    • Black out again as sendInfo(18) / sendInfo(0) tear it down.
  5. Copy the report (Envoyer (.log)) and share for triage.

Build

  • versionCode 185 → 186
  • versionName pinned "1.2.0" per channel policy
  • Touched files outside CHANGELOG / gradle: dilink5/DiLink5TestRunner.java (D10 body rewritten to shell-based ops + legacy dir cleanup, D31 added at the end of the file, catalog +1 entry, dispatcher +1 case).
  • No new dependencies, drawables, strings, layouts, permissions, or protocol verbs.

Download

The signed debug APK is attached below: DashCast-v1.2.0-debug.apk.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.1


DashCast 1.2.1 (build 187) — DiLink 5 production projection

Pre-release on branch beta/1.2.0-dilink5. Not merged into main. versionCode 186 → 187 — the car's package manager will detect this APK as a regular update of any previously installed DashCast 1.2.0 build.

This build is the first production-side integration of the DiLink 5 projection sequence that the D12 and D31 diagnostic probes had only validated shell-side in build 186 (see field log byd_report_20260522_125322.log). Start Projection and Stop Projection now route through the correct AutoContainer flavour depending on the detected platform — DiLink 3 keeps the existing field-tested sequence bit-for-bit, DiLink 5 gets the minimal validated sequence.


Platform dispatch — service-name routing

DiLink 3 and DiLink 5 expose the cluster control binder under different names. Build 187 introduces a centralised, exception-safe dispatcher in AdbLocalClient:

Platform Service name Confirmed by
DiLink 3 (BYD Seal EU / Atto 3 / …) AutoContainer (PascalCase) User on production cluster, 22/05/2026
DiLink 5 (BYD-AUTO API 32) auto_container (snake_case) D11 / D12 / D31 PASS, field log 22/05/2026

New helpers in AdbLocalClient:

  • autoContainerSvcName(Context) — returns the right name, never throws (any reflection failure on Platform.get().isDiLink5(ctx) falls back to the DL3 default).
  • isDiLink5Safe(Context) — boolean wrapper with the same exception contract, used for gating the typed-daemon path.

Three production call sites refactored to consume the helper instead of the previously hardcoded "AutoContainer" literal:

  • AdbLocalClient.sendInfo() — the central service call … relay used by every callsite that ships an info code to the cluster.
  • AdbLocalClient.restoreBydOnCluster() — Stop Projection (force-stop target + sendInfo(18) + sendInfo(0)).
  • AdbLocalClient.restoreOriginCluster() — Stop Projection + restore original screen size (the sendInfo(<screenSizeCmd>) tail is a harmless no-op on DL5 since slot 30 is empty in the _Di5 clusterdebug enum).

Typed daemon path — gated on DL3

BetaProxyClient.autoContainerSendInfo() resolves the binder via ServiceManager.getService("AutoContainer") — hardcoded in Phase4Verbs.AUTOCONTAINER_SVC. That name does not exist on DiLink 5, so every typed transact(2, …) would fail with IllegalArgumentException and force an extra round-trip before falling back to the shell relay.

Build 187 short-circuits the typed path on DL5 in all three sites that previously gated it on BetaConfig.isProxyDaemonEnabled(context) alone:

if (BetaConfig.isProxyDaemonEnabled(context) && !isDiLink5Safe(context)) {
    // typed daemon path — DL3 only
}
// shell fallback — picks svc name via autoContainerSvcName(context)

DiLink 5 always goes straight to the shell with service call auto_container 2 …. DiLink 3 behaviour is unchanged: typed path tried first, shell fallback on failure with the correct AutoContainer name.

Phase4Verbs itself is not touched in this build — the typed daemon remains a DL3-only optimisation. Migrating it to a DL5-aware variant (probing both AutoContainer and auto_container at startup with a cached IBinder) is out of scope for 1.2.1 because the shell path on DL5 is already fast enough (no service binary fork on DL5 — auto_container returns the Parcel in ~5 ms).

Start Projection — ClusterManager.activateClusterDisplay()

New short-circuit branch at the top of activateClusterDisplay() for DiLink 5:

if (isDiLink5Safe()) {
    AppLogger.i(TAG, "DL5 activation path: sendInfo(16) only on auto_container");
    AdbLocalClient.sendInfo(mContext, CLUSTER_TYPE, CMD_PROJECTION_ON, "",
        new AdbLocalClient.Callback() {
            @Override public void onSuccess(String out) {
                AppLogger.i(TAG, "DL5 activation ADB(cmd=16): " + out);
                mHandler.postDelayed(() -> resolveDl5Display(dm, callback), 500);
            }
            @Override public void onError(String err) {
                AppLogger.e(TAG, "DL5 activation ADB(cmd=16) ERROR: " + err);
                mHandler.postDelayed(() -> resolveDl5Display(dm, callback), 500);
            }
        });
    return;
}

What changed compared to the DiLink 3 sequence:

  • No sendInfo(30) (screen-size hint). Slot 30 is empty in the _Di5 variant of the clusterdebug enumeration documented in build 184 — the DL5 cluster Qt code never reads it. Sending it would just be a wasted round-trip.
  • No sendInfo(35) (Di4.0 mode / VirtualDisplay creation). On DL3 this is what triggers AutoDisplayService.createVirtualDisplay() and the fission_bg_xdjaVirtualSurface PRESENTATION display appears ~280 ms later. On DL5 the equivalent XDJAScreenProjection_0 / XDJAScreenProjection_1 displays already exist at boot as persistent PRESENTATION displays (confirmed in D2 / D21 / D31 of the 22/05/2026 field log), so sendInfo(35) is unnecessary and would target an empty slot.
  • Only sendInfo(16). This is the projection-ON trigger — the same code that D12 and D31 already validated end-to-end on the DL5 testeur device.
  • 500 ms settle delay before resolving the display. In practice the displays are already up; the delay exists to absorb the rare case where the cluster Qt side needs a frame to react to sendInfo(16).

Direct user constraint from the task brief:

Le mieux maintenant c'est de câbler Start Projection pour Dilink5 qui fait uniquement la command 16 pour activer la projection avec le bon auto_container.

Display targeting — resolveDl5Display()

New private helper on ClusterManager. Reuses the existing findClusterDisplay(dm) which already returns the first non-default PRESENTATION display — so on the DL5 testeur device this naturally picks display id 3 (shared_fission_bg_XDJAScreenProjection_0). Display id 4 (_1) is left available for a future second-screen use.

On miss, the helper polls every 250 ms for up to 3 s before calling onDisplayTimeout() — a defensive safety net that should never trigger on DL5 since the field log confirmed both XDJAScreenProjection_* displays are PRESENTATION-categorised and present from boot.

Direct user constraint:

on vient diffuser les applications lancé sur le display ID3 déjà pour commencer.

findClusterDisplay() and isClusterDisplay() themselves are unchanged — they already accept any non-default display, so the DL5 displays #3 and #4 qualify out of the box.

Stop Projection — automatic via existing path

restoreBydOnCluster() was already issuing exactly the sequence the user asked for on DL5:

force-stop <target package>
sendInfo(1000, 18, "")  // close projection
sendInfo(1000, 0, "")   // refresh Qt video stream

The only concern on DL5 was the service name — fixed in section "Platform dispatch" above. No changes to the orchestration. Same goes for restoreOriginCluster(), where the optional trailing sendInfo(<screenSizeCmd>) is a harmless no-op on DL5 (empty slot 30 in _Di5, service call returns an empty Parcel without raising).

Direct user constraint:

Il faut aussi cabler le arreter projection qui fait les commandes 18 et 0 toujours avec le bon auto_container.

Diagnostics — ADAS panel service-name fix

DiagActivity.autoContainerSvcName() had shipped a wrong DL3 default since build 184: it returned "Auto_container" (PascalCase with underscore) — a name that does not exist on either platform. The ADAS quick-toggle panel was therefore silently issuing service call Auto_container 2 … on DL3, which the cluster ignores.

Build 187 fixes the default to "AutoContainer" (PascalCase, no underscore) — the name the user confirmed in production on DL3 on 22/05/2026. DL5 keeps "auto_container" (snake_case). The pill displayed at the top of the ADAS panel now reads the correct service name on both platforms, and the Afficher ADAS (12) / Masquer ADAS (13) buttons send service call AutoContainer 2 i32 1000 i32 12 s16 "" on DL3 and the snake_case equivalent on DL5.

DiLink 3 — behavioural contract

Every DiLink 3 code path is bit-for-bit identical to build 186:

  • The new DL5 branch in ClusterManager.activateClusterDisplay() is the very first statement of the method and is gated on isDiLink5Safe(). On DL3 it short-circuits to false and the existing 30 → 6 s → 16 → 6 s → 35 sequence runs unchanged, including every onError fallback branch.
  • AdbLocalClient.sendInfo() typed-daemon path still runs first on DL3 (the isProxyDaemonEnabled && !isDiLink5Safe guard collapses to isProxyDaemonEnabled on DL3 since the negation is true).
  • The shell fallback still emits service call AutoContainer 2 … on DL3 because autoContainerSvcName(ctx) returns "AutoContainer" when Platform.isDiLink5(ctx) is false.
  • restoreBydOnCluster() / restoreOriginCluster() shell paths same story — AutoContainer on DL3, auto_container on DL5.

User constraint explicitly stated in the task brief:

Par contre on ne doit pas casser la partie Dilink3.

Build

  • versionCode 186 → 187
  • versionName 1.2.0 → 1.2.1 (first 1.2.x minor bump since the DL5 work started — reflects the production-side wiring being now in place)
  • Branch beta/1.2.0-dilink5no merge into main in this build per the user's release policy
  • Tag v1.2.1
  • Pre-release status — should be installed on testeur devices first

Files touched

  • MyBYDApp/app/build.gradle — version bump
  • MyBYDApp/CHANGELOG.md — build 187 entry at the top of Pre-releases
  • MyBYDApp/app/src/main/java/com/byd/dashcast/AdbLocalClient.java — helpers + 3 shell call-site refactors + 3 typed-daemon gates
  • MyBYDApp/app/src/main/java/com/byd/dashcast/dashboard/ClusterManager.java — DL5 short-circuit + isDiLink5Safe helper + resolveDl5Display helper
  • MyBYDApp/app/src/main/java/com/byd/dashcast/DiagActivity.java — ADAS DL3 service-name fix

No new dependencies, no new permissions, no protocol changes, no new resources, no new strings.

Validation plan

DiLink 5 testeur device

  1. Cold-reboot to ensure XDJAScreenProjection_* PRESENTATION displays are up.
  2. Install DashCast-v1.2.1-debug.apk.
  3. Open the dashboard, select a target app (Waze, com.github.standardadb, or any non-system package declaring a LAUNCHER activity).
  4. Press the existing Start Projection control.
  5. Expected logcat sequence (tag ClusterManager + AdbLocalClient):
    DL5 activation path: sendInfo(16) only on auto_container
    sendInfo ADB: service call auto_container 2 i32 1000 i32 16 s16 "" 2>&1
    DL5 activation ADB(cmd=16): Result: Parcel(...)
    DL5 cluster display ready: id=3 name=shared_fission_bg_XDJAScreenProjection_0
    
  6. The existing app-launch path (which already uses ActivityOptions.setLaunchDisplayId) takes over and the app should appear on the cluster.
  7. Press Stop Projection. Expected:
    service call auto_container 2 i32 1000 i32 18 s16 "" 2>&1
    service call auto_container 2 i32 1000 i32 0  s16 "" 2>&1
    
  8. Open Diag → ADAS tab → confirm the pill reads auto_container. Press Afficher ADAS (12) → warning lamps should appear on the cluster. Press Masquer ADAS (13) → they should disappear.

DiLink 3 reference device (regression check)

  1. Install DashCast-v1.2.1-debug.apk.
  2. Trigger Start Projection on a configured DL3 cluster. Expected logcat sequence unchanged from build 186:
    activation ADB(cmd=30): ...
    activation ADB(cmd=16): ...
    activation ADB(cmd=35): ...
    
  3. The shell path should consistently emit service call AutoContainer 2 … (PascalCase, no underscore) — any Auto_container or auto_container is a regression.
  4. Open Diag → ADAS tab → confirm the pill reads AutoContainer. Press the two buttons and verify the cluster reacts.

Telegram channel

Post the DashCast-v1.2.1-debug.apk link plus a one-line summary so testers can sideload.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.2


DashCast 1.2.2 (build 188) — DL5 app launch via ADB shell + wm overscan gated on DL3

Pre-release on branch beta/1.2.0-dilink5. Not merged into main. versionCode 187 → 188 — Android's package manager will detect this as a regular update of any installed 1.2.1 build.

This build is the direct field-driven follow-up to 1.2.1 (build 187). The DL5 field log byd_report_20260522_132030.log showed the new projection wiring from 1.2.1 working perfectly up to the display-ready event, then crashing at the next step (app launch) with a SecurityException. Build 188 fixes that and also addresses the secondary Unknown command: overscan noise visible in the same log.


What the build 187 log told us

The DL5 testeur device confirmed the projection trigger and display resolution work as designed:

[ClusterManager] DL5 activation path: sendInfo(16) only on auto_container
[AdbLocalClient] sendInfo ADB: service call auto_container 2 i32 1000 i32 16 s16 "" 2>&1
[AdbLocalClient] sendInfo ADB(1000,16) → Result: Parcel(00000000 00000001   '........')
[ClusterManager] DL5 cluster display ready: id=3 name=shared_fission_bg_XDJAScreenProjection_0
[ClusterService] Cluster display connected: id=3
[ClusterInputForwarder] Cluster dimensions: 1920x720 displayId=3

But the very next phase (launching the user's selected app on display 3) failed:

[ClusterService] Launching via IActivityManager on display=3 → ru.yandex.yandexmaps
[ClusterService] startActivityViaIAM → fallback context: null
[ClusterService] Global launch error for ru.yandex.yandexmaps
  [SecurityException: Permission Denial: starting Intent { act=android.intent.action.MAIN
   cat=[android.intent.category.LAUNCHER] flg=0x10008000
   pkg=ru.yandex.yandexmaps cmp=ru.yandex.yandexmaps/.SplashScreen }
   from ProcessRecord{... com.byd.dashcast/u0a148} (pid=9647, uid=10148)
   with launchDisplayId=3]

Our app's uid 10148 is denied cross-display launches by IActivityTaskManager on DiLink 5.0, even though INTERNAL_SYSTEM_WINDOW is granted at install time. This differs from DiLink 3.0 where the same typed-IATM reflection path works fine.

The D31 diagnostic probe (build 186) had already validated the workaround: route the launch through ADB shell (uid 2000, which on this hardware has both the BYD auto_container whitelist entry and the unrestricted activity-launch privileges granted to the shell user). Build 188 ports that workaround into production.

The same field log also revealed three wm overscan ... -> Unknown command: overscan errors. The wm overscan subcommand was removed in Android 11 (API 30) — DL5 is API 32, so every wm overscan shell call has been a no-op since 1.2.0. Build 188 stops issuing it on DL5.

DL5 app launch — ADB shell route

AdbLocalClient.isDiLink5Safe(Context) — visibility bumped

Previously private static. Now package-private so ClusterService (same package com.byd.dashcast) can reuse the existing exception-safe DL5 detector instead of duplicating the try/catch around Platform.get().isDiLink5(ctx). Behavioural contract unchanged: returns true only when both Context != null and Platform.isDiLink5(ctx) returns true; any reflection failure returns false. Helper still cannot throw.

New ClusterService.startActivityViaShell(packageName, displayId, launchIntent)

String component = null;
if (launchIntent != null && launchIntent.getComponent() != null) {
    ComponentName cn = launchIntent.getComponent();
    component = cn.getPackageName() + "/" + cn.getClassName();
}
if (component == null) {
    AppLogger.e(TAG, "startActivityViaShell: cannot resolve component for " + packageName);
    return;
}
final String cmd = "am start --display " + displayId
        + " -a android.intent.action.MAIN -c android.intent.category.LAUNCHER"
        + " -n " + component
        + " --activity-clear-task 2>&1";
AppLogger.i(TAG, "DL5 launch via shell: " + cmd);
AdbLocalClient.executeShellWithResult(this, cmd, new AdbLocalClient.Callback() {
    @Override public void onSuccess(String out) {
        AppLogger.i(TAG, "DL5 am start → " + out.trim());
    }
    @Override public void onError(String err) {
        AppLogger.e(TAG, "DL5 am start ERROR: " + err);
    }
});

Component resolution reuses the getLaunchIntentForPackage(pkg) machinery that was already populating the Intent upstream — no new PackageManager queries, no new failure modes. Command pattern is bit-identical to the D31 diagnostic probe that PASSed end-to-end on the DL5 testeur on 22/05/2026.

Dispatch via AdbLocalClient.executeShellWithResult: the existing dadb-based shell relay (with cached ADB key pair already accepted by the device) runs on the background executor. Errors are logged via AppLogger.e but not propagated to the LaunchCallback — consistent with the legacy IATM path which also reports onResult(true) after best-effort dispatch (the typed reflection path's RemoteException branches are swallowed there too).

launchOnDashboard() — DL5 branch

Right before the legacy startActivityViaIAM(launchIntent, opts) call, the method now tests AdbLocalClient.isDiLink5Safe(this):

launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
ActivityOptions opts = ActivityOptions.makeBasic();
opts.setLaunchDisplayId(displayId);
if (displayId > 0) applyClusterFreeformBounds(opts, displayId, packageName);

if (AdbLocalClient.isDiLink5Safe(ClusterService.this)) {
    startActivityViaShell(packageName, displayId, launchIntent);
} else {
    startActivityViaIAM(launchIntent, opts);
}

The applyClusterFreeformBounds() call still runs upstream and still applies setLaunchWindowingMode(FREEFORM) + setLaunchBounds(...) to the ActivityOptions — these are inert on DL5 (the shell am start ignores ActivityOptions entirely) but cost nothing to compute, and they remain necessary for the DL3 path which falls through to startActivityViaIAM unchanged.

launchOnDashboardWithBounds() — DL5 branch

Same pattern as launchOnDashboard(): on DL5 the shell path takes over, on DL3 the typed IATM path runs unchanged.

Caveat: the explicit split-mode bounds passed by the caller (Rect left/top/right/bottom) are dropped on DL5 since am start does not accept windowing bounds. Split mode on DL5 would need a different approach (e.g. two shell launches on display #3 and #4 simultaneously, one app per physical PRESENTATION display) — out of scope for 1.2.2, will be addressed once single-app projection is field-validated.

wm overscan — skipped on DL5

wm overscan was removed from WindowManagerService.ShellCommand in Android 11 (API 30) — its semantics were absorbed by wm display-add-state and DisplayInfo overrides. DL5 is API 32, so the command has been silently failing since 1.2.0. The field log confirmed:

[AdbLocalClient] executeShellWithResult: wm overscan 80,50,80,50 -d 3 -> Unknown command: overscan

(three times in the build 187 log: lines 31.016, 38.820, 46.264)

Build 188 gates all three call sites on !isDiLink5Safe(this):

  • applyClusterFreeformBounds() (per-launch overscan to apply app-specific insets). On DL5 logs DL5: skipping app-specific wm overscan (cmd removed in API 30+) at DEBUG level.
  • onDashboardDisplayConnected() (display-level overscan applied once when the cluster display first comes up). Same log line at DEBUG.
  • stopProjectionNoAdb() (reset overscan to 0,0,0,0 on display id 1 when teardown is requested via the BYD restore path). Bonus side-fix: the reset was hardcoding -d 1 (DL3 cluster display id) even on DL5 where the cluster lives at id 3 — was already producing the same Unknown command: overscan error. Now skipped entirely on DL5.

On DL3 every overscan path is unchanged — DL3 still gets wm overscan H,V,H,V -d <displayId> applied at display-connect and at each app launch, plus the wm overscan reset -d 1 on stop.

DiLink 3 — behavioural contract

Every DL3 code path is bit-for-bit identical to build 187:

  • launchOnDashboard() and launchOnDashboardWithBounds() — the DL5 branch is gated on isDiLink5Safe(this) which returns false on DL3, so the existing startActivityViaIAM(launchIntent, opts) typed reflection path runs unchanged with the same ActivityOptions (display id, freeform mode, launch bounds).
  • applyClusterFreeformBounds() — DL3 still emits wm overscan H,V,H,V -d <displayId> at every launch and logs Applied app-specific wm overscan during launch on display N.
  • onDashboardDisplayConnected() — DL3 still emits the display-level overscan and logs wm overscan applied on display N inset=H,V.
  • stopProjectionNoAdb() — DL3 still emits wm overscan reset -d 1.

The DL5 platform detection is wrapped in the same try/catch the rest of the codebase uses (isDiLink5Safe()), so any reflection failure on Platform.get().isDiLink5(ctx) falls back to the DL3 path — the production hot path can never throw because of platform detection.

Build

  • versionCode 187 → 188
  • versionName 1.2.1 → 1.2.2 (second 1.2.x minor bump in 24 hours, reflecting the iterative field-driven DL5 enablement)
  • Branch beta/1.2.0-dilink5no merge into main per release policy
  • Tag v1.2.2
  • Pre-release status

Files touched

  • MyBYDApp/app/build.gradle — version bump (187 → 188, 1.2.1 → 1.2.2)
  • MyBYDApp/CHANGELOG.md — build 188 entry at top of Pre-releases
  • MyBYDApp/app/src/main/java/com/byd/dashcast/AdbLocalClient.javaisDiLink5Safe visibility bump (private static → package-private)
  • MyBYDApp/app/src/main/java/com/byd/dashcast/ClusterService.java — new startActivityViaShell() helper, DL5 branches in launchOnDashboard() and launchOnDashboardWithBounds(), DL5 guards on the three wm overscan shell call sites

No new dependencies, no new permissions, no protocol changes, no new resources, no new strings, no new layout files.

Validation plan

DiLink 5 testeur device

  1. Cold-reboot to ensure XDJAScreenProjection_* PRESENTATION displays are up at id 3 and id 4.
  2. Install DashCast-v1.2.2-debug.apk.
  3. Open the dashboard, tap a target app — Yandex Maps, Huawei Maps, Waze, Standard ADB, or any non-system package declaring a LAUNCHER activity.
  4. Expected logcat sequence (tags ClusterManager + ClusterService + AdbLocalClient):
    DL5 activation path: sendInfo(16) only on auto_container
    sendInfo ADB(1000,16) → Result: Parcel(00000000 00000001   '........')
    DL5 cluster display ready: id=3 name=shared_fission_bg_XDJAScreenProjection_0
    Cluster display connected: id=3
    Cluster dimensions: 1920x720 displayId=3
    DL5: skipping display-level wm overscan (cmd removed in API 30+)
    DL5 launch via shell: am start --display 3 -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n <pkg>/<launcher activity> --activity-clear-task 2>&1
    executeShellWithResult: am start --display 3 ... -> Starting: Intent { ... }
    DL5 am start → Starting: Intent { ... }
    
  5. Visual check: the selected app appears on the cluster display.
  6. Absence check: no more SecurityException: Permission Denial ... with launchDisplayId=3 and no more Unknown command: overscan in the logs.
  7. Press Stop Projection. Expected:
    service call auto_container 2 i32 1000 i32 18 s16 "" 2>&1   ← from build 187
    service call auto_container 2 i32 1000 i32 0  s16 "" 2>&1   ← from build 187
    
    (no wm overscan reset line — gated out on DL5)
  8. Optional: Diag → ADAS tab → confirm pill still reads auto_container, both buttons still work (no regression on build 187 features).

DiLink 3 reference device (regression check)

  1. Install DashCast-v1.2.2-debug.apk on a known-good DL3 cluster device.
  2. Trigger Start Projection. Expected logcat sequence unchanged from build 187:
    activation ADB(cmd=30): ...
    activation ADB(cmd=16): ...
    activation ADB(cmd=35): ...
    Launching via IActivityManager on display=N → <pkg>
    Applied app-specific wm overscan during launch on display N
    wm overscan applied on display N inset=H,V
    
  3. Regression markers — any of these lines in DL3 logs is a bug:
    • DL5 launch via shell: (DL5 branch should not trigger)
    • DL5: skipping ... wm overscan (cmd removed in API 30+) (DL5 guard should not trigger)
    • DL5 activation path: (DL5 short-circuit from build 187 should not trigger)
  4. ADAS panel pill must still read AutoContainer (PascalCase, no underscore — fixed in 1.2.1) and both Afficher ADAS (12) / Masquer ADAS (13) must still light up the lamps.

Telegram

Post the DashCast-v1.2.2-debug.apk link plus a short note pointing field testers to the DL5 visual check (step 5 above).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.3


DashCast v1.2.3 — DiLink 2 diagnostics & DiLink 2 hardening

Pre-release — versionCode 192. Targets DiLink 2 (k65, Android 9, API 28/29), DiLink 3 (Seal EU, API 29) and DiLink 5 (BYD-AUTO, API 32). All three platforms share the same APK; auto-detection routes each car to the right code path.


Headlines

  • Diag opens on the right tab automatically. DiLink 2 users now land directly on the DiLink 2 tab. DiLink 5 users still land on DiLink 5. All other devices land on Beta Engine.
  • 40-test DiLink 2 diagnostic suite (was 30). Ten new shell-driven RE tests, including a dedicated APK extractor.
  • One-tap APK extraction to /storage/emulated/0/Download/dl2_apks/. com.byd.cluster is always pulled first; the whole BYD/alps/xdja whitelist follows. APKs are then directly accessible from the BYD file manager and can be attached to bug reports.
  • DL2 cluster-resize: total ban. Every shell command that could resize a physical display on DiLink 2 is rejected at three independent layers — AdbLocalClient, ShellGateway, SettingsActivity. No path can call wm size, wm density, wm overscan, am display-size, or settings put * display_* on a DL2 car. DiLink 3 and DiLink 5 are unaffected.
  • Zero impact on DiLink 3 and DiLink 5 — same APK, same versionCode, only DL2-specific code paths changed.

DiLink 2 — full 40-test catalog

Tier L — reflection only (no ADB required, 15 tests)

ID Test
L1 Platform fingerprint (Build.* + 26 ro.* props)
L2 ADB-local TCP port probe (5037 / 5554 / 5555 / 5556 / 4444)
L3 Multi-display reflective scan (DisplayManager + DisplayManagerGlobal)
L4 SurfaceFlinger physical displays via IBinder
L5 DRM / framebuffer inventory (/sys/class/drm, /dev/graphics)
L6 ServiceManager full inventory + BYD/cluster filter
L7 Cluster service brute probe (33 candidate names)
L8 IActivityManager method enumeration
L9 ActivityOptions.setLaunchDisplayId smoke test
L10 BYD packages dynamic scan via PackageManager
L11 com.byd.cluster manifest deep dive
L12 com.byd.appstartmanagement manifest deep dive
L13 BYD SDK classpath probe (8 BYDAuto*Device classes)
L14 /proc cluster process scan
L15 Hidden-API reachability sanity (6 system binders)

Tier S — ADB-TCP shell-driven (15 tests)

ID Test
S1 ADB shell reachable (id -u, id -un, pwd, uname -a)
S2 Runtime properties (getprop, filtered)
S3 Display state (wm size, wm density, wm overscan)
S4 dumpsys display full inventory
S5 dumpsys SurfaceFlinger physical displays
S6 service list filtered BYD/cluster
S7 AutoContainer probe (service call AutoContainer 1 + snake-case variants)
S8 pm list packages -f BYD/alps/xdja
S9 Activity stack snapshot (dumpsys activity)
S10 SELinux + sepolicy state (getenforce, id -Z)
S11 Daemon process scan (ps -A, filtered)
S12 System settings BYD filter (system / secure / global)
S13 Display launch probe (`am start --display 0
S14 Hardware fingerprint (cpuinfo + meminfo + df)
S15 Filesystem inventory (BYD / DRM / framebuffer)

Tier S — new in build 192 (RE-oriented, 10 tests)

ID Test
S16 BYD packages discovery via shell (pm list packages -f) — populates the discovery cache used by S17
S17 APK extraction/storage/emulated/0/Download/dl2_apks/. com.byd.cluster is extracted first, then appstartmanagement, containerservice, smarttravel, commander, freedom, overdrive, windowmanagement, crosscontrol, clusterdebug, car.server, plus any package whose name contains cluster, fission, projection, dilink, magicwindow, mirror. Re-extraction is cache-aware (size check via stat -c %s). Filename pattern: <pkg>_v<versionCode>.apk
S18 dumpsys window filtered (focused / displays / overscan / IME / stacks)
S19 dumpsys SurfaceFlinger --list + HWComposer topology grep
S20 Recent logcat tail (200 lines, BYD/cluster filter)
S21 dumpsys package com.byd.cluster full (250 lines — manifest, services, providers, permissions)
S22 dumpsys package com.byd.appstartmanagement full
S23 BYD framework JARs + permission XMLs (/system/framework, /vendor/framework, /system/etc/permissions, /vendor/etc/permissions)
S24 am stack list + am task list (API 28 only — direct evidence of multi-display task placement)
S25 Crash logs (tombstones + ANR + dropbox, BYD-filtered)

DL2 cluster-resize hard-ban (build 191)

DiLink 2 cars must never receive a display-resize command — empirical evidence shows the cluster panel reacts unpredictably and can require a reboot. Build 191 enforces total prohibition at three independent layers:

  1. AdbLocalClient — every outgoing wm size, wm density, wm overscan, am display-size, settings put system display_*, settings put secure display_*, settings put global display_* is intercepted and dropped before being written to the local-ADB socket when the platform reports DL2.
  2. ShellGateway — same predicate enforced in the higher-level shell façade used by Beta Engine / overscan / mirror / projection paths.
  3. SettingsActivity — the overscan and resize sliders are disabled and labelled DiLink 2 — resize banned on detected DL2 devices.

DiLink 3 (Seal EU) and DiLink 5 (BYD-AUTO) are unchanged: resize commands continue to be forwarded normally on those platforms.

Platform detection

DiLink 2 is detected when all of the following hold (cumulative AND, cached at process start):

Build.BRAND.equalsIgnoreCase("alps")
&& Build.PRODUCT.toLowerCase().contains("k65")
&& (Build.VERSION.SDK_INT == 28 || Build.VERSION.SDK_INT == 29)

DiLink 3 (Seal EU): Build.MANUFACTURER == BYD && Build.PRODUCT == byd_eu_* && API 29.
DiLink 5 (BYD-AUTO): Build.MANUFACTURER == BYD-AUTO && API 32.

The three predicates are mutually exclusive. Each platform only sees its own code path.


Operator workflow on DiLink 2

  1. Open DashCast → Diag (lands directly on DiLink 2 tab).
  2. Tap Lancer tous — 40 rows complete sequentially.
  3. S17 reports N APK(s) in Download/dl2_apks/.
  4. Open the BYD file manager → Downloaddl2_apkscom.byd.cluster_v<X>.apk (and the rest of the BYD apk set) is ready to pull off the car.
  5. Tap Envoyer (.log) in DashCast → full diagnostic log goes out via Telegram.

Compatibility

  • DiLink 2 (alps / k65 / API 28–29): full feature set, 40 tests, resize banned, Diag opens on DL2 tab.
  • DiLink 3 (BYD Seal EU / API 29): unchanged; Diag opens on Beta Engine.
  • DiLink 5 (BYD-AUTO / API 32): unchanged; Diag opens on DiLink 5 tab.

Install

In-app updater: tap Vérifier les mises à jour in Settings (with Include pre-releases enabled). The car will detect this build as an update if its installed versionName is lower than 1.2.3 or if its versionCode is lower than 192.

Manual install: download DashCast-v1.2.3-debug.apk below and pm install -r it via ADB.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.4


DashCast 1.2.4 (build 193) — Diag: dedicated Mirror tab + Telegram log export

Pre-release • branch beta/1.2.0-dilink5versionCode 193 / versionName 1.2.4

TL;DR

A brand-new Mirror tab has been added to the Diag screen, dedicated to investigating the DiLink 5 cluster mirror black-screen issue reported in build 192. It runs a 9-test read-only suite that captures the ground-truth SurfaceFlinger topology and the MirrorDaemon log directly on-device, saves the heavy artifacts under Download/dl5_mirror_diag/ (browsable from the BYD file manager), and ships everything via a one-tap Envoyer (.log) button that opens the standard Android share chooser — pick Telegram and the report is sent.

Zero changes to the mirror code path. MirrorDaemon, ClusterMirrorManager, ClusterService and every touch/launch code path are bit-for-bit identical to build 192. DL3 mirror behaves exactly as before. DL5 mirror behaves exactly as before. This build is purely a diagnostic surface.

Why this build exists

Field log on a DL5 device (BYD-AUTO Android 12, cluster display shared_fission_bg_XDJAScreenProjection_0 1920×720 @ 320dpi) showed the cluster-mirror path completing successfully on the daemon side:

startMirrorViaDaemon ✓ layerStack=3 1920×720 displayId=3

…but the cluster screen stayed black. Hypothesis: apps physically launched on display #3 are rendered by the XDJA fission compositor, not by SurfaceFlinger. SurfaceControl.setDisplayLayerStack(token, 3) therefore mirrors an empty SF layerStack. To confirm this without touching the production mirror code, we need the real SF topology and the daemon's own post-setup SF dump — captured by this build.

What's new

1. New Mirror tab in Diag (position 8)

DiagActivity grows from 9 to 10 tabs. TAB_MIRROR = 8 is inserted between DiLink 2 and Sniffer.

  • New view field panelMirror, new methods bindMirrorPanel / prepareMirrorTestRows / bindMirrorRow / runMirrorAllTests / updateMirrorCounters / sendMirrorLog — follow the exact pattern of the existing DL5 panel for visual consistency (same status pill, same counters block, same per-test row layout via item_beta_test.xml).
  • New layout include_diag_mirror.xml — clone of include_diag_dilink5.xml minus the per-app Spinner (no per-app target for the Mirror suite). The action row exposes two buttons: Lancer tous (run all tests) and Envoyer (.log) (share the bundled report).
  • The Envoyer (.log) button calls AppLogger.shareWithReport(this, mirrorReport) which already routes through Intent.ACTION_SEND with a FileProvider URI → Android share chooser → the user picks Telegram from the chooser and the message goes through with the full report attached.

2. MirrorTestRunner — 9 tests (M1–M9)

Self-contained suite under com.byd.dashcast.mirror. Same data model triplet (TestDef / TestResult / Status / Listener) and same single-thread executor pattern (mirror-test-runner) as the DL5 runner — intentionally not sharing nested types so a regression in either suite cannot affect the other.

The suite writes all heavy artifacts (full SF dumps, daemon log copy) to /storage/emulated/0/Download/dl5_mirror_diag/, directly browsable from the BYD file manager.

ID Test What it captures
M1 Cluster display fingerprint (no shell) DisplayManager.getDisplays() + reflective Display.getLayerStack / getOwnerPackageName / getOwnerUid for every display: id, name, real size, density, flag breakdown, owner pkg/uid, layerStack. The WindowManager-side identity of the cluster surface, decoupled from SurfaceFlinger.
M2 SurfaceControl.getPhysicalDisplayIds() (reflection) Reflective call to the hidden API plus getPhysicalDisplayToken(id) for each returned id. SecurityException expected from the app uid — the failure mode itself is useful evidence.
M3 dumpsys SurfaceFlinger --list Compositor's view of every display/layer name (top 100 lines).
M4 Full dumpsys SurfaceFlinger (head -300) Saved verbatim to Download/dl5_mirror_diag/dumpsys_sf_full.txt for offline inspection.
M5 Filtered SF topology grep -E 'Display |layerStack|format|orientation|XDJA|fission|cluster|byd|projection|mirror' — extracts only the lines we need to map layerStack ↔ cluster display ↔ XDJA fission compositor.
M6 dumpsys window cluster state grep -E 'Display #|mDisplayId|mCurrentFocus|mFocusedApp|cluster|fission|XDJA|imeLayerStack' — WindowManager's view of which apps live on which display, focus owner, IME layer.
M7 MirrorDaemon log tail (200 lines) Reads /data/local/tmp/mirrordaemon_latest.log if present. This is the gold evidence: the daemon writes its own post-setup dumpsys SurfaceFlinger | grep byd_myapp_mirror here. WARN if the file is missing (daemon never launched in this session).
M8 Copy MirrorDaemon log → Download/ Copies /data/local/tmp/mirrordaemon_latest.log to Download/dl5_mirror_diag/mirrordaemon_latest.log so it ships alongside the textual report.
M9 Mirror token presence post-setup dumpsys SurfaceFlinger | grep -E 'byd_myapp_mirror|mybyd_preview_mirror'. If the token is absent the daemon's createDisplay silently failed; if present but cluster still black, the layerStack mapping is wrong (cluster pixels live in another compositor).

3. Behavioural contract

  • Mirror tab is read-only diagnostic only. No shell command issued by the suite mutates display state, no wm overscan/size/density calls, no am start --display, no projection toggle.
  • Zero changes to MirrorDaemon, ClusterMirrorManager, ClusterService or any cluster touch/launch path.
  • DL3 mirror unchanged. DL5 mirror unchanged.
  • Existing 9 Diag tabs unchanged. DL5 test D30 keeps its broader SF dump; the M-suite is a more curated mirror-focused subset.
  • The DL2 hard-ban on display-resize shell commands (build 191) still owns that contract — none of the M-tests can bypass it.

4. Files touched

  • DiagActivity.javaTAB_MIRROR = 8, TAB_SNIFFER pushed to 9, TAB_COUNT = 10, panelMirror field, bindMirrorPanel / prepareMirrorTestRows / bindMirrorRow / runMirrorAllTests / updateMirrorCounters / sendMirrorLog, showPanelForTab Mirror branch (~150 LoC added).
  • New mirror/MirrorTestRunner.java — ~330 LoC, 9 tests + buildReport + runShellSync helper.
  • New include_diag_mirror.xml — ~140 LoC, clone of DL5 panel minus the Spinner, action row with Lancer tous + Envoyer (.log).
  • activity_diag.xml — +1 TabItem (@string/diag_tab_mirror), +1 panel <include>.
  • values/strings.xml — +8 diag_mirror_* strings (FR default).
  • build.gradleversionCode 192 → 193, versionName 1.2.3 → 1.2.4.

No new permissions, no new dependencies, no new drawables.

How to validate

On the affected DL5 device

  1. Install the OTA (versionCode 193 > 192, the car detects it as an update).
  2. Launch DashCast → tap Start projection so the cluster mirror gets attempted (this is what produces mirrordaemon_latest.log).
  3. Open Diag → swipe to the new Mirror tab → tap Lancer tous.
  4. Expected results:
    • M1 PASS — at least 1 PRESENTATION display detected, layerStack/owner reported.
    • M2 WARN or PASS — reflection may be blocked by SELinux on the app uid; the failure mode is evidence either way.
    • M3–M6 PASS — local ADB shell present.
    • M7 PASS — daemon log produced during the projection attempt in step 2.
    • M8 PASS — copy to Download/dl5_mirror_diag/mirrordaemon_latest.log succeeded.
    • M9 PASS if the mirror token exists in SF (the silent-success case we want to confirm); WARN if it does not (= the daemon's createDisplay is the failure point).
  5. Tap Envoyer (.log) → Android share chooser opens → pick Telegram → message goes through with the full report attached.
  6. The operator now has on-device, browsable via the BYD file manager:
    • Download/dl5_mirror_diag/dumpsys_sf_full.txt — live SF topology
    • Download/dl5_mirror_diag/mirrordaemon_latest.log — the daemon's own SF dump
    • Plus the textual M1–M9 report attached to the Telegram message.

On DL3 (BYD Seal EU, production reference)

  1. Install, open Diag, confirm default tab is unchanged (Beta Engine).
  2. Swipe to Mirror tab (visible at position 8). Tap Lancer tous — the same suite runs (M1 yields the DL3 cluster display fingerprint, the rest succeeds on shell). No regression on DL3 mirror behaviour.

On DL2 (alps / k65v1 / API 28-29)

  1. Install, open Diag. Default tab is still DiLink 2 (per build 192).
  2. Mirror tab is reachable but irrelevant on DL2; the suite runs read-only without issuing any resize commands (the build-191 DL2 guard still owns that contract).

Compatibility

  • versionCode 193 (was 192) — OTA-eligible for every device currently on build 192 or earlier.
  • versionName 1.2.4 (was 1.2.3).
  • minSdkVersion 28 unchanged. targetSdkVersion 29 unchanged. compileSdkVersion 33 unchanged.
  • No new runtime permission requested.

Known limitations

  • M2 is reflective access to a hidden API (SurfaceControl.getPhysicalDisplayIds). It is expected to be blocked by SELinux on the app uid on most stock ROMs — the WARN/error message is the useful signal.
  • M7 / M8 require that the mirror was launched at least once in the current boot session, so /data/local/tmp/mirrordaemon_latest.log exists. If absent, the two tests cleanly WARN with "daemon log not found — launch mirror once then re-run".
  • This build does not yet attempt a fix for the DL5 black-screen — it only collects evidence. The fix will land in a follow-up build once the M-suite output confirms the working hypothesis (layers live in the XDJA fission compositor, not in SF native layers).

Full per-build history: see CHANGELOG.md.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.5


DashCast v1.2.5 — build 194

Branch: beta/1.2.0-dilink5 · Pre-release · Not merged to main

Summary

New M10 — Cluster routing LIVE diagnostic test in the Diag → Mirror tab. End-to-end probe that fires sendInfo(1000, 16) on the BYD AutoContainer service, snapshots the SurfaceFlinger framebufferSpace of every XDJA-fission display before and after the call, then attempts to route com.android.settings/.Settings via am start-activity --display 3 and --display 4, dumps the resulting WindowManager root-task topology, and cleans up. All seven step outputs are concatenated into the test detail and shipped via Telegram with the existing Envoyer (.log) button.

What problem does M10 solve?

Build 192 (D28) visually confirmed that sendInfo(1000, 16) opens a black projection window on the cluster. Build 193 added the read-only Mirror diag suite (M1-M9) that gives full SurfaceFlinger and WindowManager snapshots. But no single field run has captured the BEFORE/AFTER framebufferSpace transition AND the activity-routing outcome together — so we still cannot say with certainty:

  1. Which of the inactive 1×1 fission slots (display 3 or 4) becomes the active 1920×720 cluster framebuffer after sendInfo(16).
  2. Whether an activity launched with am start --display N actually lands on that display (and not silently re-routed to display 0 by the XDJA fission compositor like the DL5 ROM is known to do in some cases).

M10 closes that gap in one shot.

Test steps (all captured in one detail blob)

Step What it does
STEP 1 dumpsys SurfaceFlinger | grep -E 'DisplayDevice\{|framebufferSpace=ProjectionSpace|layerStack=[0-9]' — BEFORE state of fission/XDJA displays
STEP 2 service call AutoContainer 2 i32 1000 i32 16 s16 "" — opens cluster projection
STEP 3 Same grep as STEP 1, 2 s after STEP 2 — AFTER state. The display whose framebufferSpace flips from Rect(0, 0, 1, 1) to Rect(0, 0, 1920, 720) is the cluster target.
STEP 4 am start-activity -W --display 3 -n com.android.settings/.Settings — captures synchronous -W result (Status / TotalTime)
STEP 5 Same on --display 4
STEP 6 dumpsys window | grep -E 'Display: mDisplayId=|mCurrentFocus=|mFocusedApp=|rootTaskId=' — which display actually hosts the Settings root task
STEP 7 Cleanup: am force-stop com.android.settings + sendInfo 18 (close projection) + sendInfo 0 (restore Qt native video stream)

Why the ADB-local shell (not direct Binder)

DL5 systematically returns SIGNATURE_NO_MATCH when the app uid calls ServiceManager.getService("autocontainer") — the BYD AutoContainer service is signature-protected against third-party packages. The ADB-local relay (uid 2000 / shell) is the only Binder path that works from a non-system APK, so M10 routes the service call through AdbLocalClient.executeShellWithResult exactly like the rest of the suite.

Status semantics

M10 always reports PASS — the analysis is intentionally manual. A hard PASS/FAIL would have required hard-coding an expected display id, but the whole point of M10 is to discover that id. The result message reads:

live routing run complete — compare STEP 1 vs STEP 3, then STEP 6

The operator analyzes the raw text in the shipped report.

Side-effect contract

Two non-trivial side-effects, both pre-validated as user-safe:

  1. sendInfo 16 opens a black projection window on the cluster for ~6 seconds (visually confirmed harmless in build 192 D28).
  2. Settings briefly launches on display 3 or 4 (or fails to, depending on the ROM) and is force-stopped.

STEP 7 always runs even if STEPS 4 / 5 fail, so the cluster is always returned to its baseline state regardless of intermediate failures.

Files changed

File Change
app/build.gradle versionCode 193 → 194, versionName 1.2.4 → 1.2.5
mirror/MirrorTestRunner.java +1 catalog entry, +1 dispatcher case, +runM10 (~85 LoC), +1 reused fbGrep constant
CHANGELOG.md New 1.2.5-build194 entry at the top of the pre-releases table

No new permissions, no new dependencies, no new layouts, no new strings, no new drawables. M10 reuses the existing runShellSync helper, the AdbLocalClient.executeShellWithResult infrastructure, and the existing include_diag_mirror.xml row layout (Mirror rows are generated dynamically from MirrorTestRunner.catalog() in DiagActivity.prepareMirrorTestRows, so no XML edit was needed).

DL3 / DL2 behaviour

The Mirror tab and M10 still run, but STEP 2 will likely return Service AutoContainer does not exist on DL3 (no XDJA fission compositor) or hit the DL2 single-display ROM (no cluster at all). The test still PASSES with the empty-output evidence, which is itself useful — it proves AutoContainer is DL5-only and confirms the auto-detection scope.

Validation plan

  1. Install the OTA on a DL5 device.
  2. Open DashCast → Diag → swipe to the Mirror tab → tap Lancer tous. Wait ~25 s for M10 (longest test in the suite due to the 2 s + 1 s + 1 s settle waits).
  3. Tap Envoyer (.log) → Telegram.
  4. In the report look for [PASS] M10 then under --- detail --- read STEP 1 vs STEP 3: exactly one of fission_bg_XDJAScreenProjection_0/1/2 should have flipped from framebufferSpace=ProjectionSpace(start=Rect(0, 0, 1, 1) to =Rect(0, 0, 1920, 720). That display's layerStack= value is the cluster compositor's target layerStack.
  5. Then STEP 6 should show rootTaskId=... for a mDisplayId=3 or mDisplayId=4 entry hosting com.android.settings/.Settings — confirming the activity-routing side of the pipeline.

Asset

Links

APK download

GitHub release page

  • Author

DashCast v1.2.6


DashCast v1.2.6 — Pre-release (versionCode 195)

Two coordinated payloads bundled into a single OTA: a zero-behaviour proxy daemon performance pass (P1–P4) and a focused DL5 mirror investigation gated on Platform.isDiLink5 (DL3 strictly untouched).

(A) Beta Engine proxy daemon — 4 perf passes (P1–P4)

Lockless hot paths in the typed-verb dispatcher. Zero protocol bump (PROTOCOL_VERSION = "2"), zero public API change, zero behaviour change on DL3 reference (BYD Seal EU) or DL5 testeur.

  • P1 — Lock-free hot-path verbs in BetaProxyClient. sBinder promoted to volatile; synchronized (LOCK) removed from setOverscan, getPidsByPackage, autoContainerSendInfo, forceStopPackage, runShell, runPhase4Probes, ping. sDaemonUid / sDaemonPid / sDaemonVer now volatile so the public getters are lockless too. connect(), the receiver and handshake() still take LOCK for publication — the only place strictly needed. Resize SeekBar (~30 setOverscan/s while dragging) and reconcile loop (getPidsByPackage every ~5 s) now dispatch in parallel through the daemon's binder pool instead of queuing behind any in-flight runShell.
  • P2 — DeathRecipient hooked on the live binder. isConnected() now uses the cheap local isBinderAlive() flag instead of issuing a full pingBinder() IPC roundtrip on every typed verb. Per-verb latency roughly halves (~10 ms → ~5 ms). Receiver unhooks the previous death recipient before swapping a fresh binder; linkToDeath is best-effort caught for the vanishing race between stale-broadcast check and link.
  • P3 — ThreadLocal<byte[]> scratch in Phase4Verbs.getPidsByPackage. Was allocating a fresh byte[256] per /proc/<pid>/cmdline scan iteration (~241 allocations per reconcile tick). The daemon's binder thread pool reuses threads → one buffer per thread for the daemon's lifetime, zero allocation, zero GC pressure. Loop body and matching semantics byte-for-byte unchanged.
  • P4 — ByteArrayOutputStream in ProxyDaemonMain.runShell. Replaces the per-line BufferedReader.readLine() + StringBuilder pattern. Reads the full child stream in 4 KiB chunks into a single ByteArrayOutputStream, decodes once with toString("UTF-8"), strips trailing \r/\n to exactly reproduce the legacy line-joiner semantics. ~1000 transient allocations saved on a 300-line dumpsys SurfaceFlinger.

Wire protocol: unchanged. An app-update that lands while a daemon from build 192–194 is still alive transparently talks to the old daemon until the next bootstrap kills it. No incompatibility possible.

(B) DL5 mirror investigation — 3 surgical fixes

First DL5 in-car test of the build 192–194 mirror path confirmed end-to-end routing works (Yandex on cluster mDisplayId=3 rootTaskId=50, daemon-side startMirrorViaDaemon ✓ layerStack=3 1920×720 displayId=3) but the in-app preview surface stays black. Root cause traced to the DL5 fission architecture: apps render on shared_fission_bg_XDJAScreenProjection_0/1 (layerStack=3/4, framebufferSpace bounds 1×1 — degenerate shadow displays), then com.byd.containerservice re-composes the selected slot into fission_bg_XDJAScreenProjection (layerStack=2, framebufferSpace bounds 1920×720 — the actual surface piped to the physical cluster panel). Mirroring layerStack=3 captures a 1×1 black buffer.

  • B1 — DL5 layerStack override 3→2 (and 4→2) in ClusterMirrorManager. New private helper applyDl5LayerStackOverride(Context ctx, int detected) wraps Platform.get().isDiLink5(ctx) in a try/catch (any reflection failure returns input unchanged → DL3 path bit-for-bit identical). On confirmed DL5, layerStack 3 or 4 is rewritten to 2 with an explicit log line; any other value is passed through untouched. Applied inside both startMirror (app-uid direct SurfaceControl path) and startMirrorViaDaemon (daemon-uid path). Both signatures gained a leading Context parameter; sole caller MainActivity.attemptStartMirror updated.
  • B2 — Daemon-side mirror setup logging (MirrorDaemon.setupMirror). All pre-195 mirror traces in the daemon went through Log.i (logcat-only). The on-device M7 diagnostic test reads /data/local/tmp/mirrordaemon_latest.log which only captures out() lines, so the file showed only the startup banner. Build 195 mirrors the critical-path traces to out(): setupMirror BEGIN, setupMirror createDisplay OK token=… (or FAIL createDisplay returned null), setupMirror SF dump (layerStack=N): … (or empty — token NOT in SurfaceFlinger!), setupMirror DONE ok=true, and setupMirror EXCEPTION: ClassName: message on any failure. Next M7 capture will show the full daemon chain.
  • B3 — M10 LIVE probe fixes (MirrorTestRunner.runM10). Build 194 M10 had two known-bad hard-codings: STEP 2 / STEP 7 issued service call AutoContainer 2 … (PascalCase) but the DL5 cluster service is registered as auto_container (snake_case, confirmed by production ClusterManager), and STEP 4/5 launched com.android.settings/.Settings which BYD doesn't ship under that exact AOSP name. Build 195 replaces AutoContainerauto_container (3×) and com.android.settings/.Settingscom.android.launcher3/.Launcher (2×). Test stays ALWAYS PASS (manual analysis of STEP 3 framebufferSpace diff and STEP 6 routing), but next run produces real sendInfo Parcel(...) output and real am start Status/TotalTime instead of two-bugs-cancelling-out.

Behavioural contract

  • DL3 production (BYD Seal EU) — bit-for-bit identical sequence of binder transactions and shell outputs as build 194. The DL5 layerStack override short-circuits via Platform.isDiLink5=false. The daemon out() additions are pure log writes. M10's diag-only changes don't touch any runtime path.
  • DL5 testeur (BYD-AUTO) — proxy daemon perf wins apply identically. Mirror should now capture the composed 1920×720 fission output instead of the 1×1 shadow. If still black, M7 will provide the full daemon setup chain in the file log (createDisplay token state, SF dump result, projection params) — enough evidence to triage further.
  • DL2 testeur — completely unaffected (Beta Engine disabled by default, no cluster display so mirror code path never runs).

Validation plan

  1. DL5 in-car — install, activate cluster, launch Yandex/Maps on cluster, observe in-app preview during active mirror (don't open Diag first). Expected: preview now shows the actual cluster content. If still black: Diag → Mirror → Lancer tous → M7 contains the full daemon setup chain → paste in issue.
  2. DL5 in-car — re-run M10: STEP 2 should show successful Parcel(00000000 00000001) from auto_container, STEP 4/5 should show Starting: Intent { ... cmp=com.android.launcher3/.Launcher } with a real Status/TotalTime, STEP 6 still shows rootTaskId=N mSession=... now correlated with launcher3.
  3. DL3 Seal EU — install, activate cluster, launch Yandex/Waze: zero behaviour change vs build 194. Logcat tag ClusterMirrorManager should NOT contain any DL5 override line.
  4. Resize SeekBar smoothness (any platform) — open Settings → Marges, drag the slider 0 → 80 → 0: expect noticeably smoother behaviour vs build 194 (P1+P2 → ~2× faster per-tick, ~3–5 ms instead of 8–12 ms).

Touched files

  • app/build.gradle (versionCode 194 → 195, versionName 1.2.5 → 1.2.6)
  • beta/BetaProxyClient.java (~45 LoC, no public API change)
  • beta/proxy/Phase4Verbs.java (+6 LoC)
  • beta/proxy/ProxyDaemonMain.java (~20 LoC)
  • dashboard/ClusterMirrorManager.java (~30 LoC, +Context param + DL5 helper)
  • daemon/MirrorDaemon.java (~12 LoC, out() mirrors)
  • mirror/MirrorTestRunner.java (~10 LoC, M10 fix)
  • MainActivity.java (+2 args in attemptStartMirror mirror startup)
  • CHANGELOG.md

No new permissions, no new dependencies, no new layouts, no new strings.


Branch: beta/1.2.0-dilink5 (not merged to main)
APK: DashCast-v1.2.6-debug.apk (14 MB)
Detected as update from: v1.2.5 (versionCode 194) and earlier.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.7


DashCast v1.2.7 — Pre-release (versionCode 196)

DL5 tactile fix follow-up to v1.2.6. v1.2.6 made the cluster mirror visible on DL5 (layerStack 3→2 override let us capture the composed fission output instead of the 1×1 shadow framebuffer). v1.2.7 attempts to make the cluster mirror touchable on DL5, symmetric to the layerStack work, and instruments the daemon's input injection path so the next M7 capture reports exactly where the touch chain breaks if this fix isn't sufficient.

Field test evidence (22/05/2026, v1.2.6 on DL5 testeur)

  • ✅ Preview shows actual cluster composite content (Yandex visible in TextureView in real time).
  • ❌ Touch does NOT reach Yandex in preview OR in fullscreen mirror.
  • Log: ClusterInputForwarder Cluster dimensions: 1920x720 displayId=3, Daemon Binder connected — touch/key injection via uid=2000, daemon holds INJECT_EVENTS, no injectMotion failed warning in logcat, no touch → debug line (filtered from the field report).
  • Yandex's window on WMS: mDisplayId=3 rootTaskId=79 Session{… 7775:u0a10124} → daemon was correctly targeting setDisplayId(3).
  • BUT: WMS also exposes a separate mDisplayId=2 (visible in mWindowsForAccessibilityObserver={…, 2=…, 3=…, 4=…}) backing the fission_bg_XDJAScreenProjection 1920×720 composite — that's where the user actually sees the apps.

Hypothesis: on DL5 the InputDispatcher either clips events injected on displayId=3 against its degenerate 1×1 framebufferSpace, refuses to route into PRESENTATION displays with such dims, or the WMS-side Yandex window on displayId=3 isn't actually input-eligible because it's composited away by containerservice into displayId=2's surface.

Changes

  • ClusterMirrorManager.applyDl5DisplayIdOverride(Context, int) — sibling helper to v1.2.6's applyDl5LayerStackOverride. Same shape, same DL3-safe contract: Platform.get().isDiLink5(ctx) wrapped in try/catch (any failure returns input unchanged), DL3 returns input unchanged, DL5 rewrites 3 and 4 to 2 with explicit AppLogger.i DL5 override: displayId N → 2 (touch injection on composed cluster face) log line. Wired in startMirrorViaDaemon right before data.writeInt(clusterDisplayId) — the value of sClusterDisplayId stored statically inside MirrorDaemon (and used by injectMotion for MotionEvent.setDisplayId) now becomes 2 on DL5 instead of 3.
  • MirrorDaemon.injectMotion/injectKey — first-event + failure instrumentation via out(). v1.2.6 made setupMirror trace appear in /data/local/tmp/mirrordaemon_latest.log (the file M7 reads) but the injection paths still only logged failures to logcat. New volatile boolean sMotionFirstLogged/sKeyFirstLogged flags (reset in setupMirror so each session re-arms once) make the very first successful injection emit out("injectMotion FIRST OK displayId=N setDisplayIdAvail=true|false action=K x=X y=Y ret=R") to the file log; any subsequent exception emits out("injectMotion EXCEPTION displayId=N action=K err=ClassName: message"). Pre-check path (ev==null || sInputManager==null) also emits a one-shot diagnostic. Same instrumentation pattern for injectKey. Next M7 capture on DL5 will tell us: did the override take effect (displayId=2 in the FIRST OK line), did setDisplayId reflection survive (setDisplayIdAvail=true), did InputManager accept the event (no EXCEPTION line).

Behavioural contract

  • DL3 production (BYD Seal EU) — bit-for-bit identical: applyDl5DisplayIdOverride returns input unchanged on isDiLink5=false so clusterDisplayId written to the daemon's MIRROR_START parcel stays exactly what Display.getDisplayId() returned on DL3. Daemon out() additions are pure log writes; the Log.w calls are preserved. No public API change, no protocol bump (PROTOCOL_VERSION="2").
  • DL5 testeur — daemon's setDisplayId target shifts from 3 to 2 for touch injection. Mirror visual stays exactly as v1.2.6 (layerStack capture from 2 unchanged). M7 detail now contains the actual displayId/setDisplayIdAvail/ret triplet of the first injected event so we can triage further if Yandex still doesn't respond.
  • DL2 testeur — completely unaffected (no cluster code path triggered).

Validation plan

  1. DL5 in-car — install, activate cluster, launch Yandex/Maps on cluster, then tap on the in-app preview surface (or enter fullscreen via 📺 and tap there): expected — Yandex now responds to touch (map pans, buttons react). Logcat tag ClusterMirrorManager should contain DL5 override: displayId 3 → 2 exactly once per mirror session, and startMirrorViaDaemon ✓ layerStack=2 1920×720 displayId=2 (note displayId=2 instead of =3 like v1.2.6).
  2. If still not working — open Diag → Mirror → Lancer tous, M7 detail will show the first injectMotion FIRST OK displayId=2 setDisplayIdAvail=true action=0 x=… y=… ret=… line:
    • ret=false → InputManager refused (likely needs displayId=0 + global routing, or WMS input monitor).
    • setDisplayIdAvail=false → reflection failed on this ROM, the event went to displayId=0 by default → DashCast self-consumed it (explains v1.2.6 silent failure).
    • No injectMotion FIRST OK line at all → the OnTouchListener never fired (layout/event consumption issue), pivot to View-tree debug.
  3. DL3 Seal EU — install, activate cluster, run any cluster app, confirm fullscreen mirror tactile still works exactly as before. Logcat should NOT contain any DL5 override: displayId line.
  4. DL2 — install, navigate UI, no cluster path triggered, zero behaviour change.

Touched files

  • app/build.gradle (versionCode 195 → 196, versionName 1.2.6 → 1.2.7)
  • dashboard/ClusterMirrorManager.java (+22 LoC, +applyDl5DisplayIdOverride helper, +1 call site)
  • daemon/MirrorDaemon.java (+30 LoC, +2 volatile flags, reset in setupMirror, instrumented injectMotion/injectKey)
  • CHANGELOG.md

No new permissions, no new dependencies, no new layouts, no new strings.


Branch: beta/1.2.0-dilink5 (not merged to main)
APK: DashCast-v1.2.7-debug.apk (14 MB)
Detected as update from: v1.2.6 (versionCode 195) and earlier.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.8


DashCast v1.2.8 (build 199) — Pre-release

Branch: beta/1.2.0-dilink5
Target: DiLink 5 testers (no impact on DiLink 3 production users)
Built from: commit 7e94519

Overview

Four coordinated DL5 payloads built on top of the v1.2.7 tactile fix, all gated behind Platform.isDiLink5(Context) so the DL3 production hot path is bit-for-bit identical to v1.2.7.


(A) Keyboard bridge — manual ⌨ button + KeyboardBridgeActivity

Problem. Field report (22/05/2026, post-v1.2.7): touch injection on the DL5 cluster mirror now works (Yandex maps reacts to taps), BUT tapping an EditText inside Yandex does NOT pop up the system IME on the head unit.

Root cause. WindowManager per-display IME isolation routes the focused-window IME request to display 3 (the 1×1 shared_fission_bg_XDJAScreenProjection shadow framebuffer) where the IME has nowhere to render. No clean override exists on API 32 without MANAGE_DEVICE_POLICY_LOCAL_OVERRIDE (System API, unavailable on BYD-AUTO ROM).

Workaround. A new translucent landscape KeyboardBridgeActivity is launched on display 0 (head unit) with a hidden EditText that triggers the native IME on tap. A TextWatcher computes the per-character diff and forwards each typed character to the cluster via ClusterInputForwarder.injectKeyEvent(KeyEvent), which already routes through the existing TRANSACT_INJECT_KEY daemon transaction (uid 2000, holds INJECT_EVENTS).

  • KeyCharacterMap.load(VIRTUAL_KEYBOARD).getEvents(char[]) converts each character into a meta-state-aware KeyEvent[] (handles SHIFT for capitals, ALT for symbols).
  • Fallback mapAsciiToKeyCode for degenerate ASCII when KCM lookup fails.
  • Backspace is translated to N×KEYCODE_DEL based on before count from TextWatcher.
  • EditText is reset after 80 characters to prevent unbounded growth.

A new ⌨ button (btn_keyboard_bridge, 36 dp, tertiary-container colours) is inserted in the toolbar after the Split-screen button, visibility-gated to DL5 only (DL3 → GONE).

(B) Auto-popup via ClusterImeWatcherService (AccessibilityService)

Manual ⌨ taps work, but the UX target is automatic pop-up on EditText focus. Android does not expose an in-process focus listener for foreign apps' windows, so the only supported path is an AccessibilityService.

New service com.byd.dashcast.ime.ClusterImeWatcherService listens for TYPE_VIEW_FOCUSED | TYPE_WINDOW_STATE_CHANGED events (feedbackGeneric, notificationTimeout=100, canRetrieveWindowContent=true).

Five-guard filter chain — each event must pass ALL of these to trigger:

  1. Platform.isDiLink5(this)DL3 strictly never triggers (refreshed every event).
  2. eventType == TYPE_VIEW_FOCUSED.
  3. ClusterService.sIsRunninghard gate: only react while the DashCast cluster projection is actually live. If DashCast is closed, the service is registered with the system but does nothing.
  4. event.getDisplayId() > 0 — strictly secondary displays only, blocks display 0 and the unknown-id (-1) case on pre-API-30 fallbacks.
  5. node.editable && pkg != getPackageName() — no self-trigger on the bridge's own EditText.

Additional safety:

  • 600 ms debounce via mLastLaunchAt.
  • KeyboardBridgeActivity.isShowing() static check (set in onStart/onStop/onDestroy) to skip launch when the bridge is already visible.
  • Every callback wrapped in try { … } catch (Throwable t) { AppLogger.e(…); } — the service never crashes the host app.

The launch sends an intent with EXTRA_AUTO_OPENED=true so the bridge can distinguish manual vs. auto invocations.

(C) On-boarding banner for the AccessibilityService

Android requires the user to enable an AccessibilityService manually in Settings (no programmatic enable possible by design).

A new MaterialCardView card_ime_a11y_banner (tertiary-container, corner 12 dp) is inserted in MainActivity after the top bar. It shows on first launch when all three are true:

  1. Platform.isDiLink5(this)
  2. The service is not yet in Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES (parsed colon-separated, comparing both flattenToString and flattenToShortString).
  3. The user has not dismissed it (PREF_IME_BANNER_DISMISSED).

Three actions:

  • Enable → deep-links to Settings.ACTION_ACCESSIBILITY_SETTINGS. Toast Clavier automatique actif ✓ shown on return to MainActivity if the service is then enabled.
  • Later → session-hide only, banner reappears next launch.
  • Dismiss → persists PREF_IME_BANNER_DISMISSED=true, banner never shown again unless prefs are cleared.

Fully no-op on DL3 (never shown).

(D) DL5 resize sliders — dynamic cluster routing

Problem. The per-app resize panel (btn_toggle_resize → SeekBars W/H → Apply) was a no-op on DL5.

Two hard-coded bugs:

  1. MainActivity.btnResizeApply and autoApplyInsetsIfNeeded both issued wm overscan W,H,W,H -d 1 (cluster id on DL3 only — on DL5 the cluster lives on display 3 or 4, so the command silently failed).
  2. ClusterService.resizeActiveTask hard-coded the freeform Rect to 1920 - insetH, 720 - insetV (DL3 cluster size).

Fix:

  • Both MainActivity call sites now resolve the cluster display id dynamically via ClusterService.getDisplayId()DashboardDisplayHelper.getKnownClusterDisplayId() — the same source already used by every other cluster-routed code path.
  • Skip the wm overscan call entirely when clusterId <= 0 (no cluster connected — never silently retarget to display 0).
  • ClusterService.resizeActiveTask and the inline post-moveTaskToDisplay resize block now compute bounds from mInputForwarder.getClusterWidth()/getClusterHeight() (set live from Display.getSize() in setClusterDisplay), with explicit fallback to 1920×720 if either getter returns 0.

DL3 behaviour stays bit-for-bit identical (DL3 cluster IS 1920×720; fallback path matches the old hard-coded values). On DL5 the bounds now reflect actual cluster dimensions reported by the system.

(E) Head-unit display-0 hard guards (defense in depth)

Concern. Even with the per-call-site clusterId > 0 checks in (D), a regressed caller could still ship a wm overscan|size|density … -d 0 to the head-unit display 0 and shrink the main UI (the v1.2.5 DL2 incident playbook).

Three independent layers:

(E1) ShellGateway.WM_DISPLAY_ZERO — new regex ^\s*wm\s+(?:overscan|size|density)\b.*\s-d\s+0\b.*$ blocks any wm verb explicitly targeting display 0. Checked at the top of execShellWithResult before both the proxy-ON typed-verb path AND the legacy AdbLocalClient fallback — neither can reach the system. Blocked commands emit AppLogger.e("BLOCKED wm verb on display 0 (head unit): …") and the caller's Callback.onError("blocked: wm command targets display 0 (head unit)").

(E2) New abort in ClusterService.resizeActiveTask: if mDisplayHelper.getKnownClusterDisplayId() <= 0 at call time, log and return without invoking IActivityTaskManager.resizeTask. Without this, a stale taskId that happens to live on display 0 (because a prior cluster-move failed silently) would have applied DL3 freeform bounds to the head-unit task.

(E3) The two MainActivity clusterId > 0 guards from (D) form the outermost UI-level layer.

Coverage matrix: to reach display 0 a caller would have to simultaneously bypass the UI guard, the cluster-connected guard in resizeActiveTask, AND the regex at the shell gateway — three independent layers, no single point of failure.


Behavioural contract

Platform Result
DL3 production (BYD Seal EU) ⌨ button GONE, banner hidden, AccessibilityService inert (!isDiLink5 short-circuit), resize sliders identical to v1.2.7 (cluster id resolves to 1, bounds 1920×720, command -d 1), display-0 guards never fire.
DL5 testeur (BYD-AUTO) ⌨ button visible, bridge forwards keystrokes to cluster apps. With AccessibilityService enabled, EditText focus on cluster auto-launches bridge in ~100 ms. Resize sliders actually work. Display-0 protections active.
DL2 Unaffected — no cluster surface ever runs on DL2; AccessibilityService stays inert if accidentally enabled. v1.2.5 DL2 hard-ban still in place + new WM_DISPLAY_ZERO regex adds another layer.

Touched files

  • NEW: KeyboardBridgeActivity.java, ime/ClusterImeWatcherService.java, res/xml/accessibility_ime_watcher.xml
  • MODIFIED: AndroidManifest.xml, MainActivity.java, ClusterService.java, dashboard/ClusterInputForwarder.java, beta/ShellGateway.java, res/layout/activity_main.xml, res/values/strings.xml, app/build.gradle

No new permissions. No protocol bump (PROTOCOL_VERSION=2 unchanged). All daemon traffic reuses existing TRANSACT_INJECT_KEY=3.

Validation plan

DL5 testeur

  1. Install, start cluster projection, open Yandex on cluster.
  2. Tap ⌨ button → bridge appears on head unit, typing forwards to Yandex search bar.
  3. Enable AccessibilityService in Settings (Accessibility → DashCast Cluster IME Watcher). Tap Yandex search bar on cluster → bridge auto-launches within ~100 ms.
  4. Open Adjust panel, slide W/H, press Apply → Yandex window on cluster recomputes bounds. Head unit unaffected (visual proof display 0 is untouched).
  5. Banner appears on first launch. Enable → return to app → expect Toast Clavier automatique actif ✓.

DL3 Seal EU (regression check)

  1. Install. Confirm ⌨ button is NOT visible in toolbar, banner is NOT shown.
  2. Resize sliders behave exactly as v1.2.7.
  3. Logcat tag ClusterImeWatcherService never logs anything past !isDiLink5 → return.

DL2 (regression check)

  1. Install. Everything works exactly as v1.2.7.

Install

Download DashCast-v1.2.8-debug.apk below and install via:

adb install -r DashCast-v1.2.8-debug.apk

Or via OTA: the device's existing DashCast install will see versionCode 199 > 196 and offer the update.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.9


DashCast v1.2.9 — build 200 (pre-release)

Target branch: beta/1.2.0-dilink5
Platforms validated: DiLink 3 (Seal EU production reference), DiLink 5 (testeur — same code path)
Protocol: PROTOCOL_VERSION=2 unchanged
APK: DashCast-v1.2.9-debug.apk (14.5 MB, versionCode 200, versionName 1.2.9)

Increment versionCode 199 → 200 so the BYD AUTO OTA installer recognizes this as an update over v1.2.8 build 199 — no uninstall required.


Why this release

Field report (May 23, 2026) on DL3 Seal EU surfaced two distinct user-visible symptoms that share the same architectural root cause: BYD AUTO ROM's am force-stop / IActivityManager.forceStopPackage kill the process but do not remove the corresponding TaskRecord from the Recents stack. The existing dumpsys-based cleanup fallback (dumpsys activity recents | grep | sed → am task remove → TaskRemover app_process) silently fails on this ROM (uid 2000 lacks REMOVE_TASKS, am task remove not always available).

The same code paths are shared by DL5, so the fixes apply transparently to both platforms.


Bug 1 — Long-press "Kill" leaves a ghost Recents tile

Symptom: long-press any app currently rendering on the cluster → tap Kill → the process dies (cluster goes blank) BUT the app's tile remains in the head-unit Recents. Tapping it relaunches the activity.

Root cause: in MainActivity.doKillApp, two async operations were fired in close succession on different threads:

  1. mClusterService.moveTaskToDisplay(pkg, 0, null) — move the cluster-active task back to display 0.
  2. AdbLocalClient.forceStopApp(...) — force-stop the package, which internally runs dumpsys activity recents | grep <pkg> | sed → am task remove $taskId to purge the Recents entry.

forceStopApp's dumpsys scan could parse the recents stack during the move-to-display-0, see a stale or already-moved TaskId, and am task remove would target the wrong (or no longer existing) task — leaving the real TaskRecord orphaned in Recents.

Fix (a) — serialize move → forceStop via LaunchCallback

ClusterService.moveTaskToDisplay(pkg, displayId, callback) already exposed LaunchCallback.onResult(boolean) fired on the main thread (added in v1.1.x). doKillApp now:

  1. Extracts the forceStopApp callback as a final local killCallback.
  2. Wraps the cluster-app branch's moveTaskToDisplay(pkg, 0, ...) call with a LaunchCallback whose onResult fires mSessionClusterPackages.remove + persistSessionClusterPackages + AdbLocalClient.forceStopApp(this, pkg, killCallback).
  3. The non-cluster branch (else) is structurally unchanged but routes through the same extracted killCallback.

Result: the dumpsys activity recents scan inside forceStopApp is now guaranteed to run after the move-to-display-0 has fully completed, eliminating the race window.


Bug 2 — Stop Projection relaunches the cluster app fullscreen on the head unit

Symptom: start cluster projection → launch Waze (or any app) on the cluster → tap Stop Projection. The cluster is correctly restored to the BYD dashboard, but ~1 second later Waze pops up fullscreen on display 0, covering the BYD launcher.

Root cause: in MainActivity.restoreBydDashboard (TEST 10 path) and originCluster, the call order was:

  1. moveSessionAppsToMainDisplay() — iterates mSessionClusterPackages and moves every cluster-launched app (including the currently-active capturedClusterPkg) to display 0 as a foreground task.
  2. AdbLocalClient.restoreBydOnCluster(this, capturedClusterPkg, ...) — force-stops capturedClusterPkg, then fires sendInfo(18) + sendInfo(0) to restore the Qt cluster surface.

Step 1 brings the active cluster app to display 0 as a foreground task. Step 2 force-stops it — kills the process, but the TaskRecord on display 0 survives (Bug 1 root cause applies here too). Then sendInfo(18)+sendInfo(0) triggers the Qt cluster surface restoration, which appears to fire a RESUME_TOP_ACTIVITY ripple effect on display 0 that picks up the orphan TaskRecord and resurrects the activity.

Fix (b) — invert order: remove capturedClusterPkg from session set BEFORE moveSessionAppsToMainDisplay()

Both restoreBydDashboard and originCluster now do:

if (capturedClusterPkg != null) {
    mSessionClusterPackages.remove(capturedClusterPkg);
    persistSessionClusterPackages();
}
moveSessionAppsToMainDisplay();
AdbLocalClient.restoreBydOnCluster(this, capturedClusterPkg, ...);

Now moveSessionAppsToMainDisplay() iterates a set that no longer contains the active cluster pkg — it stays on display 1 (cluster). restoreBydOnCluster (unchanged) then force-stops it in place on display 1 just before sendInfo(18). No orphan TaskRecord ever lands on display 0, no resurrection path.

Other session apps (split-mode side dashboards) are still moved to display 0 as before.


Fix (c) — verifyForceStop active escalation (defense in depth)

v1.2.8's AdbLocalClient.verifyForceStop only logged a WARN when BetaProxyClient.getPidsByPackage(pkg) reported surviving PIDs after the typed IActivityManager.forceStopPackage returned OK.

Build 200 actively escalates: surviving PIDs → BetaProxyClient.runShell("kill -9 <pid1 pid2 ...>") via the daemon (uid 2000 owns the same uid as the target app, so the kill is permitted) → sleep 200 ms → re-query PIDs → log the final state. The whole escalation is wrapped in an inner try/catch so a daemon glitch can never break the teardown sequence.

This also benefits restoreBydOnCluster / restoreOriginCluster (typed paths call verifyForceStop) — if the cluster pkg somehow survives the in-place force-stop in Fix (b), the kill -9 escalation finishes the job before sendInfo(18) fires.


Known limit (roadmap)

This v1.2.9 patch fixes the process-survival and race-window sides of both bugs, but it does not purge the TaskRecord itself from Recents — architectural limitation of the dumpsys + shell pipeline on BYD AUTO ROM.

Practical impact:

  • Bug 1 (Kill): the ghost Recents tile may still appear briefly on DL3. Tapping it now relaunches a fresh instance instead of resurrecting state on display 1, but the visual tile may linger.
  • Bug 2 (Stop Projection): fully resolved by Fix (b). No Waze flash on the head unit.

Robust fix scheduled for v1.3.x

A new typed verb TXN_REMOVE_TASKS_BY_PACKAGE will be added to the proxy daemon:

  • Uses IActivityTaskManager.getRecentTasks() + removeTask(taskId) from the stable uid 2000 daemon context.
  • Atomic, no shell parse, no race.
  • Will be called from AdbLocalClient.forceStopApp (replacing the current dumpsys + sed + am task remove + TaskRemover block) and from restoreBydOnCluster / restoreOriginCluster.
  • Requires PROTOCOL_VERSION bump 2 → 3 (first real bump since v1.1.6).

Full specification in docs/PLAN_v1.3.x_FULL_PROXY.md §7.b (this repo, this branch).


Touched files

File Change
app/build.gradle versionCode 199 → 200, versionName "1.2.8" → "1.2.9"
app/src/main/java/com/byd/dashcast/MainActivity.java doKillApp serialize move → kill via LaunchCallback (~50 LoC); restoreBydDashboard + originCluster pre-remove captured pkg from session set (~14 LoC total)
app/src/main/java/com/byd/dashcast/AdbLocalClient.java verifyForceStop active kill -9 escalation (~40 LoC)
docs/PLAN_v1.3.x_FULL_PROXY.md NEW §7.b documenting the typed-verb robust fix for v1.3.x
CHANGELOG.md v1.2.9 build 200 entry

Net stats: 5 files changed, 454 insertions, 14 deletions.

No new permissions. No new dependencies. No new strings. No protocol bump (still PROTOCOL_VERSION=2).


Behavioural contract

  • DL3 (Seal EU production): zero behaviour change outside the targeted fixes. The wm overscan -d 1 cluster routing, all typed verbs, all sendInfo Parcel layouts — all bit-for-bit identical to v1.2.8.
  • DL5 (testeur): same — shared MainActivity / AdbLocalClient code paths. The DL5-specific IME bridge / display-0 hard guards / displayId override (from v1.2.6 — v1.2.8) are untouched.
  • DL2: completely unaffected. No cluster path → only verifyForceStop escalation could trigger there, but DL2 doesn't ship the typed verbs that gate this code path.

Validation plan (suggested in-car checks)

(a) DL3 Seal EU — Bug 1

  1. Install v1.2.9 (over v1.2.8 — should NOT prompt for uninstall).
  2. Start cluster projection → launch Waze on cluster.
  3. Long-press the Waze tile in DashCast → tap Kill.
  4. Expected: Waze process dies, cluster goes blank.
  5. Open Recents on the head unit. The Waze tile may still appear (known TaskRecord limitation — see roadmap). Tap it.
  6. Expected: Waze relaunches fresh (no prior state from display 1) — proves the move-then-kill race is gone.

(b) DL3 Seal EU — Bug 2

  1. Start cluster projection → launch Waze on cluster.
  2. Tap Stop Projection.
  3. Expected: cluster restored to BYD dashboard AND NO Waze flash on the head unit (previously: Waze popped fullscreen ~1 s after restore).
  4. Logcat tag MainActivity should show mSessionClusterPackages remove com.waze before any moveSessionAppsToMainDisplay line.

(c) DL5 testeur

Same two scenarios as (a) and (b), same expected results (shared code path).

(d) DL2

Install, navigate UI, no cluster path triggered, zero behaviour change.

(e) Logcat hygiene

Search for verifyForceStop ... escalating kill -9 in logcat. Should only appear when the typed forceStopPackage left a surviving PID (rare on a healthy device) — confirms the escalation is dormant in nominal operation.


Install

Direct sideload (BYD file manager → tap APK):

DashCast-v1.2.9-debug.apk

Or via DashCast's built-in OTA updater (Settings → Check for updates) once GitHub picks up this release.


Branch & tag

  • Branch: beta/1.2.0-dilink5
  • Commit: e7f1a90
  • Tag: v1.2.9
  • Previous: v1.2.8 (build 199, May 22, 2026)
  • Next planned: v1.3.0 — full ProxyDaemon migration (see docs/PLAN_v1.3.x_FULL_PROXY.md)

Links

APK download

GitHub release page

Create an account or sign in to comment

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.