The export car has no navigation engine of its own (paper #1), so anything a driver sees on the instrument cluster arrives there from an Android app. There is exactly one route on this unit, and it is not the Android Automotive one.
Question#
How does a navigation app put a picture on the Sealion 7’s instrument cluster?
Environment#
Decompiled read-only with jadx 1.5.5 from binaries pulled off the unit: ClusterCommService.apk, /system/framework/ts-framework.jar (reached from an app with <uses-library ts.framework>), ts-androidauto-adapter.jar, CarAdapterService.apk, services.jar and framework-res.apk. Probed over adb with dumpsys package, dumpsys activity processes, dumpsys display and settings get global, plus the app’s own on-device ring log. Nothing here writes a vehicle property.
Observation#
One package does it: com.ts.appservice.cluster, shipped as ClusterCommService.apk under /system/app/ClusterCommService/, sharedUserId="android.uid.system". It declares android.car.permission.CAR_CLUSTER_COMMUNICATION and holds CAR_VENDOR_EXTENSION and WRITE_SETTINGS. It runs in a service process of its own:
Proc #51: vis F/ /IMPF --- 1966:com.ts.appservice.cluster/1000 (service)The bound component is com.ts.appservice.cluster.service.ClusterCoreService. Its onCreate constructs a ClusterCommService, its onBind returns that object — the IClusterCommService.Stub — and its onDestroy calls Process.killProcess(myPid). The service dies clean, and the system re-creates it on the next bind.
Everything a navigation app says to the cluster goes through four AIDL methods, and every command is a Bundle with an int "DataType" in it.
Evidence#
package com.ts.lib.cluster.common; // signatures as decompiled from the stub
interface IClusterCommService {
void sendCommand(int commandId, Bundle bundle);
Bundle requestCacheData(int commandId, int dataType); // null in this build
boolean registerCallback(int commandIds /* bitmask & 0x7f */, IClusterCommCallback cb);
boolean unregisterCallback(int commandId, IClusterCommCallback cb);
}
interface IClusterCommCallback {
void onReceiveCommand(Bundle bundle);
}The vendor’s own navigation app does not bind that AIDL by hand. It goes through a framework class, com.ts.lib.cluster.ClusterCommManager in ts-framework.jar, which an app reaches by declaring <uses-library ts.framework>:
ClusterCommManager.getInstance(ctx)
.tryConnectService() // bindService to ClusterCoreService; retried; binderDied -> reRegService
.isServiceReady()
.registerCallback(cmds, cb) // cmds bitmask; ClusterAdapter/SomeIp register sRegCmds = 127 (all 7 bits)
.sendCommand(commandId, bundle) // commandId is one of the registered command bits (16, 32 used)
.requestCacheData(commandId, dataType)
.unregisterCallback(commandId, cb)The command set#
sendCommand carries its meaning in the bundle. These are the navigation DataTypes, read from SomeIpClusterAdapter (the QNX bridge) and ClusterAdapter (the HAL bridge):
| DataType | Name | Bundle keys | Downstream |
|---|---|---|---|
| 15 | NAVI switch | NaviSource, NaviTbtSwitch (0/1) |
turns the cluster navi feed on/off |
| 16 | NAVI info / TBT | NaviSource, NaviIconTbt, NaviNextRoadDistance, NaviDistanceToDest, NaviToDestTime, NaviIcon*, NaviSpeedLimit, NaviNextRoadName |
the turn card payload |
| 30 | NAVI_SEND_DISPLAY_MODE |
mode |
→ Settings.Global moet_extension_navi_display_mode; an observer forwards it to QNX (msgType 3 / subtype 3) |
| 31 | NAVI_SEND_PRESENTATION_STATE |
msgType=3, subtype=15, mapType=2, state, NaviSource |
forwarded to QNX — the mode selector |
| 32 | NAVI_TBT_INFO |
TBT fields (as DT16) | forwarded to QNX (native gauge strip) |
| 33 / 34 | NAVI request / abandon map focus | mapType |
MapFocusManager stack; owner → Settings.Global moet_extension_navi_focus_map + QNX (subtype 14) |
| 35 | NAVI_SEND_NAVI_STATE |
navi state | forwarded to QNX |
| 36 | INSTRUMENT_UI_STATE |
uiState |
UI-state notify (QNX subtype 5 ↔ back) |
| 37 | NEW_NAVI_TBT_INFO |
msgType=4100 (MsgType.NAVI), subtype (SubTypeNavi), nav fields |
forwarded to QNX (the newer TBT / nav-info path) |
SubTypeNavi, under msgType 4100: LANE_INFO=0, ROAD_INFO=1, IS_SHOW_ON_CLUSTER=2, TOTAL_NAVI_INFOR=3, ELECTRONIC_EYE_INFO=4, DISPLAY_AREA_AND_RESULT=5, COMPASS=6, GPS_SIGNAL=7. Keys on that path include DisplayCluster, HasFocus, NaviDisplayArea, Perspective, IntersectionZoomStatus and ArrivalTime.
What happens after sendCommand returns#
navigation app
ClusterCommManager.sendCommand(commandId, bundle) ts-framework.jar
-> IClusterCommService (AIDL)
-> ClusterCoreService -> ClusterCommService (stub) -> ClusterCommImpl HandlerThread
-> ClusterInteractImpl a registered IClusterCommCallback
-> SomeIpClusterAdapter -> QnxManager.sendMessage(msgType, subtype, null, bundle)
-> SOME/IP
-> QNX instrument cluster: draws the gauges, composites Android display 1 into the "navi" areaThe picture itself is an ordinary Android Presentation on display 1 — local:3, 1920×720, a public physical display carrying FLAG_PRESENTATION and no FLAG_PRIVATE. QNX composites that surface into the cluster’s navi area according to the display mode and the presentation state. Where exactly it lands is paper #11.
No session, no heartbeat, no liveness#
Recovery below the client is the vendor’s, and it is real: ClusterCommManager reconnects on binderDied and onServiceDisconnected (reRegService, retried), and SomeIpClusterAdapter retries the QNX connect and registerCallbacksToQnxMessage every 300–500 ms and re-syncs the mode from the instrument (sendSyncDisplayModeRequestToInstrument, msgType 3 / subtype 13, retried every 300 ms). None of it watches the client.
The probe hook#
ClusterCoreService.dump is not the usual dump. It hands its arguments to processInnerDump(args), which reads a command, a DataType, a msgType, a subtype and key/value pairs, and injects a sendCommand — on non-user builds:
dumpsys activity service <cmd> <DataType> <msgType> <subtype> [k v]…That is a way to exercise the protocol without writing an app.
Interpretation#
This is a message router, not a session protocol. The service holds a bitmask of registered callbacks, fans a Bundle out to all of them, and one of those callbacks happens to be the SOME/IP bridge to the instrument. Nothing in it models “a projection”. registerCallback returns a boolean, not a handle. sendCommand returns nothing at all.
Two things follow, opposite in sign.
The good one: an app that can bind the service and holds CAR_CLUSTER_COMMUNICATION reaches the cluster on the same path the vendor’s own navigation app uses. We found no check on the caller’s package anywhere in the classes we read, and there is no negotiation to fail.
The bad one: because nothing tracks the client, nothing cleans up after it. A projection that dies leaves the display mode where it was and the cluster’s navi area black — the Android surface is gone, but the instrument is still told to show it — until someone sends display mode 1. Keeping a projection alive is entirely the app’s problem, which is why paper #5 and paper #6 exist: on this unit the thing most likely to end your projection is the head unit killing your process.
There is a second route, on domestic units. The same intent goes through the CarAdapter instead of SOME/IP: CarVendorInstrumentManager.ID_INS_NAV_DISPLAY_MODE_S = 557857134 (set) and _R = 557857133 (read), ICabinService transactions setINSNavDisplayMode = 183 and getINSNavDisplayMode = 182, and a full-map handshake over the HAL properties IVI_ID_CLUSTER_FULL_MAP_REQ = 560990722 / _RESP = 560990723 (ReqFullMapData: DesiredMode ORG(0)/FULL(1), App MAP(0)/GUIDANCE_VIEW(1)). Those constants were read out of the export firmware and are recorded for completeness; they were not exercised. The export unit routes through the SOME/IP path above.
Result#
Confirmed: an app reaches the Sealion 7’s instrument cluster through com.ts.appservice.cluster — by binding ClusterCoreService, registering an IClusterCommCallback on a command bitmask, and calling sendCommand(commandId, Bundle) with a DataType in the bundle, which the service forwards over SOME/IP to the QNX instrument. The picture is an Android Presentation on display 1 that QNX composites. The permission is android.car.permission.CAR_CLUSTER_COMMUNICATION.
The rest of the series takes this apart command by command: which display modes exist and how one is selected is paper #8; which endpoint actually feeds the native turn card — DataType 37, not the 15/16 pair the names suggest — is paper #9; and how its roundabout icons are indexed is paper #10.
Limitations#
One car, one market, one head-unit build, on export software. Nothing here has been checked against a domestic unit, another export market or another model year.
Only two of the seven command bits were exercised — 16 and 32, the ones the vendor’s own app uses. What the other five carry is unknown; the mask is read from code.
requestCacheData is reported as returning null from reading the implementation, not from a call observed to come back empty. DataType 103 (NAVI_STATE_2_AVM), which mirrors mapType / naviState / isForeground / mapSource into Settings.System, was read in checkData and never triggered.
DataType 15 and 16 are in the table because the adapter handles them, not because anything sends them: a grep of the export firmware — ClusterCommService.apk, ts-framework.jar, ts-androidauto-adapter.jar, the CarPlay and Android Auto apps — finds no sender of either. They are reachable and unused on this unit.
The domestic HAL path is documented from constants only. It was not called, and no domestic unit was available.
What QNX does with a command is inferred from the values sent and what then appears on the physical instrument. The cluster is composited outside Android, so screencap -d 1 captures only the Android layer and can never confirm the native drawing — that part needs eyes on the panel.
The permission set is the one Sealion 7 Pilot declares (CAR_CLUSTER_COMMUNICATION, CAR_INSTRUMENT_CLUSTER_CONTROL, CAR_MONITOR_CLUSTER_NAVIGATION_STATE, CAR_NAVIGATION_MANAGER, BYDAUTO_INSTRUMENT_*). Which of them the service actually enforces on a sendCommand was not isolated.
One drive, on 29 Aug 2026, showed an ADAS display fault after a cluster mode change; it cleared on a system reboot. Observed once, cause not established, and nothing in this chain explains it.
Reproduction#
Find the service and read its state (our adb guide covers getting a shell on the car):
adb shell dumpsys package com.ts.appservice.cluster
adb shell dumpsys activity processes | grep -i appservice.cluster
adb shell settings get global moet_extension_navi_display_mode
adb shell settings get global moet_extension_navi_focus_map
adb shell dumpsys display | grep -i -A5 'local:3'Pull the binaries and decompile them:
adb shell pm path com.ts.appservice.cluster
adb pull /system/app/ClusterCommService/ClusterCommService.apk
adb pull /system/framework/ts-framework.jar
jadx -d out ClusterCommService.apkClusterCoreService, ClusterCommService, ClusterCommImpl and ClusterInteractImpl are in the APK; ClusterCommManager and SomeIpClusterAdapter are in the jar. Read ClusterCommImpl.registerClient for the & 0x7f mask, and SomeIpClusterAdapter.handleDataToCluster for the DataType switch — that method is the command table.
From an app, declare the permission and the library, then send the one command that is safe to send first:
// AndroidManifest.xml: <uses-permission android:name="android.car.permission.CAR_CLUSTER_COMMUNICATION"/>
// <uses-library android:name="ts.framework" android:required="false"/>
Bundle b = new Bundle();
b.putInt("DataType", 30); // NAVI_SEND_DISPLAY_MODE
b.putInt("mode", 1); // 1 = stock cluster
clusterCommManager.sendCommand(16, b);Mode 1 is the value that hands the cluster back, and nothing on the car will send it for you. Do this parked.