RK3566 android12 虚拟GPS定位实现(不改 App、不 root,第三方定位软件直接可用)
适用:RK3566 / RK356X 平台 Android 12。整机没有 GPS 模组(或者有模组但装在天线收不到星的位置),而系统需要稳定上报一个固定坐标。典型场景:广告机、工控一体机、自助终端、展厅样机、车载终端等固定安装的设备。
0. 先说效果和代价
改完之后的表现:
- 任意第三方定位 App 直接可用—— 不需要 root、不需要装 mock 框架、不需要开「允许模拟位置」、App 也不需要声明任何额外权限;
- App 无法从 API 层面分辨—— 因为坐标是从真实的 GNSS provider 链路出来的,不是 Mock Provider,所以
Location.isFromMockProvider()返回false; - 开机即有定位,不依赖 adb、不依赖任何人工操作(这一点对出货设备是决定性的)。
代价也说清楚:
- 要改系统源码、要重新编译烧录;
- 坐标是静态的,不会移动,所以轨迹类应用(运动记录、导航)行为会不正常;
- 坐标存在
persist属性里,恢复出厂设置会被清掉。
和另外三种做法比一下
同样是「让设备上报一个假坐标」,路子差别很大,选错会白干:
| 方案 | 改动量 | 需要 adb / root | 开机即生效 | 所有 App 无感可用 | 适用场景 |
|---|---|---|---|---|---|
| A. 打 GNSS HAL 补丁(本文) | 改系统源码,要刷机 | 不需要 | 是 | 是 | 出货整机、固定点位设备 |
B.cmd locationTest Provider(Android 11+ 官方接口) | 零改代码 | 需要(每次开机配一次) | 否,重启失效 | 是,但是否被打 mock 标记需按版本实测 | 研发调试、样机演示 |
| C. 开发者选项 + Mock 定位 App | 零改代码 | 不需要 | 需人工开开关 | 否,会被isFromMockProvider()识别,部分 App 直接拒绝 | 个人测试 |
| D. 在自家 App 里注入坐标 | 改 App | 不需要 | 是 | 只对这一个 App 有效 | 只调自己应用 |
B 方案的具体用法(不同 Android 版本子命令名可能有细微差异,以adb shell cmd location -h为准):
adb shell cmd location providers add-test-provider gps_test adb shell cmd location providers set-test-provider-location gps_test--location22.5431,114.0579--accuracy8adb shell cmd location providers set-test-provider-enabled gps_testtrue为什么出货设备必须走 A:B 依赖 adb,且重启失效 —— 设备到了客户手里,没人会去跑 adb。A 把坐标固化进系统,开机就在。
一、原理:为什么必须从 HAL 这一层下手
Android 的定位链路是分层的:
第三方 App(地图 / 打卡 / 定位测试工具) ↓ LocationManager / LocationManagerService (在这里判断 provider 是否可用、位置是否 mock) ↓ GnssLocationProvider ↓ IGnss HAL 服务(android.hardware.gnss@1.0) ↓ 厂商 HAL 实现 ←── 本文就改在这里 ↓ /dev/ttyXXX(真实 GPS 模组,本方案没有这一步)关键在于mock 标记是在 HAL 之上、由 Mock Provider 注入时才打上的。一旦跳过 HAL 这一层、直接在厂商实现里产出一个 fix,上层全链路都会把它当成「真实卫星定位」——LocationManagerService看到的只是一个普通的 GNSS 位置更新,没有任何可疑字段。
这就是「实测第三方任意定位软件均可使用」的技术原因,也是这个方案和装 mock App 的本质区别。
下面按顺序做三步:让系统认到 GPS 能力 → 把 HAL 跑起来 → 让 HAL 上报一个固定坐标。
二、第一步:让系统认到 GPS 这个能力
2.1 在 compatibility matrix 里放开 gnss 1.0
Android 12 引入了 VINTF compatibility matrix:系统会校验「设备实际提供的 HAL」是否落在「框架允许的范围」内。矩阵里原本只声明了2.0-1,而 TD-GNSS-HAL 提供的是1.0,所以要把 1.0 加进允许集合。
--- a/hardware/interfaces/compatibility_matrices/compatibility_matrix.6.xml +++ b/hardware/interfaces/compatibility_matrices/compatibility_matrix.6.xml @@ -225,6 +225,7 @@ </hal> <hal format="hidl" optional="true"> <name>android.hardware.gnss</name> + <version>1.0</version> <version>2.0-1</version> <interface> <name>IGnss</name>2.2 在 manifest.xml 里注册 HAL 服务
matrix 声明的是「允许提供哪些版本」,manifest 声明的是「实际提供了哪些版本」。两个文件必须配套。
<manifestversion="1.0"type="device"><halformat="hidl"><name>android.hardware.gnss</name><transport>hwbinder</transport><version>1.0</version><interface><name>IGnss</name><instance>default</instance></interface></hal></manifest>⚠️两处的<version>必须一致。matrix 里写了1.0而 manifest 里写别的版本(或反过来),会出现编译期校验失败或服务起不来的问题。这是移植 GNSS 时最常犯的错,改完养成顺手对一遍的习惯。
2.3 声明 GPS feature,并拷贝权限声明文件
android.hardware.location.gps.xml这份文件声明了FEATURE_LOCATION_GPS等定位相关能力。App 调PackageManager.hasSystemFeature(FEATURE_LOCATION_GPS)判断设备有没有 GPS 时,读的就是它。
这一步容易漏:不拷这个文件,即使 HAL 已经正常跑起来、框架层也能拿到坐标,仍然会有一部分 App 认为「这台设备没有 GPS」而直接禁用定位功能。
BOARD_HAS_GPS := true ifeq ($(BOARD_HAS_GPS),true) PRODUCT_COPY_FILES += \ frameworks/native/data/etc/android.hardware.location.gps.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.location.gps.xml endif用一个BOARD_HAS_GPS开关把这段包起来,是为了同一份 device 配置能同时支持「带 GPS 的机型」和「不带 GPS 的机型」,不用改代码分支 —— 多机型项目建议保留这个写法。
三、第二步:移植 TD-GNSS-HAL
📌待补充—— 这里请把你当时实际移植的步骤和补丁贴进来(源码/so 放哪个目录、
PRODUCT_PACKAGES加了哪个 service、配置文件怎么改)。下面是我按通用流程整理的清单,可以对照着检查和补全。
TD-GNSS-HAL 是泰斗(TD1030 系列)GNSS 芯片的 HAL 实现,对外提供android.hardware.gnss@1.0的 HIDL 接口。移植一般涉及四件事:
- 放置源码 / 预编译库:按项目规范放到
hardware/或vendor/下,注意Android.mk/Android.bp里的模块名和路径要与后面的PRODUCT_PACKAGES对应。 - 加入编译:
PRODUCT_PACKAGES += android.hardware.gnss@1.0-service(或厂商自带的 service 名),否则烧完机lshal里根本看不到这个服务。 - 产品配置:串口节点名、波特率之类的配置项(常见于
tdgnss.conf之类的文件)。本文方案虽然不用真实串口,但配置文件里的必要项不能缺—— 如果 HAL 在初始化阶段因为找不到配置/节点而提前return,后面你注入的 fix 永远发不出去,现象就是「代码都改对了但 App 就是没定位」。 - SELinux:HAL 进程运行在
hal_gnss域,它需要能读取persist.gps.*这几个属性、以及访问它自身需要的节点。属性如果没有在property_contexts里登记,读属性会被策略拦掉。
移植完先验证 HAL 是否真的起来了,再往下改代码:
# 确认 HIDL 服务已注册adb shell lshal|grep-ignss# 看 GNSS 相关日志adb logcat-ball|grep-iE"gnss|gps"# 看 SELinux 拦截adb shelldmesg|grep-iavclshal里看不到android.hardware.gnss@1.0::IGnss/default的话,先别动上报代码 —— 问题一定在第 1~4 步里。这一步能省掉大量「改了半天没反应」的排查时间。
四、第三步:改上报流程,从属性里读坐标
改动位置:tdgnss.c的nmea_reader_parse。思路是把「从串口解析 NMEA 报文」换成「从系统属性读经纬度」,其余上报路径完全不动。
4.1 读取自定义属性
charpropbuf_latitude[PROPERTY_VALUE_MAX];property_get("persist.gps.latitude",propbuf_latitude,"0");charpropbuf_longitude[PROPERTY_VALUE_MAX];property_get("persist.gps.longitude",propbuf_longitude,"0");charpropbuf_latitudeHemi[PROPERTY_VALUE_MAX];property_get("persist.gps.latitudeHemi",propbuf_latitudeHemi,"N");charpropbuf_longitudeHemi[PROPERTY_VALUE_MAX];property_get("persist.gps.longitudeHemi",propbuf_longitudeHemi,"E");属性名分开用的是persist.前缀,运行时可写,setprop后重启生效 —— 这样同一套固件可以装到不同城市的不同设备上,现场只改属性、不用重编。
4.2 填充 fix 结构体
r->sv_status.num_svs=6;//卫星数量r->sv_status_changed=1;//状态回调r->sv_status.used_in_fix_mask|=(1<<(r->sv_status.num_svs));r->fix.accuracy=8.0;//精度r->fix.timestamp=1685782786;//时间戳r->fix.latitude=strtod(propbuf_latitude,NULL);r->fix.longitude=strtod(propbuf_longitude,NULL);r->fix.flags|=GPS_LOCATION_HAS_LAT_LONG;r->fix.flags|=GPS_LOCATION_HAS_ACCURACY;D("lat : %f lon : %f ",r->fix.latitude,r->fix.longitude);4.3 四处必须核对的细节(这一节比代码本身重要)
上面这段是我原始版本的做法,能跑通,但有四个点建议每台设备都对着核一遍,它们对应的是四类不同的「莫名其妙不生效」:
①used_in_fix_mask的位运算
r->sv_status.used_in_fix_mask|=(1<<(r->sv_status.num_svs));used_in_fix_mask是「哪几颗卫星参与了定位」的位图,bit i 对应sv_list[i]。当num_svs = 6时,有效下标是 0~5,掩码应该是0x3F;而1 << 6得到的是0x40,指向的是第 7 颗 —— 一个并不存在的卫星。
后果:一部分 App 会遍历0 ~ num_svs逐位检查「这颗星星用没用上」,结果全部为 0,判定为「没有卫星参与定位」,从而丢弃这个 fix。
建议改成按位数生成掩码:
uint32_tn=r->sv_status.num_svs;r->sv_status.used_in_fix_mask=(n>=32)?0xFFFFFFFFu:((1u<<n)-1);用=而不是|=,避免多次进入这个函数时掩码被越叠越乱。
② 半球标志读出来了,但没有参与换算
propbuf_latitudeHemi/propbuf_longitudeHemi这两个值在上面只被property_get读出,后面没有任何地方用到它们。而r->fix.latitude/longitude是带符号的十进制度—— 南纬、西经必须取负值。
也就是说当前代码在南北回归线以外的地区会直接定错位置:读进来S却当成北纬,坐标会跑到地球另一侧的对称点。
要支持全球坐标,补上换算:
doublelat=strtod(propbuf_latitude,NULL);doublelon=strtod(propbuf_longitude,NULL);if(propbuf_latitudeHemi[0]=='S'||propbuf_latitudeHemi[0]=='s')lat=-lat;if(propbuf_longitudeHemi[0]=='W'||propbuf_longitudeHemi[0]=='w')lon=-lon;r->fix.latitude=lat;r->fix.longitude=lon;如果项目只在国内使用,那更简单 —— 直接不用这两个属性,省得后人误以为它生效了。
③ 时间戳是硬编码的
r->fix.timestamp=1685782786;//时间戳固定值意味着这个 fix 的时间永远停在那一刻,不会前进。地图类应用会显示一个恒定的「最后更新时间」,记录轨迹的应用会因为时间不递增而丢弃后续点。
建议改成取实时时间,并注意单位要和本文件里其他位置对timestamp的处理保持一致(不同 SDK 版本里这个字段存在「秒」和「毫秒」两种写法,改之前先搜一下同文件里其他地方的赋值方式,照抄单位):
structtimevaltv;gettimeofday(&tv,NULL);r->fix.timestamp=(int64_t)tv.tv_sec*1000+tv.tv_usec/1000;④ 属性默认值"0"会让设备定位到 (0, 0)
纬度0、经度0在几内亚湾,是客户端地图上经典的「空岛」位置。没配置过属性的机器会老老实实报到这里。
给一个合理的兜底值(比如项目所在地),或者加个判断:属性没设置就不上报GPS_LOCATION_HAS_LAT_LONG,让 App 表现为「暂时无定位」而不是「定位到了非洲西海岸」。
4.4 属性配置与 SELinux
坐标用setprop写进去:
adb shell setprop persist.gps.latitude22.5431adb shell setprop persist.gps.longitude114.0579adb shell setprop persist.gps.latitudeHemi N adb shell setprop persist.gps.longitudeHemi E# 确认写进去了adb shell getprop persist.gps.latitude两个提醒:
persist.属性落盘在/data/property/下,所以/data必须先挂载好—— 如果 HAL 启动得比/data挂载还早,读到的就是默认值,现象和「属性没设」一模一样;- HAL 进程读属性同样受 SELinux 约束。
setprop能写不代表 HAL 能读,新属性名如果没有在property_contexts里登记,读侧可能被拦掉。排查方法还是看dmesg | grep avc。
五、验证
改完刷机后按顺序看:
# 1) 写入坐标adb shell setprop persist.gps.latitude22.5431adb shell setprop persist.gps.longitude114.0579adb shellreboot# 2) HAL 是否起来了adb shell lshal|grep-ignss# 3) 代码里那行 D() 打出来的坐标对不对adb logcat-ball|grep"lat :"# 4) 框架层实际吐出来的位置adb shell dumpsys location# 5) 设备有没有宣称自己支持 GPSadb shell pm list features|grep-ilocation最后一步就是装一个第三方定位软件(或者地图 App)看实际落点。
原文的实测截图(保留作为证据):
六、几个必须知道的限制
- 坐标是静态的。速度、方位角恒为 0,位置永远不动。导航类 App 会一直提示「重新规划路线」,运动记录类 App 会记不到里程。这是方案本身的边界,不是 bug。
- 部分 App 会做合理性交叉校验。除了看 GNSS,还会拿基站、Wi-Fi、IP 归属地做比对。如果你上报的坐标和设备的实际网络环境差得太远,对方平台侧仍有可能判定异常 —— 换的是 HAL,换不掉服务端的风控。
- 恢复出厂会丢。
persist属性在/data下,恢复出厂即被清空,坐标掉回默认值。要求「出厂永久固定」的场景,需要把兜底值直接写在代码里。 - 合规提示。这个方案的正确用途是自有设备的测试、演示、固定点位上报。不要用来伪造考勤打卡、或者绕过第三方平台的定位限制—— 那是另一回事,风险和责任都不在技术层面。
七、小结
整个方案的核心只有一句话:在 GNSS HAL 这一层直接把 fix 造出来,绕过真机卫星,让框架和所有上层 App 都把它当成真实定位。
三步的顺序不能乱:
- 先让系统认到 GPS 能力—— matrix + manifest 的版本要一致,权限声明文件不能漏,否则 HAL 起来了 App 也当你不支持;
- 再把 HAL 跑起来—— 用
lshal确认服务注册成功,这一步不过就别改上报代码; - 最后改上报流程—— 从属性读坐标,同时核对
used_in_fix_mask、半球符号、时间戳、默认值这四处。
第 2 步和第 3 步之间那个「HAL 初始化失败导致 fix 永远发不出去」的坑,是这类移植里花时间最多的地方 —— 现象和「属性没读到」几乎一样,但根因完全不同,lshal一查就能分流。
深耕 Android 嵌入式与 RK 平台系统级开发多年,日常处理显示、定位、存储、开机时序这类系统级问题。如果你在做 RK 平台整机项目时遇到类似的系统级疑难,欢迎留言或私信交流。