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 310
  • Created
  • Last Reply

Top Posters In This Topic

Posted Images

Featured Replies

  • Author

DashCast v0.2.0-beta


First beta release

What's new

  • OTA updates: DashCast now updates itself automatically from GitHub Releases
    • Download progress bar
    • Install via PackageInstaller with system confirmation prompt
    • Pre-release toggle in Settings to receive alpha builds
  • Package rename: com.byd.myappcom.byd.dashcast

Bug fixes

  • Removed broken watchdog (false positives on DiLink 3.0)
  • Fix OTA: removed FLAG_IMMUTABLE from PendingIntent → PackageInstaller result now received correctly
  • Fix blocking update dialog → dismissed automatically when install starts

Links

APK download

GitHub release page

  • Author

DashCast v0.2.1


Version 0.2.1

🛠️ Bug Fixes & Stability

  • Resources: Safely closed the BufferedReader stream in MirrorDaemon.java using a try-with-resources block to prevent memory leaks (orphaned streams from dumpsys).
  • Broadcaster Permissions: Applied the UnspecifiedImmutableFlag to UpdateChecker.java to resolve intent false positives during the silent OTA update process.
  • Manifest compliance: Explicitly added the android:exported="true" attribute to the main entry point, as required for Android 12+.
  • UI Memory Leaks: Added the @SuppressLint("StaticFieldLeak") annotation to FloatingRemoteButton.java to suppress warnings regarding its managed, bound lifecycle.

🧹 Optimizations

  • Orphaned Strings Cleanup: Removed unused string resources (such as menu_settings) across all localizations and dictionaries, reducing the APK build size.
  • Storage Management: Identified and cleared old, invalid PackageInstaller sessions that were consuming massive amounts of persistent cache storage (~800 MB from previous development builds).

Links

APK download

GitHub release page

  • Author

DashCast v0.6.8-beta


What's changed — v0.6.0 → v0.6.8

v0.6.1 — i18n cleanup

  • Purged 400+ unused translation keys across all 12 locales
  • Fixed hardcoded strings that were bypassing the translation system

v0.6.2 — Bug fixes & performance

  • Fix ghost-restore bug: PREF_CLUSTER_PKG/PREF_CLUSTER_NAME now correctly cleared on force-stop, preventing a killed app from reappearing on the cluster after Activity recreation
  • Dead code removal: collapsed duplicate isFavorite branches in AppListAdapter
  • i18n: extracted 5 new keys (OTA dialog buttons, overflow menu labels) to all 12 locales
  • Performance: reduced SharedPreferences open calls from 3 → 1 in ClusterService inset getters

v0.6.3 — Sanity QoL #2

  • Extracted all hardcoded French notification strings (FloatingLogButton, FloatingRemoteButton, SysInfoActivity) to R.string across all 12 locales
  • 5 new string keys added

v0.6.4 — Fix ANR risk

  • findRunningTaskId() (calls ActivityManager.getRunningTasks()) moved off the main thread to a dedicated background thread, eliminating an ANR risk in the resize button handler

v0.6.5 — UX improvements

  • Status dot (grey/yellow/green) alongside cluster status text
  • Real-time search bar to filter the installed app list
  • List/Grid toggle button relocated to the title header (always visible)
  • Cluster panel collapse strip (▼/▲) to go full-screen while keeping the mirror
  • Inset overlay preview: orange bands show resize margins during adjustment
  • One-shot first-launch tooltip: tap any app to send it to the cluster
  • 3 new string keys across all 12 locales

v0.6.6 — Robustness & polish

  • 30-second activation timeout: re-enables the button and shows a toast if the cluster never responds
  • Kill confirmation dialog: AlertDialog before force-stopping any app (avoids accidental taps)
  • Relaunch button (↺) in the control panel header: force-stops then relaunches the current app
  • OTA changelog now rendered as styled Markdown (## headings, bullet lists, bold)
  • 150 ms fade animation on list ↔ mirror transitions
  • Active row background tint (green/blue semi-transparent) on currently active items
  • Reconnect reminder: after manual re-activation, offers to relaunch the last cluster app
  • 8 new string keys across all 12 locales (179 keys total per locale)

v0.6.7 — Auto-apply saved insets

  • Per-app insets (wm overscan + resizeActiveTask) are automatically applied 500 ms after a successful cluster launch if custom values were previously saved for that package
  • Guard: cancelled if the active cluster app changes during the delay
  • No manual Apply tap required

v0.6.8 — Fix recents cleanup after kill

  • Fixed silent failure of task ID extraction on Android 10 / DiLink 3.0:
    • grep -o '#[0-9]\+' unsupported by Android busybox → replaced with portable sed BRE
    • Wrong ID extracted (recents index instead of TaskRecord ID) → new sed expression targets only the TaskRecord entry
    • am task rm does not exist on Android 10 → replaced with am task remove + app_process64/32-bit fallback via TaskRemover reflection helper
  • Added [DashCast-recents] log line in LogActivity for on-device verification of extracted task IDs

Links

APK download

GitHub release page

  • Author

DashCast v0.8.0


DashCast 0.8.0 — Pre-release

Stabilization release closing the alpha/0.7 development line. Eighteen consecutive sanity passes (P0 → P16) hardened lifecycle, concurrency and native resource handling across the entire application and the system-level MirrorDaemon (uid=2000). No new user-facing features; no API-level bump; target remains Android API 29 (DiLink 3).

Pre-release: not merged to main, remains on branch alpha/0.7.

Baseline: v0.7.9-alphav0.8.0 · 17 commits · versionCode 96 → 111


Highlights

  • Cluster mirror reliability: no more orphan SurfaceControl display tokens on daemon setup failure; Surface/SurfaceTexture lifecycle now mirrors the size-changed/destroyed contract end-to-end.
  • Concurrency hygiene: every long-lived background thread is now named for tombstone/anr triage; activity-scoped executors use named daemon ThreadFactory; volatile mDestroyed flag added to every Activity that posts back to UI from a worker.
  • No more ANR-risk paths in onCreate: boot-time cleanup of cluster display affinity is now off-loaded to a daemon thread.
  • Native resource leaks closed: 5 × Parcel.obtain, 3 × MotionEvent.obtain, 1 × InputStream and the setupMirror token are all released on every exit path (success, exception, lifecycle end).
  • Dead code removed: 4+ unused methods, 1 orphan BroadcastReceiver, the entire profile-feature placeholder UI, redundant Handler/StringBuilder instances.

Resource lifecycle & native handles

  • MirrorDaemon.setupMirror — when createDisplay succeeds but a later SurfaceControl.Transaction reflection step throws, the catch path now calls stopMirror() (which captures the token, nulls the static, then invokes destroyDisplay) instead of merely clearing the reference. The SurfaceFlinger display token is no longer leaked for the lifetime of the daemon process. (P16)
  • ClusterInputForwarder.injectTouchAtMultiMotionEvent and Parcel are recycled inside a finally on both daemon and non-daemon paths, even when InputManager.injectInputEvent() / Binder.transact() throws. (P9, P10)
  • ClusterInputForwarder.injectKeyParcel is recycled per iteration inside the daemon for-loop. (P9)
  • MirrorDaemon.readParcelable(MotionEvent) — recycled in finally. (P11)
  • ClusterMirrorManager.stopMirrorViaDaemonParcel recycled in finally, symmetric with startMirrorViaDaemon. (P9)
  • UpdateChecker.httpGetInputStream closed via try-with-resources. (P10)

Concurrency & lifecycle safety

  • MainActivity.onCreatecleanupDisplayAffinityAtBoot() (which performs N IActivityTaskManager binder calls via reflection) is now off-loaded to a daemon thread boot-cleanup-fallback using getApplicationContext() to avoid Activity retention. Eliminates a latent ANR risk when the persisted cluster-package set is non-trivial. (P15)
  • MainActivity.onDestroy — screenshot handler lambdas are now cancelled via removeCallbacksAndMessages(null); the screenshot-loop TOCTOU race against mScreenshotRunnable is closed. (P1, P12)
  • AdbLocalClient.sExecutor — now uses a named daemon ThreadFactory (adb-local-N). (P13)
  • MainActivity.loadAppsAsync — executor uses a named daemon ThreadFactory (load-apps). (P14)
  • MirrorDaemon.injectMotion (daemon path)MotionEvent is recycled in finally. (P11)
  • BootReceiver — wrapped in goAsync() + result.finish() to prevent the background reflection thread from being killed mid-execution by the system. (P1)
  • DiagActivity / SettingsActivity / SysInfoActivity — every one of the ~70 runOnUiThread posts is now guarded by a volatile mDestroyed flag, with a safeRunOnUiThread() helper for the diag screen. (P2, P4)
  • SysInfoActivity.publishUpdate — the worker-side mDestroyed check before runOnUiThread is now duplicated inside the main-thread Runnable (the original check was racy against onDestroy between queueing and dispatch). (P16)
  • FloatingRemoteButton.createOverlay — removed redundant local Handler, reuses mDimHandler. (P9)
  • ClusterMirrorManager.unlockHiddenApis — guarded by AtomicBoolean; the VMRuntime.setHiddenApiExemptions reflection chain is no longer re-run on every Activity recreation. (P2)

Correctness fixes

  • AdbLocalClient.startMirrorDaemonsafeOut() was converting empty strings to "(empty)", causing the post-launch ps -A verification to always succeed and masking real daemon-start failures. Replaced with direct getAllOutput().trim(). (P5)
  • MainActivity.moveTaskToDisplayZero — now returns true when the task is not found (the goal is "task absent from display 1", not "task moved"). (P1)
  • DiagActivity.runAdas — fixed argument inversion bug. (P8)
  • LocaleHelperapplyLocale() and saveLanguage() separated; attachBaseContext no longer writes to SharedPreferences on every Activity creation. (P1)

Robustness & defensive guards

  • AppCompat AlertDialog from a ServiceFloatingRemoteButton now uses WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY for its in-service dialogs (a non-Activity context cannot host a default AlertDialog window). (P7)
  • PendingIntent flagsFLAG_IMMUTABLE everywhere except UpdateChecker.installApk where FLAG_UPDATE_CURRENT (without IMMUTABLE) is required by PackageInstaller to inject extras (documented inline). (P7)
  • Foreground services — both ClusterService and FloatingRemoteButton call startForeground() within the 5-second window and provide setSmallIcon().

Performance

  • AppListAdapter — per-bind new ColorDrawable() allocations replaced with reusable ConstantState instances (CS_FG_ACTIVE, CS_FG_ON_MAIN). (P4)
  • LogActivity — adaptive polling switched from a fixed 500 ms cadence to 500 ms when entries change / 2000 ms when idle, then short-circuits the whole rebuild when neither the entry count nor the filter changed. (P1, P7)

Cleanup & dead code removal

  • AdbLocalClient — removed scanSniffer(), killSniffer(), checkTaskBoundsOnDisplay() (no callers), SNIFFER_KILL_CMD constant. (P2, P8)
  • AppLogger — removed shareTextAsFile() (no callers). (P3)
  • MainActivity — removed dead mShowMirrorReceiver BroadcastReceiver (FloatingRemoteButton uses startActivity, not sendBroadcast). (P3)
  • ClusterMirrorManager.startMirror — unused Context parameter removed. (P3)
  • ClusterInputForwarder — dead forwardTouchFinal() removed (only multi-pointer path is used). (P1)
  • AppShortcut — unused intent field and constructor parameter removed. (P1)
  • SysInfoActivity — redundant StringBuilder re-init before executor.submit() removed. (P1)
  • LogActivity — removed dead TAG constant. (P8)
  • Profile feature — placeholder UI removed entirely: spinner (MainActivity), section (SettingsActivity), layout blocks (activity_main.xml, activity_settings.xml), profile strings in all 12 locale files. (P2)

Code quality

  • Pref-key constants — every literal "byd_app_prefs", "recent_cluster_apps", "boot_auto_start_enabled", "show_category_filters", "visual_overscan_mode" etc. centralized as SettingsActivity.PREF_* constants and referenced from MainActivity, BootReceiver, FloatingRemoteButton. (P4, P5, P6)
  • AppListAdapter — magic colors 0x1A4CAF50 / 0x141565C0 extracted as COLOR_FG_ACTIVE / COLOR_FG_ON_MAIN named constants. (P2)
  • Misleading JavadocFloatingRemoteButton comment corrected (uses startActivity, not broadcast). (P3)

Compatibility

  • Target SDK / min SDK: unchanged (API 29 / DiLink 3, BYD Seal EU)
  • App signature: unchanged (platform-signed)
  • Permissions: unchanged
  • Cluster display id: still hardcoded to 1 (intentional, DiLink 3 constant)

Verification

  • 5-axis static audit completed (concurrence / resources / Android-API29 / security / correctness)
  • All 17 commits build green (./gradlew assembleDebug)
  • No API 30+ symbol introduced
  • No new dependency added

Known limitations carried over from 0.7.x

  • DiagActivity contains 12 anonymous new Thread(() -> ...) lambdas — kept as-is (internal diagnostic tool, short-lived tasks)
  • UpdateChecker.installApk PendingIntent deliberately mutable (PackageInstaller contract)
  • wm overscan ... -d 1 hardcoded for DiLink 3 cluster display

Artifacts

  • DashCast-v0.8.0-debug.apk — platform-signed debug build, installable on BYD Seal EU DiLink 3 head units.

Branch policy

This pre-release stays on alpha/0.7. No merge to main yet. Promotion to a stable 0.8.x line is gated on device-side validation.

Links

APK download

GitHub release page

  • Author

DashCast v0.8.1


What's changed since v0.8.0

feat: activity task manager polling (bb7bb0c)

MainActivity — replaced trust-only-our-bookkeeping approach with a 5-second poll of IActivityTaskManager.getTasks() while the activity is visible. Detects three edge cases the previous logic missed:

  • App crashed or OOM-killed → task gone → stale cluster/main state is cleared
  • App moved to display 0 externally → reclassified as main-display
  • Main-display app moved to cluster externally → promoted to cluster state

Uses the same displayId reflection pattern already proven in DiagActivity. Poll runs in onStart() / onStop() — zero background cost. Extracted clearClusterState() helper shared with the reconciliation path.

fix: eager cluster state clear before async kill/restore (f6a50cf)

MainActivity — prevents a race condition where the 5-second display-state poll could fire between moveTaskToDisplay(pkg, 0) and forceStopApp(), incorrectly setting mMainDisplayPkg.
Three paths fixed: doKillApp, restoreBydDashboard, originCluster. The captured package is still forwarded to restoreBydOnCluster / restoreOriginCluster for the force-stop before sendInfo(18).

feat: cluster surface diagnostics buttons (e1cd30b)

DiagActivity — new button row in the Fission Pipeline section to investigate the partial-surface projection issue (native Qt HUD overlays remaining visible above/below the VirtualDisplay):

Button Command Purpose
🔍 Measure surface DisplayManager + wm size/overscan -d X + dumpsys display Read actual VirtualDisplay dimensions
OSD Off sendInfo(1000, 20) Attempt to hide Qt native HUD overlays (speed / gear / battery)
OSD On sendInfo(1000, 19) Re-enable Qt native HUD overlays
Overscan 0,0,0,0 wm overscan 0,0,0,0 -d X Remove artificial margins on the Fission display

Branch: alpha/0.7 — not merged to main

Links

APK download

GitHub release page

  • Author

DashCast v0.8.2


What's changed since v0.8.1

fix: convert display-state poller to log-only mode (d863984)

Problem: The 5-second IActivityTaskManager.getTasks() poller introduced in v0.8.1 was incorrectly clearing cluster state (mirror view + app icons) every time it ran. When a user launched an app on the cluster, the mirror would appear but then get killed ~5 seconds later by reconcileDisplayState(), resetting the UI as if no app was running.

Root cause: getTasks() on DiLink 3.0 most likely does not return tasks running on the VirtualDisplay (Fission display). The poller interpreted the missing task as "app is dead" and called clearClusterState(), which:

  • Cleared mCurrentDashboardPkg / mCurrentDashboardApp
  • Removed shared prefs
  • Reset the adapter (no active icon, no red kill cross)
  • Called showAppList() — hiding the mirror view

Fix: reconcileDisplayState() now operates in log-only mode:

  • Still polls getTasks() every 5 seconds while the Activity is visible
  • Logs ALL tasks with baseActivity, topActivity, and displayId for each entry
  • Logs a summary showing whether the tracked cluster/main packages were found and on which display
  • Does NOT mutate any state — no clearClusterState(), no setMainPackage(), no shared prefs writes

Bonus improvements in the logger:

  • Also checks topActivity field as fallback when baseActivity is null
  • Logs when getTasks() returns null or empty (was silently returning before)
  • Full task dump enables post-test analysis of what the DiLink ROM actually reports

The reconciliation logic will be re-enabled once we confirm getTasks() behavior on the BYD head unit via these diagnostic logs.


Branch: alpha/0.7 — not merged to main

Links

APK download

GitHub release page

  • Author

DashCast v0.8.5


What's changed since v0.8.2

This release replaces the broken getTasks() state poller, removes a dangerous diagnostic button that could brick the cluster until reboot, and applies a full multi-axis audit pass (HIGH → LOW).


Display-state poller — getTasks()pidof (v0.8.3)

Problem: The log-only poller from v0.8.2 confirmed what the symptoms had suggested: IActivityTaskManager.getTasks() on DiLink 3.0 does not report tasks running on the cluster VirtualDisplay. Tasks on the main display were visible, tasks on display 1 (Fission) were never returned. This made the original mutating reconciliation logic — disabled in v0.8.2 — fundamentally unfixable: there is no way to ask the ATM for the cluster app's running state.

Fix: reconcileDisplayState() no longer uses getTasks() at all. It now runs a pidof <package> over the existing ADB localhost relay (uid 2000), which can read /proc despite hidepid=2. A single async call every 5 s, with the active package captured by reference; logs the PID when alive, clears cluster state and stops the mirror when the process is gone. The previous ~80 lines of IActivityTaskManager reflection were removed.

The state poll is started/stopped together with the screenshot loop, so it only runs while MainActivity is in the foreground.


Diagnostic safety — OSD On/Off buttons removed (v0.8.3)

Problem: Testing the experimental btnFissionOsdOff button in DiagActivity revealed it sends sendInfo(1000, 20), which disables the entire cluster display on the BYD Seal EU. Once triggered, the cluster cannot be re-enabled by the app: a hardware reboot via long-press of the volume button is required.

Fix: Both btnFissionOsdOff and btnFissionOsdOn and their handlers have been removed from DiagActivity, along with the four related strings (diag_btn_fission_osd_off/on, diag_fission_osd_off_sent/on_sent) and the corresponding Button widgets in activity_diag.xml. The section description was shortened to reflect the reduced set of controls.


Diagnostic correctness — overscan read fixed (v0.8.3)

Problem: DiagActivity attempted to read the current overscan via wm overscan -d 1 to confirm the value applied by the settings screen. On API 29, wm overscan is write-only: it accepts coordinates but returns an empty body. The diag screen always reported "overscan = 0,0,0,0", masking the real persisted value.

Fix: The verify step now runs dumpsys display | grep -i overscan instead, which exposes the SurfaceFlinger-side display overscan rect actually in effect.


Multi-axis audit (HIGH findings, v0.8.3)

MainActivity.reconcileDisplayState — use-after-destroy in the pidof callback.
The async pidof callback was posting a Runnable to runOnUiThread that re-checked package equality and then mutated UI fields. Between the bg-thread pidof completion and the Runnable running on the main thread, onDestroy could fire and null out the views referenced by clearClusterState()/stopClusterMirror(). Now the Runnable first checks isFinishing() || isDestroyed() and returns early.

ClusterService.findRunningTaskId — NPE on getRunningTasks().
am.getRunningTasks(50) is documented to return a non-null list, but on rare framework error paths it can return null (observed historically on third-party Android forks). The helper now null-checks the return value and returns -1 instead of dereferencing a null List.


Multi-axis audit (MEDIUM findings, v0.8.4)

ClusterServicemove-task-thread post-destroy guard.
The unnamed background thread spawned by moveTaskToDisplay() posts its result back to mMainHandler. Between onDestroy (which already drains the handler with removeCallbacksAndMessages(null)) and the bg thread posting the next Runnable, there is a small window where a posted callback may execute after the service is dead and reach mLauncher/mMirrorManager (both freed). A new volatile boolean mDestroyed is now set in onDestroy(), and every post-callback in the move-task-thread path (success branch + fallbackLaunch posted runnable) returns early when the flag is set.

MainActivity.onDestroymMirrorSurface not released.
The Surface wrapping the TextureView's SurfaceTexture was retained until GC after the Activity was destroyed. onDestroy now explicitly mMirrorSurface.release() and nulls the field, guarded by a try/catch (Exception ignored) for the case where the underlying texture was already torn down.

AdbLocalClient.sendInfo — incomplete shell argument escaping.
The s16 argument to service call AutoContainer 2 ... s16 "<infoStr>" previously escaped only ". If an infoStr ever contained $ (variable/arithmetic expansion) or ` (command substitution), the shell would interpret it before reaching AutoContainer. Escaping order is now: \\ first (avoids double-escaping the backslashes we are about to insert), then ", $, `. No current call site uses untrusted input here, but the function is on a public ADB-relay path and the fix is preventive.


Multi-axis audit (LOW findings, v0.8.5)

UpdateChecker.downloadToFile — HTTPS→HTTP redirect downgrade.
The manual 5-step redirect follower accepted any Location header, including http://.... A misconfigured GitHub CDN response or a hostile MITM could downgrade the APK download to plaintext. The platform-signed APK would still be rejected by PackageInstaller on signature mismatch, but the new check fails fast with "Insecure redirect target: ..." if the redirect target is not https:// (case-insensitive, Locale.ROOT).

MainActivity.startScreenshotLoop — Activity reference held by ADB executor queue.
The screenshot loop passed MainActivity.this as the Context argument to AdbLocalClient.captureClusterDisplay. While a capture was in flight, the static sExecutor's task queue held a strong reference to the Activity. After verifying that captureClusterDisplay only uses the context for connect(), getExternalCacheDir() and getCacheDir() — all valid on applicationContext — the call site now passes getApplicationContext(). The bitmap is still delivered on the Activity via runOnUiThread inside the BitmapCallback, so the UI path is unchanged.


Verified clean (no fix applied)

The audit also confirmed:

  • All Parcel.obtain() / MotionEvent.obtain() are recycled in finally
  • All InputStream / Reader / Writer / Socket use try-with-resources
  • Single ad-hoc ExecutorService (in SysInfoActivity) calls shutdown() after submit
  • registerReceiverunregisterReceiver paired in MainActivity
  • bindServiceunbindService paired in MainActivity.onDestroy
  • VirtualDisplay.release() in finally in DashCastDaemon
  • SurfaceFlinger display token released in MirrorDaemon.stopMirror()
  • All SharedPreferences writes use apply(), never commit()
  • PendingIntent.FLAG_IMMUTABLE used everywhere except UpdateChecker where mutability is documented to be required by PackageInstaller
  • TLS via system TrustManager (no custom verifier, no insecure Random)
  • No exported component without a system-protected action or a signature-level guard
  • No secrets/PII logged

Branch: alpha/0.7 — not merged to main
APK: debug build, signed with the BYD platform keystore
Compatibility: BYD Seal EU (DiLink 3.0, API 29), 1920×720 cluster VirtualDisplay owned by com.xdja.containerservice

Links

APK download

GitHub release page

  • Author

DashCast v0.8.6


DashCast v0.8.6 — Floating button reliability fix

Branch: alpha/0.7 — not merged to main
Build: 117 · platform-signed · API 29


Bug Fix

FloatingRemoteButton race condition (cc939a3)

The floating 📺 mirror button and the in-app "Show Mirror" button were intermittently invisible after launching an app on the cluster then hiding the mirror view. The behavior was non-deterministic ("random") because it depended on service startup timing.

Root cause: FloatingRemoteButton.show() was silently discarded when called before the overlay was ready. Two async gaps caused this:

  1. Service startup racestartService() is asynchronous; sInstance and mFloatView are null until onStartCommand() + createOverlay() complete. Any show() call during this window was lost.
  2. Overlay permission grant race — On first install, SYSTEM_ALERT_WINDOW is auto-granted via ADB shell (async). During the grant round-trip, mFloatView remains null and the overlay is created with View.GONE. Nobody re-called show() after creation succeeded.

Fix applied:

File Change
FloatingRemoteButton.java Added static volatile boolean sShouldBeVisible flag. show()/hide() now persist the desired state before attempting the view operation. After createOverlay() succeeds, the flag is read and the button is made visible immediately if an app is active.
MainActivity.java In onStart(), explicitly re-sync btnShowMirror visibility + call FloatingRemoteButton.show() whenever mCurrentDashboardApp != null and the service is already bound. Covers the stop/start lifecycle where updateDashboardStatus() is not re-invoked.

Impact: Both the floating overlay button and the top-bar "Show Mirror" button now appear reliably regardless of service/Activity lifecycle ordering.


Upgrade notes

  • Direct install over v0.8.5 — no data loss, no uninstall needed
  • versionCode incremented (116 → 117)

Links

APK download

GitHub release page

  • Author

DashCast v0.8.7


What's Changed since v0.8.6

UX Improvements

  • Reconnect popup disabled by default — The "relaunch last app on cluster reconnect" popup is now opt-in (off by default) to avoid intrusive prompts for new users. Enable it in Settings if you want the behavior.

Internationalization

  • Added settings_reconnect_popup translation in all 12 supported locales: French, English, German, Spanish, Italian, Arabic, Belarusian, Kazakh, Russian, Turkish, Ukrainian, Uzbek.
  • Corrected French wording for the reconnect popup setting.

Previous (v0.8.6)

  • Made the reconnect popup fully opt-in via a new checkbox in Settings (PREF_RECONNECT_POPUP).
  • Fixed a race condition in FloatingRemoteButton where show() was silently lost when called before the overlay was ready — added sShouldBeVisible static flag with deferred visibility after createOverlay() and onStart() re-sync.

Full Changelog: v0.8.6...v0.8.7

Links

APK download

GitHub release page

  • Author

DashCast v0.9.0


What's Changed since v0.8.7

This release starts a phased migration to Material Design 3, matching the target design in mockups/mockup_m3.html. The migration is split into 5 phases to detect any regression early. This release is Phase 1 only — foundation work. No layouts or business logic have been touched, so all existing features must keep working exactly as in v0.8.7.

Phase 1 — Foundation

Build & dependencies

  • Bumped compileSdkVersion from 29 → 33 to enable Material 3 components. targetSdkVersion and minSdkVersion are still 29, so runtime behavior on the BYD Seal (Android 10 / DiLink 3.0) is unchanged.
  • Added new dependency: com.google.android.material:material:1.9.0 (last version compatible with compileSdk 33).

Theme

  • Switched AppTheme parent from Theme.AppCompat.DayNight.DarkActionBarTheme.Material3.DayNight.NoActionBar.
  • The theme now exposes the full Material 3 color attribute set (colorPrimary, colorOnPrimary, colorPrimaryContainer, colorSurfaceVariant, colorOutline, etc.) so M3 components (MaterialButton, MaterialCardView, MaterialSwitch, …) introduced in later phases will automatically pick up the right palette.
  • All legacy theme attributes (colorPrimaryDark, colorAccent, textColorPrimary, textColorSecondary, windowBackground) are still mapped to the original color resources, so existing layouts that reference them keep rendering as before.

Color palette

  • Added the full Material 3 token set to values/colors.xml (light scheme) and values-night/colors.xml (dark scheme):
    • Primary / secondary / tertiary / error families
    • Surface container levels (lowest → highest)
    • Outline + outline variant
    • Success and warning custom tokens
  • Source color is #1565C0 (BYD blue), with the dark scheme tokens taken directly from mockup_m3.html so the rest of the migration can match the mockup pixel-for-pixel.
  • All previously existing color names (bg_main, text_primary, text_secondary, bg_card, bg_dialog, colorPrimary, etc.) are preserved untouched so existing layouts continue to render identically.

What is NOT in this release

  • No layout file has been modified.
  • No Java/Kotlin code has been modified.
  • No feature, setting, dialog, navigation flow or business logic has been changed.
  • No new Material 3 components have been introduced into screens yet — that work starts in Phase 2.

Upcoming phases

  • Phase 2 — Migrate common components (Button → MaterialButton, Switch/CheckBox → Material*, cards → MaterialCardView, FAB).
  • Phase 3 — Migrate Welcome + Settings screens (navigation rail, top bar pattern).
  • Phase 4 — Migrate Main + Diagnostics screens (most complex; keeps all cluster / ADB logic intact).
  • Phase 5 — Migrate System Info + Log screens and list-item layouts.

Full Changelog: v0.8.7...v0.9.0

Links

APK download

GitHub release page

  • Author

DashCast v0.9.94


Audit cleanup pass 2 (post-0.9.93)

Following the exhaustive read-only audit on the alpha/0.9 branch — incremental fixes, no regression.

Improvements

  • 🗑️ Dead code removed: orphan DashCastDaemon.java (~170 LoC, zero references, never invoked via app_process)
  • Perf: SysInfoActivity — 4× SimpleDateFormat allocations replaced by ThreadLocal cache (thread-safe)
  • 🎨 Layouts: explicit paddingStart on activity_main / activity_sysinfo (RtlSymmetry fix)
  • 🔒 EditText: importantForAutofill=no on log_filter + et_search_apps (debug fields)

Metrics

  • Lint: 75 → 71 issues
  • Java code: −170 LoC
  • Build: SUCCESSFUL, javac 0 warnings
  • versionCode: 152 → 153

Intentionally not fixed

  • 28 UseAppTint, 24 SmallSp, 8 ButtonStyle, 7 NestedWeights, 2 TooDeep/TooMany — UI design items, require on-device validation

Branch: alpha/0.9 (no merge to main)
APK: DashCast-v0.9.94-debug.apk (BYD platform signature)

Links

APK download

GitHub release page

  • Author

DashCast v1.0.0


DashCast v1.0.0 — Official Stable Release

DashCast graduates from alpha and beta to its first official stable release.

What's new

  • Brand-new interface: the entire UI has been redesigned from scratch in Material 3, with a consistent navigation rail, modern bottom sheets, dialogs and tiles across all screens (Main, SysInfo, Diag, Log, Settings).
  • Hardened projection lifecycle: the start/stop flow of cluster projection has been reinforced to prevent the edge-case bugs reported during beta (black card, stale surface, restart loops). Teardown and re-creation of the mirror surface are now synchronous and deterministic.
  • Stability & cleanup: full lint + javac audit pass, dead code removed, thread-safety hardening (SimpleDateFormat in SysInfoActivity now uses ThreadLocal), 12 locales kept in sync.

Community

A dedicated Telegram channel has been created for support, feedback and discussion:

https://t.me/+QPk_dmTVaNkxMjFk

Documentation

The documentation has been fully rewritten to match v1.0.0 and is available at:

https://kiroha.github.io/byd-dashcast/

Install

Download DashCast-v1.0.0-debug.apk below and install on a BYD DiLink 3.0 head unit (Android 10, platform-signed environment).

Links

APK download

GitHub release page

  • Author

DashCast v1.0.1


Bug fix release — cluster screen-size selector in Settings is now clickable

What was broken

Reported by a user shortly after the v1.0.0 stable release: in the Settings screen, the three cluster-type options (8.8", 12.3", 10.25") inside the Cluster type card could not be selected. Tapping a row did nothing, the radio button never changed, and the previously-saved size could not be modified.

Root cause

The three radio rows in activity_settings.xml were structured as follows:

RadioGroup (rg_cluster_type)
├── LinearLayout (no id, no click handler, no selectable background)
│   └── MaterialRadioButton (rb_88, clickable=false, focusable=false)
│   └── TextView (title + description)
├── LinearLayout ...   (same pattern for 12.3")
└── LinearLayout ...   (same pattern for 10.25")

Three independent layers all blocked clicks:

  1. The MaterialRadioButton widgets had android:clickable="false" and android:focusable="false", so they could not receive touch events directly.
  2. The wrapping LinearLayout rows had no id, no setOnClickListener, and no android:background="?attr/selectableItemBackground", so the rows themselves were not interactive either.
  3. The radio buttons are nested inside another LinearLayout (one level below the RadioGroup), and Android's RadioGroup only auto-checks direct child RadioButton widgets via its internal mChildOnCheckedChangeListener. Nested radio buttons never receive the auto-toggle wiring.

Net effect: the entire selection UI was inert.

Fix

  • app/src/main/res/layout/activity_settings.xml — each of the three rows now has its own id (row_cluster_88, row_cluster_123, row_cluster_1025), android:clickable="true", android:focusable="true" and android:background="?attr/selectableItemBackground" for a proper Material ripple.
  • app/src/main/java/com/byd/dashcast/SettingsActivity.java — in wireListeners(), each row delegates its click to rgClusterType.check(R.id.rb_XX). That call triggers the existing OnCheckedChangeListener which persists the corresponding sendInfo command (29 = 8.8", 30 = 12.3" Seal EU, 31 = 10.25") into the cluster_screen_size_cmd key in SharedPreferences.

Downstream wiring confirmed end-to-end

The fix preserves the existing flow from Settings to the runtime cluster restoration:

  1. User taps a row in Settings → rgClusterType.check(rb_id)onCheckedChangedprefs.putInt("cluster_screen_size_cmd", 29|30|31).
  2. User long-presses the Stop projection button in MainActivityoriginCluster() reads getClusterTypeCmd() → calls AdbLocalClient.restoreOriginCluster(ctx, screenSizeCmd, pkg, cb) → the native BYD instrument cluster is restored with the user-selected screen size.

The Settings preference is only consumed by the Restore Origin Cluster path. The projection activation sequence (ClusterManager.activateClusterDisplay) still hard-codes CMD_SCREEN_SIZE_SEAL_EU = 30, which is the only screen-size command that produces a correct projection on the BYD Seal EU 12.3" cluster — this is intentional and unchanged.

What did not change

  • No business logic was modified.
  • No new permission, manifest entry, dependency, resource id, theme attribute, color token or layout outside the three rows.
  • The full Material 3 design system, the 12-locale i18n pass, the OTA flow, the long-press "Restore Origin Cluster" bottom-sheet and every other v1.0.0 feature behave exactly as before.

Version

Before After
versionCode 154 155
versionName 1.0.0 1.0.1

Install

Sideload DashCast-v1.0.1-debug.apk over v1.0.0 — the package id and signing key are unchanged so Android will perform an in-place upgrade with no data loss.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.0-beta1


DashCast v1.1.0-beta1 — Beta Engine scaffolding + Diagnostics rebuild + file-based log sharing

First pre-release on the new beta/1.1.0 channel. Introduces an opt-in "Beta Engine" pipeline that is wired everywhere it needs to be, but stays inert at runtime by default so the v1.0.1 stable code path remains the source of truth.

⚠️ Pre-release for testers. Do not merge to main. Install only if you understand what a beta channel implies. Both Beta toggles are OFF by default — with both OFF, the app behaves exactly like v1.0.1.


What's new

1. New com.byd.dashcast.beta package (5 classes, ~654 LoC)

Class LoC Role
BetaConfig 67 SharedPreferences-backed (byd_app_prefs, same store as SettingsActivity) feature flags: beta_engine_enabled and beta_proxy_enabled (both default false).
BetaSystemContext 119 Process-wide singleton snapshot of the beta state. Exposes isEngineEnabled() / isProxyEnabled() so hot paths never re-read prefs.
BetaProxyClient 94 Stub client that will later route ADB calls through an alternate transport. Currently returns NOT_IMPLEMENTED for every probe.
BetaEngineGateway 65 Single entry point used by future call-sites. Exposes a safeCall(legacy, beta) helper that auto-falls back to the legacy implementation on any exception so the user can keep using the app even if the beta path fails.
BetaTestRunner 309 15-test self-check matrix — B1–B6 real beta-engine probes, A1–A6 SKIPPED placeholders for the ADB-bridge work, X1–X3 mixed cross-checks. Each test returns a Result with status (PASS / FAIL / SKIPPED / WARN), latency in ms and a short message.

Granularity = option B (two independent toggles).
Activation semantics = next launch (toggles read once at process start by BetaSystemContext.refresh()).
Fallback = automatic on any thrown exception in a beta code path.

2. SettingsActivity (+36 LoC) — new Beta card

  • Always-visible "Beta (expérimental)" card containing two MaterialSwitch rows: sw_beta_engine and sw_beta_proxy.
  • Amber warning banner (bg_beta_warning drawable + bg_beta_pill for the "BETA" pill) explicitly stating the beta nature.
  • Restart-required AlertDialog triggered whenever either switch is toggled.
  • The card is always visible — no hidden gesture, no developer mode — because the v1.1.0 line is a beta channel and the user opted in by installing this APK.
  • Toggle changes persist via BetaConfig.set*() but have no runtime ADB effect yet. Flipping them OFF restores the exact v1.0.1 behaviour. Flipping them ON currently only enables the diagnostics readouts and the auto-fallback path in BetaEngineGateway.

3. DiagActivity rebuilt as a 6-tab TabLayout (~257 LoC)

Tab order:

  1. Beta Engine (default selected) — runs the 15-test BetaTestRunner and renders a colour-coded card list via item_beta_test.xml + include_diag_beta_engine.xml, with a Run all button and a Send (.log) button.
  2. Système
  3. Cluster
  4. ADB
  5. Logs
  6. Apps

The five legacy tabs share a single include_diag_coming_soon.xml placeholder. The existing v1.0.1 diag screens remain reachable via the overflow menu and will be migrated tab by tab in subsequent beta drops.

4. AppLogger refactor — file-based sharing everywhere

  • New writeFile(ctx, prefix, content) — generic timestamped .log writer under getExternalFilesDir("logs") with a files-path fallback.
  • New shareFile(ctx, File, subject, chooserTitle) — generic Intent.ACTION_SEND with EXTRA_STREAM via the existing FileProvider authority (${packageName}.fileprovider; xml/file_paths.xml already covers both external-files-path and files-path) and FLAG_GRANT_READ_URI_PERMISSION.
  • Refactored share(ctx) is now file-first (attaches byd_log_<timestamp>.log) with a plain-text fallback only when the file write fails.
  • New shareWithReport(ctx, reportText) writes a combined byd_report_<ts>.log (header line + diagnostic report + full log buffer) and attaches it via shareFile.

Net effect — every "Send logs" / "Send report" action in the app now ships a real .log file instead of a raw text body. Fixes the previous ~64 KB silent truncation on Android share-sheets and lets users attach the file to GitHub issues / e-mail / WhatsApp without copy-paste loss.

5. New layouts and drawables

  • layout/include_diag_beta_engine.xml
  • layout/include_diag_coming_soon.xml
  • layout/item_beta_test.xml
  • drawable/bg_beta_pill.xml
  • drawable/bg_beta_warning.xml
  • layout/activity_diag.xml (TabLayout root)
  • layout/activity_settings.xml (Beta card insertion)

6. Strings — full i18n parity (12 locales)

Added a block of 28 new keys ending with diag_beta_run_all / diag_beta_copy_report / diag_beta_report_copied.

  • values/ (FR default) + values-en/ populated by hand.
  • 10 remaining locales (ar, be, de, es, it, kk, ru, tr, uk, uz) populated via scripts/inject_beta_strings.py — idempotent thanks to a diag_beta_run_all marker check; re-runs are no-ops.

12 locales total, full parity with the existing UI — non-FR/EN users will see the "Send (.log)" button and the Beta Engine tab in their language.

7. Build

Before (v1.0.1) After (v1.1.0-beta1)
versionCode 155 156
versionName "1.0.1" "1.1.0-beta1"

Compatibility & regression

  • No business logic from v1.0.1 is modified. Cluster projection, overscan, OTA, restore-origin-cluster, app shortcuts, category filters, reconnect popup and visual mode all behave exactly as in v1.0.1 with both beta switches OFF.
  • Installs cleanly over v1.0.1 (versionCode strictly increased).
  • All existing widget IDs preserved.
  • Same minSdk 29 / targetSdk 29, same theme (Material 3 DayNight), same FileProvider authority.

Known limitations (intentional in this drop)

  • A1–A6 tests show SKIPPED — they cover the ADB-bridge work scheduled for the next beta drop.
  • BetaProxyClient is a stub — every probe returns NOT_IMPLEMENTED.
  • The 5 legacy Diag tabs (Système / Cluster / ADB / Logs / Apps) render a "Bientôt disponible" placeholder. Use the overflow menu to reach the v1.0.1 diag screens until they are migrated.

These will be filled in subsequent 1.1.0-betaN drops on the same channel.


Install

Sideload the attached DashCast-v1.1.0-beta1-debug.apk over your existing v1.0.1 install (no uninstall required).

Branchbeta/1.1.0
Tagv1.1.0-beta1
Channel — pre-release (will not be picked by the in-app OTA unless the "Pre-release" toggle is enabled in Settings → À propos).

Links

APK download

GitHub release page

  • Author

DashCast v1.1.1


DashCast v1.1.1 (pre-release)

Channel: Beta · versionCode: 159 · Branch: beta/1.1.0

Successor to v1.1.0-beta1. Stable channel users are not affected.

✨ New features

DiLink 5 dedicated diagnostic tab

A 7th tab is added to the Diag screen, completely independent from the Beta Engine. It contains 9 tests targeting the DiLink 5.0 / Android 12 platform reported by Alexander:

ID Test
D1 Platform identity — ro.product.name, Build.MODEL/BRAND/FINGERPRINT, API level.
D2 Displays inventory — every Display exposed by DisplayManager (id, state, flags, size).
D3 Service binder probe — detects renamed AutoContainer services (e.g. crosscontrol, xdja).
D4 wm overscan availability — confirms AOSP removed the command in Android 11+.
D5 am --display flag probe — checks whether am start --display N is supported.
D6 Granted permissions inventory — what the current APK actually has at runtime.
D7 ADB local TCP reachability (127.0.0.1:5555).
D8 Guarded cluster launcham force-stopam start --display N → wait → am start --display 0 (retract) → wait → am force-stop. The retract step prevents OS display affinity from sticking the app on the cluster forever. Target package is picked from a dropdown listing every launchable app.
D9 BYD packages inventory — versionCode + signature digest for com.byd.* packages.

The tab also displays a live Mode pill (AUTO = DiLink 5 / FORCED on/off) and a counter row (✓ PASS ✗ FAIL ! WARN ⊘ SKIP).

Platform auto-detection + manual override

New Platform singleton initialised once in DashCastApp.onCreate() :

  • Reads ro.product.name via SystemProperties reflection.
  • Detects DiLink 5 from substrings (dilink5, dilink_5) or API ≥ 31 on a BYD device.
  • Stores user override in byd_app_prefs (AUTO / FORCE_ON / FORCE_OFF).

A new switch in Settings → Beta Engine named Mode DiLink 5 (experimental) lets you override the auto value. Initial position mirrors the auto-detect; toggling it triggers the same restart required dialog as the other Beta switches.

When the platform is auto-detected as DiLink 5, the Diag screen now opens on the DiLink 5 tab by default; otherwise it opens on Beta Engine as before.

Settings — checkbox renamed to Beta Channel

The pre-release toggle (which opens DashCast to alpha/beta/pre-release tags) is now labelled simply Beta Channel in all 12 locales — same channel concept as Chrome/Edge stable/beta/dev channels.

🐛 Fixes

Beta Engine — B1/B2/B3 (ActivityThread.systemMain) no longer crash

Real-device testing of 1.1.0-beta1 revealed that B1/B2/B3 always failed with:

RuntimeException: Can't create handler inside thread Thread[beta-test-runner,5,main]
that has not called Looper.prepare()
  at android.app.ActivityThread.<init>
  at android.app.ActivityThread.systemMain

ActivityThread.systemMain() instantiates an internal Handler in its constructor, which requires a Looper on the current thread. BetaTestRunner runs on a single-threaded background Executor with no Looper, so the reflection call exploded before it could return the ActivityThread instance.

Fix mirrors the BydAgent reference implementation :

if (Looper.myLooper() == null) Looper.prepare();

Added in BetaSystemContext.get() (protects every future caller, not just tests) and in BetaTestRunner.testB1(). B1/B2/B3 should now turn ✓ on any device where ActivityThread is not stripped. B4/B5/B6 still skip with not on classpath until the BYD SDK is bundled — by design.

📦 Install

DashCast-v1.1.1-debug.apk — installs cleanly over v1.1.0-beta1 (versionCode bumped 156 → 159).

⚠️ Phase status

  • Beta Engine — Component A (proxy daemon) : A1–A6 remain SKIPPED. The daemon backend is Phase 2 and not yet implemented.
  • Beta Engine — Component B (system context reflection) : B1–B3 active (fixed in this release), B4–B6 require the BYD SDK on the classpath.
  • DiLink 5 tab : Phase 1 — pure observation. Acting on the results (auto-switching am --display vs the legacy SDK, picking the renamed binder name, etc.) lands in a later beta.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.2


DashCast v1.1.2 — Beta Engine Phase 2 (re-issued)

Pre-release · beta channel · do not merge to main

This release ships Component A of the Beta Engine — the proxy daemon
that was scaffolded but stubbed in v1.1.0 / v1.1.1. The eight diagnostic
tests A1–A6, X1, X2 that previously reported ⊘ proxy daemon not implemented yet (Phase-2) are now real.

Re-issue notice. The first v1.1.2 build (dc4b565) shipped with the
daemon listening on @openbyd_proxy, a name borrowed from another
project. To avoid collisions with anyone running OpenBYD on the same
tablet, the abstract socket and setArgV0 process name have both
been renamed to dashcast_proxy. The release tag was re-cut on top
of the amended commit (692aad4), versionCode was bumped 160 → 161
so devices that already installed the previous build can upgrade
in-place.

What's new

Proxy daemon (dashcast_proxy)

A new long-lived helper process is now spawned from the app the first
time the Beta Engine tab is exercised (or the Use proxy daemon toggle
is flipped on). It is brought up by running app_process through the
existing local-ADB bridge, which means it inherits the shell UID
(2000) — the same identity used by every legacy adb shell call —
without requiring root.

The daemon:

  • listens on the Linux abstract namespace socket @dashcast_proxy,
  • renames itself to dashcast_proxy via setArgV0 so it is easy to
    spot in ps -A,
  • serves a tiny line-based protocol — PING, WHOAMI,
    EXEC <command>, QUIT — with replies framed as OK …,
    DAT <line>, or ERR <message>,
  • survives the app being killed or restarted (re-attached via the
    abstract socket on the next connect()),
  • respawns automatically if killed (kill -9) the next time the app
    tries to connect.

Client (BetaProxyClient)

The previous Phase-1 no-op was replaced by a full implementation:

  • connect(ctx) — fast path tries the socket; cold start issues a
    bootstrap script through AdbLocalClient
    (CLASSPATH=$(pm path com.byd.dashcast) nohup app_process / com.byd.dashcast.beta.proxy.ProxyDaemonMain &)
    and retries the socket connect 8× with a 300 ms backoff.
  • WHOAMI handshake caches the daemon's UID, PID and protocol version.
  • ping(), runShell(cmd), getCallerUid(), getDaemonPid(),
    getProtocolVersion() are now real RPCs.
  • Thread-safe through a single static lock; concurrent callers queue
    rather than corrupting the wire format.

Diagnostic tests now active

The Beta Engine tab of Diagnostics now exercises all 15 tests
end-to-end:

Test What it checks
A1 Proxy daemon alive connect + ps -A | grep dashcast_proxy
A2 Binder reachable isConnected() == true after connect
A3 Round-trip ping < 500 ms 5 pings; fail if worst > 500 ms
A4 Daemon UID = 2000 handshake UID equals shell
A5 Persistence across disconnect reconnect yields the same PID
A6 Restart resilience kill -9, reconnect re-bootstraps, new PID
X1 Latency comparative proxy ping vs. legacy id -u (≥ 2× faster)
X2 Permission delta both paths land on shell UID 2000

B1–B6 and X3 remain as in v1.1.1; the Looper fix shipped in
v1.1.0-beta2.1 is preserved.

Behaviour for users on the legacy path

If the Use proxy daemon toggle is left off and the Beta Engine
tab is never opened, behaviour is identical to v1.1.1 — the daemon is
never spawned, no extra processes appear, no extra sockets are bound.

If the toggle is on, BetaEngineGateway.safeCall still falls back to
AdbLocalClient on any failure, so the worst case is "behaves like
legacy".

First-run notes

  • The first invocation triggers the standard local-ADB pairing
    popup — Allow USB debugging? Always allow from this computer.
    Tick the box; the RSA key is persisted and the popup never reappears
    unless cleared from the developer settings.
  • Once paired, subsequent app launches re-attach to the existing
    daemon without going through ADB again (sub-10 ms typical).

Compatibility

  • Same minSdk 29 / targetSdk 29 / API surface as v1.1.1.
  • DiLink 3.0 confirmed (test reports from BYD Atto3 v25.02).
  • DiLink 5 still needs owner testing — the proxy daemon should behave
    identically since it relies on standard Android primitives
    (LocalServerSocket, app_process, ps, id).

Upgrade

  • Already on v1.1.x with Beta Channel enabled → the OTA picks
    v1.1.2 up automatically at next app launch.
  • Anyone who installed the original v1.1.2 (vc 160) gets the
    re-issued build (vc 161) through the same OTA flow.
  • Otherwise:
    1. Settings → tick Beta Channel.
    2. Re-open the app; OTA updater fetches v1.1.2 and installs.

Files

  • DashCast-v1.1.2-debug.apk — debug build signed with the BYD
    platform key, ~14.2 MB, versionCode 161.

Changelog (commit)

feat(beta): Phase 2 — Component A proxy daemon (v1.1.2) — see
692aad4.

Reminder

  • Branch main stays on v1.0.1 — no merge in this cycle.
  • This is a beta intended for owners who already opted into the
    Beta Channel. Please share diagnostic reports through the in-app
    Copy report button or by attaching byd_report_*.log.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.3


DashCast 1.1.3 (beta)

Hotfix release for the Phase 2 / Component A proxy daemon shipped in
1.1.2.

On real DiLink 3.0 hardware (BYD Atto3, API 29) the daemon spawned in
1.1.2 reported a successful bootstrap (OK <apk-path>) but never bound
its @dashcast_proxy abstract socket
, so every Diag test in the
A family failed with connect() returned false and there was no way
to see why (stderr was silently dropped to /dev/null).

This release fixes the bootstrap recipe and adds first-class
observability so future cold-start failures are diagnosable directly
from the in-app Diag report.


What changed

Bootstrap aligned with the proven MirrorDaemon recipe

The 1.1.2 bootstrap was:

( CLASSPATH="$APK" nohup app_process / com.byd.dashcast.beta.proxy.ProxyDaemonMain \
    >/dev/null 2>&1 </dev/null & )

It now matches the recipe that has been working for MirrorDaemon on
the same device for months:

setsid sh -c 'CLASSPATH="$APK" /system/bin/app_process64 \
    -Xnoimage-dex2oat /system/bin \
    --nice-name=dashcast_proxy \
    com.byd.dashcast.beta.proxy.ProxyDaemonMain \
    </dev/null >/data/local/tmp/dashcast_proxy.log 2>&1' &

The differences that matter on BYD Android 10 images:

Aspect 1.1.2 (broken) 1.1.3 (fixed)
Detachment ( ... & ) subshell + nohup setsid sh -c '...' & (detached from ADB session group, survives SIGHUP)
Binary bare app_process (symlink not always resolvable on BYD images) absolute /system/bin/app_process64 (explicit 64-bit)
AOT no flag (potential dex2oat crash at startup) -Xnoimage-dex2oat
Parent dir argument / /system/bin
argv[0] only the daemon's own setArgV0 reflection --nice-name=dashcast_proxy set by the runtime before main()
Output >/dev/null 2>&1 (blackhole) >/data/local/tmp/dashcast_proxy.log 2>&1

Daemon log tail surfaced in Diag failures

  • New BetaProxyClient.readDaemonLogTail(Context) reads the last 20
    lines of /data/local/tmp/dashcast_proxy.log via the existing
    legacy-ADB client.
  • When A1 — connect() fails, the tail is attached to the result's
    detail field. It is visible in the in-app test report and in the
    exported byd_report_*.log, so the actual error
    (ClassNotFoundException, dex2oat failure, SELinux denial, etc.)
    no longer requires a host-side adb shell.

No protocol or socket-name changes

  • Abstract socket name is still @dashcast_proxy.
  • Protocol version is still 1 (PING / WHOAMI / EXEC / QUIT).
  • Diag catalog (A1–A6, B1–B6, X1–X3) is unchanged.

Upgrade notes

  • If you installed 1.1.2 and saw A1A6 all marked FAIL with
    connect() returned false, upgrade to 1.1.3 and rerun the Diag suite.
    The A family should now turn green.
  • If A1 still fails, the new detail payload contains the daemon's
    stderr tail; please share it in an issue.

Files

  • DashCast-v1.1.3-debug.apk — signed debug APK
    (versionCode 162, versionName 1.1.3).

Compatibility

  • Tested target: BYD Atto3 / DiLink 3.0 / Android 10 (API 29) /
    firmware v25.02.
  • DiLink 5.x testing still pending external validation.

Branch: beta/1.1.0main remains pinned to the last stable
release v1.0.1 and is not merged in this prerelease.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.4


DashCast 1.1.4 (beta)

Hotfix release for a shell-quoting bug introduced in
1.1.3
that prevented the Phase 2 / Component A proxy daemon from ever
starting on real hardware.

The 1.1.3 release switched the daemon bootstrap to the proven
MirrorDaemon recipe (/system/bin/app_process64 -Xnoimage-dex2oat /system/bin --nice-name=... <class>) and added a daemon log tail in
test failures. The new log tail revealed a single line — Aborted
which exposed the real bug: a shell-quoting mistake that left the
JVM with an empty CLASSPATH.


Root cause

The 1.1.3 bootstrap command was:

APK=$(pm path com.byd.dashcast | head -n1 | cut -d: -f2-)
setsid sh -c 'CLASSPATH="$APK" /system/bin/app_process64 \
    -Xnoimage-dex2oat /system/bin \
    --nice-name=dashcast_proxy \
    com.byd.dashcast.beta.proxy.ProxyDaemonMain \
    </dev/null >/data/local/tmp/dashcast_proxy.log 2>&1' &

The sh -c '...' argument is wrapped in single quotes, so the
outer shell never expands $APK. The inner shell receives the literal
string $APK, treats it as a variable defined in its own context
(where it is empty), and ends up exporting CLASSPATH= (empty).
app_process64 then cannot load ProxyDaemonMain, ART aborts during
class resolution, and the only trace left in the log is the bash
runner's terse Aborted message — no stack trace, no
ClassNotFoundException, because the JVM died before it could write
anything useful to stderr.

MirrorDaemon doesn't hit this bug because its recipe concatenates
the APK path into the Java string (`"... CLASSPATH=" + apkPath

  • " ..."`) before the single quotes, so the quoting is irrelevant.

Fix

The bootstrap now uses double quotes for sh -c "...", allowing
the outer shell to expand $APK before handing the command to
setsid:

APK=$(pm path com.byd.dashcast | head -n1 | cut -d: -f2-)
LOG=/data/local/tmp/dashcast_proxy.log
# Instrumentation prologue (overwrites previous log on each bootstrap)
{ echo "[boot] $(date) apk=$APK"
  echo "[boot] id=$(id)"
  echo "[boot] getenforce=$(getenforce 2>/dev/null)"
  ls -la "$APK" 2>&1
  echo "[boot] exec app_process64..."
} > "$LOG" 2>&1
setsid sh -c "CLASSPATH='$APK' exec /system/bin/app_process64 \
    -Xnoimage-dex2oat /system/bin \
    --nice-name=dashcast_proxy \
    com.byd.dashcast.beta.proxy.ProxyDaemonMain \
    </dev/null >>'$LOG' 2>&1" &
echo OK $APK

Additional small improvements bundled in the same change:

  • Instrumentation prologue — every bootstrap now writes id,
    getenforce, and ls -la <APK> to the daemon log before exec'ing
    app_process64. If a future cold-start fails for an unrelated
    reason (SELinux, missing APK, wrong uid…), the context required to
    diagnose it is already on disk by the time the daemon log tail is
    read.
  • exec inside the inner shell — the wrapper sh is replaced
    by app_process64, so ps no longer shows an extra dangling
    sh -c parent for each daemon instance.
  • APK path single-quoted inside the inner command — defensive
    quoting in case a future package install ends up at a path with
    unusual characters.

What 1.1.4 does NOT change

  • No protocol change — wire version is still 1
    (PING / WHOAMI / EXEC / QUIT).
  • No socket-name change — still @dashcast_proxy.
  • No Diag catalog change — A1–A6, B1–B6, X1–X3 unchanged.
  • No SDK / classpath work — B4/B5/B6 still skip because they
    look for com.byd.bydautosdk.*; the real classes live under
    android.hardware.bydauto.* and will be addressed in a separate
    beta.

How to verify

  1. Install the new APK over 1.1.3 (versionCode bumps 162 → 163, so the
    in-app updater accepts it automatically).
  2. Open Diag → run Beta Engine.
  3. Expected: A1A6 turn ✓ (or at worst show a real Java stack
    trace in the detail field instead of Aborted).
  4. If A1 still fails, the new instrumentation prologue means the
    daemon log tail now starts with [boot] ... id=uid=2000(shell) ...
    etc., which is enough to pinpoint the remaining issue.

Files

  • DashCast-v1.1.4-debug.apk — signed debug APK
    (versionCode 163, versionName 1.1.4).

Compatibility

  • Tested target: BYD Atto3 / DiLink 3.0 / Android 10 (API 29) /
    firmware v25.02.
  • DiLink 5.x testing still pending external validation.

Branch: beta/1.1.0main remains pinned to the last stable
release v1.0.1 and is not merged in this prerelease.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.5


DashCast 1.1.5 (beta)

Hotfix release for the Phase 2 / Component A proxy daemon: the
@dashcast_proxy abstract socket is now reclaimed before every
bootstrap, and the minimum supported Android version is lowered to
Android 9 / API 28 so DiLink 2 owners can install and test the
beta.


Why a third hotfix in the 1.1.x line

  • 1.1.2 introduced the Phase 2 daemon but used a broken bootstrap
    (bare app_process) that crashed silently.
  • 1.1.3 switched to the proven MirrorDaemon recipe but kept a
    shell-quoting bug that left the JVM with an empty CLASSPATH.
  • 1.1.4 fixed the quoting; the daemon finally started, but the
    log tail then revealed a new failure mode:
    java.io.IOException: Address already in use at
    new LocalServerSocket("dashcast_proxy").

The cause of that last failure was a stale daemon from a previous
session
. The bootstrap uses setsid, so the daemon survives both
the ADB session it was launched from and the host app shutdown.
Once a daemon successfully binds the abstract socket, every
subsequent bootstrap loses the race to bind and SIGABRTs — and the
host app, which is now talking to a daemon it cannot identify
(potentially a different protocol version), cannot recover on its
own.

Fix — reclaim before bootstrap

The bootstrap script now starts with:

STALE=$(ps -A 2>/dev/null | grep '[d]ashcast_proxy' | awk '{print $2}')
if [ -n "$STALE" ]; then kill -9 $STALE 2>/dev/null; sleep 0.3; fi

Notes:

  • The [d] trick prevents grep from matching its own process line
    while still matching daemons whose --nice-name was set to
    dashcast_proxy.
  • awk '{print $2}' extracts the PID column from Android's ps -A
    output regardless of the active toybox version.
  • kill -9 $STALE is unconditional. The daemon is stateless and the
    client always re-bootstraps as needed, so killing is always safe.
  • A 300 ms sleep gives the kernel time to release the abstract socket
    before the new daemon's bind() call.
  • The killed PID (or none) is now logged to
    /data/local/tmp/dashcast_proxy.log as
    [boot] stale_killed=<pid>, so future failures will reveal whether
    the reclaim step actually fired.

This change is also fully compatible with A6 — Restart resilience:
that test kills the daemon then expects the next connect() to
re-bootstrap a fresh one. After the manual kill, the bootstrap's
STALE is empty and the new daemon starts on the first try.

minSdkVersion 29 → 28 — DiLink 2 owners can now install

The Android 9 (API 28) device fleet is excluded from the previous
builds because of minSdkVersion 29. Lowering it to 28 lets DiLink 2
owners install the APK and at least exercise the Diag suite. No
1.1.x runtime path uses an API-29-only symbol on the main flow.

If a 1.1.x feature does fail on API 28 (likely candidates: the cluster
mirror or the BYD SDK reflection), the in-app log will surface a
clean stack trace — which is exactly the kind of feedback this
release is meant to collect.

What 1.1.5 does NOT change

  • No protocol change — wire version is still 1
    (PING / WHOAMI / EXEC / QUIT).
  • No socket-name change — still @dashcast_proxy.
  • No Diag catalog change — A1A6, B1B6, X1X3 unchanged.
  • No SDK / classpath work — B4/B5/B6 still skip because they
    look for com.byd.bydautosdk.*; the real classes live under
    android.hardware.bydauto.* and will be addressed in a separate
    beta.

How to verify

  1. Install the new APK over 1.1.4 (versionCode bumps 163164).
  2. Open Diag → Beta Engine.
  3. Expected: A1A6 turn ✓. The first bootstrap will report
    [boot] stale_killed=<pid> in the daemon log if it found and
    killed a leftover from 1.1.4; subsequent bootstraps within the
    same session should report [boot] stale_killed=none.
  4. If A1 still fails, the daemon log tail now contains both the
    stale_killed field and the full Java stack trace, which is
    enough to pinpoint the remaining cause without an adb shell.

Files

  • DashCast-v1.1.5-debug.apk — signed debug APK
    (versionCode 164, versionName 1.1.5, minSdkVersion 28).

Compatibility

  • Tested target: BYD Atto3 / DiLink 3.0 / Android 10 (API 29) /
    firmware v25.02.
  • DiLink 2 (Android 9 / API 28) — install path now open, runtime
    behaviour pending field reports.
  • DiLink 5.x testing still pending external validation.

Branch: beta/1.1.0main remains pinned to the last stable
release v1.0.1 and is not merged in this prerelease.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.6


DashCast v1.1.6 — Beta Engine Phase 2: SELinux Fix (Binder + Broadcast IPC)

Pre-release on beta/1.1.0. Not merged into main (which stays at v1.0.1).

TL;DR

The Phase 2 proxy daemon (Component A) now works on real DiLink 3.0 devices. The abstract Unix socket used in v1.1.2–v1.1.5 was silently blocked by SELinux on Android 10+; this release replaces it with a Binder published through a one-shot broadcast — the exact same trick OpenBYD uses, and the same pattern as our own MirrorDaemon.

What broke in v1.1.5

The v1.1.5 device test (Atto3, DiLink 3.0, API 29) showed the daemon spawning correctly and printing:

[dashcast_proxy] listening on @dashcast_proxy uid=2000 pid=18924 ver=1

but every BetaProxyClient.connect() call from the app failed. Root cause: Android 10+ SELinux policy denies untrusted_app_29shell for unix_stream_socket connectto, so the app could never reach the daemon's abstract socket. The [d]ashcast_proxy regex + kill -9 stale-cleanup added in v1.1.5 worked perfectly (visible as stale_killed=<pid> in the daemon log) — the daemon side was fine, the wire was not.

What changes in v1.1.6

New IPC architecture

Concern v1.1.2 – v1.1.5 v1.1.6
Wire transport Abstract LocalServerSocket @dashcast_proxy IBinder published through a one-shot broadcast
App → daemon discovery LocalSocket.connect(ABSTRACT) (denied by SELinux) Dynamic BroadcastReceiver listening for com.byd.dashcast.beta.PROXY_CONNECTED
Request/response Line-based text protocol (PING, WHOAMI, EXEC …) Binder.transact(code, Parcel, Parcel) with three transaction codes
PROTOCOL_VERSION "1" "2"

Bootstrap (unchanged — works as of v1.1.5)

The setsid sh -c "CLASSPATH='$APK' exec /system/bin/app_process64 -Xnoimage-dex2oat /system/bin --nice-name=dashcast_proxy …" recipe, the stale-kill guard, and the diagnostic header in /data/local/tmp/dashcast_proxy.log are all preserved exactly as in v1.1.5. The process name still shows as dashcast_proxy in ps -A, so the existing kill heuristic (ps -A | grep '[d]ashcast_proxy') keeps working.

Daemon (ProxyDaemonMain.java)

  • Looper.prepareMainLooper() + Looper.loop() — keeps the binder pool alive.
  • ActivityThread.systemMain().getSystemContext() via reflection — gives the daemon a usable Context for AMS calls.
  • New ProxyBinder extends Binder with DESCRIPTOR = "com.byd.dashcast.beta.proxy.IProxyDaemon" and a hand-rolled onTransact:
    • TXN_PING (1)long epoch_ms
    • TXN_WHOAMI (2)int uid, int pid, String ver
    • TXN_EXEC (3)int exit, String combinedOutput
  • Broadcast com.byd.dashcast.beta.PROXY_CONNECTED targeted at com.byd.dashcast with the binder wrapped in a new BinderParcelable extra, plus FLAG_INCLUDE_STOPPED_PACKAGES.

Client (BetaProxyClient.java)

  • Dynamic BroadcastReceiver registered once per process on the application context.
  • CountDownLatch armed before the bootstrap script is executed, so a fast broadcast cannot be missed.
  • binder.pingBinder() used for liveness; binder.transact() for all RPCs.
  • Public API surface preserved (connect, isConnected, disconnect, ping, runShell, getCallerUid, getDaemonPid, getProtocolVersion, readDaemonLogTail, BetaProxyException) → BetaTestRunner and its 15 tests need no change.
  • readDaemonLogTail() still queries tail -n 20 /data/local/tmp/dashcast_proxy.log via legacy ADB, so cold-start failures remain self-diagnostic.

New file

  • app/src/main/java/com/byd/dashcast/beta/proxy/BinderParcelable.java — minimal Parcelable wrapping a single IBinder, because Intent.putExtra cannot carry a raw IBinder value.

Why this works (and the old approach didn't)

Mechanism Blocked by SELinux for untrusted_app_29shell?
Abstract LocalServerSocket connectto (v1.1.2–v1.1.5) Yes — denied by stock Android 10+ sepolicy
Broadcast Intent dispatched by ActivityManagerService No — capability-based, allowed
IBinder.transact() on a binder ref previously received No — capability-based, allowed

Same reasoning as OpenBYD's EntryPoint.main()systemContext.sendBroadcast(intent) → app receives ProxyBinderParcelable → calls ICarControl.runShellCommand(). Our MirrorDaemon (already shipping in this app) uses an equivalent published-binder pattern.

Compatibility

  • minSdkVersion 28 (lowered in v1.1.5, kept here) — DiLink 2.0 owners can still install and inspect.
  • targetSdkVersion 29 unchanged. The 2-arg Context.registerReceiver overload is used; on targetSdk ≥ 33 the call would need Context.RECEIVER_EXPORTED (documented inline as a future-bump TODO).
  • Beta channel only — main remains on v1.0.1.

Files changed

File Change
app/build.gradle versionCode 164 → 165, versionName "1.1.5" → "1.1.6"
app/src/main/java/com/byd/dashcast/beta/proxy/ProxyDaemonMain.java Full rewrite — Binder + broadcast pattern, no more LocalServerSocket
app/src/main/java/com/byd/dashcast/beta/BetaProxyClient.java Full rewrite — dynamic receiver + binder.transact, public API preserved
app/src/main/java/com/byd/dashcast/beta/proxy/BinderParcelable.java NewParcelable wrapper for the daemon's IBinder

Known limitations (still on the roadmap)

  • Phase 4 BYD SDK probes (B4 / B5 / B6) still skip on stock DiLink — the real package is android.hardware.bydauto.*, not com.byd.bydautosdk.*. Tracked for v1.2.x.
  • Daemon does not auto-restart if it crashes; the client always re-bootstraps on the next connect() (handled by the existing stale-kill guard).

Install

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

Then run the Beta panel → "Run all tests". On a working device you should see the four A-tests (A1A4) flip from ✗ to ✓ versus v1.1.5.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.7


DashCast v1.1.7 — Beta Engine Phase 2: Broadcast Race + Dead-Binder Fix

Pre-release on beta/1.1.0. Not merged into main (which stays at v1.0.1).

TL;DR

v1.1.6's switch to Binder + broadcast IPC successfully bypassed the SELinux block (A2 ✓ isConnected=true, A3 ✓ ping avg 0 ms on the device). However the device test exposed three subtle bugs in the dispatch logic that caused most A-tests to be reported as ✗ and made the head unit freeze repeatedly during the A5/A6 resilience tests. v1.1.7 fixes all three.

What the v1.1.6 device run revealed

21:21:11.194  bootstrapping daemon
21:21:19.976  no PROXY_CONNECTED broadcast within 8000ms   ← timeout
21:21:19.977  binder received from daemon (alive=true)      ← 1 ms too late
…
21:21:37.827  binder received from daemon (alive=false)     ← stale broadcast
21:21:46.492  binder received from daemon (alive=false)
21:21:55.143  binder received from daemon (alive=false)
21:21:55.165  binder received from daemon (alive=true)

Test results: Total: 15 ✓ 6 ✗ 4 ⊘ 5 (A1, A4, A5, A6 ✗).

Bug 1 — Latch raced the broadcast by 1 ms

ActivityThread.systemMain() in the daemon brings up the entire framework runtime inside app_process and takes 5–8 s cold on a DiLink 3.0 SoC. v1.1.6's 8 s wait was finishing essentially at the same instant the broadcast arrived. A1 was reported as ✗ even though the binder showed up 1 ms after the timeout, which is why A2 / A3 immediately succeeded on the very next call.

Fix: BROADCAST_WAIT_MS raised from 8 s → 15 s. After timeout, connect() re-checks sBinder.pingBinder() instead of treating the timeout as a hard failure.

Bug 2 — handshake() never ran on timeout (A4 ✗ uid=-1)

connect() returned false on the latch timeout and skipped handshake(), so sDaemonUid / sDaemonPid / sDaemonVer stayed at -1/-1/null. A4's "Daemon UID = 2000" check therefore failed permanently, even though A2 / A3 happily talked to the live binder.

Fix: connect() always runs handshake() when sBinder is live but the WHOAMI cache is stale (sDaemonUid < 0). The cache is also invalidated on every accepted binder swap so a fresh handshake is queued.

Bug 3 — Receiver overwrote a live binder with a dead one (A5, A6 ✗ + tablet freezes)

Each call to connect() killed the existing daemon and spawned a new one. The AMS broadcast queue still held the previous daemon's PROXY_CONNECTED intent, which got dispatched after the kill — so the receiver overwrote the live binder reference with one whose pingBinder() returned false. Worse, every spurious re-bootstrap fired up another full ActivityThread.systemMain() inside app_process, and the cumulative load is what froze the head unit during the resilience tests.

Fix:

  • Drop incoming broadcasts whose binder.pingBinder() is already false.
  • Drop duplicate broadcasts when a live binder is already cached.
  • Fast-path connect() returns immediately when sBinder.pingBinder() is true — no kill, no respawn, no ActivityThread.systemMain() thrash.

Files changed

File Change
app/src/main/java/com/byd/dashcast/beta/BetaProxyClient.java Receiver hardening, fast-path connect(), late-broadcast recovery, lazy handshake
app/build.gradle versionCode 165 → 166, versionName "1.1.6" → "1.1.7"

Expected v1.1.7 behaviour on the same device

Test v1.1.5 v1.1.6 v1.1.7 (expected)
A1 Proxy daemon alive ✗ (1 ms race)
A2 Binder reachable
A3 Round-trip ping
A4 Daemon UID = 2000 ✗ (uid=-1)
A5 Persistence after destroy ✗ (stale binder)
A6 Restart resilience ✗ (4× respawn freeze)
Head-unit freezes several none expected

B1–B3 (system-context probes) and X3 (cluster restore) already passed in v1.1.6 and are unaffected.

B4 / B5 / B6 still skip — the real BYD SDK package is android.hardware.bydauto.*, not com.byd.bydautosdk.* (tracked for v1.2.x).

Install

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

Run the Beta panel → "Run all tests". On a working device you should see all six A-tests flip to ✓ and zero head-unit freezes.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.8


DashCast v1.1.8 — Beta Engine Phase 2: Monitor/Latch Deadlock Fix

Pre-release on beta/1.1.0. Not merged into main (which stays at v1.0.1).

TL;DR

The persistent A1 / A5 / A6 failures and the head-unit freezes observed in v1.1.6 and v1.1.7 had a single root cause: a textbook synchronized + CountDownLatch.await() deadlock in BetaProxyClient.connect(). v1.1.8 restructures the method so the lock is released across the wait. No other changes.

The smoking gun

v1.1.7 device log showed the same pattern on every single connect:

21:29:21.206  bootstrap result OK
21:29:36.207  no live binder after 15000ms (latch=timed-out)   ← exactly 15 s
21:29:36.207  live binder received from daemon                  ← 0 ms later

The broadcast wasn't arriving late — it was arriving promptly, but onReceive() was blocked behind the lock that connect() was holding while waiting on the latch.

Why

public static boolean connect(Context ctx) {
    synchronized (LOCK) {                          // ← acquire
        ...
        latch.await(BROADCAST_WAIT_MS, ...);       // ← does NOT release LOCK
        ...
    }
}

@Override public void onReceive(Context c, Intent intent) {
    ...
    synchronized (LOCK) {                          // ← blocked for 15 s
        sBinder = bp.binder;
        latch.countDown();                         // ← never reached in time
    }
}

CountDownLatch.await() is not Object.wait() — it does not drop the monitor of the enclosing synchronized. So the receiver thread was queued behind the connect thread for the entire timeout window, the latch was never counted down in time, and connect() returned false even though the daemon had broadcast its binder seconds earlier.

The tablet freezes follow directly from the same deadlock: A1 + A5 + A6 each held LOCK for 15 s, and any other component (test runner, AdbLocalClient callbacks, the broadcast dispatcher itself) that needed it queued up. Combined with the heavy ActivityThread.systemMain() work happening in the daemon process, the head unit appeared to lock up.

The fix

connect() is split into three short synchronized sections:

  1. Acquire — check fast-path, register receiver, arm the latch, drop LOCK.
  2. Bootstrap + await — LOCK-free. The receiver can now run.
  3. Reacquire — late-arrival check, handshake, return.

That's it. ~30 lines of code shuffled, no behavioural change to anything else.

Expected behaviour on the same device

Test v1.1.6 v1.1.7 v1.1.8 (expected)
A1 Proxy daemon alive ✗ (deadlock)
A2 Binder reachable
A3 Round-trip ping
A4 Daemon UID = 2000
A5 Persistence after destroy ✗ (deadlock)
A6 Restart resilience ✗ (deadlock)
Head-unit freezes several A1 + A5 (≥ 2) none expected

B-tests, X-tests, and the SDK-related skips are unchanged.

Files changed

File Change
app/src/main/java/com/byd/dashcast/beta/BetaProxyClient.java connect() split into three synchronized sections — LOCK released across the latch wait
app/build.gradle versionCode 166 → 167, versionName "1.1.7" → "1.1.8"

Install

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

Run the Beta panel → "Run all tests". Expected: all six A-tests ✓, no freezes during A5 / A6.

Links

APK download

GitHub release page

  • Author

DashCast v1.1.9-build169


DashCast v1.1.9 build 169 — Revert REQUEST_BINDER

Pre-release on beta/1.1.0. Replaces the deleted build 168 (which was a regression).

What happened with build 168

Build 168 introduced an ACTION_REQUEST_BINDER broadcast so the app could ask an existing daemon to re-emit its binder (intended fix for A5). On-device run regressed to 10 ✓ / 2 ✗ with much slower test latencies (3 s per connect instead of <100 ms). Symptoms from byd_report_20260521_214713.log:

  • no existing daemon answered REQUEST_BINDER within 1500ms on every connect() — the daemon never responds
  • Daemon respawned on every single test (PIDs 19216 → 19268 → 19361 → 19413 → 19464 → 19513)
  • A1 EXEC ps failed: transact: null — daemon answers pingBinder() (kernel-level) but transact() (Looper-level) fails. Looper is dead.

Root cause hypothesis: systemContext.registerReceiver() called from a non-app process (app_process64 running as shell uid 2000 with no AMS ProcessRecord) is silently killing the daemon shortly after the initial broadcast. The daemon survives just long enough to broadcast and answer one pingBinder(), then dies. transact() failures cascade through runShellsBinder = null → next test must re-bootstrap → stale-kill of the just-spawned daemon → respawn. Each extra bootstrap also adds 1.5 s REQUEST_BINDER wait + ~1 s spawn.

Fix

Revert REQUEST_BINDER entirely. Back to the v1.1.8 connect() flow (fast-path + bootstrap), which was:

[✓] A1  Proxy daemon alive
[✓] A2  Binder reachable
[✓] A3  Round-trip ping < 500 ms
[✓] A4  Daemon UID = 2000
[✗] A5  Persistence across Activity destroy   ← known limitation
[✓] A6  Restart resilience
[✓] X1, X2, X3
+ B1/B2/B3 (Phase 2 system context)
- B4/B5/B6 (skip, Phase 4)
= 11 ✓ / 1 ✗ / 3 ⊘ — no freezes

A5 is now accepted as a known limitation of the one-shot ACTION_PROXY_CONNECTED architecture. A proper fix (likely ServiceManager.addService if SELinux ever allows, or another recovery channel that doesn't trigger AMS-side process validation) is deferred.

Versioning policy change

Per user request:

  • versionName is pinned at "1.1.9" for all subsequent DiLink 3.0 builds
  • Only versionCode increments (this build: 169)
  • "1.2.0" is reserved for the DiLink 5 port (Dilink5 Dashboard pipeline already reverse-engineered)

Files changed vs build 168

File Change
ProxyDaemonMain.java Remove ACTION_REQUEST_BINDER constant + dynamic BroadcastReceiver registration
BetaProxyClient.java Remove tryRequestExistingBinder(), REQUEST_BINDER_WAIT_MS, restore v1.1.8 connect()
app/build.gradle versionCode 168 → 169 (versionName unchanged)

Install

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

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.