深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

Amap Navigation Wouldn't Reach My Dashboard, So I Found an Unlocked Door in the Car

English 阅读中文版

My car is a Lynk & Co CS11. The head unit is an ECARX build running Android 4.4.3, and it ships with Baidu Maps. When Baidu is navigating, my instrument cluster lights up with the turn arrow, the next street name and the remaining kilometres — I’ve always been happy with that part.

But I can’t stand Baidu’s route planning, so I switched to the car edition of Amap (Gaode) a long time ago.

The cost: the navigation area on my instrument cluster has been blank ever since. Every time I drive I have to turn my head to the centre console, and that one panel stays dark — like the car is missing a part.

Amap has no idea the ECARX cluster API exists, and ECARX was never going to adapt to a third-party navigator. Neither side did anything wrong; nobody just ran the cable. I spent two days running it myself. The code is open source at monkey-wenjun/amap-dim-bridge. This post is about the parts of that process I didn’t see coming.

The cluster side: a door that was never locked

I started by finding out what actually drives the instrument cluster. Under /system/framework/ there’s a jar called ecarx.adapterapi.jar. The important detail is that it sits on the boot classpath — which means my third-party app doesn’t have to do anything special; reflection alone gets me the classes inside it.

Inside is a facade:

com.ecarx.xui.adaptapi.diminteraction.DimInteraction
.create(Context)
.getNaviInteraction() // -> INaviInteraction

The methods on INaviInteraction are readable at a glance: updateTurnByTurnArrow(int) draws the turn arrow, updateNextGuidancePointName(String) writes the street name, updateDistanceToDestination(int) reports kilometres left, updateETA(int) reports the arrival time, notifyDestinationReached() says I’ve arrived.

Stock Baidu Maps uses exactly this to put navigation on the cluster.

The next step is the one I genuinely didn’t expect. I assumed an interface this system-level would have some permission check, so I pulled up the server-side DiServer.onBind() —

It looks at one Intent extra: app_type=com.ecarx.navigation. No caller package check, no signature check, no permission required.

So the door simply isn’t locked. I can reflect my way in and draw on the instrument cluster, and I don’t even need that jar at compile time — a package built against compileSdk 34 still runs fine on API 19.

I sat with that for a while. An interface that can rewrite what’s displayed directly in the driver’s line of sight, gated by a single string extra. This isn’t a backdoor left for me — it’s the front door ECARX built for stock Baidu, and nobody ever walked back around to lock it.

The Amap side: it has been shouting all along

With a way into the cluster, what’s left is where the data comes from.

Amap’s car edition continuously sends a standard broadcast with the action AUTONAVI_STANDARD_BROADCAST_SEND. It’s a plain broadcast, so registering a receiver is enough for me to get it. The frame with KEY_TYPE=10001 is the navigation info, and it gives me far more fields than I need:

ICON / NEW_ICON          turn icon id
CUR_ROAD_NAME current street
NEXT_ROAD_NAME next street
SEG_REMAIN_DIS metres to next manoeuvre
ROUTE_REMAIN_DIS metres to destination
ROUTE_REMAIN_TIME seconds remaining
ETA_TEXT / CUR_SPEED / LIMITED_SPEED / TRAFFIC_LIGHT_NUM / CAMERA_DIST ...

While navigating it pushes several of these per second.

I also checked who else on the head unit listens to this broadcast, and the answer surprised me — three apps already do: ecarx.voiceservice (the car’s voice assistant), net.easyconn (phone projection), and com.baidu.che.codriver (DuerOS, which uses an AmapReceiverWrapper to read state and control Amap).

None of the three push to the cluster. The cluster path has only ever been walked by stock Baidu Maps. So what I was facing was never a hard technical problem — it was just work nobody had claimed.

Three traps

The bridge itself is nothing to talk about: register a broadcast receiver, and on KEY_TYPE=10001 turn around and call INaviInteraction, all inside a foreground service. Most of my two days went into these three traps instead.

One: don’t call notifyTurnByTurnStarted().

The name sits right there, and semantically it looks like the thing to call once when navigation begins — so of course I called it. The cluster did nothing, and logcat gave me one line:

E/JavaBinder: java.lang.RuntimeException: Navigation state is not correct.

I had to read the implementation to understand it. The client SDK keeps an internal Turn object; every setter mutates a field and also sets a turnState:

Call turnState
notifyTurnByTurnStarted 0
updateTurnByTurnArrow / updateDistanceTo* / updateETA 1
notifyDestinationReached 2
notifyTurnByTurnStopped 3

And the first thing the server-side NavigationDimInteraction.turnByTurn() does is throw when turnState == 0.

The cluster only switches into its navigation screen when it receives a frame with turnState=1. The one method that looks most obligatory is the single one guaranteed to blow up. There is no such call anywhere in my code now.

Two: I got every turn-icon code wrong in my first version.

XCDimInteractionService.apk contains a tidy row of constants: E_ICON_NAVI_STRAIGHT=113, E_ICON_NAVI_TURN_L=118, E_ICON_NAVI_TURN_R=117… the names are so neat they look purpose-made for updateTurnByTurnArrow(). I mapped Amap’s icon ids onto those values and went out to the car feeling good about it.

The route told me to turn left. The cluster drew me a big roundabout.

I sat in the car staring at that icon for a few seconds. It took me a while to work out why: that whole E_ICON_NAVI_* range (112–138) is the head unit’s own “linked image” resource ids, not the arrow codes the cluster understands at all.

I dug the real value space out of stock Baidu Maps’ Dueros-BaiduMapAuto.apk — NaviDashboardManager holds a HashMap<baidu icon name, arrowId>, and that is what actually gets fed into updateTurnByTurnArrow():

turn_left   -> 102      turn_right  -> 103      turn_front -> 109
turn_back -> 108 turn_ring -> 111 turn_dest -> 115

So I rewrote the mapping table entirely as “Amap ICON id -> Baidu’s arrowId space”. For the Amap side, I extracted every navistate_landback_<hex id>.png out of the APK and worked out what each one meant by looking at the pictures one by one. Only id=3 (turn right) is something I verified on the real car; the rest I inferred from the icon shapes.

Same API, two entirely different value spaces — one sitting openly in a system service, one hidden inside a competitor’s app. There is no documentation in a situation like this, so all I can do is compare against what the first-party caller actually passes.

Three: updateDriveDirection(int) is empty.

The decompiled body is a single return-void. Don’t call it; it does nothing.

While I was at it: changing the voice assistant’s default navigator

Once the cluster worked, one thing was still bothering me: when I say “navigate to the office”, the voice assistant still wakes up Baidu.

That default-navigator preference lives in DuerOS’ private directory, /data/data/com.baidu.che.codriver/shared_prefs/prefs_amap.xml — without root I can’t change it. But while reading through the DuerOS SDK I found a command called fun_navi_set_default_app, whose server-side implementation is one line: saveDefaultNaviApp(NaviApp.valueOf(data)). Passing the literal string "Amap" in data is enough.

The problem is that this SDK (com.baidu.che.codriversdk) is embedded inside XCVoiceService.apk and its dex is 3.4 MB. Using it would mean bundling that dex into my app — and this car runs Android 4.4, Dalvik, no multi-dex. There’s no way 3.4 MB of methods fits.

The way around it is to not want the SDK at all. Underneath it’s just bindService plus AIDL, and the interface is ICoDriverService.sendCommand(pkg, cmd, param, data) at transaction code 1. So I bind that service myself and pack the Parcel by hand:

out.writeInterfaceToken("com.baidu.che.codriver.ICoDriverService");
out.writeString(pkg); out.writeString("map.tool");
out.writeString("fun_navi_set_default_app"); out.writeString("Amap");
binder.transact(1, out, reply, 0);

No root, no third-party code bundled, a few dozen lines. This changed how I think about decompilation a little: an interface signature I read out of a decompiler can often be used directly as a protocol, with no dependency on the library at all.

Some walls I can’t get past without root

Not everything went that smoothly.

The vertical shortcut bar on the left of the screen (Launcher3’s hotseat) used to hold Baidu Maps. Earlier, while testing whether Baidu could be removed, I ran pm block com.baidu.naviauto. After pm unblock the app itself came back — but that hotseat slot was gone for good.

This is damage from my own operation. I tracked the cause down: the hotseat isn’t dynamic. It’s a set of fixed rows in launcher.db with container=-101. When a package becomes “unavailable”, Launcher3’s loading path deletes the matching favorite row from the database; on unblock, it never re-seeds the default layout.

Can I add it back programmatically? LauncherProvider is exported, but WRITE_SETTINGS is signature|privileged and I can’t get it. INSTALL_SHORTCUT isn’t registered in this build, and the AOSP implementation only adds to the workspace, never to the hotseat.

Conclusion: without root there’s exactly one path left — fake a finger doing a long-press drag. I wrote a script that synthesises the gesture with sendevent. Another trap here: the CS11’s touch device has no ABS_MT_SLOT but does have ABS_MT_TRACKING_ID, and I had originally coupled the two in my checks, so TRACKING_ID never got emitted and the kernel refused to treat it as a touch at all — every sendevent was silently dropped, while input tap kept working perfectly (because input injects at the InputManager layer and never touches /dev/input). Emitting ABS_MT_TRACKING_ID fixed it.

pm block leaves permanent damage on this car. I’ve noted that down.

A few engineering details

  • Throttling. Amap pushes several frames a second; the cluster doesn’t need that. My rule is push immediately on change, otherwise at most one frame per second.
  • Watchdog. After I stop navigating, 10001 simply goes quiet (measured: decays to zero within 40 s) and there’s no explicit “ended” event. So I fall back to “no 10001 for 20 seconds” and send notifyTurnByTurnStopped() to clear the cluster. Without it the cluster stays frozen on the last frame forever.
  • Staying alive. On 4.4 a long-lived service needs a foreground notification or the LMK kills it. I also register that Amap broadcast in the manifest — so if the process is killed, Amap’s next broadcast starts it again.
  • All IPC on a single-thread queue. The CPU is an i.MX6 / Cortex-A9. It’s weak. I can’t make Binder calls on the main thread.
  • UI written purely in code. One LinearLayout and a few TextViews, no XML. This is a debug status page for me to look at, so showing the cluster connection state plus the broadcasts arriving live is enough.

Every broadcast’s full extras are appended to /sdcard/Android/data/com.awen.amapdim/files/probe.log (rolling at 2 MB). When a field doesn’t match my expectations, that file is where I look.

Finally

Code: github.com/monkey-wenjun/amap-dim-bridge, Apache-2.0. Besides the app, the repo has three reverse-engineering write-ups I did: the full call chain and complete field list, the Amap icon code table, and the post-mortem of the hotseat business.

Verified on my Lynk & Co CS11 only. On a different model or a different ECARX build, the DimInteraction class and method signatures may well differ — that’s why I surface ClassNotFoundException and NoSuchMethodException directly on the status page, to make that case a bit easier to diagnose.

Let me be explicit too: this is me tinkering with my own car, driving undocumented system interfaces through reflection and raw Binder. It may void my warranty and it may affect driving safety. If you try it, do it parked. No affiliation with, and no authorisation from, Lynk & Co, ECARX, Amap or Baidu.

文章作者:阿文
文章链接: https://www.awen.me/post/38b5df1e.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客!

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️