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

Top Posters In This Topic

Posted Images

Featured Replies

  • Author

DashCast v1.2.10


DashCast v1.2.10 — build 201 (pre-release)

Target branch: beta/1.2.0-dilink5
Platforms affected: DiLink 5 (testeur) — banner visibility / fix is DL5-only. DL3 / DL2 unaffected.
Protocol: PROTOCOL_VERSION=2 unchanged
APK: DashCast-v1.2.10-debug.apk (14.5 MB, versionCode 201, versionName 1.2.10)

Increment versionCode 200 → 201 so the BYD AUTO OTA installer recognises this as an update over v1.2.9 build 200 — no uninstall required.


Why this release

Field report (May 23, 2026) on DL5 testeur, immediately after the v1.2.9 install, surfaced two issues with the IME accessibility onboarding banner introduced in v1.2.8:

  1. The banner was hardcoded in French — non-FR system locales (German, English, Italian, Spanish, etc.) fell back to the French strings.
  2. The "Enable" button appeared to do nothing — the user tapped it multiple times, no visible reaction.

Both root causes identified and fixed in this build.


Root cause #1 — Missing translations

The 8 IME-related string keys introduced in v1.2.8 (a11y_ime_watcher_label, _summary, _description, ime_banner_title, _body, _btn_enable, _btn_later, _btn_dismiss, _toast_enabled) only existed in res/values/strings.xml (the default = French). None of the 11 sibling locale folders (values-en, values-de, values-es, values-it, values-ar, values-be, values-kk, values-ru, values-tr, values-uk, values-uz) had these keys, so Android's resource resolver fell back to the default French strings on any non-FR system locale.

Fix — 10 strings × 11 locales = 110 new translations

Hand-crafted, locale-appropriate translations of every IME banner string for all 11 supported locales:

Locale Status
values-en/ (English) +10 strings
values-de/ (German) +10 strings
values-es/ (Spanish) +10 strings
values-it/ (Italian) +10 strings
values-ru/ (Russian) +10 strings
values-uk/ (Ukrainian) +10 strings
values-be/ (Belarusian) +10 strings
values-tr/ (Turkish) +10 strings
values-ar/ (Arabic) +10 strings
values-kk/ (Kazakh) +10 strings
values-uz/ (Uzbek) +10 strings
values/ (FR default) +1 new key (ime_banner_toast_cannot_open_settings)

Every translation uses locale-appropriate keyboard / cluster terminology (e.g. "Cluster auto-keyboard" in English, "Cluster-Auto-Tastatur" in German, "Auto-pernetaqta" in Kazakh, etc.).


Root cause #2 — "Enable" button silently failing

The v1.2.8 button handler fired a single intent:

Intent i = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
// caught Throwable, logged only — no toast, no fallback, no visible feedback

On BYD AUTO ROM (heavily customised Android 10 on DL3 and Android 12 on DL5), the standard AOSP Settings.ACTION_ACCESSIBILITY_SETTINGS action is not advertised by BYD's custom Settings.apk. The intent silently failed to resolve, the Throwable was caught, only an AppLogger line was written — and the user saw nothing happen. Tapping multiple times produced no effect because each tap hit the same dead path.

Fix — one-click activation via daemon shell + three-tier Settings fallback

The "Enable" button now bypasses the Settings UI entirely and uses the proxy daemon's shell (uid=2000, same uid as adb shell) to write directly to Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES via the settings binary.

Primary path — enableImeA11yServiceOneClick():

COMP='com.byd.dashcast/com.byd.dashcast.ime.ClusterImeWatcherService'
CURRENT=$(settings get secure enabled_accessibility_services 2>/dev/null)
if [ "$CURRENT" = "null" ] || [ -z "$CURRENT" ]; then
    NEW="$COMP"
elif echo "$CURRENT" | grep -q "$COMP"; then
    NEW="$CURRENT"
else
    NEW="$CURRENT:$COMP"
fi
settings put secure enabled_accessibility_services "$NEW"
settings put secure accessibility_enabled 1
echo OUT=$(settings get secure enabled_accessibility_services)

The shell is posted through ShellGateway.execShellWithResult which already routes through either the typed proxy daemon or the legacy AdbLocalClient fallback chain. On onSuccess, the OS state is re-read via ClusterImeWatcherService.isEnabled(this) (we trust the OS, not the shell echo). If true:

  • Banner card hidden.
  • Toast: localised ime_banner_toast_enabled (e.g. "Auto-keyboard active ✓" in English, "Auto-Tastatur aktiv ✓" in German).
  • AppLogger info line for forensics.

Fallback path — openA11ySettingsFallback():

If the shell route fails (no daemon, no legacy fallback, ROM blocks settings put, or the shell succeeds but the OS still reports the service as disabled — rare SELinux edge case), three intents are tried in sequence, each wrapped in its own Throwable catch:

  1. Standard AOSPSettings.ACTION_ACCESSIBILITY_SETTINGS (works on stock AOSP).
  2. Direct ComponentNamecom.android.settings/.Settings$AccessibilitySettingsActivity (BYD ROM may not advertise the standard action but still ship the activity).
  3. Generic SettingsSettings.ACTION_SETTINGS (last resort — user manually navigates to Accessibility).

If all three fail, a localised toast ime_banner_toast_cannot_open_settings is shown ("Cannot open Settings — please enable the accessibility service manually") so the user finally gets visible feedback instead of the silent failure of v1.2.8 / v1.2.9.

Button is always re-enabled on every code path so a failure never leaves it stuck disabled (the user can tap again).


Behavioural contract

  • DL3 (Seal EU production): banner is never shown (Platform.isDiLink5(this) == false), the new shell helper and fallback chain are never invoked → zero behavioural change.
  • DL5 (testeur):
    • Tap "Enable/Activate/Aktivieren/Attiva/etc." → if proxy daemon is reachable → service is enabled in <500 ms with no Settings trip required → banner disappears → localised toast confirms.
    • If daemon down → falls back transparently to the Settings UI chain → user manually toggles the service.
    • If Settings UI also blocked → localised toast surfaces the failure (no more silent dead button).
  • DL2: completely unaffected (no cluster, banner is DL5-gated).

Touched files

File Change
app/build.gradle versionCode 200 → 201, versionName "1.2.9" → "1.2.10"
app/src/main/java/com/byd/dashcast/MainActivity.java Enable click handler now calls one-click helper (~5 LoC). New enableImeA11yServiceOneClick(card, btnEnable) (~70 LoC). New openA11ySettingsFallback() three-tier fallback (~40 LoC).
app/src/main/res/values/strings.xml +1 new key ime_banner_toast_cannot_open_settings
app/src/main/res/values-en/strings.xml +10 IME banner strings
app/src/main/res/values-de/strings.xml +10 IME banner strings
app/src/main/res/values-es/strings.xml +10 IME banner strings
app/src/main/res/values-it/strings.xml +10 IME banner strings
app/src/main/res/values-ru/strings.xml +10 IME banner strings
app/src/main/res/values-uk/strings.xml +10 IME banner strings
app/src/main/res/values-be/strings.xml +10 IME banner strings
app/src/main/res/values-tr/strings.xml +10 IME banner strings
app/src/main/res/values-ar/strings.xml +10 IME banner strings
app/src/main/res/values-kk/strings.xml +10 IME banner strings
app/src/main/res/values-uz/strings.xml +10 IME banner strings
CHANGELOG.md v1.2.10 build 201 entry

Net stats: 15 files changed, 245 insertions, 10 deletions.

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


Validation plan (suggested in-car checks)

(a) DL5 testeur — translation

  1. Install v1.2.10 over v1.2.9 (no uninstall required).
  2. System Settings → Language → switch to English (or German / Spanish / Italian / etc.).
  3. Open DashCast → banner appears at the top.
  4. Expected: banner title, body, and buttons are in the selected language (no French fallback).

(b) DL5 testeur — one-click activation (primary path)

  1. Tap the "Enable" button (or its localised label).
  2. Expected within ~500 ms:
    • Localised toast "Auto-keyboard active ✓" appears.
    • Banner disappears.
  3. Go to Settings → Accessibility → confirm "Cluster auto-keyboard (DashCast)" is checked.

(c) DL5 testeur — end-to-end IME bridge

  1. Start cluster projection → launch any cluster app with a text field (e.g. Yandex Maps search).
  2. Tap the text field on the cluster.
  3. Expected: KeyboardBridgeActivity auto-launches on the head unit ~100 ms later → typing forwards to the cluster.

(d) Fallback path

  1. If the proxy daemon happens to be unavailable (rare, post-bootstrap), tap "Enable".
  2. Expected: Settings → Accessibility opens directly (no error visible to user, transparent fallback).
  3. User manually flips the toggle, returns to DashCast → toast confirms + banner disappears.

(e) DL3 / DL2 regression check

  1. Install v1.2.10 on DL3 Seal EU / DL2.
  2. Expected: banner never shown. All other features behave identically to v1.2.9 (the new code path is never invoked).

Install

Direct sideload (BYD file manager → tap APK):

DashCast-v1.2.10-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: 20b8a49
  • Tag: v1.2.10
  • Previous: v1.2.9 (build 200, May 23, 2026 — DL3/DL5 Kill race + Stop Projection ghost resurrection fixes)
  • Next planned: v1.3.0 — full ProxyDaemon migration (see docs/PLAN_v1.3.x_FULL_PROXY.md)

Links

APK download

GitHub release page

  • Author

DashCast v1.2.11


DashCast v1.2.11 — build 202 (pre-release)

Target branch: beta/1.2.0-dilink5
Platforms affected: DiLink 5 (testeur) only — fix is gated by Platform.isDiLink5. DL3 / DL2 unaffected.
Protocol: PROTOCOL_VERSION=2 unchanged
APK: DashCast-v1.2.11-debug.apk (14 MB, versionCode 202, versionName 1.2.11)

versionCode 201 → 202 so the BYD AUTO OTA installer recognises this as an update over v1.2.10 — no uninstall required.


Why this release

Field report (May 23, 2026) on DL5 testeur with v1.2.10 installed, system locale = Russian, Yandex Maps active on the cluster. Two coupled symptoms reported from a single screenshot:

  1. A French opaque banner appeared at the top of the head-unit screen the moment the keyboard bridge was triggered (manually or via the a11y service). Title Clavier déporté, hint Tapez ici : chaque caractère est envoyé au champ actif du cluster., three buttons Entrée / Retour / Fermer. The banner was in French regardless of the system locale and intrusively covered part of the live cluster mirror.
  2. Characters typed on the system IME never reached the Yandex search field on the cluster. The keyboard popped up, the user typed, but nothing appeared on the cluster side.

The user explicitly hypothesised this was the same root cause as the May 22 touch-injection bug (v1.2.7 fix: MotionEvent.setDisplayId(2)). They were right — same pattern, applied to KeyEvent this time.


Root cause #1 — Visible chrome (always FR, intrusive)

KeyboardBridgeActivity was authored in v1.2.8 with a full LinearLayout: title TextView + hint TextView + visible EditText + 3-button row, semi-opaque dark background 0xCC202020, MATCH_PARENT × WRAP_CONTENT pinned at the top of display 0. The visible chrome was purely cosmetic — the system IME pops up regardless of EditText visibility — but the strings were never translated to the 11 supported locales and the overlay was always present, covering part of the cluster mirror.

Fix — invisible 1×1 px bridge window

KeyboardBridgeActivity.onCreate rewritten:

  • Removed the entire LinearLayout + title + hint + 3-button row.
  • Window is now 1×1 px pinned BOTTOM | END, transparent background (android.R.color.transparent), dim 0f.
  • Sole content view is the focused EditText, stripped of every visible property:
    • setBackground(null)
    • transparent text & hint colours (0x00000000)
    • setCursorVisible(false)
    • zero padding
  • The IME still pops up automatically thanks to SOFT_INPUT_STATE_ALWAYS_VISIBLE + mImm.showSoftInput posted on the EditText.
  • The 1 px footprint means the rest of display 0 (DashCast UI, cluster mirror tile) stays interactive — touches outside that 1 px fall through to the windows below.
  • Enter forwarding preserved via the existing OnEditorActionListener (IME's Done/Search/Send button → KEYCODE_ENTER injected to cluster).
  • Back key naturally closes the activity (Android default onBackPressed).
  • Unused imports (View, Button, LinearLayout) and the unused dp(int) helper removed.

The translated keyboard_bridge_* strings remain in values/strings.xml for the activity label in the manifest (android:label=@string/keyboard_bridge_title, but excludeFromRecents=true so the label is never user-visible).


Root cause #2 — Keys never reach the cluster

MirrorDaemon.injectKey(KeyEvent) (uid 2000 daemon) called InputManager.injectInputEvent(kev, ASYNC) without first calling KeyEvent.setDisplayId(sClusterDisplayId). Without the displayId override, the system InputDispatcher routes the key to the globally focused window — and on multi-display Android 12, focus is per-display: our own KeyboardBridgeActivity on display 0 owns the head-unit focus and swallows every injected key. The cluster app on display 2 never sees a single KeyEvent.

This is the exact same root cause as the May 22 touch-injection bug (MotionEvent wasn't routed to display 2). v1.2.7 fixed touch by adding MotionEvent.setDisplayId(clusterId). The symmetric KeyEvent.setDisplayId(clusterId) call was simply never added when the keyboard bridge shipped in v1.2.8.

Fix — KeyEvent.setDisplayId(clusterId) in both injection paths

daemon/MirrorDaemon.java (primary daemon-uid path):

  • New static sSetDisplayIdKey Method cached via KeyEvent.class.getDeclaredMethod("setDisplayId", int.class) alongside the existing sSetDisplayId for MotionEvent in initInputManager().
  • injectKey(KeyEvent) now calls sSetDisplayIdKey.invoke(kev, sClusterDisplayId) (with inner try/catch so a reflection failure still attempts the bare inject) BEFORE InputManager.injectInputEvent.
  • First-event log line upgraded: injectKey FIRST OK displayId=N setDisplayIdAvail=true/false keyCode=K action=A (mirror of the existing motion log) so the next field capture exposes whether the override took effect.

dashboard/ClusterInputForwarder.java (direct app-uid fallback, used when daemon Binder is null):

  • New instance mSetDisplayIdKeyMethod cached in the constructor.
  • Applied in the direct-fallback path of injectKeyEvent(KeyEvent) immediately before InputManager.injectInputEvent.

Both paths now route keys to the cluster display id (typically 2 on DL5 — same as touch).


Behavioural contract

  • DL3 Seal EU (production): the ⌨ button is hidden (Platform.isDiLink5(this) ? VISIBLE : GONE), the a11y service short-circuits on !isDiLink5, the bridge is never launched → zero observable change.
  • DL5 testeur:
    • Tap the ⌨ button or focus an EditText on the cluster (with the a11y service enabled) → no visible overlay, IME pops up at the bottom of display 0.
    • Each typed character → KeyCharacterMapKeyEvent[]setDisplayId(clusterId) → daemon injectKey → cluster app's focused EditText receives the keys natively (no app-level integration needed).
    • IME's Done/Search/Send button → KEYCODE_ENTER injected to cluster (e.g. triggers Yandex Maps search).
  • DL2: completely unaffected (no cluster, ⌨ button hidden, a11y service inert).

Touched files

File Change
app/build.gradle versionCode 201 → 202, versionName "1.2.10" → "1.2.11"
app/src/main/java/com/byd/dashcast/daemon/MirrorDaemon.java +sSetDisplayIdKey field, +KeyEvent.class.getDeclaredMethod("setDisplayId") lookup in initInputManager, +setDisplayIdKey.invoke(kev, sClusterDisplayId) in injectKey with inner try/catch, +upgraded first-event log (~15 LoC net).
app/src/main/java/com/byd/dashcast/dashboard/ClusterInputForwarder.java +mSetDisplayIdKeyMethod field, cached in ctor, applied in direct-fallback path (~10 LoC).
app/src/main/java/com/byd/dashcast/KeyboardBridgeActivity.java Entire onCreate body replaced (~70 LoC → ~50 LoC). All visible chrome stripped, 1×1 px transparent window, invisible focused EditText. Unused imports + dp(int) helper removed.
CHANGELOG.md v1.2.11 build 202 entry.

Net stats: 5 files changed, 55 insertions, 76 deletions (the chrome removal is bigger than the displayId-routing addition).

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


Validation plan (in-car checks)

(a) DL5 testeur — invisible bridge

  1. Install v1.2.11 over v1.2.10 (no uninstall, versionCode bumped).
  2. Start cluster projection, launch Yandex Maps on the cluster.
  3. Tap the search field on the cluster (with a11y service enabled) or tap the ⌨ button manually.
  4. Expected: NO French overlay at the top, only the IME keyboard at the bottom of display 0. The cluster mirror remains fully visible.

(b) DL5 testeur — characters reach the cluster

  1. Continuing from (a), type any text on the IME.
  2. Expected: every character appears immediately in the Yandex search field on the cluster.
  3. Tap IME's Done/Search button.
  4. Expected: KEYCODE_ENTER reaches Yandex → search fires.

(c) Logcat smoking gun

  1. Run adb logcat | grep MirrorDaemon.
  2. Type the first character.
  3. Expected line: injectKey FIRST OK displayId=2 setDisplayIdAvail=true keyCode=29 action=0
    • displayId=2 → the cluster display id was applied.
    • setDisplayIdAvail=true → reflection succeeded on this ROM.
    • If setDisplayIdAvail=false → reflection failed, falls back to default routing (cluster won't receive — escalate to alternative routing).

(d) DL3 Seal EU regression

  1. Install v1.2.11 on DL3 production.
  2. Navigate UI.
  3. Expected: ⌨ button remains GONE, no log lines from KeyboardBridge, no post-!isDiLink5 log lines from ClusterImeWatcherService. All other features behave identically to v1.2.10.

(e) DL2 regression

  1. Install v1.2.11 on DL2.
  2. Expected: zero impact (no cluster path triggered).

Install

Direct sideload (BYD file manager → tap APK):

DashCast-v1.2.11-debug.apk

Or via DashCast's built-in OTA updater once GitHub picks up this release.


Branch & tag

  • Branch: beta/1.2.0-dilink5
  • Commit: ede59ee
  • Tag: v1.2.11
  • Previous: v1.2.10 (build 201, May 23, 2026 — DL5 IME banner one-click activation + i18n)
  • Next planned: v1.3.0 — full ProxyDaemon migration (see docs/PLAN_v1.3.x_FULL_PROXY.md)

Links

APK download

GitHub release page

  • Author

DashCast v1.2.12


v1.2.12 (build 203) — DL5 KeyboardBridge: a11y ACTION_SET_TEXT pivot (replaces KeyEvent injection)

What changed

Field log of DL5 testeur (log/BYD_RE_Sniffer_20260523_140358.txt, captured after v1.2.11 install — logcat confirms "package":"com.byd.dashcast","versionCode":"202") showed v1.2.11's 1×1 invisible EditText was never served by the system InputMethodManager:

W InputMethodManager: Ignoring showSoftInput() as view=android.widget.EditText{… VFED..CL. .F....ID 0,0-1,1} is not served.

Consequences observed:

  • IME window pops up (because SOFT_INPUT_STATE_ALWAYS_VISIBLE is independent of the served view) — user sees the Yandex keyboard.
  • But no InputConnection is bound to the EditTextTextWatcher.onTextChanged never fires → zero injectKey FIRST OK traces from MirrorDaemon over the 30-min capture window.
  • The whole v1.2.8/v1.2.11 KeyEvent forwarding chain (KeyCharacterMap → KeyEvent → ClusterInputForwarder.injectKeyEvent → MirrorDaemon → InputManager.injectInputEvent) is never exercised.

Architectural pivot

v1.2.12 replaces the entire KeyEvent injection chain with the standard a11y subsystem path:

node.performAction(
    AccessibilityNodeInfo.ACTION_SET_TEXT,
    Bundle{ ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE: text }
);

The a11y subsystem is cross-display by construction (TalkBack uses the same mechanism to read out content from any display) and is immune to per-display IME isolation or the DL5 cluster fission compositor (composed face on displayId=2, focused app window on mDisplayId=3 1×1 shadow framebuffer). This sidesteps the unverified KeyEvent.setDisplayId(2) cross-display routing that v1.2.11 relied on.

Implementation

ClusterImeWatcherService (existing AccessibilityService gains static helpers + cluster window scanner):

  • static volatile ClusterImeWatcherService sInstance published in onServiceConnected, cleared in onDestroy.
  • onServiceConnected: info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS so getWindows() returns cluster windows.
  • static boolean setTextOnCluster(CharSequence text) — finds the focused editable on a cluster window via findClusterFocusedEditable(), calls node.performAction(ACTION_SET_TEXT, …), logs WARN on refusal, recycles the node in finally.
  • static boolean performImeEnterOnCluster() — same node discovery, then AccessibilityAction.ACTION_IME_ENTER.getId() on API 30+ or ACTION_CLICK fallback.
  • findClusterFocusedEditable() walks getWindows(), prefers windows whose getDisplayId() > 0, falls back to first editable on any window.

KeyboardBridgeActivity rewritten (~410 LoC → 200 LoC):

  • Window 220 dp × 48 dp (was 1×1, not served), Gravity.BOTTOM | Gravity.END, transparent, dim 0.
  • EditText with TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_NO_SUGGESTIONS, IME_ACTION_DONE | IME_FLAG_NO_EXTRACT_UI, single-line, transparent text/hint, no cursor, no padding.
  • showSoftInput is now called from onWindowFocusChanged(true) instead of onCreate.post(…) — fixes the race that produced the "is not served" warning.
  • TextWatcher.onTextChanged body reduced to a single line: ClusterImeWatcherService.setTextOnCluster(s.toString()).
  • OnEditorActionListenerperformImeEnterOnCluster(); on success clear the local field.
  • Removed: KeyCharacterMap mKcm, CharSequence mLastText, boolean mSuppressWatcher, forwardKeyCode, forwardKeyEvent, mapAsciiToKeyCode, all ClusterInputForwarder references.

accessibility_ime_watcher.xml: added android:accessibilityFlags="flagRetrieveInteractiveWindows" (mirror of the runtime flag).

Daemon paths preserved (dormant): MirrorDaemon.injectKey + sSetDisplayIdKey reflection + ClusterInputForwarder.injectKeyEvent are NOT removed — they remain available for any future code path that needs raw KeyEvent injection. The bridge simply no longer calls them.

Behavioural contract

  • DL3 Seal EU production: ClusterImeWatcherService.onAccessibilityEvent early-returns on !Platform.isDiLink5, the bridge button is View.GONE, neither helper is ever invoked — zero behavioural change.
  • DL5 testeur: user taps a cluster EditText → a11y service detects focus on displayId > 0, launches bridge → bridge appears as a 220×48dp invisible window → IMM serves the EditText → IME pops up → user types → each onTextChanged calls setTextOnCluster which performs ACTION_SET_TEXT on the cluster Yandex editable → text appears character-by-character on the cluster Yandex search field → tap IME's Enter → performImeEnterOnCluster fires ACTION_IME_ENTER → Yandex executes the search.
  • DL2: completely unaffected (a11y service inert, no cluster path).

Validation plan

  1. DL5 testeur — install v1.2.12 over v1.2.11 (no uninstall, versionCode 202→203 bumped). Start cluster projection, launch Yandex Maps on the cluster, tap the search field. Expected: IME pops up at the bottom of the head unit, no Ignoring showSoftInput() … is not served warning in logcat, each typed character causes the Yandex search field on the cluster to update, IME Enter triggers search.
  2. Logcat — search for ClusterImeWatcher tag. Look for setTextOnCluster ACTION_SET_TEXT refused WARN (rare, possible if Yandex's EditText doesn't expose ACTION_SET_TEXT). Look for findClusterFocusedEditable scan failed ERROR (would indicate FLAG_RETRIEVE_INTERACTIVE_WINDOWS propagation issue on this ROM).
  3. DL3 Seal EU — install, navigate UI: ⌨ button remains GONE, no log lines from ClusterImeWatcher (early-return via !isDiLink5).
  4. DL2 — install, navigate UI: zero impact.

Touched files

  • app/build.gradle (versionCode 202→203, versionName 1.2.11→1.2.12)
  • app/src/main/java/com/byd/dashcast/KeyboardBridgeActivity.java (rewritten)
  • app/src/main/java/com/byd/dashcast/ime/ClusterImeWatcherService.java (+sInstance, +3 methods, +FLAG_RETRIEVE_INTERACTIVE_WINDOWS)
  • app/src/main/res/xml/accessibility_ime_watcher.xml (+flagRetrieveInteractiveWindows)
  • CHANGELOG.md

No new permissions, no new dependencies, no new strings, no protocol bump (PROTOCOL_VERSION=2 unchanged).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.13


DashCast v1.2.13 (build 204) — DL5 per-app resize fix

Pre-release for the DL5 testeur (Android API 32 cluster fission topology). DL3 Seal EU production and DL2 are byte-for-byte identical to v1.2.12 except for the dormant code paths described below.

Symptom (field log BYD_RE_Sniffer_20260523_141144.txt, 15.3 MB)

The user moved the W/H sliders for ru.yandex.yandexmaps repeatedly and tapped Apply (40+ times spread over 14:12:16–14:13:14). Each tap produced:

I BYDApp: Applied custom resize 323/193 for ru.yandex.yandexmaps
D AdbLocalClient: executeShellWithResult: wm overscan 323,193,323,193 -d 3 -> Unknown command: overscan

…and zero ClusterService: findRunningTaskId … or resizeActiveTask … OK log lines. The Yandex window on the cluster never resized.

Root causes

Bug A — wm overscan removed in API 30+

wm overscan W,H,W,H -d N was removed from frameworks/base/services/core/java/com/android/server/wm/WindowManagerShellCommand.java in API 30. DL5 is API 32, so every shell invocation returns Unknown command: overscan. ClusterService.applyClusterFreeformBounds had already been gated by isDiLink5Safe in a prior release, but MainActivity.btnResizeApply and MainActivity.autoApplyInsetsIfNeeded still issued the dead command.

Bug B — ActivityManager.getRunningTasks only returns the caller's own task on API 21+

ClusterService.findRunningTaskId(packageName) enumerated ActivityManager.getRunningTasks(50) looking for the matching package. On API 21+ Android restricts this API: non-system apps see only their own task. DashCast is not system, so the loop never finds Yandex → returns -1resizeActiveTask(-1, …) silently returned at if (taskId <= 0) return; → no log line, no resize.

Display 3 vs display 2 — no action needed

The cluster app's task lives on displayId=3 (1920×1080 shadow framebuffer managed by containerservice). The composed face users see is on displayId=2 (1920×720, projected by the XDJA fission compositor). IActivityTaskManager.resizeTask(taskId, bounds, RESIZE_MODE_FORCED) operates in the task's display coord system (display 3, 1920×1080), and the compositor handles the projection to display 2 with automatic scaling at composition time. Bounds were already correctly computed (Rect(215, 164 - 1705, 916) display=3 1920×1080 in the same log). The only thing missing was a valid taskId. The display-3→2 override is only required for the touch path (MotionEvent.setDisplayId(2), v1.2.7) because the InputDispatcher routes events from the composed face, not for resize.

Fixes

1. ClusterService.findRunningTaskId — daemon dumpsys activity recents fallback

Path 1 (kept): ActivityManager.getRunningTasks(50) — works on legacy ROMs / DL3 where DashCast may still have GET_TASKS.

Path 2 (new): if BetaProxyClient.isConnected(), call BetaProxyClient.runShell("dumpsys activity recents"). The proxy daemon runs uid 2000 (same uid as adb shell) and has SHELL privileges, so dumpsys returns the full recent-tasks table cross-package. Parse with a fast-path regex Task\{[^}]*#(\d+)[^}]*\bA=<pkg>\b (matches * Recent #0: Task{xxxxxxx #88 type=standard A=ru.yandex.yandexmaps U=0 ...) and a fallback block-split scanner that also accepts realActivity=<pkg>/... or cmp=<pkg>/... within the same * Recent #N: block (covers BYD ROM variants).

Every path emits a discrete log line:

  • findRunningTaskId <pkg> → taskId=N (via AM)
  • findRunningTaskId <pkg> → taskId=N (via daemon dumpsys recents)
  • findRunningTaskId <pkg> — not found in dumpsys recents (out.length=N)
  • findRunningTaskId <pkg> — daemon not connected; cannot fallback to dumpsys

The parser is a static helper parseTaskIdFromDumpsysRecents(String dump, String pkg) so it's unit-testable in isolation.

2. ClusterService.resizeActiveTask — log on invalid taskId

The silent if (taskId <= 0) return; now emits:

W ClusterService: resizeActiveTask: taskId<=0 for pkg=<pkg> — cannot resize (lookup via AM + daemon dumpsys both failed)

so a residual failure surfaces in the log instead of vanishing.

3. MainActivity — skip wm overscan on DL5

Both btnResizeApply.onClick and autoApplyInsetsIfNeeded now wrap the wm overscan call with:

if (AdbLocalClient.isDiLink5Safe(MainActivity.this)) {
    AppLogger.d(TAG, "Apply resize DL5: skipping wm overscan (cmd removed in API 30+) — resizeTask handles it");
} else {
    ShellGateway.execShell(MainActivity.this, "wm overscan " + w + "," + h + "," + w + "," + h + " -d " + clusterId);
}

Mirrors the existing skip in ClusterService.applyClusterFreeformBounds. DL3 (API 29) still sends wm overscan -d 1 as before.

Behavioural contract

  • DL3 Seal EU productionfindRunningTaskId path 1 succeeds (path 2 never reached); wm overscan -d 1 still sent on DL3 (API 29 accepts it). Byte-for-byte identical to v1.2.12.
  • DL5 testeur — Apply now finds Yandex's taskId via the daemon dumpsys, IActivityTaskManager.resizeTask(taskId, bounds, RESIZE_MODE_FORCED) applies, the XDJA compositor reflects the new bounds on the composed display-2 face → visible resize.
  • DL2 — no cluster path, neither code change ever fires.

Touched files

  • app/build.gradle — versionCode 203→204, versionName 1.2.12→1.2.13
  • ClusterService.javafindRunningTaskId rewritten (~50 LoC), new static parseTaskIdFromDumpsysRecents (~40 LoC), resizeActiveTask early-return logs (~5 LoC)
  • MainActivity.java — both wm overscan call sites gated by isDiLink5Safe (~8 LoC each)

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

Validation plan

  1. DL5 testeur — install v1.2.13 over v1.2.12 (no uninstall, versionCode bumped). Start cluster projection, launch Yandex Maps on cluster, open the Adjust panel, move W/H sliders, tap Apply. Expected: Yandex window on cluster resizes visibly within the same frame. Logcat sequence:
    I BYDApp: Applied custom resize W/H for ru.yandex.yandexmaps
    D ClusterService: findRunningTaskId ru.yandex.yandexmaps → taskId=NN (via daemon dumpsys recents)
    I ClusterService: resizeActiveTask ru.yandex.yandexmaps Rect(...) OK
    
    No more Unknown command: overscan lines.
  2. Auto-apply on launch — restart cluster projection, observe autoApplyInsetsIfNeeded (500 ms post-launch) produces the same successful resizeTask sequence.
  3. DL3 Seal EU — install, navigate, resize sliders work exactly as before (path 1 still succeeds).
  4. Edge case (daemon down) — WARN findRunningTaskId … daemon not connected surfaces, resize quietly fails with the new taskId<=0 WARN. Both diagnostics visible, no crash.
  5. DL2 — install, navigate, no cluster path triggered.

APK

DashCast-v1.2.13-debug.apk (≈14.5 MB).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.14


DashCast v1.2.14 (build 205) — Diag → Sniffer: "Nettoyer" button restored

Why

Field report (23/05/2026, DL3 Seal EU): on long-lived installs the app's external storage footprint grows past 500 MB because BYD_RE_Sniffer_*.txt captures (~10–20 MB each), byd_log_*.log and byd_report_*.txt files accumulate across sessions. The user-facing cleanup button existed before the M3 redesign (wiped at commit 229059c, v0.9.88) but was never restored when the new tabbed Diag UI shipped. Only the static helper AppLogger.cleanupFiles(Context) survived (unused).

What

A new Nettoyer button at the right end of the action row in Diag → Sniffer, next to Exporter.

Behaviour

  • If a capture is currently active → Toast Arrête le sniffer avant de nettoyer (la capture écrit dans un fichier). and no deletion (otherwise we'd delete the file the sniffer is writing to and leak its setsid background processes).
  • Otherwise → confirmation AlertDialog ("Nettoyer les fichiers DashCast" / "Supprime tous les logs et captures… Les clés ADB et préférences sont préservées.") → on confirm:
    • Spawns sniffer-cleanup-thread.
    • Calls existing AppLogger.cleanupFiles(this) which deletes:
      • byd_log_*.log (AppLogger.saveToFile)
      • byd_report_*.txt (SysInfoActivity)
      • BYD_RE_Sniffer_*.txt (DiagActivity)
      • cluster_live.png (AdbLocalClient.captureClusterDisplay)
      • Clears the in-memory log buffer
    • Preserved: ADB keys (adb.key / adb.pub), SharedPreferences.
    • Walks the same dirs to compute remaining bytes (B / KB / MB auto-format).
    • Clears mSnifferFile + PREF_SNIFFER_PATH (the just-cleared capture is no longer exportable).
    • Posts back to UI: re-enables the button, updates the Sniffer status line with Nettoyage : N fichier(s) supprimé(s) · restant : SIZE, Toast confirms.

Touched files

  • app/build.gradle — versionCode 204→205, versionName 1.2.13→1.2.14.
  • DiagActivity.javabtnSnifferCleanup field, bindSnifferPanel wiring, cleanupSnifferFiles() + doCleanupSnifferFiles() (~80 LoC restored from the pre-wipe pattern).
  • include_diag_sniffer.xml — 5th button in action row.

Reuses the long-existing AppLogger.cleanupFiles(Context) (no logic change there). No new permissions, no new dependencies, no new strings, no protocol bump.

Behavioural contract

  • DL3 / DL5 / DL2 — identical surface: Sniffer tab gains the Nettoyer button. Zero impact on cluster projection, mirror, IME, resize, or any other DashCast subsystem.

Validation

  1. Open Diag → Sniffer → button Nettoyer visible at the right of Exporter.
  2. Start sniffer → tap Nettoyer → Toast asks to stop first, no deletion.
  3. Stop sniffer → tap Nettoyer → AlertDialog → confirm → within a few hundred ms Toast confirms N fichier(s) supprimé(s) — restant : SIZE and the status line updates.
  4. On a device with > 500 MB DashCast footprint: post-cleanup, Settings → Apps → DashCast → Storage shows < 1 MB of "Data" (only prefs + ADB keys remain).

APK

DashCast-v1.2.14-debug.apk (≈14.5 MB).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.15


DashCast v1.2.15 (build 206) — DL5: KeyboardBridge IME + resize fixes

Field log: BYD_RE_Sniffer_20260523_150803.txt (post-v1.2.14 install, DL5 testeur).

Bug A — IME never appears when tapping keyboard icon

Logcat:

W InputMethodManager: Ignoring showSoftInput() as view=android.widget.EditText{... 0,0-330,0} is not served.

Geometry 0,0-330,0: width 330 px = 220 dp ≈ correct, but height 0. Root cause: v1.2.12 set SOFT_INPUT_ADJUST_RESIZE on a window pinned BOTTOM | END with height 48 dp. When the IME tries to show at the bottom, ADJUST_RESIZE shrinks the window to make room above the IME — but our window IS the bottom → no vertical room left → height collapses to 0 → IMM rejects the EditText as "not served" → IME never shows.

Fix: replace SOFT_INPUT_ADJUST_RESIZE with SOFT_INPUT_ADJUST_NOTHING. Window keeps 220×48 dp regardless of IME. The IME visually overlaps the bridge but that's fine (transparent invisible chrome).

Bug B — Resize still doesn't work (daemon not connected)

Logcat repeating 40+ times during 12:00–12:05:

W ClusterService: findRunningTaskId ru.yandex.yandexmaps — daemon not connected; cannot fallback to dumpsys
W ClusterService: resizeActiveTask: taskId<=0 for pkg=ru.yandex.yandexmaps — cannot resize

v1.2.13's daemon-dumpsys fallback is correct in principle but useless when the proxy daemon isn't running on the device. Same log proves AdbLocalClient.executeShellWithResult IS functional (D AdbLocalClient: executeShellWithResult: wm overscan ... lines present).

Fix: add Path 3 in ClusterService.findRunningTaskId — if daemon disconnected, fall back to AdbLocalClient.executeShellWithResult("dumpsys activity recents") synchronously via CountDownLatch (5 s timeout). Reuses the existing parseTaskIdFromDumpsysRecents regex helper.

Behavioural contract

  • DL3 Seal EU — Path 1 (AM) succeeds, Paths 2/3 never reached. Unchanged.
  • DL5 with daemon connected — Path 2 (daemon) succeeds, Path 3 never reached. Unchanged.
  • DL5 with daemon off (this testeur) — Path 3 runs dumpsys over AdbLocal, parses Yandex's taskId, resizeTask applies.
  • DL2 — no cluster path, both changes inert.

Touched files

  • app/build.gradle — versionCode 205→206, versionName 1.2.14→1.2.15.
  • KeyboardBridgeActivity.javaSOFT_INPUT_ADJUST_RESIZESOFT_INPUT_ADJUST_NOTHING.
  • ClusterService.java — Path 3 AdbLocal sync fallback in findRunningTaskId (~50 LoC).

Validation

  1. Tap keyboard icon → IME pops up at bottom, no is not served warning. Typed text reaches Yandex on cluster via a11y.
  2. Apply resize → logcat: findRunningTaskId ru.yandex.yandexmaps → taskId=NN (via AdbLocal dumpsys recents) followed by resizeActiveTask ... OK → visible resize.
  3. If resizeTask still fails with SecurityException (MANAGE_ACTIVITY_TASKS required), the WARN resizeActiveTask failed: ... surfaces — next iteration would route through a typed daemon verb.

APK

DashCast-v1.2.15-debug.apk (≈14.5 MB).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.16


DashCast v1.2.16 (build 207) — DL5: real KeyboardBridge IME fix + resize exception unwrapping

Field log: BYD_RE_Sniffer_20260523_152515.txt (post-v1.2.15 install).

Bug A — IME still doesn't appear (re-diagnosed)

v1.2.15 changed SOFT_INPUT_ADJUST_RESIZESOFT_INPUT_ADJUST_NOTHING. Log shows the EditText is still 0,0-330,0 (height=0):

W InputMethodManager: Ignoring showSoftInput() as view=android.widget.EditText{... 0,0-330,0} is not served.

Real cause: @android:style/Theme.Translucent.NoTitleBar is translucent but not floating (windowIsFloating=false). For non-floating activities, Window.setLayout(wPx, hPx) is partially ignored — the width somehow ends up at 330 px (220 dp), but the height collapses to 0 (likely the IME insets the non-floating window from the bottom, and our window IS at the bottom).

Fix: new dedicated KeyboardBridgeTheme extending Theme.DeviceDefault.Dialog with android:windowIsFloating=true, translucent, transparent background, no frame/title/dim/animations. Manifest switched to it. With a floating theme setLayout(220dp, 48dp) + Gravity.BOTTOM | END are properly honoured → EditText becomes 330×72 px in the corner → IMM serves it → IME pops up.

Bug B — resizeActiveTask failed: null (partial success)

Good news: Path 3 AdbLocal fallback in findRunningTaskId works:

D ClusterService: findRunningTaskId ru.yandex.yandexmaps → taskId=109 (via AdbLocal dumpsys recents)

Bad news: the actual reflection call failed 30+ times with an uninformative resizeActiveTask failed: null. Root cause of the null: we logged e.getMessage() on an InvocationTargetException wrapper — its own message IS null; the real cause is hidden behind getTargetException().

Fix:

  • Unwrap InvocationTargetException → log class + message of the true cause.
  • Try the 2-arg resizeTask(int, Rect) signature as a fallback when the 3-arg form throws NoSuchMethodException (vendor signature variance).
  • Success log now includes cw / ch / clusterDisplay for forward debugging.

Once the next field log surfaces the real exception (probably SecurityException requiring MANAGE_ACTIVITY_TASKS, or a vendor signature mismatch), the appropriate code path can be added.

Behavioural contract

  • DL3 / DL5 / DL2 — only the KeyboardBridge theme is new and only that Activity uses it. ClusterService.resizeActiveTask is byte-for-byte identical on success; the failure log is richer.
  • No new permissions, no new dependencies, no protocol bump.

Touched files

  • app/build.gradle — 206 → 207, "1.2.15" → "1.2.16".
  • res/values/styles.xml+KeyboardBridgeTheme.
  • AndroidManifest.xml — KeyboardBridgeActivity theme.
  • ClusterService.java — ~40 LoC: dual-signature resizeTask + cause unwrapping + richer log.

Validation

  1. Tap keyboard icon → IME pops up at bottom of head unit (small invisible 220×48 dp window in corner). Typed text reaches Yandex on cluster via a11y.
  2. Apply resize → logcat shows EITHER resizeActiveTask … OK (cw=… ch=… clusterDisplay=…) (success) OR the real exception name + message — no more null.

APK

DashCast-v1.2.16-debug.apk.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.17


DashCast v1.2.17 (build 208) — DL5: IME finally served + resize via am task resize (bypasses MANAGE_ACTIVITY_TASKS)

Field log: BYD_RE_Sniffer_20260523_153544.txt (post-v1.2.16 install). Two clean diagnostics this time, each pinpointing the exact fix.

Bug A — IME still doesn't appear (now width=0 instead of height=0)

With v1.2.16's floating Dialog theme the EditText moved from 0,0-330,0 (height collapsed) → 0,0-0,37 (width collapsed):

W InputMethodManager: Ignoring showSoftInput() as view=android.widget.EditText{... 0,0-0,37} is not served.

Cause: a floating Dialog window auto-wraps its content. An empty EditText sized MATCH_PARENT × MATCH_PARENT wraps to width 0 because there's no text and no min size → IMM still rejects width-0 views as not-served.

Fix: pass the EditText explicit pixel size via its own LayoutParams (not MATCH_PARENT) plus setMinWidth/setMinHeight belt-and-braces:

mInput.setMinWidth(wPx);
mInput.setMinHeight(hPx);
setContentView(mInput, new ViewGroup.LayoutParams(wPx, hPx));

The Dialog wraps around 220×48 dp of concrete pixels → IMM sees a real bounding box → IME pops up.

Bug B — SecurityException: requires android.permission.MANAGE_ACTIVITY_TASKS

v1.2.16's exception unwrapping paid off — the real cause is now visible:

W ClusterService: resizeActiveTask failed: SecurityException: Permission Denial:
  resizeTask() from pid=N, uid=10148 requires android.permission.MANAGE_ACTIVITY_TASKS

That permission is signature|privileged — unreachable for a normal APK no matter what we declare in the manifest.

Fix: when the reflection call throws, fall back to am task resize <taskId> <l> <t> <r> <b> via AdbLocalClient.executeShellWithResult — the exact same shell pipe that already executes wm overscan successfully. am runs in shell uid (2000) context, which has enough capabilities to bypass the app-level permission check. Async (callback-based), success/error logged.

Behavioural contract

  • DL3 — reflection succeeds on the legacy ROM, fallback never runs. Unchanged.
  • DL5 — reflection now reliably throws SecurityException (logged once per attempt); shell fallback handles the actual resize.
  • DL2 — cluster path inert; AdbLocalClient.executeShellWithResult already has a blockDiLink2Resize guard.
  • No new permissions, no new dependencies, no protocol bump.

Touched files

  • app/build.gradle — 207 → 208.
  • KeyboardBridgeActivity.java — explicit EditText pixel size + setMinWidth/setMinHeight (~5 LoC).
  • ClusterService.java — shell fallback in resizeActiveTask after the reflection failure branch (~20 LoC).

Validation

  1. Tap keyboard icon → invisible 220×48 dp window appears at bottom-right of head unit → IME pops up → typing routes through setTextOnCluster (a11y ACTION_SET_TEXT) to Yandex on the cluster.
  2. Move sliders + Apply → logcat shows:
    W ClusterService: resizeActiveTask reflection failed: SecurityException ...
    D AdbLocalClient: executeShellWithResult: am task resize 129 ... -> ...
    I ClusterService: resizeActiveTask (via AdbLocal `am task resize`) OK
    
    → visible resize on the cluster.
  3. If am task resize is missing on this BYD ROM (AOSP removed it API 30+; vendor may have kept it), the log will show the shell error verbatim and the next release will try the modern cmd activity syntax.

APK

DashCast-v1.2.17-debug.apk.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.18


DashCast v1.2.18 (build 209) — DL5: prompt user to enable a11y for keyboard + chain cmd activity task resize

Field log: BYD_RE_Sniffer_20260523_161033.txt (post-v1.2.17 install). Confirmed both v1.2.17 surgical fixes landed (IME pops up cleanly, am task resize shell pipe runs) but two follow-on issues surfaced.

Bug C — a11y service never bound (typed text doesn't reach Yandex)

The IME now appears (✅ v1.2.17 worked) and the user types, but zero traces of ClusterImeWatcherService appear in the entire 20 MB log:

$ grep -c ClusterImeWatcher BYD_RE_Sniffer_20260523_161033.txt
0

Meaning: the user hasn't enabled « DashCast Cluster IME » under Android Settings → Accessibility, so ClusterImeWatcherService.sInstance is null, setTextOnCluster() returns false silently, and the typed text never reaches Yandex. Android system policy: an app cannot self-grant accessibility — the user must opt in manually.

Fix: in KeyboardBridgeActivity.onWindowFocusChanged, read Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, compare against our component name, and if absent:

  • emit a clear WARN line in logcat,
  • show a Toast in French explaining what to do,
  • launch Settings.ACTION_ACCESSIBILITY_SETTINGS and finish() the bridge so the user lands directly on the right settings screen.

Also added explicit AppLogger.w lines in setTextOnCluster() / performImeEnterOnCluster() for the null-instance and no-focused-editable branches (previously silent-returned false).

Bug D — am task resize is a silent no-op on this BYD ROM

Same field log: 30+ shell invocations succeed with empty output and no visible effect:

D AdbLocalClient: executeShellWithResult: am task resize 26 348 244 1572 476 ->
I ClusterService: resizeActiveTask (via AdbLocal `am task resize`) OK:

On AOSP API 30+ the am verb was rewritten into cmd activity and am task resize is now a stub that prints nothing and does nothing.

Fix: wrap the shell command with 2>&1; echo "exit=$?" to capture stderr + exit code; classify the result as looksOk = exit=0 && no "unknown command" / error / exception substrings. If not OK, chain a cmd activity task resize <id> <l> <t> <r> <b> attempt (the modern AOSP equivalent). Both attempts log their raw output for forward debugging. If neither works on the BYD ROM, the next iteration will route through BetaProxyClient when the daemon reconnects.

Behavioural contract

  • DL3 / DL2KeyboardBridgeActivity launch is gated by MainActivity.isDiLink5 upstream, so the new a11y prompt only fires on DL5. ClusterService resize is byte-for-byte identical on success (reflection wins on DL3); only the fallback chain is richer on DL5.
  • No new permissions, no new dependencies, no protocol bump.

Touched files

  • app/build.gradle — 208 → 209.
  • KeyboardBridgeActivity.java — a11y enablement check + Toast + Settings intent + isClusterImeWatcherEnabled helper (~55 LoC).
  • ime/ClusterImeWatcherService.java — 3 WARN lines on silent-no-op paths.
  • ClusterService.java — exit-code capture + cmd activity task resize chain (~40 LoC).

Validation

  1. First tap on the keyboard icon with a11y still disabled → Toast + immediate Accessibility settings → user enables « DashCast Cluster IME » → next tap on keyboard icon → IME appears + typing routes to Yandex via ACTION_SET_TEXT. Logcat should show ClusterImeWatcher: onServiceConnected — watching cluster focus events.
  2. Move sliders + Apply → logcat shows EITHER:
    • resizeActiveTask \am task resize` -> "...exit=0" (looksOk=true)` (resize works on the BYD ROM), OR
    • looksOk=false followed by resizeActiveTask \cmd activity task resize` -> "..."` revealing which path the ROM accepts.

APK

DashCast-v1.2.18-debug.apk.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.19


v1.2.19 — build 210 — i18n: localized a11y prompt Toast

Pure i18n release. No logic change. No new permissions. No protocol bump.

What changed

  • v1.2.18 introduced a hardcoded French Toast in KeyboardBridgeActivity.onWindowFocusChanged to nudge the user to enable the « DashCast Cluster IME » accessibility service when it is missing from Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES. That string is now extracted into a string resource and translated across all 11 existing locale folders.
  • New string resource: keyboard_bridge_a11y_required_toast.
  • Translated in: values (FR base), values-en, values-de, values-es, values-it, values-ru, values-uk, values-be, values-kk, values-uz, values-tr, values-ar.
  • KeyboardBridgeActivity now reads it via getString(R.string.keyboard_bridge_a11y_required_toast).

Behavioural contract

  • DL5 testeur (non-French system locale): the a11y-prompt Toast now appears in the user's language.
  • DL3 Seal EU: no impact (a11y prompt path is DL5-only).
  • DL2: no impact (no cluster path).

Touched files

  • app/build.gradle (209→210, 1.2.18→1.2.19)
  • app/src/main/java/com/byd/dashcast/KeyboardBridgeActivity.java (~3 LoC)
  • 12 × app/src/main/res/values*/strings.xml (+1 line each)
  • CHANGELOG.md

Links

APK download

GitHub release page

  • Author

DashCast v1.2.20


v1.2.20 — build 211 — i18n: 26 missing translations filled across 11 locales

Pure i18n release. Translation parity restored. No logic change. No new permissions. No protocol bump.

What changed

Audit revealed that all 11 locale files (values-en/de/es/it/ru/uk/be/kk/uz/tr/ar) were each missing the same 29 keys vs the French base. 3 are translatable="false" (ota_progress_percent, ota_progress_unknown, ota_version_label) and rightfully excluded — leaving 26 strings × 11 locales = 286 entries that were silently falling back to French at runtime.

Translated keys

  • Keyboard bridge UI: keyboard_bridge_{title,hint,input_hint,btn_enter,btn_back,btn_close} (still used as the activity label in the manifest).
  • ⌨ button + content description: btn_keyboard, btn_keyboard_desc.
  • Diag tab labels: diag_tab_adas, diag_tab_dilink2, diag_tab_mirror.
  • DL2 recon panel: diag_dl2_header_{title,subtitle,subtitle_fmt}, diag_dl2_pill_{unknown,detected,other}, diag_dl2_counters_fmt.
  • Mirror diagnostic panel: diag_mirror_header_{title,subtitle,subtitle_fmt}, diag_mirror_pill_{unknown,dl5,other}, diag_mirror_hint, diag_mirror_send_log.

Universal symbols / brand names (, ✓ ✗ ! ⊘ counter format, ADAS, DiLink 2, Mirror) are carried as-is across all locales.

Behavioural contract

  • Runtime resource resolution now serves the user's locale for every translatable key — no more silent French fallback on Russian/English/German/Spanish/Italian/Ukrainian/Belarusian/Kazakh/Uzbek/Turkish/Arabic system locales.
  • DL3 / DL5 / DL2: all benefit equally.

Power-user surface exemption

DiagActivity Toasts and SysInfoActivity build-info text remain FR-hardcoded — inside the power-user Sniffer/Diag surface that the project convention (tools:ignore="HardcodedText" on the layout root) intentionally exempts from i18n.

Touched files

  • app/build.gradle (210→211, 1.2.19→1.2.20)
  • 11 × app/src/main/res/values-*/strings.xml (+26 lines each, +286 lines total)
  • CHANGELOG.md

Links

APK download

GitHub release page

  • Author

DashCast v1.2.21


v1.2.21 — build 212 — DL5 KeyboardBridge: actually open Accessibility settings

Symptom (v1.2.20 field feedback)

The user saw the localized « DashCast Cluster IME » Toast but the Accessibility settings page never opened, leaving no way to enable the service from the prompt.

Root cause

v1.2.18 fired startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) from inside onWindowFocusChanged with a single try/catch — on this BYD ROM the bare action either has no resolver or the AM silently drops the launch when called from a floating Dialog activity's focus callback. The catch branch never even fired (no ACTION_ACCESSIBILITY_SETTINGS unavailable line in the log).

Fix

  1. Multi-attempt launcher. New openAccessibilitySettings() tries three intents in order:
    • Settings.ACTION_ACCESSIBILITY_SETTINGS (canonical)
    • explicit AOSP component com.android.settings/.Settings$AccessibilitySettingsActivity
    • generic Settings.ACTION_SETTINGS as last resort
      Each attempt is gated by PackageManager.resolveActivity so we never start an unresolvable Intent; each branch logs attempt N unresolved/launched/failed.
  2. Deferred launch. Posted via mInput.post(…) instead of inline inside onWindowFocusChanged — some ROMs refuse startActivity from a focus callback.
  3. One-shot guard. New mPromptedA11y boolean prevents repeat Toasts / launch attempts as onWindowFocusChanged fires multiple times (IME pop/close).

Behavioural contract

  • DL5 testeur with a11y disabled: tap ⌨ → invisible bridge → localized Toast → next frame, Accessibility settings open automatically (one of the 3 intents will resolve).
  • DL5 with a11y already enabled: prompt path skipped.
  • DL3 / DL2: not exercised (bridge entry gated).

Touched files

  • app/build.gradle (211→212, 1.2.20→1.2.21)
  • app/src/main/java/com/byd/dashcast/KeyboardBridgeActivity.java (+mPromptedA11y, +openAccessibilitySettings() ~30 LoC)
  • CHANGELOG.md

Links

APK download

GitHub release page

  • Author

DashCast v1.2.22


DL5 — Real fix for the a11y enable prompt

Field log from v1.2.20 finally revealed the canonical android.settings.ACCESSIBILITY_SETTINGS action has no resolver on this BYD ROM:

W KeyboardBridge: ACTION_ACCESSIBILITY_SETTINGS unavailable: No Activity found to handle Intent { act=android.settings.ACCESSIBILITY_SETTINGS flg=0x10000000 }

Reverse-engineered settings_byd AndroidManifest reveals BYD ships its own action:

  • android.intent.action.BYD_ACCESSIBILITYcom.byd.carsettings/com.byd.systemsettings.accessibility.AccessibilityMainActivity

AccessibilityMainActivity calls AccessibilityManager.getInstalledAccessibilityServiceList() — same list where the user enables « DashCast Cluster IME ».

Updated intent fallback chain (5 attempts)

  1. android.intent.action.BYD_ACCESSIBILITY (preferred on BYD)
  2. Explicit com.byd.carsettings/...AccessibilityMainActivity
  3. Canonical Settings.ACTION_ACCESSIBILITY_SETTINGS
  4. AOSP com.android.settings/...AccessibilitySettingsActivity
  5. Generic Settings.ACTION_SETTINGS

Each gated by PackageManager.resolveActivity, each per-attempt logged.

Files changed

  • app/build.gradle (212 → 213 / 1.2.21 → 1.2.22)
  • KeyboardBridgeActivity.java (+10 LoC, 2 new intents prepended)

Links

APK download

GitHub release page

  • Author

DashCast v1.2.23


DL5 — No more Settings round-trip to enable Cluster IME

User reasonably asked: « On ne peut pas forcer l'activation de accessibilité pour Dashcast via ADB ? » — and yes, we already speak to the localhost ADB daemon (Dadb 127.0.0.1:5555) everywhere else in the app, so we now flip the secure flag directly.

New flow when bridge detects service not bound

  1. tryAdbEnableA11y() runs a POSIX one-liner via AdbLocalClient.executeShellWithResult:
    • reads settings get secure enabled_accessibility_services,
    • appends our component com.byd.dashcast/com.byd.dashcast.ime.ClusterImeWatcherService if missing (preserves TalkBack & co — uses case ":$CUR:" in *":$COMP:"*) NEW="$CUR";; *) NEW="$CUR:$COMP";; esac),
    • settings put secure enabled_accessibility_services "$NEW" && settings put secure accessibility_enabled 1 && echo OK:$NEW.
  2. On OK:… reply → short Toast + finish(). Next ⌨ tap routes typing end-to-end.
  3. On ADB failure (port 5555 closed, pairing dialog not accepted) → fallback to the v1.2.22 5-intent BYD-Accessibility launcher.

Why this works

SettingsProvider#assertWritePermissionsForSecureSettings admits any caller with WRITE_SECURE_SETTINGS. UID 2000 (shell) holds it system-wide, so AccessibilityManagerService rebinds our service on the next tick.

Files changed

  • app/build.gradle (213 → 214 / 1.2.22 → 1.2.23)
  • KeyboardBridgeActivity.java (+~80 LoC: tryAdbEnableA11y() shell + 2-branch callback, promptAndOpenSettings() fallback helper)

Links

APK download

GitHub release page

  • Author

DashCast v1.2.24


DashCast v1.2.24 — Pre-release (beta/1.2.0-dilink5)

Branch: beta/1.2.0-dilink5 · versionCode: 215 · versionName: 1.2.24
APK: DashCast-v1.2.24-debug.apk (14 MB, debug-signed, drop-in update over v1.2.23)

Pre-release on the DiLink 5 development line. Not merged into main.
Production DL3 (Seal EU) users should keep using the latest 1.0.x stable release.


What this fixes

v1.2.23 finally got the ADB auto-enable path to work end-to-end on DL5
(the tryAdbEnableA11y SUCCESS Toast confirmed that
AccessibilityManagerService rebound ClusterImeWatcherService on the
next tick, with BYD's own a11y services correctly preserved in the
colon-separated list). But field testing of v1.2.23 surfaced three
user-visible regressions:

  1. Typed characters never reached the cluster Yandex search field.
  2. The hardware Back button was completely unresponsive for 17-18 seconds.
  3. After stopping projection and relaunching another app, mirror touch
    was broken
    .

A single root cause explains all three symptoms.

Smoking gun in the field log

BYD_RE_Sniffer_20260523_164557.txt:

16:46:18.804 I KeyboardBridge: tryAdbEnableA11y SUCCESS: OK:com.byd.airconditioning/.gesture.AcGestureService:com.android.systemui/.custom.StatusBarAccessibilityService:com.byd.autovoice/.../SceneSayService:com.byd.dashcast/com.byd.dashcast.ime.ClusterImeWatcherService
16:46:18.791 I ClusterImeWatcher: onServiceConnected — watching cluster focus events (DL5=true)
...
16:46:36.233 I am_anr (991): [0,15738,com.byd.dashcast,...,Input dispatching timed out
                              (KeyboardBridgeActivity (server) is not responding.
                               Waited 5000ms for KeyEvent)]
16:46:47.206 E ActivityManager: ANR in com.byd.dashcast (com.byd.dashcast/.KeyboardBridgeActivity)
16:46:47.277 I am_proc_died: [0,15738,com.byd.dashcast,0,2]
16:46:47.350 F DEBUG: at com.byd.dashcast.ime.ClusterImeWatcherService.setTextOnCluster:256

Root cause

ClusterImeWatcherService.setTextOnCluster() (introduced in v1.2.12)
ran fully synchronously on the caller's thread, which on every
keystroke was the UI thread (TextWatcher.afterTextChanged inside
KeyboardBridgeActivity).

Each call performed findClusterFocusedEditable(), which iterates
getWindows() and runs root.findFocus(FOCUS_INPUT) against every
interactive window — including the cluster windows on display 3 reached
through cross-display Binder round-trips into
AccessibilityManagerService. With FLAG_RETRIEVE_INTERACTIVE_WINDOWS
and the XDJA fission compositor topology each walk takes dozens of
milliseconds.

A fast typist queued enough walks back-to-back on the UI thread to push
the next KeyEvent dispatch past the 5 000 ms input-dispatch timeout
→ ANR → process killed → the 17-18 s the user spent tapping the Back
button was the system-wide SIGQUIT dump + process restart window
→ after restart the daemon Binder was dropped, hence the
attemptStartMirror : service non disponible repeats around 16:46:55.

The three reported symptoms collapse to one root cause: the a11y
tree walk was running on the UI thread.

Fix

ClusterImeWatcherService now owns a private HandlerThread
"cluster-ime-relay" (BACKGROUND priority), started in onCreate and
torn down via removeCallbacksAndMessages(null) + quitSafely() in
onDestroy.

  • setTextOnCluster(CharSequence) is strictly non-blocking. It
    stores the latest text in volatile mPendingText, removes any
    in-flight runnable, and reposts with 80 ms debounce (last-writer
    wins) so a typist filling « apple street » triggers the a11y tree
    walk + ACTION_SET_TEXT at most ~12 times per second. Returns true
    optimistically; runtime failures are logged inside the worker.

  • performImeEnterOnCluster() also dispatches to the worker:
    cancels the debounced setText, flushes it inline so Enter sees the
    final text, then performs ACTION_IME_ENTER (API 30+) or the
    ACTION_CLICK fallback (API 28/29).

The on-cluster typing UX is identical from the user's perspective
(text appears in the cluster field as they type, Enter executes the
search) — only the threading model changes. The UI thread is never
blocked on a Binder round-trip again.

Behavioural contract

  • DL5 (this branch's target): typing in the bridge no longer
    blocks the UI thread → no ANR → Back stays responsive → process is
    not killed → mirror touch pipeline is preserved across the typing
    session. v1.2.23's ADB auto-enable + 5-intent Settings fallback kept
    as-is.
  • DL3 (Seal EU production, 1.0.x stable): bridge entry point is
    gated by isDiLink5 (early-return), zero behavioural change.
  • DL2: no cluster path, zero impact.

Touched files

  • app/build.gradle — 214 → 215, 1.2.23 → 1.2.24
  • app/src/main/java/com/byd/dashcast/ime/ClusterImeWatcherService.java
    — +mWorkerThread/mWorker/mPendingText/mSetTextRunner fields;
    onCreate spins up the worker; onDestroy tears it down;
    setTextOnCluster rewritten to non-blocking dispatch + debounce;
    performImeEnterOnCluster rewritten to worker-dispatched
    flush-then-enter (~90 LoC net).
  • CHANGELOG.md1.2.24-build215 entry on top.

No new permissions, no manifest change, no new strings, no new
dependency, no protocol bump.

Install

Drop-in update over v1.2.23 — Android Package Installer accepts the
upgrade because versionCode is now 215. No uninstall required.

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

Validation plan

  1. Install over v1.2.23, tap ⌨ → bridge opens (a11y already enabled
    from v1.2.23, no Toast/ADB round-trip) → IME pops up.
  2. Type rapidly « apple street » → each character (debounced at 80 ms)
    appears in the cluster Yandex search field within a frame.
  3. Tap IME Enter → Yandex executes the search.
  4. Test the hardware Back button mid-typing — every press is
    acknowledged immediately, no 5-18 s freeze.
  5. logcat | grep com.byd.dashcast — no am_anr line, no
    ANR in com.byd.dashcast, same pid throughout the session.
  6. Stop projection from MainActivity, relaunch Yandex on the cluster
    — mirror touch forwards correctly (process did not die, daemon
    Binder still connected).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.25


DashCast v1.2.25 — Pre-release (beta/1.2.0-dilink5)

Branch: beta/1.2.0-dilink5 · versionCode: 216 · versionName: 1.2.25
APK: DashCast-v1.2.25-debug.apk (drop-in update over v1.2.24)

Pre-release on the DiLink 5 development line. Not merged into main.
Production DL3 (Seal EU) users should keep using the latest 1.0.x stable release.


What this fixes

v1.2.24 killed the ANR that was murdering the app on every fast type
session, but field testing surfaced two follow-on bugs:

  1. The « DashCast Cluster IME ✓ » Toast appeared every time the
    user tapped the ⌨ icon, and forced them to tap twice before
    they could actually type.
  2. Typing in the bridge produced zero text on the cluster Yandex
    search field — the worker logged setTextOnCluster no-op: no focused editable on cluster on every keystroke.

Bug 1 — Root cause

tryAdbEnableA11y's success branch (introduced in v1.2.23) called
finish() on the bridge after the OK:… reply, on the assumption
that the user would retap ⌨ and the second bridge would find a freshly
bound service.

The field log BYD_RE_Sniffer_20260523_170436.txt proves
AccessibilityManagerService rebinds ClusterImeWatcherService
within ~30 ms of the settings put:

17:05:02.243 I ClusterImeWatcher: onServiceConnected — watching cluster focus events (DL5=true)
17:05:02.272 I KeyboardBridge: tryAdbEnableA11y SUCCESS: OK:com.byd.airconditioning/.gesture.AcGestureService:...:com.byd.dashcast/com.byd.dashcast.ime.ClusterImeWatcherService

The bridge already has IME focus, the soft keyboard is up, the worker
sInstance is alive — there is nothing to wait for. Finishing the
bridge here was pure user friction.

The « Toast every time » perception is because Android revokes a11y
enablement on every package update
(AccessibilityManagerService.onPackageUpdateFinished, standard
security measure) — so after each install the secure setting really
did contain no entry for our service. With finish() removed, the
user sees the enable Toast exactly once after each install, then
keeps typing in the same bridge instance from then on.

Bug 2 — Root cause

findClusterFocusedEditable() called AccessibilityService.getWindows()
— which on every Android API returns only the windows on the
default display, even with FLAG_RETRIEVE_INTERACTIVE_WINDOWS
set. The cluster Yandex search EditText lives on display 2 (composed
face) / display 3 (shadow framebuffer driven by the XDJA fission
compositor), neither of which is the default. So the window walk found
exactly zero candidates and the worker logged no focused editable on cluster on every keystroke.

Fix

  • KeyboardBridgeActivity.tryAdbEnableA11y no longer calls finish()
    in the success branch — the bridge stays open, the Toast confirms
    the enablement, the user keeps typing.

  • ClusterImeWatcherService.findClusterFocusedEditable() rewritten
    around AccessibilityService.getWindowsOnAllDisplays() (API 30+,
    DL5 ships API 32). Returns SparseArray<List<AccessibilityWindowInfo>>
    keyed by displayId — exactly what we need to find the cluster
    windows. Falls back to the single-display walk on older APIs (DL3
    stays gated by isDiLink5 upstream so this is just belt-and-braces).

  • New private helper pickFocusedEditableFrom(window, selfPkg)
    factored out so the cross-display loop is readable, and which
    explicitly skips windows from our own package. This is necessary
    because v1.2.25 keeps the bridge open after ADB success: the
    bridge's own EditText would otherwise compete for
    findFocus(FOCUS_INPUT) and win on display 0, causing us to push
    the user's keystrokes back into the local field instead of the
    cluster Yandex search.

Behavioural contract

  • DL5 with a11y already enabled (steady-state after this release):
    tap ⌨ → bridge opens → IME pops up → no Toast → typing routes
    to cluster Yandex immediately.
  • DL5 right after install: tap ⌨ → bridge opens → IME pops up
    → Toast (ADB enabled the service) → user keeps typing in the
    same bridge
    → routes immediately.
  • DL3 (Seal EU production, 1.0.x stable): bridge entry gated by
    isDiLink5, zero behavioural change.
  • DL2: no cluster path, zero impact.

Touched files

  • app/build.gradle — 215 → 216, 1.2.24 → 1.2.25
  • app/src/main/java/com/byd/dashcast/KeyboardBridgeActivity.java
    — ~5 LoC: dropped finish() from the ADB success branch, expanded
    the inline comment to document why the bridge must stay open.
  • app/src/main/java/com/byd/dashcast/ime/ClusterImeWatcherService.java
    — ~110 LoC: rewrote findClusterFocusedEditable around
    getWindowsOnAllDisplays, extracted pickFocusedEditableFrom
    helper with self-package filter.
  • CHANGELOG.md1.2.25-build216 entry on top.

No new permissions, no manifest change, no new strings, no new
dependency, no protocol bump.

Install

Drop-in update over v1.2.24 — Android Package Installer accepts the
upgrade because versionCode is now 216. No uninstall required.

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

Validation plan

  1. Install v1.2.25 over v1.2.24, tap ⌨ once → bridge opens + IME
    up + Toast once → type « test » → cluster Yandex search shows
    « test » within a frame → tap IME Enter → search executes.
  2. Tap ⌨ a second time later: bridge opens + IME up, no Toast,
    typing routes immediately.
  3. logcat | grep ClusterImeWatchersetTextOnCluster no-op: no focused editable on cluster should be silent during normal
    typing
    ; only emitted if the cluster app has no focused EditText
    (i.e. user opened the bridge with no search field in focus on the
    cluster).
  4. Hardware Back button mid-typing — instant response, no freeze
    (v1.2.24 guarantee preserved).
  5. Stop projection from MainActivity, relaunch another app on the
    cluster — mirror touch forwards correctly (v1.2.24 guarantee
    preserved).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.26


DashCast v1.2.26 — Pre-release (beta/1.2.0-dilink5)

Branch: beta/1.2.0-dilink5 · versionCode: 217 · versionName: 1.2.26
APK: DashCast-v1.2.26-debug.apk (drop-in update over v1.2.25)

Pre-release on the DiLink 5 development line. Not merged into main.
Production DL3 (Seal EU) users should keep using the latest 1.0.x stable release.


What this fixes

v1.2.25 nailed the keyboard end-to-end (user confirmed « ENFIN ! the
keyboard works, it sends the text correctly »). Same field session
surfaced one remaining bug: the per-app resize / overscan controls
in MainActivity have been a silent no-op on DL5 all along
.

Root cause

Field log BYD_RE_Sniffer_20260523_172007.txt shows the exact pattern
on every press of Apply Resize on Yandex Maps (taskId=38), repeated
11 times:

17:21:15.717 W ClusterService: resizeActiveTask reflection failed:
    SecurityException: Permission Denial: resizeTask() from pid=22748,
    uid=10148 requires android.permission.MANAGE_ACTIVITY_TASKS
17:21:15.758 D AdbLocalClient: am task resize 38 206 91 1714 629
    2>&1; echo "exit=$?" -> exit=0
17:21:15.758 I ClusterService: resizeActiveTask `am task resize`
    -> "exit=0" (looksOk=true)
  • The reflection path IActivityTaskManager.resizeTask() rightly
    fails with SecurityException (app uid 10148 lacks
    MANAGE_ACTIVITY_TASKS, which is signature|privileged).
  • The shell fallback am task resize returns exit=0 deterministically
    on DL5 (API 32) — but the cluster doesn't move.
  • v1.2.18 already commented this exact symptom (« am task resize
    returning empty stdout but no visible effect — on AOSP API 30+ the
    am verb was rewritten »). It added a cmd activity task resize
    fallback gated on a looksOk heuristic. On DL5 am returns exit=0
    cleanly, looksOk=true, the fallback was never reached, ever.

Fix

ClusterService.resizeActiveTask now branches on
AdbLocalClient.isDiLink5Safe(this):

  • DL5 (API 32): skip am task resize entirely and dispatch
    cmd activity task resize <taskId> <l> <t> <r> <b> directly. This is
    the canonical AOSP API 30+ verb (modern replacement for am task),
    reached through the same uid=shell pipe that already executes
    pm grant, service call AutoContainer, settings put secure ….
  • DL3 (API 29): preserve the legacy am task resize → fallback
    chain verbatim — am task resize is healthy on Android 10.

Behavioural contract

  • DL5: drag overscan sliders in MainActivity (or the global Settings
    sliders → auto-applied per-app via autoApplyInsetsIfNeeded) → press
    Apply → cmd activity task resize fires → cluster app shrinks/expands
    within the safe area in ≤ 1 frame.
  • DL3: zero behavioural change.
  • DL2: cluster path doesn't exist (zero impact).

Touched files

  • app/build.gradle — 216 → 217, 1.2.25 → 1.2.26
  • app/src/main/java/com/byd/dashcast/ClusterService.java — ~30 LoC
    in resizeActiveTask: DL5 branch dispatches cmd activity task resize
    directly; legacy DL3 chain preserved in else. Inline comment
    expanded to document the v1.2.26 field-log evidence.
  • CHANGELOG.md — v1.2.26-build217 entry prepended.

No new permissions, no manifest change, no new strings, no new
dependency, no protocol bump.

Install

Drop-in update over v1.2.25 — Android Package Installer accepts the
upgrade because versionCode is now 217.

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

Validation plan

  1. DL5 testeur — install v1.2.26 over v1.2.25, launch Yandex Maps
    on the cluster, open MainActivity, move H/V sliders to e.g. 200/100,
    press Apply → cluster Yandex visibly indents by 200 px H / 100 px V.
  2. logcat | grep ClusterService should show
    resizeActiveTask DL5: dispatching 'cmd activity task resize' (skipping 'am task resize' — known silent no-op on API 30+) then
    resizeActiveTask 'cmd activity task resize' -> "exit=0", with no
    further am task line per resize.
  3. Press Reset Insets → cluster Yandex returns to full 1920×720 within
    a frame.
  4. DL3 — typing app keyboard sliders still work as before (legacy
    am task resize path, unchanged).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.31-beta


Pre-release beta — DiLink5 hardening cycle LOT 3.

LOT 3 — 6 P2 perf hardening fixes (build 227)

  • #18 ClusterInputForwarder.injectTouchAtMultiPointerProperties[16] + PointerCoords[16] pré-alloués (constructor), réutilisés via .clear() sous mTouchInjectLock. -2 array + -2N obj par touch event cluster (60-120 Hz).
  • #19 MainActivity.forwardTouchFromMirrormFwdPointerIds[16] + mFwdClusterXs/Ys[16] pré-alloués. -3 array allocs par touch event mirror.
  • #20 BetaProxyClient.connect() late-arrival — pingBinder()isBinderAlive() (linkToDeath déjà hooked). -1 IPC par recovery.
  • #21 Phase4Verbs.autoContainerBinderpingBinder()isBinderAlive() + DeathRecipient sAutoContainerDeath qui invalide cache + descriptor à la mort du host. -1 IPC par autoContainerSendInfo().
  • #22 AppLogger.get() — snapshot via toArray() sous LOCK, format StringBuilder hors LOCK. Threads loggers (Beta probes, ADB, daemon connect) ne bloquent plus sur UI.
  • #23 AppLogger.getCountByLevel() + LogActivity.refreshLog — compteurs int[] incrémentaux dans addEntry/clear/evict ; LogActivity ne walk plus 3000 entrées toutes les 500 ms.

Cycle de hardening

  • LOT 1 (1aff110) — 5 P1 fixes
  • LOT 2 (fe153bf) — 10 P2 fixes
  • LOT 3 (54ee184) — 6 P2 perf fixes ← cette release

Test

  • BUILD SUCCESSFUL (4s), lint 0 erreur sur les 6 fichiers touchés.
  • ⚠️ Smoke test device requis avant LOT 4 : touch cluster DL5, Beta engine probes Phase4 cmd 0/16/18, LogActivity refresh.

Branche : beta/1.2.0-dilink5 · Commit : 54ee184

Links

APK download

GitHub release page

  • Author

DashCast v1.2.27


DashCast v1.2.27 (pre-release)

Public versionName: 1.2.27 · versionCode: 222 · Track: beta/1.2.0-dilink5

Pre-release published from the beta/1.2.0-dilink5 branch. Not merged into main. The car-side updater detects this APK as an upgrade because versionCode 222 > 220 (last shipped to field). The public versionName is intentionally kept at 1.2.27 to stay aligned with the announced 1.2.x cadence; build identity is tracked via versionCode and the CHANGELOG below.


Highlights since v1.2.26 (build 217)

This pre-release rolls up five internal builds (218 → 222), covering one new hardware target (DiLink 4), one full diagnostic suite, the new field-log sniffer panel, a finished i18n campaign at 100% coverage, and — most importantly for current DL5 users — a root-cause fix for the cluster resize regression that has been silently swallowing every margin setting on DiLink 5 hardware.


Build 222 — versionName 1.2.31 — i18n 100% (ADAS / Sniffer panels + Toasts)

Cross-cutting i18n audit. All Toast.makeText call sites across every Activity were verified — 14 of 15 were already wired to R.string.*; the remaining literal ("DashCast Cluster IME ✓" inside the success branch of KeyboardBridgeActivity.tryAdbEnableA11y) was promoted to R.string.keyboard_bridge_active_toast.

All 9 diagnostic layouts (activity_diag.xml + the 8 include_diag_*.xml) were grep-audited for raw android:text values. Three documentation cards were still leaking French prose because their content mixes translatable text with non-translatable code identifiers, shell commands, and Chinese CJK literals:

  • include_diag_adas.xml:188 — Auto_container service doc (PascalCase vs snake_case naming, codes 12/13, infoListInit variants per DiLink generation, R-series unsupported) → diag_adas_doc_help
  • include_diag_sniffer.xml:183 — captured content description (header, live logcat threadtime, auto/manual snapshots, setsid persistence) → diag_sniffer_doc_content
  • include_diag_sniffer.xml:220 — 5-step user workflow (Start → Reproduce → Snapshot → Stop → Export) → diag_sniffer_doc_workflow

Four new keys were translated manually (no machine pseudo-translation) and added to all 12 supported locales: ar, be, de, en, es, fr (default), it, kk, ru, tr, uk, uz. Code identifiers (Auto_container, infoListInit, infoListInit_Di5, SecondActivity, clusterdebug, setsid, ro.product.model, dumpsys SurfaceFlinger, logcat threadtime, etc.) and CJK literals (显示Adas, 关闭Adas — the actual on-device resource labels in the BydDashboard APK) are preserved verbatim across every locale because translating them would be a bug. Total final coverage: 342 string keys × 12 locales = 4 104 translated entries.

Identified but deliberately left untranslated as a design decision: adapter placeholders (row_log.xml "INFO" / "00:00:00" / "Tag" / "message", item_beta_test.xml "Title" / "Description" / "X0" — all overwritten at bind time), Unicode symbols (⚡, ✕, ✓, ➖, ➕, ⊞, ▼, ↺), universal units ("50 px" / "80 px" — SeekBar default placeholders replaced at runtime), brand and URLs ("DashCast Cluster IME", "RE Sniffer", "github.com/Kiroha/byd-dashcast"), and binder service identifiers ("Auto_container" as a single-word card label).


Build 221 — versionName 1.2.30 — DL5 cluster resize root-cause fix (three cascading patches in ClusterService.java)

The DL5 cluster resize regression that was silently absorbing every user-defined margin on DiLink 5 hardware has been root-caused and fixed. Diagnosis was triggered by field log log/BYD_RE_Sniffer_20260523_184727.txt from a DL5 user running Yandex Maps on build 219: the log confirms the app is correctly launched on display 3 (XDJAScreenProjection_0, taskId=49 verified), cmd activity task resize 49 374 275 1546 445 deterministically returns exit=0… yet the cluster face (physical display 2, 1920×720) remains full-bleed with no visible margin whatsoever. Three tightly interlocked causes were identified and fixed together.

Fix #1startActivityViaShell() now passes --windowingMode 5 (FREEFORM)

The shell command was previously am start --display 3 -a MAIN -c LAUNCHER -n pkg/cls --activity-clear-task with no explicit windowing mode flag. As a result the task landed in WINDOWING_MODE_FULLSCREEN (=1). On API 30+, cmd activity task resize is a silent no-op against fullscreen tasks — the internal AOSP path ActivityTaskManagerService.resizeTask detects fullscreen and bails without raising any error, returning exit=0. Adding --windowingMode 5 (the official AOSP am start flag, available since API 26 and still present in API 32) between --display N and -a MAIN makes the task land directly in FREEFORM mode, where bounds resize is honored. Note: applyClusterFreeformBounds() was already computing ActivityOptions.setLaunchWindowingMode(5) + setLaunchBounds(…) correctly, but on DL5 those ActivityOptions were silently discarded because startActivityViaShell() replaces startActivityViaIAM() (which raises SecurityException uid 10148 for cross-display launches on DL5).

Fix #2resizeActiveTask() now expresses bounds in display-3 framebuffer space

The field log revealed a coordinate-space mismatch: at launch time applyClusterFreeformBounds correctly logs cluster FREEFORM bounds=Rect(374, 275 - 1546, 805) display=3 1920×1080 (computed via Display.getRealSize() on display 3 → height 1080), but the subsequent resize logs bounds=Rect(374, 275 - 1546, 445) (height 720, computed from the hard-coded ClusterInputForwarder.mClusterHeight). Two different coordinate spaces — incoherent results.

The task actually lives inside the display-3 framebuffer (1920×1080), not in the physical cluster face space (1920×720) — Qt-side native code downscales 1080 → 720 onto the dalle via the Surface consumed by the XDJA TextureView (pipeline confirmed by RE: SecondaryDisplayService.addView(TextureView) on display 2 → onSurfaceTextureAvailabletransact(2, surface) → native createVirtualDisplay("remote_dashboard", 1920, 720, 320, surface, 320)DISPLAY_SECONDARY_ID = 3am start --display 3). resizeTask applies bounds in the task's own display space, so bounds must be in display-3 dimensions (1920×1080) — otherwise we shrink the task inside an already-larger buffer.

The code now queries DisplayManager.getDisplay(clusterId).getRealSize() directly at resize time, with fallback to mInputForwarder.getClusterWidth/Height() on detection failure (preserves DL3 behavior, where display 1 IS the dalle and both paths return 1920×720). On DL3 this is a no-op equivalence. Consequence: user-entered H/V margins are interpreted in framebuffer space; on DL5 they will be very slightly compressed vertically by Qt (factor 720/1080 ≈ 0.667 on V), but visually consistent.

Fix #3 — Post-resize verification probe (dumpsys activity activities)

After cmd activity task resize returns success, the code now chains a tightly filtered dumpsys activity activities | grep -E 'taskId=N|Task=…#N|mBounds|getWindowingMode|windowingMode=' shell call. The result is logged at INFO with the prefix resizeActiveTask VERIFY. This allows field-log analysis to confirm that (a) the task is genuinely in windowingMode=5 (FREEFORM) after our --windowingMode 5 launch flag, and (b) mBounds exactly matches the Rect(insetH, insetV, fbW - insetH, fbH - insetV) we requested. If both invariants hold but nothing changes visually, the next investigation will focus on the Qt/XDJA layer (the TextureView consuming the display-3 Surface may filter or stretch differently).

Cross-platform compatibility

DL3 (Seal EU, API 29): (i) Fix #1 is inactive — DL3 uses startActivityViaIAM(), not the shell fallback; (ii) Fix #2 is transparent — getRealSize() on display 1 returns 1920×720, identical to getClusterHeight(); (iii) Fix #3 is inactive — DL3 uses the legacy else branch with am task resize. DL2 (HMI only, no cluster display): clusterId<=0 → method bails upfront, no path touched. DL4: same code path as DL3 if platform auto-detection routes through IATM. Zero regression risk on non-DL5 platforms.


Build 220 — versionName 1.2.29 — i18n campaign Phase 4 (DiagActivity + SettingsActivity, 46 keys × 12 locales)

Translation campaign completion phase. 46 previously-French-only strings spanning DiagActivity, SettingsActivity and the diagnostic layouts (include_diag_adas.xml, include_diag_sniffer.xml, activity_diag.xml) were promoted to R.string.* and translated across all 12 locales. Coverage went from partial (≈70% on diag screens) to 100%. Zero regression on existing UI; one new Spanish translation polish.


Build 219 — versionName 1.2.28 — DiLink 4 diagnostic tab in DiagActivity (40 tests)

Companion of build 218 (DL4 platform detection). The user now needs a way to validate end-to-end that the DL3-style activation sequence (sendInfo(1000, 16) open / sendInfo(1000, 18) close / sendInfo(1000, 0) restore) is being correctly applied on DL4 hardware after the DL5 override neutralisation.

Full DL4 panel added: 40 grouped tests covering L1–L15 (recon: package presence, service binder probes, manifest provider scans, framebuffer dumpsys snapshots), S1–S15 (shell: am start --display N per non-main display, dumpsys SurfaceFlinger, dumpsys display, framebuffer state pre/post sendInfo, force-stop sequences), and D1–D10 (live cluster activation probes: open projection → wait → spawn canary on display N → wait → retract → dump activities). All wired to AdbLocalClient with per-test result rows, copy-to-clipboard, and a one-tap "Run all" button. Pure additive: zero impact on existing DiagActivity tabs (DL3, DL5, Mirror, etc.). Untracked DL4 source files were not included in this pre-release (still on the testeur's side).


Build 218 — versionName 1.2.27 — DiLink 4 auto-detection + FORCE_ON DL5 override neutralisation

Field log BYD_RE_Sniffer_20260523_175751.txt from a new testeur on DiLink 4 hardware (BYD-AUTO/DiLink4.0, API 29) proved the cluster never activated: every projection attempt logged Service auto_container does not exist (snake_case spelling, repeated 11+ times across log/byd_log_20260523_*.txt). Root cause: the build had been hardcoded with a FORCE_ON DiLink 5 debug override that forced every device through the DL5 codepath, including the auto_container service binding which doesn't exist on DL4 (DL4 uses Auto_container in PascalCase, matching DL3).

Platform auto-detection was added in Platform.java based on ro.build.product + ro.product.brand + API level matrix (DL3 = Seal* API 29, DL4 = BYD-AUTO/DiLink4.0 API 29, DL5 = BYD-AUTO/DiLink5.x API 32). The FORCE_ON override is hard-neutralised on non-DL5 hardware. DL4 now correctly routes to the PascalCase Auto_container service and the DL3-equivalent sendInfo sequence, restoring cluster activation on the new testeur's car.


Compatibility & risk

  • DL3 (Seal EU, API 29) — Pure additive. All DL3 codepaths unchanged: legacy resize chain (am task resize + cmd activity task resize fallback) preserved, startActivityViaIAM() still used (no SecurityException on DL3), getRealSize() on display 1 equals getClusterHeight() so Fix #2 is a no-op equivalence.
  • DL4 (BYD-AUTO/DiLink4.0, API 29) — Now auto-detected since build 218. Routes through DL3-equivalent activation path. Resize fixes inactive (DL4 doesn't use the DL5 shell fallback).
  • DL5 (BYD-AUTO/DiLink5.x, API 32) — Primary target of build 221 fixes. Worst case if Fix #1+#2 don't produce the expected visual effect = current state (silent resize no-op) plus the new VERIFY log from Fix #3 explaining why. Strict behavioral superset.
  • DL2 (HMI only) — No cluster display → resizeActiveTask bails upfront → no codepath touched.

What testers should look for

After installing, switch the cluster app to anything you want to resize (e.g. Yandex Maps, Yandex Navi, any maps/media app), set H=100 / V=80 in the DashCast main settings, send it to the cluster, then capture a field log via Diag → Sniffer → Start → reproduce → Stop → Export. Grep for resizeActiveTask VERIFY in the captured BYD_RE_Sniffer_*.txt file:

  • windowingMode=5 (FREEFORM) confirms Fix #1 took effect.
  • mBounds=Rect(100, 80 - 1820, 1000) (approximately, depends on framebuffer size) confirms Fix #2 + Fix #1 cooperate correctly.
  • If both are present but the cluster face still shows no margin: please share the field log, the investigation moves to the Qt/XDJA scaling layer.

Files attached

  • DashCast-v1.2.27-debug.apk — public versionName 1.2.27, internal versionCode 222.

Git track

  • Branch: beta/1.2.0-dilink5
  • Tag: v1.2.27 (internal cadence marker)
  • Base: v1.2.26 (build 217)
  • Not merged into main.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.28


DashCast v1.2.28 (beta pre-release)

Branch: beta/1.2.0-dilink5 · Not merged into main · versionCode 224

This pre-release bundles the cumulative work shipped on the DiLink 5 beta branch since v1.2.27 was first cut (build 218 → 224 inclusive). The car-side updater will pick it up as an upgrade strictly via versionCode 224 (any installed build with a lower versionCode triggers the update prompt).

⚠️ This is a beta pre-release. Field-tested on a single DL5 vehicle (Yandex Maps cluster routing). DL3 (Seal EU), DL4 (BYD-AUTO/DiLink4.0) and DL2 paths are preserved byte-for-byte through dynamic platform detection — every new DL5 code path is gated by Platform.isDiLink5(ctx) so non-DL5 cars are not affected.


Highlights

DL5 cluster app resize — full root-cause fix (4 layers)

The cluster-app resize sliders (Adjust H / Adjust V in MainActivity, and per-app auto-apply on launch) were silently no-op'ing on DL5 since the platform was introduced. Four cascading bugs were diagnosed from four successive field logs, each fix peeling back the next layer:

  1. Fix #1am start --windowingMode 5 (FREEFORM). Without an explicit windowing mode the launched task lands in WINDOWING_MODE_FULLSCREEN, and cmd activity task resize is a silent no-op on fullscreen tasks since API 30+. Adding the AOSP am start --windowingMode 5 flag is now the prerequisite for any downstream resize.
  2. Fix #2 — bounds expressed in display-3 framebuffer space. The DL5 cluster topology uses a 1920×1080 shadow framebuffer on display 3 (XDJAScreenProjection_0), which Qt then scales down to the 1920×720 physical cluster dalle at composition time. The previous code computed bounds against the dalle's 720 height (via mInputForwarder.getClusterHeight()), which shrunk the task inside an already-larger buffer. resizeActiveTask() now queries DisplayManager.getDisplay(clusterId).getRealSize() to express bounds in the task's actual display space, with a mInputForwarder fallback that preserves DL3 behaviour.
  3. Fix #3 — post-resize VERIFY probe. After every successful cmd activity task resize, a stanza-targeted dumpsys activity activities is dispatched to confirm the task ended up at the expected mBounds and mWindowingMode. Earlier versions used a noisy grep | head -20 that truncated before reaching the task's actual stanza; v1.2.28 uses an awk range /Task=Task\{[^}]*#N[ }]/,/Task=Task\{[^}]*#[0-9]+[ }]/ to extract just the task's stanza, then greps only the relevant fields (mBounds, WindowingMode, displayId, resizeMode).
  4. Fix #4force_resizable_activities + am force-stop chain. Even after Fix #1+#2+#3 landed, field logs showed the activity being coerced to mWindowingMode=fullscreen immediately after launch. Root cause: most navigation apps (Yandex Maps, Yandex Navi, Google Maps, Sygic, …) declare android:resizeableActivity="false" in their manifest to block split-screen. API 32 honors this flag strictly: non-resizable activities cannot enter freeform, regardless of the parent root task's windowing mode. The standard Android developer-option override is Settings.Global.force_resizable_activities=1 (visible as "Force activities to be resizable" in Developer Options). DashCast now sets this flag idempotently via the local ADB shell pipe (uid 2000 holds WRITE_SECURE_SETTINGS) before every cluster launch, and am force-stops the target package so its new process picks up the override at task creation time. The setting is persistent; re-applying every launch is self-healing.

DL4 (BYD-AUTO/DiLink4.0) auto-detection — neutralises mis-applied DL5 toggle

A new DL4 testeur reported "Service auto_container does not exist" 11+ times in production logs. Root cause: their car is a genuine DiLink 4.0 (Build.MODEL=DiLink4.0 For BYD AUTO, API 29), but the "DiLink 5" toggle in Settings had been manually flipped FORCE_ON. With FORCE_ON, the platform layer routed every service call through the DL5 snake_case binder auto_container, which doesn't exist on DL4 (DL4 uses PascalCase AutoContainer, like DL3).

  • New Platform.detectDiLink4() auto-recognises DL4 from fingerprint/product/model (dilink4 / dilink_4 / dilink 4 substring match), gated by API 28-29 to prevent future ROM mis-routing.
  • New Platform.isDiLink4(Context) accessor (read-only, no user override — DL4 detection is purely automatic, mirroring isDiLink2).
  • Platform.isDiLink5(Context) is hard-neutralised on DL4 hardware: returns false unconditionally before reading the override, with describeMode() reporting AUTO=off (DL4 detected — DL5 FORCE_ON ignored) so the Settings panel surfaces the absorbed mistake to power users.
  • Downstream code is auto-rerouted with zero changes: AdbLocalClient.autoContainerSvcName falls back to AutoContainer, ClusterManager.activateClusterDisplay falls back to the DL3 path (sendInfo 30 → 6 s → 16 → 6 s → 35 → poll DisplayManager).

DL4 Diagnostic tab — 40 reconnaissance + live tests

A new "DiLink 4" tab appears in DiagActivity between "DiLink 2" and "Mirror", auto-selected when Platform.isAutoDetectedDiLink4() is true. 40 tests across three tiers:

  • L1–L15 (recon): platform fingerprint, AutoContainer binder reachability, DisplayManager inventory, SurfaceFlinger probe, BYD/XDJA package enumeration, ServiceManager filter, IAutoContainer Stub class probe, /sys/class/drm framebuffer detection, 20 candidate service names brute probe, 25 BYD-specific properties, granted system permissions audit, ADB 5555 socket probe, launchable BYD apps + com.byd.clusterdebug presence.
  • S1–S15 (shell): shell smoke id -u, getprop filter, wm size/density/overscan health, dumpsys display, dumpsys SurfaceFlinger --display-id, shell-side service list, service call AutoContainer PascalCase probe, service call auto_container snake_case negative probe (PASS = correctly absent on DL4), pm list packages -f, ResumedActivity snapshot, getenforce + id -Z, cluster/fission process filter, BYD-filtered settings, am start --display 0 smoke, hardware fingerprint.
  • D1–D10 (destructive, live cluster activation): sendInfo(1000,30,"") prep → 6 s wait + DisplayManager dump → sendInfo(1000,16,"") projection open → 6 s wait + dump → sendInfo(1000,35,"") cluster size 10.25" → 6 s wait + dump + cluster display discovery → am start --display N against com.byd.clusterdebug → 2.5 s wait + dump confirms target landed → cleanup (am force-stop + sendInfo(1000,18,"")) → final sendInfo(1000,0,"") restore + 2 s wait.

"Run all" + "Copy report" buttons let DL4 users dump a plain-text report (Build identity + 40 results) for forward debug.

i18n — 100% coverage across 12 locales (fr, en, ar, be, de, es, it, kk, ru, tr, uk, uz)

342 string keys × 12 locales = 4104 translated entries. Final passes shipped between builds 220 and 222:

  • Phase 4 (46 keys): ADAS / Sniffer diagnostic panels, runtime Sniffer status formats, 6 Sniffer toasts, cleanup dialog, chooser title, Settings warnings (DL2 no-margins, no cluster, error prefix, no browser).
  • Phase 5 (4 keys): 3 long technical doc cards in include_diag_adas.xml / include_diag_sniffer.xml (previously left FR because of mixed prose + code identifiers + Chinese ROM literals), plus the IME-bridge a11y success toast ("DashCast Cluster IME ✓") that was inlined in Java.

Technical invariants (binder names, Chinese ROM resource literals, version fingerprints, brand strings, single-letter symbols) are intentionally not translated.


Diagnostic-tier changes (DL5)

  • DL5 KeyboardBridge — pivot to AccessibilityNodeInfo.ACTION_SET_TEXT (replaces brittle per-character KeyEvent injection). Cross-display-native, immune to per-display IME isolation and fission compositor asymmetry. Bridge window is now floating 220×48dp with deferred showSoftInput via onWindowFocusChanged, fixes the "Ignoring showSoftInput as view is not served" warning and the head-unit overlay covering cluster mirror.
  • DL5 KeyboardBridge — auto-enable a11y service via local ADB. settings put secure enabled_accessibility_services with colon-list preservation (no clobbering TalkBack or BYD a11y services). Single-tap to enable, no Settings round-trip needed.
  • DL5 KeyboardBridge — ANR fix. ClusterImeWatcherService.setTextOnCluster / performImeEnterOnCluster moved off the UI thread onto a dedicated HandlerThread cluster-ime-relay, with 80ms debounce coalescing — fast typists no longer ANR the bridge.
  • DL5 KeyboardBridge — cross-display window walk. findClusterFocusedEditable now uses AccessibilityService.getWindowsOnAllDisplays() (API 30+) instead of getWindows() (default-display-only), with explicit self-package skip so the bridge's own EditText doesn't compete for findFocus(FOCUS_INPUT) and steal keystrokes back to the head unit.
  • DL5 IME a11y banner — one-click activation via daemon shell (uid=2000 / WRITE_SECURE_SETTINGS), 3-tier Settings fallback (canonical AOSP action → BYD-specific BYD_ACCESSIBILITY action → explicit com.byd.carsettings/.AccessibilityMainActivity component → generic Settings.ACTION_SETTINGS).
  • DL5 per-app resize — cmd activity task resize direct shell dispatch (skips am task resize which is a silent no-op on API 30+).
  • DL5 task lookup — daemon dumpsys activity recents fallback (the default ActivityManager.getRunningTasks(50) returns only the caller's own task for non-system apps on API 21+; the daemon shell uid 2000 sees the full cross-package recents table).
  • Diag → Sniffer — "Nettoyer" button restored (lost in the v0.9.88 M3 redesign). Refuses to run while a capture is active, AlertDialog confirms scope, deletes byd_log_*.log / byd_report_*.txt / BYD_RE_Sniffer_*.txt from getExternalFilesDir(null) + getFilesDir() plus cluster_live.png from getExternalCacheDir(), reports remaining bytes.

Compatibility matrix

Platform Auto-detected via Behavioural change in v1.2.28
DL5 Platform.isDiLink5(ctx) (manual toggle or build signature) All 4 resize fixes, all DL5 KeyboardBridge fixes, IME a11y banner, cmd activity task resize direct dispatch, daemon dumpsys fallback for task lookup.
DL4 Build.MODEL / ro.product.name substring dilink4 + API 28–29 New: auto-detection, hard-neutralisation of DL5 FORCE_ON, new Diag tab (40 tests). No regression on existing DL4 paths.
DL3 (Seal EU) API 29 + non-DL4 fingerprint Zero behavioural change. New code paths gated upstream.
DL2 Single HMI (no cluster) Zero behavioural change. Cluster paths inert as before.

Internal build cadence (English summary, 218 → 224)

versionCode Internal tag Summary
224 1.2.28-build224 Public pre-release — bumps versionName to 1.2.28, no code change vs build 223.
223 1.2.32-build223 DL5 resize Fix #4: force_resizable_activities + am force-stop chain, VERIFY probe stanza-targeted.
222 1.2.31-build222 i18n 100% — 4 missing keys × 12 locales (ADAS / Sniffer doc cards + IME-bridge toast).
221 1.2.30-build221 DL5 resize Fixes #1+#2+#3: --windowingMode 5 + framebuffer-space bounds + VERIFY probe.
220 1.2.29-build220 i18n Phase 4 — 46 keys × 12 locales (ADAS / Sniffer panels + Toasts + Settings warnings).
219 1.2.28-build219 DiagActivity DiLink 4 tab — 40 tests (L1-15 recon, S1-15 shell, D1-10 live cluster activation).
218 1.2.27-build218 DL4 auto-detection + hard-neutralisation of mis-applied DL5 FORCE_ON.

Install instructions

The APK is signed with the project debug key. On a BYD car with a previous DashCast install (versionCode < 224), the in-app updater will detect the upgrade automatically. For a manual install: download DashCast-v1.2.28-debug.apk below and adb install -r <path> (or sideload via the car's file manager).

To roll back to 1.2.27 in case of regression, uninstall first (adb uninstall com.byd.dashcast), then install the 1.2.27 APK — Android refuses downgrades by versionCode otherwise.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.32-beta


Pre-release beta — DiLink5 hardening cycle LOT 4.

LOT 4 — hygiene + 1 bonus fix (build 228)

  • #24 Repo hygiene — 7 patch_*.py scripts déplacés de MyBYDApp/ racine vers MyBYDApp/scripts_archive/ (filesystem only, ils étaient déjà gitignored).
  • #25 ClusterService.sIsRunningvolatile — écrit main thread, lu workers (auto-resize-thread, BetaProxyClient). Évite stale read.
  • #26 Dead code purgeAdbLocalClient.captureClusterDisplay + BitmapCallback interface supprimés (0 caller). Imports Bitmap/BitmapFactory pruned.

Bonus (diagnostiqué depuis le sniffer 20260523_204155)

  • #27 Waze taskId race fixMainActivity.autoApplyInsetsIfNeeded() retry findRunningTaskId jusqu'à 3× avec 500 ms backoff quand taskId<=0. Le log a montré Waze lancé à 20:42:25.669 mais absent de dumpsys recents à 20:42:26.696 (1027 ms après) → resizeActiveTask abortait. Fenêtre 1.5 s couvre le lag dumpsys sur DL3. Early-exit si l'utilisateur change d'app entre-temps.

Faux positifs vérifiés (skip)

  • btnCapture n'est PAS dead (placeholder intentionnel + layout + 11 locales)
  • ImageView import encore utilisé
  • Cache resolveClusterDisplayId → risque d'invalidation (cluster dynamic)
  • Cache réflexion → aucune dans hot path
  • Toast lifecycle → différé LOT 5

Cycle de hardening

  • LOT 1 (1aff110) — 5 P1 fixes
  • LOT 2 (fe153bf) — 10 P2 fixes
  • LOT 3 (54ee184) — 6 P2 perf fixes
  • LOT 4 (8196654) — 3 hygiene + 1 Waze bonus ← cette release

Test

  • BUILD SUCCESSFUL, lint 0 erreur sur les 4 .java touchés.
  • ⚠️ Smoke test device requis : focus sur lancement Waze sur cluster avec insets per-app sauvegardés (le path bonus #27 doit retry et applique enfin le resize).

Branche : beta/1.2.0-dilink5 · Commit : 8196654

Links

APK download

GitHub release page

  • Author

DashCast v1.2.33-beta


Pre-release beta — DiLink5 hardening cycle LOT 5 (clôture).

LOT 5 — Toast lifecycle (build 229)

  • #28 Toast.makeText() → applicationContext — 20 sites dans MainActivity passent de MainActivity.this/this/activity à getApplicationContext(). Élimine les warnings NotificationService: Toast already killed observés dans le sniffer 20260523_204155 à 20:41:58 et 20:44:03 quand l'Activity finit entre makeText() et show(). Comportement utilisateur strictement identique.

Vérifié sans action

  • #17 signature permission broadcast IBinder — déjà entièrement implémenté depuis LOT 2 / 1.2.30. MirrorDaemon.broadcastBinder() utilise sendBroadcast(intent, PERM_DAEMON_READY), manifest déclare <permission protectionLevel="signature"/>. ⚠️ Ajouter broadcastPermission côté registerReceiver casserait le binder (le daemon tourne en uid=2000 shell, ne détient pas les perms signature de notre package). Option A est au max safe.

Différé volontairement

  • #29 i18n audit — gap de 3-6 strings × 10 locales. Différé : qualité de traduction LLM variable, bénéfice marginal.

Fin du cycle DiLink5 hardening

LOT Commit Build Items
LOT 1 1aff110 225 5 P1 fixes
LOT 2 fe153bf 226 10 P2 fixes
LOT 3 54ee184 227 6 P2 perf
LOT 4 8196654 228 3 hygiene + Waze retry
LOT 5 6c7351b 229 Toast lifecycle

Total : 25 fixes vérifiés réels, 5 faux positifs rejetés, 0 régression remontée terrain.

Branche beta/1.2.0-dilink5 prête pour merge vers main après validation device de cette build.

Links

APK download

GitHub release page

  • Author

DashCast v1.2.34-beta


v1.2.34-beta — Cluster projection lifecycle hardening + non-live resize editor

Builds on top of v1.2.33-beta with 13 commits focused on the cluster projection lifecycle (start/stop reliability, automatic re-arming after a stop, instant first activation on app launch) and a major rework of the in-car resize editor (no more flashes, fullscreen-safe gesture handling).

Internal versionName: 1.2.85 · versionCode: 281.


Highlights

  • Projection lifecycle is now self-healing. Stopping the projection and tapping any app afterwards re-activates the cluster automatically; previously the second activation silently failed with a launchDisplayId=-2 SecurityException.
  • Faster first activation. The fast/warm/slow path now correctly recognises that Qt is still in projection mode after a stop, so the very first app tap after a stop is processed in the warm path (no full 6 s replay).
  • Cluster resize editor — flash-free by default. The frame editor used to issue a moveAndResize on every drag, causing visible cluster flashes; the new editor only applies on Valider, with an opt-in Live preview switch (with a warning popup) for users who prefer the old behaviour.
  • Fullscreen resize is now usable. Edge swipes (back, home, recents, notifications) no longer steal touches when the frame is at the screen border — the editor reserves the full per-edge area (within Android's 200 dp limit).
  • Simpler UI. The Capture / Activate buttons are gone (auto-activation on first app tap, always-on); the SysInfo panel gains a Projection state row + a permanent slow-path replay action.

Changes since v1.2.33-beta

v1.2.85 — Cluster control card fullscreen-only + edge gesture exclusion

  • Bug fix (apps page): the cluster control card (Ajuster / ↺ / Split / ⌨) no longer leaks into the apps page preview. It is now revealed exclusively when the mirror is expanded to fullscreen and hidden again on exit. Internal: showMirrorView() no longer sets panel_cluster_control visible; enterFullscreenMirror() does. The four sites that used the panel visibility as a proxy for "is the mirror active?" now check frameMirror (the actual TextureView host). setDashboardOffState() also hides the panel defensively.
  • Bug fix (resize editor): system swipe gestures no longer steal touches when the resize frame is at a screen edge. ResizeFrameView.updateGestureExclusion() now reserves 198 dp strips along every canvas edge (capped just below the 200 dp Android limit) in addition to the existing 48 dp handle pads.

v1.2.84 — Non-live resize editor by default + Live preview switch

  • The resize editor no longer triggers Phase4Verbs.moveAndResize on every drag/preset (which caused noticeable cluster flashes when scrubbing the frame). Drag/preset updates now only paint the overlay; the cluster is updated only when the user taps Valider.
  • Valider applies and stays open (was: apply + finish).
  • New floating Close button (top-end IconButton) to dismiss the editor.
  • New MaterialSwitch (top-start) to opt-in to live preview; toggling it ON shows a MaterialAlertDialog warning about cluster flashes. State is persisted in SharedPreferences (cluster_resize_live_mode).
  • 5 new i18n strings × 12 locales (Italian apostrophe escaped).

v1.2.83 — Auto-restart projection on app tap after stop

  • After a Stop Projection, mClusterService is not null (still bound) but getDisplayId() == -1. The previous auto-activation logic in onSendToDashboard() only handled the service null case, so tapping an app after a stop hit a launchDisplayId=-2 SecurityException.
  • Fix: also queue the pending app + call activateCluster() when the service is bound but getDisplayId() <= 0.

v1.2.82 — Sync ClusterService display state after SysInfo replay

  • stopProjectionNoAdb() sets mLauncher.setDashboardDisplayId(-1) + stopSelf(). The service stays alive (MainActivity bound) but the display state is wiped.
  • After a SysInfo "replay" the bus-level sendInfo verbs were re-triggered but ClusterService was never told to re-discover the display, leading to launchDisplayId=-2 SecurityException on the next launch.
  • Fix: call ClusterService.getInstance().restartProjection() (or startForegroundService(ClusterService.class) if null) after notifyProjectionActive() in replayProjectionSlowPath().
  • MainActivity.onStart() also re-attaches the listener and, if mClusterService.getDisplayId() > 0, calls updateDashboardStatus(mCurrentDashboardApp) + setActivateBtnEnabled(true) to sync the UI.

v1.2.80 — Reduce sendInfo inter-command delay 6 s → 3 s

  • The 6 s spacing between consecutive sendInfo calls in the slow path was conservative; field testing on DiLink 3 (Seal EU) shows 3 s is sufficient and halves the cold-start latency.

v1.2.79 — SysInfo: Projection state row + always-on slow-path replay button

  • The SysInfo panel now surfaces the current projection state (Qt mode + virtual-display lifecycle) at the top.
  • The slow-path replay action is now always available (was: only when projection was OFF), so the user can force a clean re-arming whenever the cluster gets into an inconsistent state.

v1.2.78 — Fix fast-path: track Qt projection mode independently of VD lifecycle

  • The fast-path check was gated on the virtual display being alive, but on a stop the VD is destroyed while Qt itself remains in projection mode for a few seconds.
  • New flag sQtInProjectionMode is set/cleared by notifyProjectionActive/Stopped and consulted by the fast/warm/slow split, so a tap immediately after a stop now correctly takes the warm path instead of replaying the whole sequence.

v1.2.77 — Auto-start projection on app launch is now always-on

  • The "auto-activate on app tap" setting is no longer a user preference; it is the only flow. Simplifies the codebase and matches user expectation.

v1.2.76 — Simplify UI: drop Capture/Activate buttons + auto-activate on app tap + SysInfo restart play-button

  • Removed the Capture and Activate buttons from MainActivity (their functions are now automatic: capture happens implicitly, activation triggers on the first app tap).
  • Added a small play action in the SysInfo panel header for manual restart of the slow path.

v1.2.75 — Auto-démarrage de la projection à l'ouverture (build 271)

  • The projection is now armed automatically when the app opens (previously the user had to tap Activate first).

v1.2.74 — Stop projection: restauration cluster d'origine (build 270)

  • Stop projection now also restores the original cluster surface (sendInfo(0) to resume the native video stream) in addition to sendInfo(18) to close the projection tunnel.

v1.2.72 — Cluster resize ergonomie voiture (build 268)

  • Larger touch targets and snap-to-edges in the resize editor.

v1.2.71 — Cluster Resize Activity (option B minimal) (build 267)

  • New dedicated full-screen Activity for cluster resize (replaces the previous in-MainActivity inline editor).

v1.2.70 — Cluster fission Move/Resize POC + verb library (build 266)

  • First end-to-end implementation of Phase4Verbs.moveAndResize using setLaunchBounds + setLaunchWindowingMode(FREEFORM), gated to DL3 (Seal EU) for now.

Build / install

  • APK: DashCast-v1.2.85-debug.apk (15 MB, debug, signed with the BYD platform key).
  • versionCode is 281, higher than every previously published pre-release (v1.2.33-beta shipped versionCode 229), so any car already running an older DashCast will detect this as an update.
  • Min SDK 28 · Target SDK 29 · Compile SDK 33.

Compatibility notes

  • Tested on BYD Seal EU (DiLink 3, fission VirtualDisplay id 1, 1920×720).
  • The non-live resize editor change is platform-agnostic (does not touch Phase4Verbs.moveAndResize itself — the v1.2.70 cascade is frozen).
  • 12 locales (default fr + ar/be/de/en/es/it/kk/ru/tr/uk/uz) carry the new strings (resize_btn_close, resize_live_label, resize_live_warn_title, resize_live_warn_msg, resize_live_warn_ok).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.35-beta


v1.2.35-beta — Revert force_resizable_activities side effect on DiLink 5

Hotfix release on top of v1.2.34-beta. Three commits, all focused on undoing a global Android setting that v1.2.32 – v1.2.34 had unintentionally turned on system-wide on DiLink 5 head units.

Internal versionName: 1.2.35 · versionCode: 283.


The bug

Since v1.2.32 (build 223) DashCast had been running, on every DL5 cluster launch:

settings put global force_resizable_activities 1

The goal was legitimate: the cluster's freeform root task on display=3 is only honored by activities whose manifest declares android:resizeableActivity="true", and most BYD navigation apps (Yandex Maps, Yandex Navi, Google Maps, …) declare it false, so ATM was coercing their task back to fullscreen. Setting the force_resizable_activities developer-option global makes ATM ignore the manifest flag and treat every activity as resizable, which is exactly what the cluster freeform path needed.

But that global is system-wide and persistent (Settings.Global). DashCast was setting it on every launch and never resetting it, so every other app on the head unit also became resizable. Field-reproduced side effect: BYD's 360° camera app — normally locked to fullscreen — started accepting split-screen and freeform on DL5 head units that had ever opened DashCast since v1.2.32. The setting also survives DashCast uninstall.

The fix

v1.2.35 stops setting the flag entirely and proactively heals devices that already received v1.2.32 – v1.2.34. DiLink 2 / 3 / 4 are not touched (they never set the flag in the first place).

Changes since v1.2.34-beta

1. ClusterService.startActivityViaShell() — stop setting the flag.
The settings put global force_resizable_activities 1 prefix has been removed from the DL5 launch command. The cluster freeform root task is still created via am start --display 3 --windowingMode 5, and per-task cmd activity task resize still applies to apps whose manifest already allows resize. Apps that declare resizeableActivity="false" will no longer be forced into freeform on the cluster, which matches their author's intent and the OS contract.

2. ClusterService.onCreate() — DL5-only one-shot cleanup.
On every DashCast launch on DL5, the service now runs:

v=$(settings get global force_resizable_activities)
if [ "$v" = "1" ]; then
    settings put global force_resizable_activities 0
    echo RESET
else
    echo OK=$v
fi

Gated by AdbLocalClient.isDiLink5Safe(context), so DL2 / 3 / 4 send no shell at all. The check is idempotent and cheap (one shell roundtrip per onCreate), and ClusterService is started from MainActivity.onCreate() via startForegroundService(), so simply opening DashCast once on an affected car restores the device. Note that activities already running keep their resizable state until they are killed and relaunched (Android does not re-evaluate the global on live processes), but new launches honor the new value immediately — no reboot required.

3. SysInfo — new "Force Resizable" status row (DL5 only).
The Services card in the SysInfo screen now shows a new row, rendered only on DL5:

  • Label: Force Resizable
  • Sub-text: DL5 dev override · value 0 (or 1, reflecting the current Settings.Global value)
  • Badge: RUN (green) when the flag is 1, OFF (red) when it is 0
  • Tap on the leading icon: toggles the value (1↔0), then re-reads the setting and refreshes the row so the user can visually confirm the change.

The implementation reuses the same addServiceRow(..., alwaysClickable=true) pattern as the Projection state row introduced in v1.2.79.

Why no reboot is needed

force_resizable_activities is a Settings.Global row read live by ActivityTaskManager at activity / task creation. Setting it back to 0 takes effect immediately for any subsequent activity launch. Apps already in memory keep their current resizable state until killed (e.g. swiped from recents or am force-stop), which is a one-time cost on the affected devices and does not require rebooting the head unit.

Files touched

  • app/build.gradle — versionCode 282 → 283, versionName 1.2.34 → 1.2.35
  • app/src/main/java/com/byd/dashcast/ClusterService.java — remove force-set, add DL5 cleanup
  • app/src/main/java/com/byd/dashcast/SysInfoActivity.java — new row + probeForceResizable() + toggleForceResizable()
  • app/src/main/res/values{,-ar,-be,-de,-en,-es,-it,-kk,-ru,-tr,-uk,-uz}/strings.xml — 3 new keys × 12 locales

Install

APK: DashCast-v1.2.35-beta.apk (15 MB, debug, BYD platform-signed).
versionCode 283 — every car running an older DashCast will detect this as an update.

Compatibility

  • Tested on BYD Seal EU (DiLink 3, fission VirtualDisplay id 1, 1920×720).
  • DiLink 2 / 3 / 4: behaviour unchanged.
  • DiLink 5: the side effect is healed on first launch of DashCast. Cars where the 360° camera (or any other normally non-resizable BYD app) was wrongly accepting split-screen will recover after killing the affected app from recents (or rebooting).

Links

APK download

GitHub release page

  • Author

DashCast v1.2.36-beta


DashCast v1.2.36-beta — TetherFi Hotspot integration (build 294)

Pre-release. Not for daily-driver yet — only the hotspot subsystem changed; everything else is identical to v1.2.35-beta.

Highlights

  • Full Hotspot screen redesign built around the open-source TetherFi proxy (no carrier tether quota, no BYD MIUI tether restrictions).
  • Live traffic stats (uptime / RX / TX) sampled every 5 s without root.
  • Connected clients list populated from dumpsys wifip2p + /proc/net/arp via the on-device ADB shell proxy.
  • One-shot TetherFi update checker that pings GitHub Releases at boot and surfaces an "Update available" badge on the hotspot card.

What's new in detail

Hotspot UI (activity_hotspot.xml)

  • New landscape-first 2-column layout matching the project mockup.
  • Left column — Hero card: large status pill (running / stopped / unknown), update-available badge, primary actions (Start / Stop / Toggle / Open TetherFi).
  • Right column — three stacked cards:
    • Clients & traffic — connected-client count pill, scrollable client rows (icon + friendly name + IPv4), live uptime / RX / TX counters.
    • Watchdog — auto-restart switch (decoupled from stats sampling so stats keep working when the watchdog is OFF).
    • Autostart on boot — toggle persisted in SharedPreferences and consumed by BootReceiver.
  • New drawables: bg_hero_icon, bg_side_icon, bg_count_pill, ic_devices, ic_monitor_heart, ic_wifi_tethering, ic_sim_card.

TetherFi integration (HotspotActivity.java)

  • Single supported entry point: com.pyamsoft.tetherfi/.activity.ProxyTileActivity with key_actionSTART / STOP / TOGGLE. No reflection, no private APIs.
  • Independent stats probe (statsTick, 5 s period) probes dumpsys activity services com.pyamsoft.tetherfi | grep ProxyForegroundService via the local ADB client. Watchdog and stats now run on separate cadences.
  • Session lifecycle (onProbeTransition):
    • DOWN → UP : snapshot SystemClock.elapsedRealtime() + TetherFi UID + RX/TX baselines from TrafficStats.getUidRxBytes / getUidTxBytes.
    • UP → DOWN : reset all counters to -1 and clear the clients list.
  • TetherFi UID resolved once via PackageManager and cached for the activity lifetime.
  • Format helpers: formatUptime(ms)Hh Mm Ss, formatBytes(b)B / KB / MB / GB, friendlyMacName(mac)Device …XX:XX fallback.
  • Stats handler properly torn down on onPause (no callbacks leak).

Connected clients enumeration

  • New combined ADB shell payload: dumpsys wifip2p 2>/dev/null; echo '===ARP==='; cat /proc/net/arp 2>/dev/null.
  • Static parseClients(String) splits on the separator, builds a MAC → IPv4 map from arp, then walks the wifip2p dump looking for client sections (Client list, mClients, clients:, wifip2pdevice lines) and pairs each MAC with the nearest deviceName= token.
  • Static inner type HClient { mac, name, ip } + regex P_MAC / P_NAME.
  • renderClients(List<HClient>) updates tv_clients_count, repopulates ll_clients_list via buildClientRow(c) (programmatic LinearLayout with an ic_devices icon, name TextView weight=1, monospace IP TextView), and toggles tv_clients_empty.
  • Per-client RX/TX is intentionally not shown — kernel does not expose per-MAC counters without root.

TetherFi update checker (TetherFiUpdateChecker.java)

  • Background HEAD/JSON probe against the TetherFi GitHub Releases API at boot.
  • Compares installed APK version (PackageManager.getPackageInfo) against tag_name.
  • Result cached in SharedPreferences; the hotspot card reads it on resume and shows the Update available badge with a link button.
  • Pure best-effort: any network failure / parse failure is swallowed silently.

Boot autostart (BootReceiver.java)

  • New autostart path triggered by the existing BOOT_COMPLETED receiver: when the new pref hotspot_autostart_on_boot is true, fire ProxyTileActivity with key_action=START as part of the existing boot fan-out.
  • Also kicks TetherFiUpdateChecker once.

Strings (values/strings.xml + values-en/strings.xml)

  • 40+ new strings for the redesigned hotspot screen (titles, empty states, stats labels, update-available copy). Both fr (default) and en localized.

Misc

  • MainActivity / SettingsActivity / activity_main.xml / activity_settings.xml: small wiring tweaks to surface the hotspot entry point with the new card style.
  • Platform.java: minor helper added for the hotspot card.
  • versionCode 294, versionName 1.2.36-beta.

Known limitations

  • Per-client bandwidth: not supported (root required to read iptables per-MAC counters).
  • Client list only shows devices currently in the ARP table — peers without recent IP traffic may briefly be missing.
  • Update checker requires internet on the head unit at boot; otherwise the badge appears on the next app launch with connectivity.

Install

Sideload DashCast-v1.2.36-beta-debug.apk. Platform-signed; no uninstall needed when upgrading from v1.2.35-beta.

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.