深夜提醒

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

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

高德导航投不上仪表盘,我在车机里翻出一扇没上锁的门

中文 Read in English

我开的是领克 CS11,车机是亿咖通(ECARX)那套方案,Android 4.4.3。原厂只给百度地图,导航的时候仪表盘上会亮出转向箭头、下一条路的名字、还剩多少公里——这块体验我一直挺满意。

但百度地图的路线规划我实在受不了,很早就换成了高德车机版。

代价是:**仪表盘上那块导航区域,从此永远是空的。**每次开车我都得扭头去看中控,仪表上那一格一直黑着,像车上少了个零件。

高德不知道亿咖通这套仪表接口的存在,亿咖通也不会去适配一个第三方导航。两边都没做错什么,就是没人接这根线。我花了两天把它接上了,代码开源在 monkey-wenjun/amap-dim-bridge。这篇想讲讲过程里几个我自己没料到的地方。

仪表这侧:一扇没上锁的门

我先去找仪表盘到底是被谁驱动的。车机 /system/framework/ 下面有个 ecarx.adapterapi.jar,关键在于它挂在 boot classpath 上——这意味着我这个第三方 App 什么都不用做,反射就能拿到里面的类。

里面是个门面:

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

INaviInteraction 上的方法名一眼就懂:updateTurnByTurnArrow(int) 画转向箭头、updateNextGuidancePointName(String) 写路口名、updateDistanceToDestination(int) 报剩余公里、updateETA(int) 报到站时间、notifyDestinationReached() 说到了。

原厂百度地图就是调这套东西把导航送上仪表的。

接下来这一步是我真没想到的。我原以为这种系统级接口多少会有权限校验,就把服务端的 DiServer.onBind() 翻出来看——

它只看 Intent 里的一个 extra:app_type=com.ecarx.navigation。 不校验调用方包名,不校验签名,不要任何 permission。

也就是说这扇门根本没锁。我反射调一下就能往仪表盘上画东西,连那个 jar 都不用参与编译,compileSdk 34 打出来的包在 API 19 上照样跑。

我在这儿停了挺久。一个能直接改驾驶员视野正前方显示内容的接口,靠一个字符串 extra 就把关——这不是给我开的后门,是给原厂百度开的正门,只是没人回头把它锁上。

高德这侧:它一直在喊,只是没人听

仪表这头有入口了,剩下的是数据从哪来。

高德车机版会持续发一条标准广播,action 叫 AUTONAVI_STANDARD_BROADCAST_SEND,普通广播,我注册一下就能收。KEY_TYPE=10001 那一帧就是导航信息,字段给得比我需要的多得多:

ICON / NEW_ICON          转向图标 id
CUR_ROAD_NAME 当前路名
NEXT_ROAD_NAME 下一条路名
SEG_REMAIN_DIS 距下一个路口(米)
ROUTE_REMAIN_DIS 距终点(米)
ROUTE_REMAIN_TIME 剩余时间(秒)
ETA_TEXT / CUR_SPEED / LIMITED_SPEED / TRAFFIC_LIGHT_NUM / CAMERA_DIST ...

导航的时候它每秒推好几条。

我顺手查了一下车机里还有谁在听这条广播,结果有点意外——已经有三个了:ecarx.voiceservice(车机语音)、net.easyconn(手机互联)、com.baidu.che.codriver(DuerOS,用 AmapReceiverWrapper 读状态、反向控制高德)。

但这三个都不往仪表盘推。 推仪表那条路,从头到尾只有原厂百度地图在走。所以我面对的压根不是技术难题,纯粹是这活儿没人干。

三个坑

桥本身没什么可讲的:注册广播接收器,收到 10001 就转手调 INaviInteraction,套一个前台服务常驻。我两天里绝大部分时间花在了下面这三个坑上。

第一个:不要调 notifyTurnByTurnStarted()。

方法名就摆在那儿,语义上就该在导航开始时调一次,我当然调了。然后仪表毫无反应,logcat 里蹦出来一行:

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

我去翻实现才搞明白。客户端 SDK 内部维护一个 Turn 对象,每个 setter 在改字段的同时会设一个 turnState:

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

而服务端 NavigationDimInteraction.turnByTurn() 干的第一件事就是:turnState == 0 直接抛异常。

仪表是靠收到 turnState=1 的帧才切进导航界面的。那个看着最该调的 Started(),恰恰是唯一一个调了必炸的。我现在的代码里根本没有这个调用。

第二个:转向图标码,我第一版全用错了。

XCDimInteractionService.apk 里有一整排常量,E_ICON_NAVI_STRAIGHT=113、E_ICON_NAVI_TURN_L=118、E_ICON_NAVI_TURN_R=117……名字规整得像是专门为 updateTurnByTurnArrow() 准备的。我把高德图标 id 映射到这套值上,兴冲冲下楼上车试。

导航让我左转,仪表盘上给我画了一个大转盘。

我在车里对着那个图标愣了几秒。回头才想明白:那套 E_ICON_NAVI_*(112~138)是车机自己「联动图片」的资源 id,根本不是仪表认的转向码。

真正的值域我是从原厂百度地图的 Dueros-BaiduMapAuto.apk 里挖出来的——NaviDashboardManager 有一张 HashMap<百度图标名, arrowId>,那才是真正喂给 updateTurnByTurnArrow() 的东西:

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

于是我把映射表整个重写成「高德 ICON id → 百度那套 arrowId」。高德那侧的 id 含义,是把 APK 里的 navistate_landback_<十六进制 id>.png 全解出来、一张一张看图对出来的,其中只有 id=3(右转)是我在真车上验证过的,其余按图标形状推。

同一个 API,两套完全不同的值域,一套明晃晃写在系统服务里、一套藏在竞品 App 里。这种情况下文档是不存在的,我只能去比对原厂调用方到底传了什么。

第三个:updateDriveDirection(int) 是空的。

反编译出来方法体就一条 return-void。不用调,调了也没用。

顺手把语音的默认导航也改了

仪表通了之后还剩一个膈应人的地方:我说「导航去公司」,车机语音还是去唤起百度。

默认导航应用这个偏好存在 DuerOS 的私有目录 /data/data/com.baidu.che.codriver/shared_prefs/prefs_amap.xml,不 root 我改不了。但翻 DuerOS SDK 的时候我发现它有个命令叫 fun_navi_set_default_app,服务端实现就一行 saveDefaultNaviApp(NaviApp.valueOf(data)),data 直接传字符串 "Amap" 就行。

问题是这个 SDK(com.baidu.che.codriversdk)内嵌在 XCVoiceService.apk 里,dex 有 3.4MB。想用它就得把 dex 打进我的 App——而这台车是 Android 4.4,Dalvik,没有 multi-dex,3.4MB 的方法数我根本塞不进去。

绕过去的办法是干脆不要 SDK。它底层就是 bindService + AIDL,接口是 ICoDriverService.sendCommand(pkg, cmd, param, data),事务码 1。那我直接 bind 那个服务,自己拿 Parcel 拼一遍:

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);

不用 root,不用打包任何第三方代码,几十行搞定。 这件事之后我对反编译的看法变了一点:看到的接口签名很多时候可以直接当协议用,不必真的去依赖那个库。

有些墙我不 root 就是翻不过去

不是每件事都这么顺。

车机左侧那一竖排快捷栏(Launcher3 的 hotseat)里原本有百度地图。之前我为了验证「能不能卸掉百度」,执行过一次 pm block com.baidu.naviauto。pm unblock 之后应用本身恢复了,但左侧栏那一格永久消失了。

这是我自己操作留下的后遗症。原因查清楚了:hotseat 不是动态的,是 launcher.db 里 container=-101 的固定记录。某个包变得「不可用」时,Launcher3 的加载流程会顺手把对应的 favorite 记录从数据库里删掉;unblock 之后它不会重新灌默认布局。

能不能程序化加回去?LauncherProvider 是 exported 的,但 WRITE_SETTINGS 权限是 signature|privileged,我拿不到。INSTALL_SHORTCUT 在这个 build 里没注册,而且 AOSP 的实现只往桌面加、不往 hotseat 加。

结论:不 root 就只剩一条路——模拟手指长按拖拽。 我写了个脚本用 sendevent 合成手势。这里又踩一个坑:CS11 的触摸设备没有 ABS_MT_SLOT,但有 ABS_MT_TRACKING_ID,而我原来把这两个绑在一起判断,导致 TRACKING_ID 从来没发出去过,内核直接不认这次触摸——sendevent 全程无效,input tap 却一切正常(因为 input 是在 InputManager 层注入的,压根不走 /dev/input)。补上 ABS_MT_TRACKING_ID 就好了。

pm block 在这台车上会留永久后遗症,这条我记下来了。

几个工程细节

  • 节流:高德每秒推好几条,仪表不需要这么快。我的策略是有变化立刻推,没变化最多 1 秒一帧。
  • 看门狗:退出导航后 10001 会停发(实测 40 秒内衰减到 0),但没有明确的「结束」事件。所以我用「20 秒没收到 10001」兜底,自动发 notifyTurnByTurnStopped() 清仪表。不加这个,仪表会一直卡在最后那一帧上。
  • 保活:4.4 上常驻服务得靠前台通知,不然被 LMK 干掉。我还在 manifest 里注册了那条高德广播——进程被杀之后,高德的下一条广播能把它拉起来。
  • IPC 全放单线程队列:车机 CPU 是 i.MX6 / Cortex-A9,弱,我不能在主线程上做 Binder 调用。
  • 界面纯代码写:一个 LinearLayout 加几个 TextView,不写 XML。这只是个给我自己看的调试状态页,能显示仪表连接状态和实时收到的广播就够了。

完整广播 extras 会落到 /sdcard/Android/data/com.awen.amapdim/files/probe.log(2MB 滚动),字段对不上我就翻这个文件。

最后

代码在 github.com/monkey-wenjun/amap-dim-bridge,Apache-2.0。仓库里除了 App,还有我写的三篇逆向笔记:完整链路和字段全量、高德图标码对照表、以及 hotseat 那件事的踩坑记录。

只在我这台领克 CS11 上验证过。换车型、换亿咖通版本,DimInteraction 的类名和方法签名很可能不一样——我在代码里把 ClassNotFoundException 和 NoSuchMethodException 直接显示到状态页上,就是为了让这种情况好排查一点。

也说清楚:这是我改自己车玩的,通过反射和裸 Binder 调用车机上未公开的系统接口,可能影响保修,也可能影响行车安全。要试的话请停车时试。 和领克、亿咖通、高德、百度都没有任何关系,也没拿到任何一家的授权。

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

评论

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

留言反馈

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