附录
最后更新于:2022-04-02 06:06:29
[-->笔者发给吴劲良先生的邮件]
吴兄:
我有个问题想和你深层次讨论,就是关于这本书的定位。先说说我的看法。
1)对于初学者(就是完全没有Wi-Fi、NFC、GPS经验的人),这本书肯定是入门书,但是它
的难度比普通意义上认为的入门书难。
2)对于中级学者,这些人定位在1~2年或者有过实际修改bug的经验,但是缺乏全局理解
的人,这本书也合适。不过,可能有部分内容对他们来说比较简单。另外,关于NFC和GPS的知
识,从统计情况来看,NFC和GPS的问题非常少。从面试情况来看,对NFC芯片datasheet的了解
(GPS应该是没有这方面公开的资料)也很重要。不过本书没有考虑NFC、GPS以及Wi-Fi HAL
层的内容。一方面我感觉Wi-Fi驱动层和协议结合非常紧密,有点钻研精神的读者在本书基础
上,再有一些驱动经验就可以搞定。而NFC HAL层未来发展趋势可能会和wpa_supplicant一
样,即不会出现NXP、Broadcom这样太过特定的内容。GPS一般不太可能让外人看到驱动的代
码。我专门看过QC开源项目CODEAurora[^①]中GPS的HAL代码,它是C/S架构的,只有Client端内
容,而且都是简单地发些命令,然后接收回复,没有核心技术。
3)对于高级学者,经验和理论知识都比较到位的人,本书可以当做参考书来看,不过内容
相对会显得浅显。
一方面,昨天和Eva沟通后,觉得本书没有太多实际经验,确实如此。我自己定位这本书还
是想打通整个知识面,实际经验的话,需要理论联系实践。现在很多工程师只有实践,没有理
论,或者理论关注较少。另一方面,如果专门讲实践,这种书反而价值不高,因为可操作性太
低。它不像网管类的书籍,一步一步跟着做就行了。
这是我对本书定位的一些看法,吴兄,你能否从一线工程师,培养新人等多方面讲下你的
感受?不足之处也提出来哈。
最后,写完这本书后,我感觉在Wi-Fi、NFC和GPS这几块,核心都是芯片厂商做好了,我
们唯一可做的就是改改bug,攒足实战经验,似乎可发挥的地方非常少(NFC CE模式还有很多
发挥空间,尤其是安全交付解决方案之类的)。吴兄,对这个问题又怎么看呢?
诚挚欢迎吴兄的金玉良言!
邓凡平
[-->吴劲良先生的回复]
邓兄:
正如邓兄提到,本书对不同知识深度的学者而言,会有各自的收获,可引初学者入门,可
给中级学者问题分析的线索,可给高级学者一个知识思索的机会(对比自己的理解和补充知
识),本书能起抛砖引玉的作用,不同读者从中收获多少还得看个人,多思考的读者还可以从
书中学习到邓兄分析问题的思路,反思如何提升自己的搜索技巧。
本书以理论分析为主,没有具体问题的解答,但是我觉得够了。这不是一本Q&A的书,
Wi-Fi、NFC、GPS这三大部分,Android涉及的主干支知识都有,读者可以选择性地深入分析,
每个人对知识点的追求都不一样,很难满足所有人的需求。就个人而言,我会对Android Wi-Fi
的休眠策略、Location的网络定位感兴趣,跟实际工作遇到的问题相关。
“NFC和GPS问题非常少”,这会跟功能模块是否被广泛使用和应用的广度有关,被使用多
了可能会暴露问题多些,应用场景多也会促使功能的开发,自然会引出新问题。GPS HAL的代
码各厂家都不提供,Broadcom、MTK、RDA只是提供so,有可能是涉及核心技术,估计是一些
Command的实现,GPS一般是UART接口,UART只负责上层与模组的数据通信。
对于负责无线模组的新人,我对他们工作的安排是:先做功能的验证测试,让他从测试中
加深对功能点的理解,知道哪些点是容易出问题;然后会给一些已经调试好的模组让其单独调
试,目的是熟悉调试一个模块需要做哪些工作;最后会渐渐地让其承担一些实际任务。学习的
安排是:学习NL802.11,USB、SDIO、UART和I2C等模组接口驱动的分析,然后会从内核开始学
习,如:Wi-Fi driver->netd->wpa_supplicant->HAL->framework,Android会安排一些核心知识点
的学习,主要是理清工作的机制。最终是希望新人在头脑中有一幅Android网络结构图,并能
将其画出来。
由于需要先确保相关的外围模组能配合主控使用,这也决定平常无线工作的重点会在模
组的移植调试上,涉及的内核驱动的调试较多,现在Android做得越来越完善,大问题很少,小
问题还是有,但解决起来还好(Android 4.4上Wi-Fi目前测试出原生代码有几个bug,较严重的
一个是在关闭Wi-Fi时没关闭supplicant创建的socket,每次打开Wi-Fi时又创建,socket打开个数
累积超过65536时,后续操作将失败)。
无线模块Wi-Fi、BT、NFC和GPS,核心的技术是在芯片厂,而且是在芯片设计中,driver的
编写只是其中的很小一部分,即使是相对复杂的Wi-Fi driver,投入两三个人,花两个月的时间
把driver写出来完全没问题。这个我也认同发挥的地方很少,除非是从应用角度去开发新的功
能或做一些功能创新。但从工作的角度看,要把这些无线模块支持好,也很容易,调一款新的
Wi-Fi就像在弄一个小系统,需要把系统调稳,没有bug并可以达到量产的标准,往往会耗上一
两个月的时间。虽然发挥的地方少,但当前看来这方面技术人员的需求还是挺大的。
吴劲良
[^①]:https://www.codeaurora.org/ , 此处可下载高通参考设计(QC Reference Design)的代码。
';
9.4.2 参考资料说明
最后更新于:2022-04-02 06:06:26
**1、概述**
[1] http://baike.baidu.com/subview/152851/5072513.htm?fromId=152851&from=rdtself
百度百科对LBS的介绍,读者可了解其“产生背景”一节的内容。
**2、GPS基础知识**
GPS基础知识这一节主要的参考资料是GPS Essentials of Satellite Navigation Compendium一书。它是笔者找到的对软件工程师而言关于GPS知识最全面和最通俗易懂的一本书。该书电子版可参考http://www.docin.com/p-67411894.html。
**3、坐标系介绍**
[2] GPS Essentials of Satellite Navigation Compendium第2章“Coordinate systems”
对GPS坐标系相关知识的介绍,读者可先略过投影那几节的内容。
[3] http://principles.ou.edu/earth_figure_gravity/geoid/
对GPS高度和海拔高度的解释。
**4、时间系统**
[4] 《GPS基本原理及其Matlab仿真》第3.2节“GPS时间系统”
时间系统是本章最难讲解的部分了,读者可在本章基础上再做深入学习。
[5] http://www.doc88.com/p-241652413475.htmlhttp://www.doc88.com/p-241652413475.html
GPS与民用时间的转换
[6] http://www.hko.gov.hk/gts/time/basicterms-localtimec.htm
香港天文台官方网站对本地时间的介绍
**5、卫星轨道等知识**
[7] GPS Essentials of Satellite Navigation Compendium第3章“Foundations of satellite technology”
**6、GPS基础知识**
[8] http://www.gps.gov/systems/gps/
GPS系统的美国官方网站,内容非常详细和生动,建议读者仔细阅读。
[9] http://www.gps.gov/systems/gps/space/
GPS官方网站对GPS空间段建设历史的描述。
[10] http://en.wikipedia.org/wiki/List_of_GPS_satellite_launches
维基百科对GPS卫星发射规划的介绍。
[11] GPS Essentials of Satellite Navigation Compendium第4章“GNSS technology: the GPS example”
**7、GPS通信频段介绍**
[12] http://www.gps.gov/systems/gps/modernization/civilsignals/#L2C
[13] http://www.gps.gov/systems/gps/modernization/civilsignals/#L5
GPS官方网站对L2C和L5频段的介绍。
[14] Interface Specification GPS 705C
该资料是GPS官方规范,用于定义空间段和地面控制段以及用户段通过L5频段交互的接口。如果不需要对L5做进一步了解的话,读者可略过它。
GPS官方规范的下载地址为http://www.gps.gov/technical/
**8、GPS信号介绍**
[15] Understanding the GPS:Introduction to the GPS第2部分“Basic Signal Structure And Error”
这本书是GPS Essentials of Satellite Navigation Compendium一书的升级参考书籍,讲解得非常透彻,覆盖面也比较广,建议读者深入阅读。
**9、GPS导航电文介绍**
[16] Interface Specification GPS 200G
该资料是GPS官方规范,用于定义空间段和地面控制段以及用户段通过L1及L2频段交互的接口。这应该是GPS的核心规范了,建议读者详细阅读。其中,附录II,III和IV详细介绍了GPS卫星发送导航电文的组成及相关参数。
**10、定位计算相关知识**
[17] GPS Essentials of Satellite Navigation Compendium第6章“Calculating position”
[18] http://www.doc88.com/p-797227641283.html
“GPS测速精度研究及应用”,一篇硕士学位论文,读者权当参考。
[19] http://wenku.baidu.com/view/3541a8a0b0717fd5360cdcc4.html
中国移动A-GPS终端技术规范。
[20] http://www.gpsinformation.org/dale/why12.htm
该资料对多channel功能可提升GPS接收器定位速度的原因进行了介绍。
**11、NMEA-0183和GPX**
[21] http://www.doc88.com/p-992198901288.html
NMEA Reference Manual. NMEA规范文档,感兴趣的读者可详细阅读它。
[22] http://www.topografix.com/gpx.asp
GPX官方网站。GPS格式比较简单,读者可轻松学会它。
[23] http://en.wikipedia.org/wiki/GPS_eXchange_Format
维基百科对GPX的介绍。
[24] https://developers.google.com/kml/documentation/
Google关于KML的介绍,上面有一些初学者入门指南资料。
**12、GPS增强系统介绍**
[25] GPS Essentials of Satellite Navigation Compendium第7章“Improved GPS: DGPS, SBAS, A-GPS and HSGPS”
[26] http://www.tutorialspoint.com/lte/lte_radio_protocol_architecture.htm
Control Plane和User Plane的区别
**13、OMA-SUPL介绍**
[27] OMA Secure User Plane Location Architecture 3.0版
OMA-SUPL官方技术文档,它对SUPL整个架构和相关功能进行了定义,读者务必认真阅读它。
[28] OMA User Plane Location Protocol 3.0版
ULP协议文档,建议读者认真阅读。
注意,OMA-SUPL相关协议的最高版本为3.0,读者可从官方网站http://technical.openmobilealliance.org/Technical/release_program/supl_V3_0.aspx下载。
[29] http://wenku.baidu.com/view/cc6b150703d8ce2f006623e2.html
“基于SUPL的移动定位系统的研究和设计”,一篇硕士学位论文,对移动定位技术介绍得非常全面,建议读者认真阅读。
Android中的位置管理
**14、LocationManager应用示例**
[30] https://developer.android.com/google/play-services/location.html
Android SDK中关于Google Play Service中Location API的官方介绍,建议那些对开发高级LBS相关应用程序感兴趣的读者阅读它。
**15、NTP相关**
[31] http://en.wikipedia.org/wiki/Network_Time_Protocol
维基百科关于NTP协议的介绍。
**16、LTO相关**
[32] http://www.broadcom.com/products/GPS/Location-Based-Services/LTO-AGPS
博通公司关于LTO的介绍,建议读者仔细阅读。
**17、数据短信收发**
[33] http://blog.fordemobile.com/2012/09/use-sms-to-send-and-receive-raw-data.html
国外一篇关于如何在Android平台上收发数据短信的文章。
';
9.4.1 本章总结
最后更新于:2022-04-02 06:06:24
本章内容分为两大块:
* 首先对本章所用到的GPS基础知识进行了介绍,这部分内容比较广,难度不大,读者可轻松掌握它们。
* 然后对Android平台中的位置管理模块进行了详细介绍。从代码上看,这些模块的难度都不大。
从整体情况来看,GPS是全书难度最大的一章,其中一个主要原因是GPS所涉及到的背景知识非常庞杂,而且历经几十年的发展,相关的知识更新速度也非常快。
目前,与GNSS相关的LBS还在高速发展中,谁能在如此激烈的竞争中拔得头筹呢?当然是那些有技术积淀,并能灵活快速应对市场需求的公司和组织了。在此,笔者希望读者不要满足于掌握本章甚至本书的内容,而应该把目标放得更长,更远一些,只有这样才能跟上技术发展的脚步,从而在激烈竞争中处于有利的位置。
';
9.4 本章总结和参考资料说明
最后更新于:2022-04-02 06:06:22
9.3.3 LocationManager系统模块
最后更新于:2022-04-02 06:06:20
根据前文对Android平台中位置管理相关模块的介绍可知,LocationManagerService(LMS)是系统模块的核心,我们先来看它的初始化。
**1、LMS初始化**
系统启动时,LMS将由SystemServer创建,其初始化函数为systemReady,代码如下所示。
**LocationManagerService.java::systemReady**
~~~
public void systemReady() {
// 创建一个工作线程,其线程函数为LocationManagerService中的run函数
Thread thread = new Thread(null, this, THREAD_NAME);
thread.start();
} // 直接来看LMS的run函数
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Looper.prepare();
// 创建一个LocationWorkerHandler,其主要工作为将LP投递的位置信息返回给客户端
mLocationHandler = new LocationWorkerHandler();
init();// 重要的初始化函数
Looper.loop();
}
~~~
LMS的主要初始化工作都集中在init中,该函数的代码如下所示。
**LocationManagerService.java::init**
~~~
private void init() {
......// 创建Wakelock等工作。关于Wakelock的工作原理,可阅读《深入理解Android:卷Ⅱ》第5章
// 系统有一个黑白名单用于禁止使用某些特定的LP。在黑白名单中,LP由其对应的Java包名指定
mBlacklist = new LocationBlacklist(mContext, mLocationHandler);
mBlacklist.init();
/*
LocationFudger很有意思,Android平台提供粗细两种精度的位置信息。其中,粗精度的位置信息由下面这个
LocationFudger根据细精度的位置信息进行一定数学模糊处理后得到。粗精度默认的最大值为2000米,
最小值为200米。厂商可在Settings.db数据库secure表的“locationCoarseAccuracy”设定。
本章不讨论LocationFudger的内容,请感兴趣的读者自行研究。
*/
mLocationFudger = new LocationFudger(mContext, mLocationHandler);
synchronized (mLock) {
loadProvidersLocked(); // 关键函数:创建或加载系统中的LP,下节将详细分析它
}
/*
GeofenceManager为地理围栏管理对象。Android平台中其作用如下。
客户端可以设置一个地理围栏,该地理围栏的范围是一个以客户端设置的某个点(指定其经/纬度)为中心
的圆,圆的半径也由客户端设置。当设备进入或离开该地理围栏所覆盖的圆时,LMS将通过相关回调函
数通知客户端。目前Android 4.2中还没有在SDK中公开地理围栏相关的API。有需要使用该功能
的程序可通过Java放射机制调用LocationManager的addGeofence函数。
GeofenceManager比较简单,感兴趣的读者也可自行阅读。
*/
mGeofenceManager = new GeofenceManager(mContext, mBlacklist);
......// 监听APK安装\卸载等广播事件以及监听用户切换事件
/*
根据Settings中的设置情况来开启或禁止某个LP。在此函数中,各LP实现的
LocationProviderInterface接口的enable/disable函数将被调用。
*/
updateProvidersLocked();
}
~~~
init函数的内容比较多,本节重点关注其中的loadProvidersLocked函数。
>[info] 提示 读者学完本章后不妨研究LocationFudger和GeofenceManager,其内容比较有意思。
**①、loadProvidersLocked流程**
loadProvidersLocked用于创建及加载系统中所有的LocationProvider,其代码如下所示。
**LocationManagerService.java::loadProvidersLocked**
~~~
private void loadProvidersLocked() {
/*
先创建PassiveProvider。该LP名称中的Passive(译为“被动”)所对应的场景比较有意思,此
处举一个例子。假设应用程序A使用GpsLP。GpsLP检测到位置更新后将通知应用程序A。应用程序B
如果使用PassiveProvider,当GpsLP更新位置后,它也会触发PassiveProvider以通知应用程
序B。也就是说,PassiveProvider自己并不能更新位置信息,而是靠其他LP来触发位置更新的。
特别注意,PassiveProvider的位置更新是由LMS接收到其他LP的位置更新通知后主动调用
PassiveProvider的updateLocation函数来完成的。
目前,PassiveProvider被SystemServer中的TwilightService使用,TwilightService
将根据位置信息来计算当前时间是白天还是夜晚(Twilight有“黎明”之意)①。
另外,GpsLP也会使用PassiveProvider来接收NeworkLP的位置信息。
PassiveProvider非常简单,请读者在学完本章基础后再自行研究它。
*/
PassiveProvider passiveProvider = new PassiveProvider(this);
// LMS将保存所有的LP
addProviderLocked(passiveProvider);
// PassiveProvider永远处于启用状态。mEnabledProviders用于保存那些被启用的LP
mEnabledProviders.add(passiveProvider.getName());
mPassiveProvider = passiveProvider;
if (GpsLocationProvider.isSupported()) {
// 创建GpsLP实例,我们将用两节来介绍
GpsLocationProvider gpsProvider = new GpsLocationProvider(mContext, this);
mGpsStatusProvider = gpsProvider.getGpsStatusProvider();
mNetInitiatedListener = gpsProvider.getNetInitiatedListener();
addProviderLocked(gpsProvider);// 保存GpsLP
// GpsLP属于真实的位置提供者,所以把它单独保存在mRealProvider中
mRealProviders.put(LocationManager.GPS_PROVIDER, gpsProvider);
}
Resources resources = mContext.getResources();
ArrayList providerPackageNames = new ArrayList();
/*
config_locationProviderPackageNames存储了第三方LP的Java包名。Android原生代码中该值
只有一个,为“com.android.location.fused”。FusedLP对应的源码路径为frameworks/base/
packages/FusedLocation。
*/
String[] pkgs = resources.getStringArray(
com.android.internal.R.array.config_locationProviderPackageNames);
if (pkgs != null) providerPackageNames.addAll(Arrays.asList(pkgs));
/*
加载应用程序实现的LP服务时,LMS将检查它们的签名信息以及版本信息。笔者研究了这部分代码,
感觉其目的可能是想提供一种数据一致性的保护机制,举个例子。
笔者的Galaxy Note 2中默认安装的是百度提供的NetworkLocation_baidu.apk,
笔者安装Google的NetworkLocation.apk时,由于二者签名不一致(根据LMS相关代码工的作原理可
知,百度的NetworkLP属于config_locationProviderPackageNames指定的,其签名信息将被保存。
后续再安装的LP将检查其签名是否和之前保存的签名信息是否一致。也就是说,后续的LP必须使用
NetworkLocation_Baidu或FusedLP相同的签名才能被LMS加载。而FusedLP属于Android原生
应用,一般由手机厂商提供,第三方应用程序不太可能拿到其签名),根据上述信息可知,只有百度旗
下或得到它授权的第三方LP才能在笔者的Galaxy Note 2中安装和使用。
签名检查这部分代码在下文介绍的LocationProviderProxy中也有,读者仅了解其目的即可。
ensureFallbackFusedProviderPresentLocked用来检查FusedLP的签名和版本信息。
*/
ensureFallbackFusedProviderPresentLocked(providerPackageNames);
/*
加载NetworkLP,其中参数如下。
NETWORK_PROVIDER:类型为字符串,值为“network”。
NETWORK_LOCATION_SERVICE_ACTION:类型为字符串,值为
“com.android.location.service.v2.NetworkLocationProvider”,
它代表应用程序所实现的LP服务名,其作用见下节关于LocationProviderProxy的介绍。
注意,和Android 4.1比起来,Android 4.2 LMS相关的代码变化较大,
下文将介绍LocationProviderProxy的工作流程。
*/
LocationProviderProxy networkProvider = LocationProviderProxy.createAndBind(
mContext,LocationManager.NETWORK_PROVIDER,
NETWORK_LOCATION_SERVICE_ACTION,
providerPackageNames, mLocationHandler, mCurrentUserId);
if (networkProvider != null) {
// NetworkLP也属于真实的LP
mRealProviders.put(LocationManager.NETWORK_PROVIDER, networkProvider);
// 应用程序实现的LP保存在mProxyProviders变量中
mProxyProviders.add(networkProvider);
addProviderLocked(networkProvider);
} ......
// 加载FusedLP
LocationProviderProxy fusedLocationProvider = LocationProviderProxy.createAndBind(
mContext, LocationManager.FUSED_PROVIDER,
FUSED_LOCATION_SERVICE_ACTION,
providerPackageNames, mLocationHandler, mCurrentUserId);
if (fusedLocationProvider != null) {
addProviderLocked(fusedLocationProvider);
mProxyProviders.add(fusedLocationProvider);
// FusedLP默认处于启用的状态
mEnabledProviders.add(fusedLocationProvider.getName());
mRealProviders.put(LocationManager.FUSED_PROVIDER, fusedLocationProvider);
} ......
// 创建Geocoder。GeocoderProxy的工作流程和LocationProviderProxy类似
mGeocodeProvider = GeocoderProxy.createAndBind(mContext, providerPackageNames,
mCurrentUserId);
......
}
~~~
在LMS的初始化函数中,loadProvidersLocked用于创建和加载系统中所有的LP如下。
* PassiveProvider:提供被动式的位置数据更新服务,其位置数据来源于其他的LP。
* GpsLocationProvider:由LMS创建并加载,运行在LMS所在的进程system_process中,属于系统提供的LP服务。
* NetworkLocationProvider:该LP服务由应用程序提供。
* FusedLocationProvider:由FusedLocation.apk服务,属于系统提供的应用程序。其内部将使用其他的LP。
* GeocodeProvider:由第三方应用程序提供。一般和NetworkLP位于同一个应用程序中。
对于本章来说,GpsLP是重中之重。不过在介绍它之前,我们先来看看LMS是如何与位于应用进程中的其他LP交互的。
**②、LocationProviderProxy介绍**
由loadProvidersLocked的代码可知,LMS通过LocationProviderProxy(简称LPProxy)来加载应用进程中的LP。相关函数是LPProxy的createAndBind,代码如下所示。
**LocationProviderProxy.java::createAndBind**
~~~
public static LocationProviderProxy createAndBind(Context context, String name, String action,
List initialPackageNames, Handler handler, int userId) {
// 创建一个LPProxy对象
LocationProviderProxy proxy = new LocationProviderProxy(context, name, action,initialPackageNames, handler, userId);
if (proxy.bind())
return proxy;
else
return null;
}
~~~
LPProxy的createAndBind中有两个关键函数,分别是LPProxy的构造函数以及bind。我们先来看其构造函数,代码如下所示。
**LocationProviderProxy.java::LocationProviderProxy**
~~~
private LocationProviderProxy(Context context, String name, String action,
List initialPackageNames, Handler handler, int userId) {
mContext = context;
mName = name;
/*
ServiceWatcher是LPProxy中最重要的对象。在Android LM架构中,应用程序实现的LP服务都
通过Android四大组件中的Service提供。ServiceWatcher就是LPProxy中用来连接和监视应用程序
实现的LP服务的。
*/
mServiceWatcher = new ServiceWatcher(mContext, TAG, action, initialPackageNames,
mNewServiceWork, handler, userId);
}
~~~
在createAndBind函数的最后,LPProxy将调用bind函数,而这个bind将触发ServiceWatcher的start被调用,我们直接来看它。
**ServiceWatcher.java::start**
~~~
public boolean start() {
synchronized (mLock) {
// bindBestPackageLocked见下文解释
if (!bindBestPackageLocked(null))
return false;
}
// 监听应用程序安装、卸载、更新等广播事件
mPackageMonitor.register(mContext, null, UserHandle.ALL, true);
return true;
}
~~~
bindBestPackageLocked的工作包括如下。
* 检查目标应用程序的签名。
* 根据createAndBind第三个参数查找该目标应用程序实现的Service(Android四大组件之一)。以NetworkLP为例,目标应用程序必须提供一个名为"com.android.location.service.v2.NetworkLocationProvider"的服务。
* 然后绑定到该服务上,并获取一个类型为ILocationProvider接口的实例。通过该实例,LPProxy可与位于应用程序中的LP服务交互。
bindBestPackageLocked中最重要的函数是bindToPackageLocked,其代码如下所示。
**ServiceWatcher.java::bindToPackageLocked**
~~~
private void bindToPackageLocked(String packageName, int version) {
unbindLocked();
Intent intent = new Intent(mAction);
intent.setPackage(packageName);
mPackageName = packageName;
mVersion = version;
// 绑定到应用程序的LP Servic
mContext.bindService(intent, this, Context.BIND_AUTO_CREATE |
Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE, mCurrentUserId);
}
~~~
绑定成功后,ServiceWathcer的onServiceConnected函数将被调用。
**ServiceWatcher.java::onServiceConnected**
~~~
public void onServiceConnected(ComponentName name, IBinder binder) {
synchronized (mLock) {
String packageName = name.getPackageName();
if (packageName.equals(mPackageName)) {
mBinder = binder;
if (mHandler !=null && mNewServiceWork != null)
mHandler.post(mNewServiceWork);// mNewServiceWork由LPProxy提供
} ......
}
}
~~~
mNewServiceWork是一个Runnable对象,它由LPProxy提供,其代码如下所示。
**LocationProviderProxy.java::Runnable**
~~~
private Runnable mNewServiceWork = new Runnable() {
public void run() {
boolean enabled;
ProviderProperties properties = null; ProviderRequest request;
WorkSource source; ILocationProvider service;
synchronized (mLock) {
enabled = mEnabled; request = mRequest;
source = mWorksource;
// 返回ILocationProvider接口实例,它可和应用程序中LP交互
service = getService();
}
try {
/*
获取LP的属性信息,这些属性统一封装在类型为ProviderProperties的对象中,LP的属性
由ProviderProperties的成员变量表示,这些变量如下。
mRequiresNetwork:是否需要使用网络。
mRequiresSatellite:是否需要使用卫星。
mRequiresCell:是否需要使用基站。
mHasMonetaryCost:是否需要计费。
mSupportsAltitude:是否能提供海拔高度信息。
mSupportsSpeed:是否提供速度信息。
mSupportsBearing:是否支持方位信息。
mPowerRequirement:耗电量级别,可取值有Criteria类定义的HIGH、MEDIUM和LOW三种级别。
mAccuracy:精度级别,可取值有Criteria类定义的COARSE、FINE、HIGH、LOW四种级别。
*/
properties = service.getProperties();
......
if (enabled) {
service.enable();// 启动这个LP
if (request != null) {// 如果客户端有请求的话,则将该请求发送给LP
service.setRequest(request, source);
}
}
}catch ......
synchronized (mLock) {
mProperties = properties;
}
}
};
~~~
至此,LMS的初始化工作暂告一段落,下面来看看GpsLocationProvider的创建。
**③、GpsLocationProvider初始化**
GpsLocationProvider类本身有一段初始化代码,如下所示。
**GpsLocationProvider.java::static语句**
~~~
// GpsLP定义了一些native函数,此处的class_init_native将初始化相关JNI方法。后文介绍
static {
class_init_native();
}
~~~
接着来看GpsLP的创建,其代码如下所示。
**GpsLocationProvider.java::GpsLocationProvider**
~~~
public GpsLocationProvider(Context context, ILocationManager ilocationManager) {
mContext = context;
/*
NTP为Network Time Protocol之意,它是一种用来同步计算机时间的协议。该时间的源是UTC。
在下面这行代码中,GpsLP将创建一个NtpTrustedTime对象,该对象将采用SNTP(Simple NTP)协议
来和指定NTP服务器通信以获取准确的时间。Android平台中,NTP服务器可在两个地方设置。
1)在系统资源文件中设置,由字符串config_ntpServer表示,默认值为“2.android.pool.ntp.org”。
请求处理的超时时间由整型参数config_ntpTimeout控制,默认值为20000ms。
2)也可在Settings的数据库中设置,对应的控制选项为“ntp_server”和“ntp_timeout”。
NtpTrustedTime优先使用Settings设置的信息。
NtpTrustedTime比较简单,对NTP感兴趣的读者见参考资料[31]。
*/
mNtpTime = NtpTrustedTime.getInstance(context);
mILocationManager = ilocationManager;
// GpsNetInitiatedHandler和ULP Network Initiated请求有关
// 主要处理来自GPS HAL层通知的NI(Network Initiated)事件
mNIHandler = new GpsNetInitiatedHandler(context);
mLocation.setExtras(mLocationExtras);
......
mConnMgr = (ConnectivityManager)context.getSystemService(
Context.CONNECTIVITY_SERVICE);
mProperties = new Properties();
try {
/*
读取GPS配置文件,PROPERTIES_FILE的位置为“/etc/gps.conf”。
笔者的Galaxy Note中,该文件的内容如下所示,其中“#”后的内容为笔者添加的注释。
NTP_SERVER=north-america.pool.ntp.org #指定NTP_SERVER
#下面这几个参数指定AGPS LTO(Long Term Orbits)数据的下载地址。LTO存储了GPS卫星
#的星历数据。从下面这些地址中的“4day”来看,这些数据的有效期为4天。AGPS使用时需要
#先从服务器上下载一些辅助数据。如果周围没有网络的情况下该怎么办呢? LTO就好比离线地图,
#当周围没有网络时,终端可以利用事先保存的LTO数据来初始化GPS的定位。当然,LTO有时
#效限制,一般是4天。
XTRA_SERVER_1=http://gllto.glpals.com/4day/glo/v2/latest/lto2.dat
XTRA_SERVER_2=http://gllto.glpals.com/4day/glo/v2/latest/lto2.dat
XTRA_SERVER_3=http://gllto.glpals.com/4day/glo/v2/latest/lto2.dat
SUPL_HOST=supl.google.com #指定SUPL的主机和端口
SUPL_PORT=7276
关于LTO,见参考资料[32]。
*/
File file = new File(PROPERTIES_FILE);
FileInputStream stream = new FileInputStream(file);
mProperties.load(stream);// 解析该配置文件
stream.close();
// 获取配置文件中设置的SUPL主机地址以及端口号
mSuplServerHost = mProperties.getProperty("SUPL_HOST");
String portString = mProperties.getProperty("SUPL_PORT");
if (mSuplServerHost != null && portString != null) {
mSuplServerPort = Integer.parseInt(portString);
......
}
// C2K是CDMA2000的缩写。C2K_HOST和C2K_PORT主要用于GPS模块的测试
// 对用户来说,这两个参数没有太大的意义②
mC2KServerHost = mProperties.getProperty("C2K_HOST");
portString = mProperties.getProperty("C2K_PORT");
if (mC2KServerHost != null && portString != null) {
mC2KServerPort = Integer.parseInt(portString);
......
}
}......
mHandler = new ProviderHandler();
// SUPL的初始化可以由两种特殊的短信触发,下文将简单介绍listenForBroadcasts这个函数
listenForBroadcasts();
mHandler.post(new Runnable() {
public void run() {
LocationManager locManager =
LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
// 接收来自NetworkLP的位置更新通知
// 当GpsLP收到来自NetworkLP的位置信息后,将把它们传给GPS HAL层去处理
locManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER,
0, 0, new NetworkLocationListener(), mHandler.getLooper());
}
});
}
~~~
下面来看上述代码中提到的listenForBroadcasts函数,其内容如下所示。
**GpsLocationProvider.java::listenForBroadcasts**
~~~
private void listenForBroadcasts() {
IntentFilter intentFilter = new IntentFilter();
/*
SUPL INIT流程可由一条特殊的数据短信(Data Message)触发。注意,数据短信和我们接触最多的
文本短信(Text Message)不同。下面这个IntentFilter将接收发往127.0.0.1:7275的数据短信。
7275为OMA-SUPL使用的端口号,关于OMA-SUPL端口号见可参考资料[28]。
关于Android中数据短信的收发,见参考资料[33]。
*/
intentFilter.addAction(Intents.DATA_SMS_RECEIVED_ACTION);
intentFilter.addDataScheme("sms");
intentFilter.addDataAuthority("localhost","7275");
mContext.registerReceiver(mBroadcastReciever, intentFilter, null, mHandler);
// SUPL INIT也可由WAP推送短信触发,该短信包含的数据类型为MIME中的
// “application/vnd.omaloc-supl-init”
intentFilter = new IntentFilter();
intentFilter.addAction(Intents.WAP_PUSH_RECEIVED_ACTION);
try {
intentFilter.addDataType("application/vnd.omaloc-supl-init");
} ......
mContext.registerReceiver(mBroadcastReciever, intentFilter, null, mHandler);
// 监听ALARM事件和网络事件(CONNECTIVITY_ACTION)
intentFilter = new IntentFilter();
intentFilter.addAction(ALARM_WAKEUP);
intentFilter.addAction(ALARM_TIMEOUT);
// 监听网络事件,下文介绍AGPS时还会讨论它
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
mContext.registerReceiver(mBroadcastReciever, intentFilter, null, mHandler);
}
~~~
当GpsLP收到指定的数据短信或WAP推送短信后,checkSmsSuplInit或checkWapSuplInit函数将被调用。这两个函数的功能比较简单,就是将短信的内容传递到GPS HAL层,来看看它们的代码。
**GpsLocationProvider.java::checkSmsSuplInit和checkWapSuplInit**
~~~
private void checkSmsSuplInit(Intent intent) {
SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
for (int i=0; i [info] 注意 高通公司在其开源的QRD(高通参考设计)代码中提供了高通平台GPS HAL层模块的实现,但高通平台HAL层实现采用的是C/S架构。GPS HAL模块仅仅将上层请求转成QMI(QC MSM Interface)消息并发送给相关服务去处理。同样,由于缺乏相关文档,笔者很难搞清楚高通平台上GPS模块的具体工作流程。
下面我们将从Java层开始介绍GpsLP的工作流程。首先是GPS的启动流程。
**①、启动GPS**
Android平台中,GPS的开启和关闭主要在设置程序中控制,相关控制界面如图9-32所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/de02db005c92bd1f9d2926ff2b9fe655_539x419.jpg)
图9-32 定位服务设置界面
当用户单击图9-32中三个选择框时,以下函数将被触发。
**LocationSettings.java::onPreferenceTreeClick**
~~~
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
Preference preference) {
final ContentResolver cr = getContentResolver();
if (preference == mNetwork) {// 对应图9-32中“使用无线网络”选项
Settings.Secure.setLocationProviderEnabled(cr,
LocationManager.NETWORK_PROVIDER, mNetwork.isChecked());
} else if (preference == mGps) {// 对应图9-32中“使用GPS卫星”选项
boolean enabled = mGps.isChecked();
Settings.Secure.setLocationProviderEnabled(cr,
LocationManager.GPS_PROVIDER, enabled);
if (mAssistedGps != null) {
mAssistedGps.setEnabled(enabled);
}
} else if (preference == mAssistedGps) {// 对应图9-32中“使用辅助性GPS”选项
// 注意,国内某些运营商定制的手机没有该选项。此处将直接修改Settings
// 数据库中secure表中“assisted_gps_enabled”字段值
Settings.Global.putInt(cr, Settings.Global.ASSISTED_GPS_ENABLED,
mAssistedGps.isChecked() ? 1 : 0);
}
return true;
}
~~~
上述代码中的相关操作将修改settings数据库中location_providers_allowed字段。当“GPS卫星”和“无线网络”两项都选中时,location_providers_allowed字段取值将变成"network,gps"。显然,LMS只要通过ContentObserver③监听该字段的变化就可快速响应用户的设置。LMS中,设置对该字段监听的相关代码如下所示。
**LocationManagerService.java::init**
~~~
mContext.getContentResolver().registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCATION_PROVIDERS_ALLOWED), true,
new ContentObserver(mLocationHandler) {
public void onChange(boolean selfChange) {
synchronized (mLock) {
updateProvidersLocked();// 最终调用的处理函数是updateProvidersLocked
}
}
}, UserHandle.USER_ALL);
~~~
updateProvidersLocked除了根据设置的情况调用对应LP的enable或disable函数外,还需要通知LP的监听者(即9.3.1节应用示例中介绍的LocationListener对象)。根据LP的启动或禁止情况,LocationListener的onProviderEnabled/onProviderDisabled将被调用。
现在,重点介绍GpsLP的enable函数。该函数内部将发送ENABLE_MSG消息,而该消息最终将调用GpsLP的handleEnable进行处理。该函数的代码如下所示。
**GpsLocationProvider.java::handleEnable**
~~~
private void handleEnable() {
synchronized (mLock) {
if (mEnabled) return;
mEnabled = true;
}
boolean enabled = native_init(); // 初始化GPS HAL层模块
if (enabled) {
mSupportsXtra = native_supports_xtra(); // 判断GPS模块是否支持Xtra
if (mSuplServerHost != null) {// 设置SUPL服务器地址和端口
native_set_agps_server(AGPS_TYPE_SUPL, mSuplServerHost, mSuplServerPort);
}
if (mC2KServerHost != null) {
native_set_agps_server(AGPS_TYPE_C2K, mC2KServerHost, mC2KServerPort);
}
} ......// 处理disabled的情况
}
~~~
在handleEnable函数中,GpsLP主要通过调用native函数集中的几个函数来初始化底层GPS模块。这些函数将在介绍GPS HAL层时再来详细介绍。
当GPS模块启动成功后,GPS HAL层将通过JNI回调函数通知GpsLP底层GPS引擎的工作能力,这个回调函数是setEngineCapabilities,其代码如下所示。
**GpsLocationProvider.java::setEngineCapabilities**
~~~
private void setEngineCapabilities(int capabilities) {
mEngineCapabilities = capabilities; // capabilities代表GPS引擎的工作能力,详情见下文解释
if (!hasCapability(GPS_CAPABILITY_ON_DEMAND_TIME) && !mPeriodicTimeInjection) {
mPeriodicTimeInjection = true;
requestUtcTime();// 输入UTC时间信息
}
}
~~~
目前,Android平台中定义的GPS引擎工作能力取值有如下几种。
·GPS_CAPABILITY_SCHEDULING:如果设置,则GPS模块在工作周期内(即GPS开启之后,关闭之前这一段时间)能定时通知位置信息,例如每10秒通知一次位置信息。如果GPS模块不支持该功能,表明GPS模块在每个工作周期内只能通知一次位置信息。对于这种GPS,GpsLP将通过不断启动和关闭GPS模块来模拟实现GPS_CAPABILITY_SCHEDULING的功能。下文代码中将介绍模拟实现方面的内容。
* **GPS_CAPABILITY_MSB**:如果设置,则GPS模块支持Mobile Service Based AGPS。
* **GPS_CAPABILITY_MSA**:如果设置,则GPS模块支持Mobile Service Assisted AGPS。
* **GPS_CAPABILITY_SINGLE_SHOT**:如果设置,表明GPS模块支持单次定位。与单次定位相对应的是连续定位。
* **GPS_CAPABILITY_ON_DEMAND_TIME**:GPS在工作周期内可能需要UTC时间信息。如果设置此能力,GPS模块在需要UTC时间信息时将主动通过相关回调函数(即代码中的requestUtcTime函数,JNI层也会直接调用它)从GpsLP那获取UTC时间。如果没有设置它,则GpsLP将每隔24小时获取UTC时间并输入给GPS模块。
>[info] 提示 简单来说,GPS_CAPABILITY_SCHEDULING表示GPS模块支持连续定位,即GPS模块会不断更新位置信息。而GPS_CAPABILITY_SINGLE_SHOT表示GPS模块支持单次定位,即只GPS模块只会通知一次位置信息。单次定位功能适用于那些无需连续获取位置信息的应用程序。这样,GPS通知完位置信息后即可停止工作以节省电力。另外,LocationManager中有一个requestSingleUpdate函数,其功能和单次定位类似。但由于不是所有GPS模块都支持单次定位,所以代码中并没有利用GPS_CAPABILITY_SINGLE_SHOT标志。
现在,假设GPS启动成功,接下来的工作就是启动GPS导航功能。
**②、启动GPS导航**
当客户端调用LocationManager的requestLocationUpdates并设置使用GpsLP(可参考9.3.1节中的示例代码)后,GpsLP的setRequest函数将被调用,该函数的代码如下所示。
**GpsLocationProvider.java::setRequest**
~~~
public void setRequest(ProviderRequest request, WorkSource source) {
// 发送SET_REQUEST消息
// 注意,GpsLP将把客户端发送的ProviderRequest转换成GpsLP使用的GpsRequest
sendMessage(SET_REQUEST, 0, new GpsRequest(request, source));
}
~~~
SET_REQUEST消息将由handleSetRequest处理,其代码如下所示。
**GpsLocationProvider.java::handleSetRequest**
~~~
private void handleSetRequest(ProviderRequest request, WorkSource source) {
if (request.reportLocation) {// 该变量用于判断客户端是否要求接收位置更新信息
......// 监视客户端的用电情况。略去相关内容
mFixInterval = (int) request.interval;// 客户端设置的位置更新间隔
if (mFixInterval != request.interval)
mFixInterval = Integer.MAX_VALUE;
// mStarted变量用于判断导航是否已经开启
if (mStarted && hasCapability(GPS_CAPABILITY_SCHEDULING)) {
/*
如果GPS模块支持定时通知位置信息,则设置其运行模式为GPS_POSITION_RECURRENCE_PERIODIC,
同时还需要将间隔时间传递给GPS模块。
*/
if (!native_set_position_mode(mPositionMode, GPS_POSITION_RECURRENCE_PERIODIC,
mFixInterval, 0, 0))
Log.e(TAG, "set_position_mode failed in setMinTime()");
} else if (!mStarted) {// 如果之前没有启动导航,则此处启动它
startNavigating();// 下文分析
}
} else {
updateClientUids(new int[0]);// 计算客户端耗电情况
stopNavigating();// 停止导航
mAlarmManager.cancel(mWakeupIntent);
mAlarmManager.cancel(mTimeoutIntent);
}
}
~~~
假设之前没有启动导航,根据上述代码可知,startNavigating将被调用,相关代码如下所示。
**GpsLocationProvider.java::startNavigating**
~~~
private void startNavigating() {
if (!mStarted) {
mTimeToFirstFix = 0; mLastFixTime = 0;
mStarted = true;
mPositionMode = GPS_POSITION_MODE_STANDALONE;
// 默认工作模式为GPS,即不使用AGPS
if (Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.ASSISTED_GPS_ENABLED, 1) != 0) {
if (hasCapability(GPS_CAPABILITY_MSB))
// 如AGPS启用,则设置工作模式为AGPS,并且采用MSB
mPositionMode = GPS_POSITION_MODE_MS_BASED;
}
// 如果GPS模块不支持定时通知位置信息,则interval取值为1秒
int interval = (hasCapability(GPS_CAPABILITY_SCHEDULING) ? mFixInterval : 1000);
if (!native_set_position_mode(mPositionMode, GPS_POSITION_RECURRENCE_PERIODIC,
interval, 0, 0)) {
......// 设置定位模式失败处理
}
if (!native_start()) {
......// 导航启动失败处理
}
// 清空GPS卫星信息
updateStatus(LocationProvider.TEMPORARILY_UNAVAILABLE, 0);
mFixRequestTime = System.currentTimeMillis();
// 如果GPS模块不支持定时通知位置信息
if (!hasCapability(GPS_CAPABILITY_SCHEDULING)) {
/*
NO_FIX_TIMEOUT为60秒。对于那些不支持定时通知的GPS模块来说,如果客户端要求的
更新间隔大于60秒,并且在这之间没有收到GPS的位置通知(这表明GPS还没定位自己
的位置),则此处会设置一个超时控制以停止GPS导航。此处的代码逻辑需要结合GpsLP
中正常的超时管理逻辑来理解。
*/
if (mFixInterval >= NO_FIX_TIMEOUT)
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + NO_FIX_TIMEOUT, mTimeoutIntent);
}
}
}
~~~
此处介绍GpsLP如何处理那些不能定时通知位置信息的GPS引擎。GpsLP将注册两个定时触发Intent用于启用和关闭GPS。
* ALARM_TIMEOUT:在该Intent的处理中,GpsLP将停止导航。
* ALARM_WAKEUP:在该Intent的处理中,GpsLP将启用导航。
**③、位置信息通知处理**
当GPS模块更新位置时,GPS JNI层将调用GpsLP的reportLocation函数,其代码如下所示。
**GpsLocationProvider.java::reportLocation**
~~~
private void reportLocation(int flags, double latitude, double longitude, double altitude,
float speed, float bearing, float accuracy, long timestamp) {
synchronized (mLocation) {
mLocationFlags = flags; // 标志信息
// 此次通知的位置消息是否有经纬度信息
if ((flags & LOCATION_HAS_LAT_LONG) == LOCATION_HAS_LAT_LONG) {
mLocation.setLatitude(latitude);// 设置经纬度及时间戳
mLocation.setLongitude(longitude);
mLocation.setTime(timestamp);
mLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
}
// 检查是否有海拔信息
if ((flags & LOCATION_HAS_ALTITUDE) == LOCATION_HAS_ALTITUDE)
mLocation.setAltitude(altitude);
else mLocation.removeAltitude();
// 检查是否有速度信息
if ((flags & LOCATION_HAS_SPEED) == LOCATION_HAS_SPEED)
mLocation.setSpeed(speed);
else mLocation.removeSpeed();
// 检查是否有方位信息
if ((flags & LOCATION_HAS_BEARING) == LOCATION_HAS_BEARING)
mLocation.setBearing(bearing);
else mLocation.removeBearing();
// 是否有精度信息(以米为单位)
if ((flags & LOCATION_HAS_ACCURACY) == LOCATION_HAS_ACCURACY)
mLocation.setAccuracy(accuracy);
else mLocation.removeAccuracy();
mLocation.setExtras(mLocationExtras);
try {
// mILocationManager指向LMS,它会把此处的位置信息通知给客户端
mILocationManager.reportLocation(mLocation, false);
} ......
}// synchronized (mLocation)代码段结束
mLastFixTime = System.currentTimeMillis();
// 计算首次定位时间,即TTFF
if (mTimeToFirstFix == 0 && (flags & LOCATION_HAS_LAT_LONG) ==
LOCATION_HAS_LAT_LONG) {
mTimeToFirstFix = (int)(mLastFixTime - mFixRequestTime);
synchronized (mListeners) {
int size = mListeners.size();
for (int i = 0; i < size; i++) {
Listener listener = mListeners.get(i);
try {
/*
GpsLP还支持另外一种类型的监听者,即GpsStatusListener,
主要用来通知GPS卫星信息。客户端通过LocationManager的
addGpsStatusListener来注册监听对象。
此处将调用GpsStatusListenr的onFirstFix函数。
*/
listener.mListener.onFirstFix(mTimeToFirstFix);
} ......
}
}
}
......
/*
对于不支持GPS_CAPABILITY_SCHEDULING的GPS模块来说,当GpsLP收到一次位置通知事件后,
它将先暂停GPS导航,然后等到超时时间到达后,再启用导航。
*/
if (mStarted && mStatus != LocationProvider.AVAILABLE) {
// 和startNavigating函数最后一段代码相对应,取消在那里设置的超时监控
if (!hasCapability(GPS_CAPABILITY_SCHEDULING) && mFixInterval < NO_FIX_TIMEOUT)
mAlarmManager.cancel(mTimeoutIntent);// 取消超时控制
Intent intent = new Intent(LocationManager.GPS_FIX_CHANGE_ACTION);
intent.putExtra(LocationManager.EXTRA_GPS_ENABLED, true);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
updateStatus(LocationProvider.AVAILABLE, mSvCount);
}
// 对于不支持GPS_CAPABILITY_SCHEDULING功能的GPS模块,GpsLP将软件模拟连续定位功能
if (!hasCapability(GPS_CAPABILITY_SCHEDULING) && mStarted &&
mFixInterval > GPS_POLLING_THRESHOLD_INTERVAL)
hibernate(); // 停止导航。该函数中将设置ALARM_WAKEUP定时任务以重新启用导航
}
~~~
GpsLP处理位置通知的相关代码主要就在上面介绍的reportLocation中。GpsLP最终会将位置信息告诉LMS,而LMS的handleLocationChanged函数还有许多工作要接着开展。这部分内容请读者自行阅读。
**④、reportStatus和reportSvStatus**
除了通知位置信息外,GPS模块还会通过reportStatus向GpsLP反馈GPS模块的工作状态,该函数如下所示。
**GpsLocationProvider.java::reportStatus**
~~~
private void reportStatus(int status) {
synchronized (mListeners) {
boolean wasNavigating = mNavigating;
// 根据状态来更新mNavigating和mEngineOn这两个变量
switch (status) {
// 下面这两个状态分别表示GPS导航开始和结束
case GPS_STATUS_SESSION_BEGIN: {......}
case GPS_STATUS_SESSION_END: {......}
// 下面这两个状态分别表示GPS引擎开启和关闭
case GPS_STATUS_ENGINE_ON: {......}
case GPS_STATUS_ENGINE_OFF:{......}
}
if (wasNavigating != mNavigating) {
int size = mListeners.size();
for (int i = 0; i < size; i++) {
Listener listener = mListeners.get(i);
try {// 调用GpsListener的onGpsStarted函数或onGpsStopped函数
if (mNavigating) listener.mListener.onGpsStarted();
else listener.mListener.onGpsStopped();
} ......
}
// 发送广播
Intent intent = new Intent(LocationManager.GPS_ENABLED_CHANGE_ACTION);
intent.putExtra(LocationManager.EXTRA_GPS_ENABLED, mNavigating);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
}
}
~~~
reportStatus比较简单,读者了解即可。另外,GPS模块也会通过reportSvStatus返回卫星的信息,这个函数比较有意思,我们来看看。
**GpsLocationProvider.java::reportSvStatus**
~~~
private void reportSvStatus() {
/*
下面这个函数用于从GPS HAL层读取卫星的状态,其返回值为卫星的个数。
该函数每个参数的类型都是一个int数组,其中,除最后一个参数对应的int数组元素个数为3外,
而其他参数的int数组元素个数为MAX_SVS(值为32),每个参数的作用如下。
mSvs:用于存储卫星的编号。
mSnrs:用于存储卫星的SNR(信号噪声比)。
mSvElevations:存储卫星的高度信息。
mSvAzimuths:存储卫星的方位信息。
mSvMasks:该数组包含三个整型值,每个整型值字长32位,分别对应32颗卫星。
EPHEMERIS_MASK(值为0):该变量的每一位代表GPS模块是否获取了该卫星的ephemeris数据。
ALMANAC_MASK(值为1):该变量的每一位代表GPS模块是否获取了该卫星的almanac数据。
USED_FOR_FIX_MASK(值为2):该变量的每一位代表该卫星是否参与了上一次的定位信息计算。
*/
int svCount = native_read_sv_status(mSvs, mSnrs,
mSvElevations, mSvAzimuths, mSvMasks);
synchronized (mListeners) {
int size = mListeners.size();
for (int i = 0; i < size; i++) {
Listener listener = mListeners.get(i);
try {// 通知客户端
listener.mListener.onSvStatusChanged(svCount, mSvs, mSnrs,
mSvElevations, mSvAzimuths, mSvMasks[EPHEMERIS_MASK],
mSvMasks[ALMANAC_MASK], mSvMasks[USED_FOR_FIX_MASK]);
} ......
}
}
updateStatus(mStatus, Integer.bitCount(mSvMasks[USED_FOR_FIX_MASK]));
......
}
~~~
只要监听这些信息,读者就能实现类似GpsTestPlus应用的效果,如图9-33所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/1b6746591067a56a2b86cac634ae9363_408x595.jpg)
图9-33 GpsTestPlus应用效果
>[info] 提示 GpsTestPlus是一个比较常用的GPS测试软件。笔者曾经反编译过GpsTestPlus,其中和LM相关的函数调用比较简单,感兴趣的读者可尝试研究其代码。
本节主要介绍了GpsLP的主要工作流程,与之相关的知识点如下。
* GpsLP启动GPS、启动导航、位置信息处理、状态和卫星信息通知等几个主要工作的代码分析。从流程上来说,这部分代码难度非常小,笔者觉得大多数读者都能学会。
* GPS工作能力等重要标志及相关处理逻辑。
* GPS Java层调用的native函数。下文还将详细介绍这些函数的作用。
**3、AGPS工作流程分析**
Android平台中,AGPS的处理逻辑也集中在GpsLocationProvider.java中,本节就来介绍GpsLP中AGPS的相关代码。
**①、处理网络事件**
AGPS需要使用移动网络,所以早在GpsLP初始时,GpsLP就注册了对CONNECTIVITY_ACTION广播的监听。该广播事件由ConnectivityService发送,并且属于Sticky的广播。根据《深入理解Android:卷Ⅱ》6.4.1节“registerReceiver分析之二”的介绍可知,对于Sticky广播的接收者而言,系统会立即调度一次广播发送流程以将当前的网络事件传递给监听者。也就是说,GpsLP的onReceive函数将很快被触发。该函数的代码如下所示。
**GpsLocationProvider.java::onReceive**
~~~
public class GpsLocationProvider implements LocationProviderInterface {
...............
private final BroadcastReceiver mBroadcastReciever = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// 我们只关注CONNECTIVITY_ACTION事件
else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
int networkState;
// 获取网络状态
if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) {
networkState = LocationProvider.TEMPORARILY_UNAVAILABLE;
} else {
networkState = LocationProvider.AVAILABLE;
}
// 获取网络事件的相关信息。NetworkInfo表示此次事件主角,它描述了哪个网络发生了什么事件
NetworkInfo info =
intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
ConnectivityManager connManager = (ConnectivityManager)
mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
info = connManager.getNetworkInfo(info.getType());
// 更新网络状态,其内部将发送UPDATE_NETWORK_STATE消息
updateNetworkState(networkState, info);
}
}
};
...............
}
~~~
UPDATE_NETWORK_STATE消息最终由handleUpdateNetworkState处理,此函数的代码如下所示。
**GpsLocationProvider.java::handleUpdateNetworkState**
~~~
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
......
// 我们只关注CONNECTIVITY_ACTION事件
}
private void handleUpdateNetworkState(int state, NetworkInfo info) {
mNetworkAvailable = (state == LocationProvider.AVAILABLE);
if (info != null) {
// 判断系统是否允许使用移动数据
boolean dataEnabled = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MOBILE_DATA, 1) == 1;
// 假设移动数据功能已经启用,故networkAvailable为true
boolean networkAvailable = info.isAvailable() && dataEnabled;
// 获取APN,APN(Acess Point Number)用于确定使用哪种方式连接到网络
String defaultApn = getSelectedApn();
if (defaultApn == null)
defaultApn = "dummy-apn";
// 将这些信息传递到GPS HAL层
native_update_network_state(info.isConnected(), info.getType(),
info.isRoaming(), networkAvailable,
info.getExtraInfo(), defaultApn);
}
/*
mAGpsDataConnectionState初始状态为AGPS_DATA_CONNECTION_CLOSED。该值在reportAGpsStatus
中被修改。下面if这段代码逻辑需要结合reportAGpsStatus来综合理解。初次进来,if条件不满足。
*/
if (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE_SUPL
&& mAGpsDataConnectionState == AGPS_DATA_CONNECTION_OPENING) {
/*
注意,这段代码只有在mAGpsDataConnectionState状态为AGPS_DATA_CONNECTION_OPENING
才起作用。
*/
String apnName = info.getExtraInfo();
if (mNetworkAvailable) {
if (apnName == null) apnName = "dummy-apn";
mAGpsApn = apnName;
if (mAGpsDataConnectionIpAddr != 0xffffffff) {
boolean route_result;
// requestRouteToHost:将SUPL的数据都路由到指定的主机地址上
route_result = mConnMgr.requestRouteToHost(ConnectivityManager.TYPE_MOBILE_SUPL,
mAGpsDataConnectionIpAddr);
}
native_agps_data_conn_open(apnName);// 打开数据通道
mAGpsDataConnectionState = AGPS_DATA_CONNECTION_OPEN;
}
// 如果当前有可用网络,则GpsLP将获取NTP时间以及下载Xtra数据
if (mNetworkAvailable) {
if (mInjectNtpTimePending == STATE_PENDING_NETWORK)
sendMessage(INJECT_NTP_TIME, 0, null);// 触发handleInjectNtpTime函数
if (mDownloadXtraDataPending == STATE_PENDING_NETWORK)
sendMessage(DOWNLOAD_XTRA_DATA, 0, null);// 触发handleDownloadXtraData函数
}
} else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
int networkState;
// 获取网络状态
if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false))
networkState = LocationProvider.TEMPORARILY_UNAVAILABLE;
else networkState = LocationProvider.AVAILABLE;
// 获取网络事件的相关信息。NetworkInfo表示此次事件主角,它描述了哪个网络发生了什么事件
NetworkInfo info =
intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
ConnectivityManager connManager = (ConnectivityManager)
mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
info = connManager.getNetworkInfo(info.getType());
// 更新网络状态,其内部将发送UPDATE_NETWORK_STATE消息
updateNetworkState(networkState, info);
}
}
~~~
handleUpdateNetworkState还算比较简单,其主要工作如下。
* 如果GPS模块要求开启AGPS数据下载(这部分逻辑下节再介绍,即mAGpsDataConnectionState的值为AGPS_DATA_CONNECTION_OPENING),则handleUpdateNetwork将开展相关操作。
* 如果网络启用并且GpsLP之前没有获取过NTP时间以及下载过Xtra数据,GpsLP将通过INJECT_NTP_TIME和DOWNLOAD_XTRA_DATA两个消息获取NTP时间以及下载Xtra数据。
GPS模块什么时候会要求开启AGPS数据下载呢?相关函数集中在reportAGpsStatus中。
**②、reportAGpsStatus分析**
GPS模块将通过reportAGpsStatus和GpsLP交互,该函数代码如下所示。
**GpsLocationProvider.java::reportAGpsStatus**
~~~
private void reportAGpsStatus(int type, int status, int ipaddr) {
switch (status) {
case GPS_REQUEST_AGPS_DATA_CONN:// GPS模块要求启用数据链接
/*
此处,设置mAGpsDataConnectionState为AGPS_DATA_CONNECTION_OPENING,
该状态值表示数据链接处于开启过程中。
*/
mAGpsDataConnectionState = AGPS_DATA_CONNECTION_OPENING;
/*
要求使用移动数据。startUsingNetworkFeature是ConnectivityManager中一个比较重要的函
数,其第一个参数表示此处操作的网络是移动网络,第二个参数表示使用SUPL相关的功能。
*/
int result = mConnMgr.startUsingNetworkFeature(
ConnectivityManager.TYPE_MOBILE, Phone.FEATURE_ENABLE_SUPL);
mAGpsDataConnectionIpAddr = ipaddr; // 保存地址
// APN_ALREADY_ACTIVE表示startUsingNetworkFeature指定的网络及功能已经启用
if (result == PhoneConstants.APN_ALREADY_ACTIVE) {
if (mAGpsApn != null) {// mAGpsApn在onReceive函数中被设置
if (mAGpsDataConnectionIpAddr != 0xffffffff) {
boolean route_result;
route_result = mConnMgr.requestRouteToHost(
ConnectivityManager.TYPE_MOBILE_SUPL,mAGpsDataConnectionIpAddr);
}
native_agps_data_conn_open(mAGpsApn);// 调用native函数表示AGPS数据链接
// 开启AGPS数据链接开启成功
mAGpsDataConnectionState = AGPS_DATA_CONNECTION_OPEN;
} else if (result == PhoneConstants.APN_REQUEST_STARTED) {
// APN_REQUEST_STARTED表示startUsingNetworkFeature设置的请求已经发送给相关模块处理
} else......
break;
// 下面这个值表示AGPS不再需要使用数据链接
case GPS_RELEASE_AGPS_DATA_CONN:{......}
break;
// 下面这个值表示GPS模块中AGPS数据链接初始化完毕
case GPS_AGPS_DATA_CONNECTED: {......}
break;
// 下面这个值表示GPS模块中AGPS数据链接完毕
case GPS_AGPS_DATA_CONN_DONE: {......}
break;
// 下面这个值表示GPS模块中AGPS数据链接失败
case GPS_AGPS_DATA_CONN_FAILED: {......}
break;
}
}
}
~~~
reportAGpsStatus主要对两个状态进行处理,即GPS_REQUEST_AGPS_DATA_CONNGPS_RELEASE_AGPS_DATA_CONN。
总之,当网络准备好后,GpsLP将调用native_agps_data_conn_open启用GPS模块中AGPS数据链接功能。
**③、下载Xtra数据**
最后,看看Xtra数据的下载处理,这部分功能由handleDownloadXtraData实现,代码如下所示。
**GpsLocationProvider.java::handleDownloadXtraData**
~~~
private void handleDownloadXtraData() {
if (mDownloadXtraDataPending == STATE_DOWNLOADING)
return;
......
mDownloadXtraDataPending = STATE_DOWNLOADING;
mWakeLock.acquire();
// 利用后台线程下载数据
AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
public void run() {
GpsXtraDownloader xtraDownloader = new GpsXtraDownloader(
mContext, mProperties);
// 下载LTO数据
byte[] data = xtraDownloader.downloadXtraData();
// 将这些数据传递给GPS HAL层
if (data != null) native_inject_xtra_data(data, data.length);
// 发现XTtra数据下载完毕通知
sendMessage(DOWNLOAD_XTRA_DATA_FINISHED, 0, null);
......
mWakeLock.release();
}
});
}
~~~
GpsLP中的AGPS流程也比较简单。GpsLP只要监控网络事件以及接收来自GPS HAL层的AGPS状态通知即可顺利完成相关工作。
结合前面对GpsLP工作流程的分析可知,Android平台中,Java层的GPS模块和Native层(包括JNI和HAL层)GPS模块交互非常多,下面将重点分析GPS Native层方面的知识。
**4、GPS JNI与HAL层介绍**
在GpsLocationProvider初始化一节我们曾提到说GpsLocationProvider类有一个静态的初始化代码,在那段代码中,class_init_native函数将初始化JNI层相关模块。马上来看它。
**①、JNI与HAL层初始化**
class_init_native函数对应的JNI函数如下所示:
**com_android_server_location_GpsLocationProvider.cpp::android_location_GpsLocationProvider_class_init_native**
~~~
static void android_location_GpsLocationProvider_class_init_native(JNIEnv* env, jclass clazz) {
int err;
hw_module_t* module;
//获取JNI回调函数集对应的Java MethodId。如果读者不熟悉JNI,请阅读《深入理解Android:卷1》
//第2章“深入理解JNI”
method_reportLocation = env->GetMethodID(clazz, "reportLocation", "(IDDDFFFJ)V");
.......//JNI回调函数集的其他函数,下文会详细介绍它们
method_setEngineCapabilities = env->GetMethodID(clazz, "setEngineCapabilities", "(I)V");
.......
//加载GPS HAL层模块,GPS_HARDWARE_MODULE_ID的值为“gps”
err = hw_get_module(GPS_HARDWARE_MODULE_ID, (hw_module_t const**)&module);
if (err == 0) {
hw_device_t* device;
//打开GPS HAL层模块
err = module->methods->open(module, GPS_HARDWARE_MODULE_ID, &device);
if (err == 0) {
gps_device_t* gps_device = (gps_device_t *)device;
//获取GPS HAL层最主要的交互接口GpsInterface,它是JNI层和HAL层的重要交互
//通道
sGpsInterface = gps_device->get_gps_interface(gps_device);
}
}
//GPS HAL模块对外还提供了几个重要交互接口。
if (sGpsInterface) {
sGpsXtraInterface = //用来和GPS HAL层中xtra模块交互的接口
(const GpsXtraInterface*)sGpsInterface->get_extension(GPS_XTRA_INTERFACE);
sAGpsInterface = //用来和GPS HAL层AGPS模块交互的接口
(const AGpsInterface*)sGpsInterface->get_extension(AGPS_INTERFACE);
sGpsNiInterface = //用来和GPS HAL层NI(Network Initiated)模块交互的接口
(const GpsNiInterface*)sGpsInterface->get_extension(GPS_NI_INTERFACE);
sGpsDebugInterface = //用来调试的接口
(const GpsDebugInterface*)sGpsInterface->get_extension(GPS_DEBUG_INTERFACE);
sAGpsRilInterface = //用来和GPS HAL层AGPS及RIL相关的接口
(const AGpsRilInterface*)sGpsInterface->get_extension(AGPS_RIL_INTERFACE);
}
}
~~~
当GpsLP启用后,native_init函数将被调用,它对应的JNI函数代码如下所示:
**com_android_server_location_GpsLocationProvider.cpp::android_location_GpsLocationProvider_init**
~~~
static jboolean android_location_GpsLocationProvider_init(JNIEnv* env, jobject obj)
{
// this must be set before calling into the HAL library
if (!mCallbacksObj)
mCallbacksObj = env->NewGlobalRef(obj);
//向GPS HAL层设置回调函数接口。当GPS HAL层有情况需要通知JNI层时,这些回调函数
//将被调用
if (!sGpsInterface || sGpsInterface->init(&sGpsCallbacks) != 0)
return false;
//设置其他接口的回调函数
if (sGpsXtraInterface && sGpsXtraInterface->init(&sGpsXtraCallbacks) != 0)
sGpsXtraInterface = NULL;
if (sAGpsInterface) sAGpsInterface->init(&sAGpsCallbacks);
if (sGpsNiInterface) sGpsNiInterface->init(&sGpsNiCallbacks);
if (sAGpsRilInterface) sAGpsRilInterface->init(&sAGpsRilCallbacks);
return true;
}
~~~
通过上述代码可知,Android平台中通过定义多个交互接口实现了GPS Java层、JNI层以及HAL层的交互问题。显然,理解这些接口的作用将非常有助于我们学习Android平台中GPS模块的实现。下面将先介绍Java层与JNI层的交互接口。
**②、Java层与JNI层交互接口函数介绍**
先来看GPS Java层调用的JNI函数,它们的作用如下所示:
**GpsLocationProvider.java**
~~~
//初始化JNI层相关的类型
native void class_init_native();
//判断系统是否支持GPS
native boolean native_is_supported();
//初始化GPS HAL层模块
native boolean native_init();
//清理GPS HAL层模块所分配的资源
native void native_cleanup();
/*
设置GPS模块工作模式,其各个参数取值含义如下:
mode:GPS工作模式,目前可取值有GPS_POSITION_MODE_STANDALONE(值为0,仅GPS工作),
GPS_POSITION_MODE_MS_BASED(值为1,AGPS MSB模式),GPS_POSITION_MODE_MS_ASSISTED
(值为2,AGPS MSA模式)
recurrence:位置更新模式,目前可取值有GPS_POSITION_RECURRENCE_PERIODIC(值为0,连续
定位)、GPS_POSITION_RECURRENCE_SINGLE(值为1,单次定位)
min_interval:最短位置更新时间,单位为毫秒
preferred_accurary: 期望的位置更新精度,单位为米
preferred_time:期望的TTFF时间,单位为毫秒
*/
native boolean native_set_position_mode(int mode, int recurrence, int min_interval,
int preferred_accuracy, int preferred_time);
//下面这个两个函数用于启动和关闭导航
native boolean native_start();
native boolean native_stop();
//删掉AGPS辅助数据,其flags参数的取值请读者阅读GpsLocationProvider.java的deleteAidingData函数
native void native_delete_aiding_data(int flags);
//读取卫星信息,该函数在“reportStatus和reportSvStatus介绍”一节中已介绍过了
native int native_read_sv_status(int[] svs, float[] snrs,
float[] elevations, float[] azimuths, int[] masks);
//读取NMEA数据
native int native_read_nmea(byte[] buffer, int bufferSize);
//输入位置信息,在GpsLP中,该位置信息由NetworkLP提供
native void native_inject_location(double latitude, double longitude, float accuracy);
/*
输入NTP时间,其中:
time:为NtpTimeTrustedTime从网络中获取到的NTP时间
timeReference:为设备从开机到一次NTP请求处理成功后的所耗费的时间,由SystemClock的elapsedRealtime
函数返回
uncertainty:准确度。在Android系统中,该值为NTP请求发送和接收往返时间的一般。详情可参考
SnetpClient.java文件
*/
native void native_inject_time(long time, long timeReference, int uncertainty);
//GPS模块是否支持XTRA数据
native boolean native_supports_xtra();
//输入XTRA数据,即LTO数据
native void native_inject_xtra_data(byte[] data, int length);
//用于调试,获取GPS模块内部状态
native String native_get_internal_state();
//打开AGPS数据下载通道,参数apn指明了所要使用的APN
native void native_agps_data_conn_open(String apn);
//关闭AGPS数据下载通道
native void native_agps_data_conn_closed();
//GpsLP处理AGPS相关事宜失败时候调用下面这个函数
native void native_agps_data_conn_failed();
//将来自数据短信或WAP推送短信得到的信息传递给GPS模块。
native void native_agps_ni_message(byte [] msg, int length);
//设置AGPS服务端地址,其中type取值有两种:
//AGPS_TYPE_SUPL:值为1,代表SUPL服务器
//AGPS_TYPE_C2K:值为2,代表C2K服务器
native void native_set_agps_server(int type, String hostname, int port);
/*
当GPS模块需要使用APGS时,会调用reportNiNotification函数(详情见下文)以通知用户。
用户处理完后,将通过下面这个函数告知GPS模块处理结果。注意,这部分内容涉及到OMA-SUPL相关知识,请读者
阅读参考资料[28]。
该函数的参数如下:
notificationId:通知id,代表GPS HAL层的某一个处理请求
userResponse有三种取值,分别是GPS_NI_RESPONSE_ACCEPT(值为0,代表用户允许相关操作)、
GPS_NI_RESPONSE_DENY(值为1,用户拒绝相关操作)、GPS_NI_RESPONSE_NORESP
(值为2,代表用户无回应)。NI和GpsNetInitiatedHandler有关,读者可自行研究它
*/
native void native_send_ni_response(int notificationId, int userResponse);
/*
设置AGPS参考位置信息,其各参数解释如下:
type:取值可为AGPS_REF_LOCATION_TYPE_GSM_CELLID(值为1,代表GMS网络的cell id)、
AGPS_REF_LOCATION_TYPE_UMTS_CELLID(值为2,代表CDMA网络的cell id)、
AGPS_REG_LOCATION_TYPE_MAC(值为3,代表MAC地址)
mcc:Mobile Country Code(移动国家码),由3位数字组成,唯一地识别移动用户所属的国家,中国为460
mnc:Mobile Network Code(移动网络码),用于识别移动用户所归属的移动网络。中国移动TD系统使用00,
中国联通GSM系统使用01,中国移动GSM系统使用02,中国电信CDMA系统使用03
lac:Location Area Code(位置区码)。移动通信中,为了确定终端台的位置,每个移动网络的覆盖区都被划分
成许多位置区,位置区码(LAC)则用于标识不同的位置区。
cid:cell id(基站编号)
*/
native void native_agps_set_ref_location_cellid(int type, int mcc, int mnc,
int lac, int cid);
/*
设置终端与移动网络相关的一些参数信息,其type参数决定了setid参数的取值。type可取值有:
type: AGPS_SETID_TYPE_NONE,值为0,无意义
AGPS_SETID_TYPE_IMSI,值为1,代表IMSI(international mobiles subscriber identity,
国际移动用户号码标识)。IMSI信息存储在SIM卡上。
AGPS_SETID_TYPE_MSISDN,值为2,代表Mobile Subscriber ISDN(用户号码),即手机号码
*/
native void native_agps_set_id(int type, String setid);
/*
通知GPS HAL层系统当前网络的状态,其各参数解释如下:
connected:网络是否连接
type:网络类型,可取值请参考ConnectivityManager中各网络类型的定义情况。常用的有TYPE_MOBILE、
TYPE_WIFI等
roaming:是否处于漫游状态
available:网络是否可用
extraInfo:附加信息
defaultAPN:默认APN
*/
native void native_update_network_state(boolean connected, int type,
boolean roaming, boolean available, String extraInfo, String defaultAPN);
~~~
现在来看看JNI回调Java层的函数,如下所示:
**GpsLocationProvider.java**
~~~
//GPS模块汇报位置信息
void reportLocation(int flags, double latitude, double longitude, double altitude,
float speed, float bearing, float accuracy, long timestamp):
//GPS模块通知GPS工作状态
void reportStatus(int status):
//GPS模块通知卫星状态
void reportSvStatus():
//GPS模块通知AGPS状态
void reportAGpsStatus(int type, int status, int ipaddr):
//GPS模块通知NMEA信息
void reportNmea(long timestamp):
//GPS模块通知GpsLP自己的能力
void setEngineCapabilities(int capabilities):
//GPS模块要求下载XTRA数据
void xtraDownloadRequest():
//GPS模块通知Network Initiated通知,其各参数解释如下:
void reportNiNotification(
int notificationId,//GPS HAL层分配的通知ID
int niType,//NI类型,可取值请参考gps.h的GpsNiType的定义
int notifyFlags,//标志信息,可取值请参考gps.h的GpsNiNotifyFlags定义
int timeout,//等待用户处理的超时时间
//当超时发生后,系统采用的默认处理结果。其取值和native_send_ni_respons中第二个参数一样
int defaultResponse,
String requestorId,//代表请求者的id
String text, //通知信息
int requestorIdEncoding,//requestorId的编码格式,参考gps.h GpsNiEncodingType的定义
int textEncoding,//text的编码格式
String extras //附加信息
):
//要求获取参考位置信息,flags参数目前没有使用
void requestRefLocation(int flags):
//要求设置移动网络相关信息,flags参数表示要获取什么样的信息,可取值同native_agps_set_id的type参数
//一致
void requestSetID(int flags):
//要求获取UTC时间
void requestUtcTime():
~~~
接下来看GPS JNI与HAL层的交互接口。我们主要介绍JNI调用HAL层的接口。
**③、 GpsInterface及其他交互接口介绍**
GpsInterface是GPS JNI与HAL层交互的主要接口。在Android平台中,该接口定义在一个同名的结构体中,其内容如下所示:
**gps.h::GpsInterface结构体**
~~~
typedef struct {
size_t size;//GpsInterface结构体的长度
//初始化GPS模块。其参数为GPS模块所需的回调函数
int (*init)( GpsCallbacks* callbacks );
//下面这两个函数用于开启或关闭导航
int (*start)( void );
int (*stop)( void );
//清理GPS模块所分配的资源
void (*cleanup)( void );
//输入UTC时间,参数解释同native_inject_time
int (*inject_time)(GpsUtcTime time, int64_t timeReference,int uncertainty);
//输入位置信息,参数解释同native_inject_location
int (*inject_location)(double latitude, double longitude, float accuracy);
//删除赋值信息,其参数解释同native_delete_aiding_data
void (*delete_aiding_data)(GpsAidingData flags);
//设置GPS模块的工作模式,其参数解释同native_set_position_mode
int (*set_position_mode)(GpsPositionMode mode, GpsPositionRecurrence recurrence,
uint32_t min_interval, uint32_t preferred_accuracy, uint32_t preferred_time);
//获取GPS模块实现的扩展接口,AGPS扩展接口对应的name为“agps”、xtra扩展接口对应的name为“xtra”
const void* (*get_extension)(const char* name);
} GpsInterface;
~~~
比较GpsInterface和Java层定义的native函数,读者可发现二者结合非常紧密。实际上,JNI实现的那些native函数最终都会把请求通过HAL层接口交给GPS模块去处理。
再来看gps.h定义的GpsXtraInterface接口,相关内容封装在同名的结构体中,如下所示:
**gps.h::GpsXtraInterface结构体**
~~~
typedef struct {
size_t size;
//初始化GPS中的xtra相关模块
int (*init)( GpsXtraCallbacks* callbacks );
//输入xtra数据,其参数解释同native_inject_xtra_data
int (*inject_xtra_data)( char* data, int length );
} GpsXtraInterface;
~~~
接下来看AGpsInterface结构体,代码如下所示:
**gps.h::AGpsInterface**
~~~
typedef struct {
size_t size;
//初始化AGPS模块
void (*init)( AGpsCallbacks* callbacks );
//打开AGPS数据连接,其参数解释同native_data_conn_open
int (*data_conn_open)( const char* apn );
//关闭AGPS数据连接,其参数解释同native_data_conn_close
int (*data_conn_closed)();
//AGPS数据连接操作失败,同native_data_conn_fail
int (*data_conn_failed)();
//设置AGPS服务器地址等相关信息,参数解释同native_agps_set_server
int (*set_server)( AGpsType type, const char* hostname, int port );
} AGpsInterface;
~~~
最后来看GpsNiInterface和AGpsRilInterface接口。GpsNiInterface的代码如下所示:
**gps.h::GpsNiInterface**
~~~
typedef struct
{
size_t size;
//初始化NI模块
void (*init) (GpsNiCallbacks *callbacks);
//发送NI回复,请参数同native_send_ni_response
void (*respond) (int notif_id, GpsUserResponseType user_response);
} GpsNiInterface;
~~~
而AGpsRilInterface的代码如下所示:
**gps.h::AGpsRilInterface**
~~~
typedef struct {
size_t size;
//初始化AGPS Ril相关的处理模块
void (*init)( AGpsRilCallbacks* callbacks );
//设置参考位置信息,其第一个参数类型为AGpsRefLocation,该结构体的成员与
//native_agps_set_ref_location_cellid函数的参数一一对应
void (*set_ref_location) (const AGpsRefLocation *agps_reflocation, size_t sz_struct);
//设置AGPS移动网络id信息,其参数解释同native_agps_set_id
void (*set_set_id) (AGpsSetIDType type, const char* setid);
//设置NI消息,其参数解释同native_agps_ni_message
void (*ni_message) (uint8_t *msg, size_t len);
//注意,下面这两个函数的参数合起来就是native_update_network_state的参数。Java层调用
//一次native_update_network_state将触发下面这两个函数被调用
//更新移动网络状态
void (*update_network_state) (int connected, int type, int roaming, const char* extra_info);
//设置网络连接状态
void (*update_network_availability) (int avaiable, const char* apn);
} AGpsRilInterface;
~~~
本节对Android平台中LocationManagerService及相关模块进行了介绍,尤其对本章的核心GpsLocationProvider及GPS各层次及之间的交互接口进行了重点讲解。
和本书前面介绍的WifiService、WifiP2pService以及NfcService比起来,LMS一点也不复杂,这其中的几个主要原因包括:
- LMS提供的服务本身就比较简单,它的核心功能就是提供位置信息。
- GpsLP通过合理的分层接口设计使得GPS HAL层之上的代码能够不受底层硬件的影响。
另外,笔者希望读者在学习完本章后,对以下内容开展进一步的学习:
- 研究PassiveProvider和FusedLocationProvider的内容。掌握LMS如何与位于应用进程的LP进行交互。
- 学习LocationFudger的内容,掌握如何模糊位置信息。
- 学习GeofenceManager的内容。
最后,读者可尝试反编译NetworkLocation.apk,掌握NetworkLP以及Geocoder的实现原理[^①]。
[^①]:出于对版权的考虑,笔者不能在书中对NetworkLocation.apk反编译的结果开展详细讨论。如果时机合适,笔者将在博客上对它的实现进行介绍。
';
9.3.2 LocationManager应用示例
最后更新于:2022-04-02 06:06:17
本节所使用的示例运行后的界面如图9-30所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/a5d5bee9b545c03d43f02a215d102517_891x477.jpg)
图9-30 示例运行效果
* 左边按钮为"Fine-grain provider",表示通过GpsLP来获取位置信息。
* 右边按钮为"Both Providers",表示同时使用GpsLP和NetworkLP来获取位置信息。
* 按钮下方的"Lat/Long"表示当前设备的位置信息(经纬度值),而"Address"表示根据该位置信息得到的地址信息。
示例非常简单,所有内容都集中在LocationActivity.java文件中。先来看onCreate函数,代码如下所示。
**LocationActivity.java::onCreate**
~~~
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
......// UI 等初始化
// Geocoder:只有Android 2.3版本以后系统才支持该功能
// 同时还需要判断是否存在GeocoderProvider
mGeocoderAvailable =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD &&
Geocoder.isPresent();
mHandler = new Handler() {......// Handler的作用是更新图9-30中的位置和地址信息};
// 客户端必须要获取LocationManager来和LMS交互
mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
}
}
~~~
接着,LocationActivity在其onResume中将调用setup函数完成进一步的初始化工作,其代码如下所示。
**LocationActivity.java::setup**
~~~
private void setup() {
Location gpsLocation = null; Location networkLocation = null;
mLocationManager.removeUpdates(listener);
mLatLng.setText(R.string.unknown); mAddress.setText(R.string.unknown);
if (mUseFine) { // mUseFine对应为图9-30中的“Fine-grain Provider”按钮
......
// requestUpdatesFromProvider为关键函数,下文将详细分析
gpsLocation = requestUpdatesFromProvider(
LocationManager.GPS_PROVIDER,// 该参数为字符串,值为“gps”
R.string.not_support_gps);
if (gpsLocation != null) updateUILocation(gpsLocation);
} else if (mUseBoth) {// mUseBoth对应为图9-30中的“Both Providers”按钮
gpsLocation = requestUpdatesFromProvider(LocationManager.GPS_PROVIDER, R.string.not_support_gps);
networkLocation = requestUpdatesFromProvider(
LocationManager.NETWORK_PROVIDER, // 该参数值为“network”
R.string.not_support_network);
......
}
}
~~~
看setup中的关键函数requestUpdatesFromProvider代码如下所示。
**LocationActivity.java::requestUpdatesFromProvider**
~~~
private Location requestUpdatesFromProvider(final String provider,
final int errorResId) {
Location location = null;
// 判断由provider指定的LP是否启用。
if (mLocationManager.isProviderEnabled(provider)) {
/*
调用LocationManager的requestLocationUpdates函数,该函数用于注册一个回调函数以
接收位置变化信息,其各个参数的作用如下。
provider:用于指明使用哪个LP,目前可取参数有“gps”(对应为GpsLP)、“network”(对应
为NetworkLP)、“passive”(对应为PassiveProvider)。
TEN_SECONDS:用于指明多少毫秒更新一次位置数据,本例中使用的值为10秒。
TEN_METERS:用于指明位置变化多少时更新一次数据,本例中使用的值为10米。
listener:类型为LocationListener,当位置发生变化时,其onLocationChanged函数将被调用。
*/
mLocationManager.requestLocationUpdates(provider, TEN_SECONDS,
TEN_METERS,listener);
// getLastKnownLocation用于获取LP上一次保存的位置信息数据
// Android平台中,位置信息用Location类表示
location = mLocationManager.getLastKnownLocation(provider);
}
return location;
}
~~~
requestLocationUpdates是LM中非常重要的函数,请读者务必把握其用法。
当位置信息发生变化后,LocationListener的onChange函数将被调用。本例中使用的LocationListener相关代码如下所示。
**LocationActivity.java::LocationListener**
~~~
private final LocationListener listener = new LocationListener() {
public void onLocationChanged(Location location) {
updateUILocation(location);// 下文将分析它
}
// 当用户在设置中启用或禁止相关LocationProvider后,下面这两个函数将被调用
public void onProviderEnabled(String provider) { }
public void onProviderDisabled(String provider) { }
// 当LocationProvider的状态发生变化时,下面这个函数将被调用
public void onStatusChanged(String provider, int status, Bundle extras) {}
};
~~~
updateUILocation函数代码如下所示。
**LocationActivity.java::updateUILocation**
~~~
private void updateUILocation(Location location) {
// updateUILocation的参数location代表对应LP得到的位置信息
// 示例程序将根据该信息来更新图9-30中“Lat/Long”的值
Message.obtain(mHandler, UPDATE_LATLNG,
location.getLatitude() + ", " + location.getLongitude()).sendToTarget();
// doReverseGeocoding根据位置信息来获取地址信息
if (mGeocoderAvailable) doReverseGeocoding(location);
}
~~~
doReverseGeocoding用于根据位置信息来获取地址信息,由于该工作往往需要通过网络来查询,所以doReverseGeocoding内部将创建一个AsyncTask用来完成此工作。我们直接来看AsyncTask的代码。
**LocationActivity.java::ReverseGeocodingTask**
~~~
private class ReverseGeocodingTask extends AsyncTask {
Context mContext;
......
protected Void doInBackground(Location... params) {
// 创建一个Geocoder对象,它可用于处理地址信息和位置信息的转换
Geocoder geocoder = new Geocoder(mContext, Locale.getDefault());
Location loc = params[0];
List addresses = null;
try {
// getFromLocation用于根据位置信息来查询对应的地址信息
addresses = geocoder.getFromLocation(loc.getLatitude(), loc.getLongitude(), 1);
} ......
if (addresses != null && addresses.size() > 0) {
Address address = addresses.get(0);
String addressText = String.format("%s, %s, %s",
address.getMaxAddressLineIndex() > 0 ? address.getAddressLine(0) : "",
address.getLocality(),address.getCountryName());
// 更新图9-30中的“Address”信息
Message.obtain(mHandler, UPDATE_ADDRESS, addressText).sendToTarget();
}
return null;
}
}
~~~
通过上述示例可以发现,Android平台中使用LM非常简单,其主要工作如下。
1. 先创建一个LocationManager对象,用于和LMS交互。
2. 然后调用requestLocationUpdates以设置一个回调接口对象LocationListener,同时还需要指明使用哪个LP[^①]。
3. 当LP更新相关信息后,LocationListener对应的函数将被调用,应用程序在这些回调函数中做相关处理即可。
4. 如果应用程序需要在位置和地址信息做转换,则使用Geocoder类提供的函数即可。
虽然LM比较简单,但它提供的都是一些很基本的功能,如果想实现一些诸如显示地图信息这样的功能,LM就无能为力了。为了实现一些更复杂的位置相关的功能,Google提供了更高级的API来帮助开发者。关于这一部分内容,建议读者阅读参考资料[30]。
[^①]:根据审稿专家的意见,有些应用会只传进定位的条件(精度),而不去指定使用哪个LP。在不带GPS功能的平台,若应用只指定从GPS中获取定位数据,则它将得不到位置信息。
';
9.3.1 LocationManager架构
最后更新于:2022-04-02 06:06:15
GPS的根本目的是为使用者提供位置相关的信息。Android系统设计了一个以LocationManagerService为核心的位置管理架构提供相关的位置服务。
图9-29所示的Android平台LocationManager架构按顺时针可分为四部分。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/cd38516c0f5074bc8da561bbd68df8b7_1274x682.jpg)
图9-29 Android平台中LocationManager架构
第一部分为LocationManagerService(简称LMS)和其客户端LocationManager(简称LM)。LMS和Android Java Framework中其他Service一样由SystemServer创建并运行在system_process进程中[^①]。LMS内部将统一管理Android平台中能提供位置服务的相关模块,而LM为那些需要使用位置服务的应用程序服务。LM和LMS之间通过Binder进行交互。下节所示的示例应用程序将介绍LM的用法。
Android平台中能提供位置服务的相关模块统称为Location Provider(位置提供者,LP)。位置提供者必须实现LocationProviderInterface接口。这些接口对应的对象实例由LMS来创建和管理。在所有这些位置提供者中,Android Framework实现了其中的PassiveProvider和GpsLocationProvider。这两个LP由LMS创建并运行在system_process进程中。下文介绍LMS时还会详细介绍PassiveProvider和GpsLP。
除了使用GPS定位外,系统还支持网络定位(Network Location)方法来获取位置信息。这种方法大致的工作原理是,某地区的移动通信基站(Cell Tower)或无线网络AP的位置信息都已事先获取并保存在相关服务提供商的服务器上。当手机使用网络定位时,它首先向服务器查询自己所连接或搜索到的基站位置或AP的位置,然后根据信号的强度推算自己的大致位置。相比GPS定位而言,网络定位速度快,耗电少,适用于室内和室外,但精度较GPS差。Android原生代码并不提供Network Location Provider相关的功能,它一般由第三方应用厂商提供,例如Google的GMS(Google Mobile Service)包中有一个NetworkLocation.apk就提供了该功能,而国内上市的手机则使用百度公司提供的NetworkLocation_Baidu.apk。由于它们运行在应用程序所在的进程中,所以系统定义了ILocationProviderProxy接口使LMS能管理这些由应用程序提供的位置服务。这些应用的位置服务需要实现LocationProviderBase抽象类。相关类结构如图中区域3所示。
区域3中的FusedLocationProvider是一个比较有意思的LP。它本身不能提供位置信息,其内部将综合GpsLP和NetworkLP的位置信息,然后向使用者提供最符合使用者需求的数据。即它能根据使用者对电源消耗、精度两方面的要求以选择GpsLP或/和NetworkLP作为真实的LP。同时,FusedLP能选择GpsLP或NetworkLP提供的位置信息中最好的那一个返回给使用者。简单点说,FusedLP出现之前,一个比较完善的LP客户端需要同时操作和管理GpsLP和NetworkLP,而有了FusedLP后,客户端只需要使用它即可,其余事情由FusedLP内部来管理。注意,FusedLP也由应用程序提供,它运行在FusedLocationProvider.apk所在的进程中。
除了提供位置信息外,系统(借助第三方应用提供)还支持位置信息和地址信息相互转换,即得到某个地址(如国家、市区、街道名等)的位置信息(如经纬度信息),或者根据位置信息得到其对应的地址信息。由于地址和位置信息的映射关系一般也由第三方应用提供,所以LMS利用GeocodeProxy和第三方应用中实现IGeocodeProvider的对象交互。相关类结构如图中
区域4所示。
>[info] 提示 FusedLocationProvider的代码非常简单,感兴趣的读者可自行研究。可参考SDK中关于这方面的讨论,其位置为https://developer.android.com/guide/topics/location/strategies.html。
了解Android平台中LM的架构后,笔者从以下两个方面详细介绍Android中的位置管理模块。
* 通过一个示例展示如何利用LocationManager功能来获取自己的位置信息以及地址信息。
* 介绍LMS相关的模块及工作原理。这些模块包括LocationManagerService、GpsLocationProvider、GPS HAL层相关控制接口等。
[^①]: 关于system_process,读者可阅读《深入理解Android:卷Ⅱ》第3章。
';
9.3 Android中的位置管理
最后更新于:2022-04-02 06:06:13
9.2.3 OMA-SUPL协议
最后更新于:2022-04-02 06:06:11
OMA-SUPL包含一套非常复杂的协议,它综合了移动通信领域现有的一些标准和协议(如3GPP相关协议、WAP等),其目的是充分利用移动网络的相关特性以为用户提供更好的位置服务。OMA-SUPL目前最新版本是3.0,表9-7列举了OMA-SUPL各版本的特点。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/7a80d934062cdbea142a8f30ebbd43f0_1259x552.jpg)
关于代理模式和非代理模式的区别,请参考9.2.3节SUPL架构。
表9-7中的缩写词含义如下。
* RRLP(Radio Resource LCS Protocol)是一种协议,LCS是Location Services的缩写。
* RRC(Radio Resource Control)是一种协议。
* LPP(LTE Positioning Protocol)是基于LTE的定位协议。
* LPPe(OMA LPP Extensions)是LPP扩展协议。
* TIA(Telecommunications Industry Association)是美国电信工业协会。
* OTDOA(Observed Time Difference of Arrival)是一种移动定位技术。
>[info] 提示 OMA-SUPL涉及很多来自移动通信领域的概念和词汇。由于篇幅问题,笔者不打算对它们进行深入介绍,感兴趣的读者可自行研究。
**1、SUPL架构**
OMA-SUPL是一个比较复杂的系统,图9-26所示为它的架构。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/eda647211329fc7c66a3928b5e3cc115_816x670.jpg)
图9-26 SUPL架构
图9-26中主要包含三个部分。
* 左下方的SET代表AGPS服务的客户端,例如我们的Android智能手机。在规范中,SET全称是SUPL Enabled Terminal(终端)。
* 右下方的SLP①(SUPL Location Platform)包含两个重要组成部分,一个是SLC(SUPL Location Center),其作用是和SET交互,例如处理来自SET的请求;另外一个是SPC(SUPL Positioning Center),其作用是进行定位计算。如果SET直接和SPC交互,则称为非代理工作模式。相反,如果SET借助SLC与SPC交互的话,则称为代理模式[29]。SUPL 3.0版协议只支持代理模式,故SET将只能和SLC交互。
* 右上方的SUPL Agent②,是一个需要获取位置信息的应用程序。SUPL Agent可以运行在SET中(如图中左下方的SET和SUPL Agent),也可以运行在SUPL Network③中,如图中右上方单独绘制的SUPL Agent。不论哪种情况,定位请求只能由SUPL Agent发起。如果SUPL Agent在SET中,这种请求方式叫SET Initiated请求(终端始发定位请求)[29]。如果SUPL Agent位于SUPL Network中,则这种请求方式叫Network Initiated请求(网络始发定位请求)。
除了上述三个部分外,图9-26中的连线用于表示它们之间交互所使用的协议等信息。
对于网络始发定位请求而言,SLP需要通知目标SET参与定位工作(而在终端始发定位请求中,请求的发起者与SET在一个设备上),这个流程也叫SUPL INIT。SUPL INIT支持的协议很多,例如通过SIP、WAP、SMS等,或者直接利用UDP、TCP等。在使用SIP、WAP或SMS等协议时还需要借助移动通信领域中现有的组件(如SMS需要先通过短信息中心SMS Center来处理),
所以图中也绘制了这些必要的组件以及这些组件和SLP交互的协议,如SMSC、SIP/IP Core、WAP PRG(Wireless Application Protocol Push Proxy Gateway)、PAP(Push Access Protocol)、POTAP(Push Over The Air Protocol)等。SET和SLP交互的流程由ULP(User Location Protocol,
下节将详细介绍它)描述。
在SLP中,SLC和SPC交互的协议叫ILP(Internal Location Protocol)。
OMA-SUPL还为SET、SLC和SPC定义了一组Function来描述它们应该具有的功能,表9-8
列举了这些Function的名称和功能。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/abfb020a324a466a49b33e6f21cdb4db_1254x654.jpg)
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/6e9f922c3b0c27aa14209e80e944dea1_1253x537.jpg)
SUPL的内容非常多,不过和本章后文代码分析相关的内容大多集中在ULP协议即工作流程上。下面来看看ULP。
**2、ULP介绍**
ULP主要描述SET和SLP之间该如何交互以完成定位请求。根据上一节对SUPL Agent的介绍,ULP的使用分为两大类④。
* SUPL Agent位于SET中,由于定位请求只能由SUPL Agent发起,所以在ULP中,它被称为SET Initiated定位请求。其典型的使用案例就是在Android手机中打开导航类应用,这将触发手机发起一次定位请求。
* SUPL Agent位于SUPL Network中,这种情况称为Network Initiated定位请求。例如,某些网络服务需要跟踪SET的位置,就会使用这种方式。不过,笔者目前没有找到与之相关的典型使用案例,有知晓的读者不妨与大家分享相关知识。
这两大类ULP应用场景对应的工作流程各不相同,我们先来看最常见的SET Initated请求的工作流程。
**①、SET Initiated ULP工作流程**
图9-27描述了SET Initiated ULP的工作流程。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2ba7db2810fce644a40349ac4dd159cf_913x608.jpg)
图9-27 SET Initiated ULP流程
1. SET首先和SLP建立数据链接。为了保证数据的安全性,这个链接需要基于TLS(Transport Layer Security,传输层安全)。图中的D/H-SLP为Discovered/Home-SLP的缩写,H-SLP即SET所在运营商所建立的SLP,而D-SLP为SET搜索到的SLP。
2. SET发送SUPL START命令给SLP,该命令携带了一些参数,包括locationId(如果使用移动通信网络,则该参数包括基站的Cell Info。如果使用Wi-Fi,则该参数包括AP的信息)、sETCapablilities(SET的能力,如支持的定位数据封装协议、支持的定位方法等,详情可参考表9-7)。
3. SLP回复SUPL RESPONSE命令给SET。RESPONSE命令包含了SLP支持的定位方法(由posMethod表示),以及SLP支持的定位能力(由sLPCapabilities描述)。
4. SET发送SUPL POS INIT命令给SLP,该命令包含了SET的初始位置等信息。
5. 接着,SET和SLP通过一个或多个SUPL POS消息来计算位置。根据AGPS使用的模式(MSB或MSA),位置的计算方法也不尽相同。
6. 当位置计算完毕后,SLP发送SUPL END命令给SET,二者随后断开TLS链接。
>[info] 提示 关于ULP各消息所包含的参数信息,请读者自行阅读参考资料[28]。
**②、Network Initiated ULP工作流程**
图9-28所示为Network Initiated(NI)ULP的工作流程图。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/4c669de3fed62257fdfbc68aa062f5ac_1214x616.jpg)
图9-28 Network Initiated ULP流程
图9-28中,由于SUPL Agent位于SUPL Network,所以它和SLP的交互遵守MLP(Mobile Location Protocol)。SLP收到SUPL Agent的SLIR(Standard Location Immediate Request)请求后,它将发送SUPL INIT命令给SET。此处需强调,如果SET和SLP此时还没有建立数据链接,SUPL INIT将通过OMA Push消息或数据短信等方式发送给SET,SET收到SUPL INIT命令后将和SLP 建立数据链接。
此后,SLP和SET之间的交互与图9-27类似。SLP最终通过SLIA(Standard Location Immediate Answer)将定位信息发送给SUPL Agent。
>[info] 提示 此处不再详述ULP的细节,请读者自行阅读参考资料[28]。
至此,本章所涉及的GPS相关基础知识就全部介绍完毕。相信读者能感觉到这些内容背后的专业知识是多么庞大和复杂。在此,希望立志成为GPS专家的读者继续保持谦虚的态度,认真学习,争取为中国的北斗导航系统添砖加瓦。
>[info] 提示 本节所述内容能覆盖Android GPS相关模块(不含驱动及芯片底层模块)代码中80%左右的背景知识。
① OMA-SUPL中有大量的缩写词汇,请读者阅读时务必注意它们的全称。
② 规范中的定义是"A Software and/or hardware entity accessing the SUPL enabler in order to
obtain location information"。
③ 读者可将其理解为一个帮助定位的系统,规范中的定义是"Access network which facilitates
the location determination functionality and provides the SUPL bearer"。
④ 此处讨论仅针对OMA SUPL ULP规范中的Immediate Service。
';
9.2.2 GPS系统组成及原理
最后更新于:2022-04-02 06:06:08
本节将主要介绍和GPS相关的基础知识,先来看GPS系统的组成[8]。如图9-9所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/440f75a4d8becf08fa41a297b17c25e4_640x627.jpg)
图9-9 GPS系统组成
由图9-9可知,GPS包含如下三个段。
* 空间段(Space Segment,SS):空间段由GPS卫星组成。
* 控制段(Control Segment,CS):控制段用来控制和监视GPS的运行。控制段包括一个主控站(Master Control Station,位于美国科罗拉多州)、数个监控站(Monitoring Station)、地面控制站(Ground Control Station)以及地面天线(Ground Antenna)。图9-10为目前GPS系统的CS站点分布图。
* 用户段(User Segment,US):用户段主要是GPS的使用者。GPS中,用户被分为民用用户(Civilian Users)和军用用户(Military Users)两大类。其中,军用用户需要得到相关部门的授权才能获取更高精度的GPS数据。
GPS这三个段将借助GPS规定的通信频段以及数据封装格式进行通信。其中,空间段和控制段能双向通信,而用户段只能从空间段获取数据。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/f1dd150af65c45467645990c4160acf1_1139x609.jpg)
图9-10 CS站点分布图
>[info] 说明 GPS提供两种类型的服务,分别是标准定位服务(Standard Positioning Service,SPS)和精密定位服务(Precision Positioning Service,PPS)。其中,SPS主要面向全世界的民用用户,而PPS主要面向美国及其盟国的军事部门以及民用的特许用户。
下面介绍GPS空间段以及GPS通信及数据包方面的知识。
**1、GPS空间段**
GPS空间段的建设历经了30多年的时间。表9-1展示了这期间GPS卫星更新换代的几次重要事件。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/b56efff30c0ad0d6f4dadcb497370365_1260x701.jpg)
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/6b1496625af3e45fb6cce5c038f82e10_1261x588.jpg)
参考资料[10]总结了GPS空间段建造历史以及GPS卫星发射计划。
目前为止,GPS空间段由32颗GPS卫星①(卫星的英文名为Satellite,也称为Space Vehicle,简写为SV)组成,这些卫星分布在6个轨道上,每个轨道与地球赤道面的倾角为55度。GPS卫星轨道高度为20180千米,卫星在轨道上的运行周期大约为12小时。不过,由于地球的自转,人们在地面上观测GPS卫星,在23小时56分左右会回到最初的观测位置。图9-11所示为GPS卫星轨道分布图。
由于每颗GPS卫星的信号只能覆盖地球表面的一部分,所以GPS空间段在设计时就保证任何时候,地球表面任何地方都能被至少4颗GPS卫星信号覆盖。图9-12为某时刻从地面观测到的GPS卫星的位置分布图。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/93568eca3c12912a2b0291a9bc464a92_704x604.jpg)
图9-11 GPS卫星轨道分布
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/ec571be74d693cb3bf1f138a8e18db9f_1174x633.jpg)
图9-12 2001年4月14日UTC时间12点整GPS卫星分布图[11]
>[info] 提示 为什么要确保至少4颗卫星的信号能覆盖到地球表面任意地方呢?根据前面介绍的测距原理可知,要计算接收器的位置即(x,y,z)坐标值就需要3颗卫星,而由于接收器时钟和卫星时钟的不同步,所以还需要至少一颗卫星用来计算信号传输时间。综上,GPS定位需要至少4颗卫星参与。
在此推荐使用GpsPredict软件获取和展示GPS卫星轨道及相关信息。图9-13所示为该软件运行时的界面。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/ff91f78286d600f5c716ac0fd92a7944_1053x574.jpg)
图9-13 GpsPredict运行界面
>[info] 提示 GpsPredict的软件下载地址为http://sourceforge.net/projects/gpredict/files/。
**2、GPS通信频段**
GPS卫星和地面监控站以及接收器使用无线电波进行通信,GPS一共使用了三个频段的无线电波来传输数据,如图9-14[11]所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/565bdf76178605d7285c8e4b77762250_889x397.jpg)
图9-14 GPS卫星通信频段
GPS一共使用三种频段的无线电波,由于它们都位于无线电频谱的L频段,所以它们分别被称为L1(中心频率1575.42MHz)、L2(中心频率1227.60MHz)和L5(中心频率1176.45MHz)。
在2005年之前,GPS卫星使用L2和L1频段的无线电波。其中,L1频段传输两种GPS信号,一个是民用的C/A码(全称是Coarse/Acquisition Code),它代表粗捕获码数据。另外一个是军用的P(Y)码,它代表精测数据(P代表Presice,Y代表数据是加密的)。L2频段仅传输P(Y)码,即仅供军用。下文还将详细GPS信号方面的知识。
IIR(M)型号的GPS卫星在L2频段上增加了一个名为L2C(C为Civil的意思)的GPS信号。L2C信号可以和C/A信号共同使用(即所谓的双频)以减少大气电离层②的影响从而提高定位精度(其精度甚至能超过军用级定位的精度,详情见参考资料[12])。另外,L1和L2频段上新增了针对军用用户的L1M和L2M信号,它们均采用BOC(Binary Offset Code)方法进行调制和解调,可显著增强军用信号的抗干扰能力。
IIF卫星能在L5上发射民用GPS信号,这类信号主要为航空安全服务,它具有更高的功率,更大的带宽和更稳定的服务。详情见参考资料[13]。
在L1频段,III型卫星将支持一种名叫L1C的新GPS信号。L1C信号可增强GPS系统和其他GNSS系统(如中国的北斗导航系统也将广播L1C信号)之间的交互性(interoperability)。
图9-15总结了各类型GPS卫星所支持的通信频段信息。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/a7fe892423215a9976875fad466d2fa3_780x458.jpg)
图9-15 GPS各类型卫星所支持的通信频段[14]
了解了GPS卫星通信频段的知识后,下面让我们把注意力放到GPS卫星通过这些频段所传输的数据上来,即和GPS信号相关的知识点。
**3、GPS信号[11]**
GPS信号将借助上一节所述的GPS卫星通信频段进行无线电传输,它由三部分组成,如图9-16所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/3b40d11d94a5464783df54eddce75334_640x326.jpg)
图9-16 GPS信号的组成
由图9-16可知,GPS信号包含主要三个组成部分。
**1)载波**:分别是L1和L2(注意,本书不讨论L5的情况,感兴趣的读者可阅读参考资料[14]),其中心频率分别是1575.42MHz和1227.60MHz。
**2)测距码(Ranging Code)**:用来测量卫星和接收器之间距离的一种信号。测距码其实是一种经过精心设计的伪随机噪声③(Pseudo-Random Noise,PRN)。
GPS有C/A码和P码两种测距码。
* C/A码(粗捕获码),频率为1.023MHz,周期为1ms,码长为1023,码元的宽度为293.05m,测距精度为2m到3m。
* P码(精捕获码),频率为10.23MHz,是和粗捕获码对应的测距码,其周期为7天,码长为6.1871*1012,码元周期0.097752微秒,相应码元宽度为29.3m,测距精度为0.3m。P码供军事应用,故可以对它进行密。加密后的P码称为“Y码”。
**3)导航电文(Navigation Data,也叫D码)**:在定位计算时,除了测距码外还需要卫星的一些信息,例如星历、时间等。这些数据封装在GPS导航电文中,其传输频率为50比特每秒(即50Hz)。导航电文的详情见下节。
C/A码仅在L1频段上发送,而P码同时在L1和L2频段发送,根据前面介绍的双频知识,接收器可通过接收L1和L2频段的P码以消除大气电离层造成的延时影响从而进一步提高定位精度。
>[info] 注意 在数字通信中,一个数字脉冲称为一个码元。一个周期中码元的个数称为码字的长度,简称为码长,常用n表示。
C/A码的码元宽度为293.05m,这是通过以下公式得来。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2fc1a7e9259ee489c8b73051d3541fdc_518x105.jpg)
其测距精度是如何计算出来的呢?接收器在工作时会生成一个C/A码,这个C/A码将和某
个卫星发送的C/A码进行匹配。匹配时涉及码相位数字信号处理方面的工作,理想情况下其最
高精度能达到码元宽度的1%,所以C/A码的测距精度为293.05*1%(约3m)。
关于GPS信号方面的进一步知识,请读者阅读参考资料[15]。下面来看看GPS导航电文的内容。
**4、GPS导航电文**
**①、数据格式及内容**
GPS导航电文(Navigation Message)有其特定的格式,如图9-17[11]所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/4082ff546d6cd258f87d2f755d868a6d_1043x607.jpg)
图9-17 GPS导航电文格式
如图9-17所示,GPS导航电文的基本单位是帧(Frame,也叫Page)。一帧包含1500比特。导航电文的传输速率是每秒50比特,故传输完整的一帧数据需30s。
每一帧中的1500比特又被平均分配,每300比特组成一个子帧(Sub-Frame),所以一帧包含5个子帧。每一个子帧又由10个字码(word)组成,每一个字码包含30位数据。子帧的第一个字码叫TLM(Telemetry Word,遥测码),第二个字码叫HOW(Handover Word,转换字)。下文将详细介绍TLM和HOW的组成。
一个完整的GPS导航电文由25帧组成,共37500比特,故全部传输完它们共需12.5分钟。
表9-2为读者总结了GPS导航电文25帧所包含的数据。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/94b7c8d8669012c602f599c92114989d_1264x629.jpg)
由表9-2可知,所有的25帧数据中,其子帧1到子帧3的内容相同。它们都用来描述信号发射卫星的一些信息。此处特别提醒读者,子帧1~3包含的是某颗卫星自己的GPS时间和星历数据。所以,对地面接收器来说,某颗卫星的数据每隔30s(每一帧传输的时间为30s,而每一帧的前三个子帧都包含了该卫星最新的信息)就可以得到更新。
>[info] 特别注意 一个GPS卫星所发送的导航电位包括自己的星历数据以及其他卫星的历书数据。
现在来看TLM和HOW的内容,如图9-18[16]所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/281891307b14e47a84ddc92ef4c4abcd_924x571.jpg)
图9-18 TLM和HOW的构成
图9-18中,TLM前8位(Preamble,也叫前导码)由用于同步的二进制数10001011开始。其第9~22位为TLM消息,供PSS用户及控制段和空间段使用。第23位为完整性状态标志(Integrity Status Flag,ISF),第24位保留,最后六位是奇偶校验码。
HOW前17位用于传输星期时间(Time of the Week,TOW),第18位为警告(Alert)标志,该值为1时将提醒SPS用户测量精度较差。第19位为反欺骗(Anti-Spoof,A-S)标志,该值为1表示A-S功能开启。20到22位为子帧的ID(一个帧中包含五个子帧,子帧的ID从1开始编号)。最后几位用于奇偶校验。
**②、星历和历书**
星历和历书的内容如表9-3所示。
表9-3星历和历书所包含的参数[^4]
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/112f5e15751c18447925201b2ac57911_1260x214.jpg)
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/8f83722c07194a90cec1ce4c34351e48_1271x498.jpg)
注意,表9-3中仅包含了星历和历书全部参数项的一部分。完整的星历和历书参数定义见参考资料[16]的Table 20-III和Table20-VI。另外,从上表中读者也会发现,对于同样的参数而言,其在历书数据中的精度要比它在星历数据中的精度低(即参数的位长较短)。
>[info] 提示 为了避免翻译不准带来的误解,表9-3中的参数含义说明直接使用了其在官方文档中的英文说明。参考资料[16]也有数学公式描述这些参数的作用。
**5、定位计算相关知识**
**①、定位计算原理[17]**
本节将介绍GPS定位计算相关的知识。在“测距原理介绍”一节中我们曾提到说要计算三维坐标系中接收器的位置需要三颗GPS卫星,而为了解决接收器和GPS卫星时钟的不同步问题,则需要第四颗GPS卫星参与计算。图9-19展示了定位计算的原理图。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/9bcb6acd0ca55acd600da8b7185ba092_966x528.jpg)
图9-19 GPS定位计算原理
图9-19中,接收器的位置由(Xuser,Yuser,Zuser)表示。
GPS卫星的位置由(XSat_i,YSat_i,ZSat_i)唯一确定(注意,接收器可根据卫星的星历等参数信息将卫星在ECI坐标系的值转换成ECEF坐标系的值)。GPS卫星发送的GPS信号到达接收者所用的传输时间由Δti表示。这样,每个GPS卫星到接收器的距离就可以计算出来。图9-19中,该距离由Ri表示。
现在来考虑GPS卫星与接收器的时间误差问题。借助高精度的原子时钟以及地面控制站的监控与修正,可以认为GPS卫星之间的时钟是同步。这样,GPS卫星和地面接收器的时间误差就可以用一个参数来表示了。
基于上述内容,得到公式三。
[公式三]
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/c6b9a46978981e70f0a1a455770683aa_373x173.jpg)
Δt0为接收器的时间误差,Δt为真实的信号传输时间,Δtmeasured为接收器的GPS信号传输时间。PSR为伪距(pseudorange)。R为GPS卫星到接收器的距离。
显然,在三维笛卡尔坐标系中,R的值可由下面的公式计算得到。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/47fa3525965b1b4ea18cfab64b5cf4a7_591x84.jpg)
最终,我们可得到一组方程式,见公式四。
[公式四]
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/4c62d17ae295b09aa33a5f5dbdb189d0_734x94.jpg)
以图9-19为例,上述公式的i从1~4。
如何计算上述方程组呢?一种常用的方法是通过泰勒级数将其线性化,然后再借助偏微分方程求解。图9-20展示了该方法的原理。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/b9077152c15c4e9a0b5ed7c6f12cc9bc_918x476.jpg)
图9-20 公式四求解原理
如图9-20所示,一个新的位置点被引入,该位置点叫估算位置点(Estimated Position)。GPS卫星离该位置点的距离由RTotal_i表示。
估算位置点离接收器真实的位置之间有一段距离,二者坐标的差别由公式五表达。
[公式五]
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/7511e7b96aa0609e4ae20c7abdef9b1a_964x283.jpg)
Xuser为接收器的X坐标,XTotal为估算点的X坐标,二者的差值为Δx。
经过一系列的公式替换和变量求偏导,可得到图9-21所示的矩阵。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/31198cab4c267cdab5a65919f24502c3_1062x368.jpg)
图9-21 GPS定位矩阵计算公式
接收器将利用图9-21所示的公式进行迭代计算,直到图左边的Δx等参数值小于期望误差
(如0.1m)为止。然后,接收器的位置可通过公式五和根据图9-21中得到的Δx等值计算出来。
>[info] 注意 除了计算接收器的坐标位置外,GPS还能计算出接收器的移动速度,这是基于多普勒效应来实现的。以GPS为例,多普勒效应就是当GPS卫星与接收器之间存在相对运动时,接收器一端收到的GPS信号的频率和GPS卫星实际发送的信号的频率并不相同,二者之差称为多普勒频移。由于GPS卫星的速度可根据其导航电文中的信息推算出来,故接收器根据多普勒频移的相关公式就很容易计算出自己的移动速度了。关于GPS测速方面的知识,可阅读参考资料[18]。
**②、DOP介绍[17]**
上一节介绍了GPS定位计算的原理。在真实环境中,GPS定位计算中还存在某些误差,这些误差的原因大体由如下几个部分组成。
* 卫星时钟:虽然卫星时钟的精度已经很高了,但由于光速的值很大,这就造成时间上10ns的偏差都会造成距离上3m的误差。
* 卫星本身的轨道位置:卫星在轨道上的位置精度在5m左右。
* 光速:GPS信号从太空中的卫星到地面接收器传输时其速度不是固定值,而是会受到电离层和对流层的影响。
* 接收器的时钟:接收器的时钟和卫星时钟不同步,这也会造成相应的误差。
* GPS信号的多路径效应:GPS信号传输过程中常会因为建筑物或其他反射物发生反射。显然,这些反射信号的传输时间比没有反射的信号的传输时间要长,这就给接收器测距时造成一定的误差。
参与定位计算的GPS卫星的空间分布也会对最终计算结果有较大影响。本节重点介绍它。
如上,本节重点关注GPS卫星空间分布情况对定位计算的影响。在GPS系统中,因卫星的空间分布造成的测距误差可用DOP(Dilution Of Precision,精度衰减因子)等一组值来描述,这一组值如下。
* GDOP(Geometric-DOP,几何精度衰减因子):描述卫星空间分布情况对位置计算和时间测量的影响。
* PDOP(Positional-DOP,位置精度衰减因子):描述卫星空间分布情况对位置计算的影响。
* HDOP(Horizontal-DOP,水平精度衰减因子):描述卫星空间分布情况对水平位置(二维空间)位置计算的影响。
* VDOP(Vertical-DOP,垂直精度衰减因子):描述卫星空间分布情况对高度计算的影响。
* TDOP(Time-DOP,时间精度衰减因子):描述卫星空间分布情况对时间测量的影响。
从上述各项DOP的描述可知,卫星空间分布的情况将影响定位计算的精度,这是为什么呢?来看图9-22的DOP原理。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/26e3ecd5b38f65c60f44736a7936c90e_1001x406.jpg)
图9-22 DOP原理
左图中,Sat 1和Sat 2这两颗卫星相距较远,而右图中Sat 1和Sat 2两颗卫星相距较近。
计算接收器Pos的位置时,Sat 1和Sat 2的测距都会存在一定的测距误差。每个卫星测距所造成的误差由图中的圆环表示。根据9.2.1节对测距原理的介绍,Pos的位置位于两个圆环的重合区域之中。
很明显,左图中Pos所在的重合区域面积较小,而右图中Pos所在的重合区域面积较大。这说明左图的测距误差比右图的测距误差小。所以,左图卫星分布区域情况对应的DOP值较小,而右图的DOP较大。
>[info] 提示 根据DOP的取值情况,定位测量的质量可划分为4个级别[15]。质量非常高:DOP值为1~3。质量高:DOP值为4~5。质量一般:DOP值为6时。质量差:DOP值大于6时。
关于DOP更详细的信息,可进一步阅读参考资料[17]。
**③、首次定位时间**
首次定位时间(Time To First Fix,TTFF)是衡量GPS接收设备性能的一个重要指标,它描述的是GPS接收器需要花费多长时间来捕获GPS卫星信号直到计算出自己的位置。目前,TTFF因启动模式[19]不同而有所区别,这些启动模式分别如下。
* 冷启动模式(也叫出厂模式):在这种模式下,GPS接收器没有保存有效星历、时间及位置等信息,所以它需要从周围可搜索到的GPS卫星那接收信号并获取用于定位的数据(星历、历书、时间等信息)。前面曾介绍,传输一个完整的GPS导航电文需要12.5分钟。在这种模式下,TTFF至少为12.5分钟。
* 暖启动模式:在这种模式下,GPS接收器保存有历书数据(不超过180天)、旧时间信息(不超过20秒)及旧位置信息(不超过100千米范围),但星历数据失效(超过4小时)。如此,在计算位置时,GPS接收机需要从GPS卫星那接收星历数据(读者还记得吗,导航电文的25个帧中第2、3子帧包含发送卫星的星历数据,由于每一帧发送时间为30秒,所以星历数据每隔30秒就会更新一次)。所以,在这种模式下,TTFF至少为30秒。
* 热启动模式:在这种模式下,GPS接收机具有有效星历数据、时间及位置等信息,这样,GPS接收器就无须解码GPS导航电文中的星历数据,它只要利用GPS信号进行测距计算就可以了。热启动模式下,TTFF速度很快,能做到10秒以内。
显然,减少TTFF对提升用户的使用体验有极大的帮助。根据上述内容可知,TTFF的瓶颈主要在星历、历书数据等信息的获取上。为了解决此问题,人们设计了Assisted GPS(辅助GPS)方法。AGPS使得GPS接收机能通过移动通信网络(如2G/3G等,传输速度远超GPS卫星信号的传输速度)下载星历数据等信息,从而加快首次定位时间。关于AGPS的内容,请读者阅读9.2.2节。
>[info] 提示 如果确实需要接收和解析GPS卫星信号,多通道接收方法可用来同时接收多个卫星的信号从而提升TTFF。关于这一点见参考资料[20]。
**6、NMEA-0183和GPX**
本节将介绍和GPS相关的两种数据文件格式,先来看NMEA-0183。
**①、NMEA-0183[21]**
NMEA-0183是美国国家海洋电子协会(National Marine Electronics Association,NMEA)为海用电子设备制定的标准格式。GPS接收机可按照该标准定义的格式输出诸如定位时间、纬度、经度、高度、定位所用卫星数、DOP值等很多信息。
>[info] 提示 NMEA-0183的输出内容由ASCII字符组成。简单点说,可以用文本软件来查看和修改NMEA数据。
NEMA文件的内容由一条一条的语句组成,每一条语句都有对应的类型,表9-4列举了其中一些常用语句的类型以及它们所包含的数据信息。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/d5ee9ac48340f27103c1a361f4dfd400_1273x298.jpg)
下面我们来看一个NMEA语句示例。
**NMEA语句示例**
~~~
$GPGGA,161229.487,3723.2475,N,12158.3416,W,1,07,1.0,9.0,M,18.0,M,50,0000*18
~~~
上面这条NMEA语句中各项信息的含义如表9-5所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/8506abb6418e25fe407da78289a925e8_1260x636.jpg)
NMEA的介绍就到此为止。更详细的信息请阅读参考资料[21]。
**②、GPX[22]**
和NMEA不同,GPX(GPS eXchange Format)将GPS数据封装在XML文件中,所以它遵循XML相关的语法。GPX比较简单,通过一个例子来介绍。
**GPX示例**
~~~
44.586548
5066
Crossing
BELLEVUE
1
23.469600
BELLEVUE
BELLEVUE
Parking Area
~~~
上述例子展示了GPX文件格式中的一些主要构成部分。GPX中有三个比较重要的概念。
* Waypoint:路点,由标签标示,代表一个感兴趣的点或者地图上的某个点。wpt英文解释为"wpt represents awaypoint,point of interest,or named feature on amap"。
* Route:路径,由标签标示。路径由一组有序的路点构成。rte的英文解释为"rterepresents route-an ordered list of waypoints representing aseries of turn points leading toadestination"。注意,在Route中,wpt是转向点(turn points)。
* Track:轨迹,由标签标示。用于记录某人从源地址到目标地址所走过的那些路点。Track的英文解释为"represents atrack-an ordered list of points describing apath"。
Track和Route的区别很微妙,二者关系如图9-23所示。标有WP字样的点为Waypoint。源地址的路点为WP0297,目标地址的路点为WP0307。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2a65d505b94d47d51b7430d087e1e996_479x642.jpg)
图9-23 Track和Route
图中的实线表示路径,而由路点组成的那条曲线就是轨迹。轨迹记录了某人在某个时间段从源到目标所经过的那些WP。所以在GPX文件的轨迹标签内所列出的WP同时还会记录此人经过该点时的时间信息。
和轨迹略微不同,路径表示从源点达到目标点之间的一些关键点。
>[info] 提示 参考资料[23]描述的Track和Route的区别似乎和GPX官网所给示例(本节所分析的GPX实例就来自于GPX官网)不同。其中,路径点和是否有人在某时刻途经过它没有关系,路径点天然就存在,所以不含时间戳信息(时间信息描述的就是人们在什么时刻达到过此处)。但是本节的GPX实例中路径点却包含了时间戳信息,并且官方对路径中路点的描述是"turn point",其英文解释是"a point at which there is achange in direction or motion"。
除了GPX外,还有一种名为KML的文件格式被谷歌地球和谷歌手机地图等软件使用。KML(Keyhole Markup Language)最初由Keyhole公司开发,是一种基于XML标准的格式。KML可描述点、线、图像等多种地理信息。Android的DDMS也支持KML。
>[info] 注意 请读者自行研究KML的知识,相关资料见参考资料[24]。
**7、GPS增强系统**
为了提升GPS定位的精确度和易用性,人们还设计了一些GPS增强系统,这些增强系统大
体可分为如下几种。
* DGPS(Differential Global Positioning System,差分GPS),用于提高GPS的定位精度。
* SBAS(Satellite-Based Augmentation System,星基增强系统),用于提高GPS定位精度以及可靠性。
* AGPS(Assisted GPS,辅助GPS),通过从移动网络下载星历等数据以提升GPS定位速度。
* HSGPS(High Sensitivity GPS,高精度GPS),用于提升GPS接收器的灵敏度。
下面介绍DGPS、SBAS以及AGPS的内容。
>[info] 注意 关于HSGPS见参考资料[25]。
**①、DGPS和SBAS[25]**
在介绍DOP时曾提到过GPS定位计算时的一些误差,而DGPS以及SBAS的目标就是减少这些误差所带来的影响。SBAS和DGPS有一定关联,所以我们先来看DGPS,其工作原理如下。
* 为了减少(或者修正)定位计算的误差,人们事先把GPS接收机放在位置已精确测定的点上,这些点叫基站。基站的接收机通过接收GPS卫星信号,测得并计算出它们到卫星的伪距,将伪距和已知的精确距离相比较,求得该点在GPS系统中的伪距测量误差。
* 然后这些基站再将这些误差作为修正值以标准数据格式通过播发台向周围空间播发。
* 在基站附近的DGPS用户一方面接收GPS卫星信号进行测距,同时它接收来自基站的误差修正信息,并以此来修正定位结果,从而提高定位精度。
DGPS用户离基站多远才算附近呢?下面有两个参考距离[15]。
* 如果使用伪距差分定位(Code Differential Positioning)技术,则DGPS和基站的距离最好在200千米以内。
* 如果使用载波相位差分定位(Carrier-Phase Differential Positioning)技术,则DGPS和基站的距离最好在20千米内。
如上所述,基站负责将修正数据以标准格式向周围空间播发。为此,人们也制订了一些协
议来规范化这一工作。这些规范以及它们的优缺点如表9-6所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/71740578e8d569eacf63de0bf8455933_1259x569.jpg)
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/4e10b58c8eb24b42bafe87aecbe47447_1260x194.jpg)
* RTCM(Radio Technical Commission for Maritime,国际海运事业无线电技术委员会)。SC 104(Special Committee 104)定义了DGPS修正数据的格式。
* RTCA(Radio Technical Commission for Aeronautics,航空无线电技术委员会),DO-229C是为它指定的一个标准。
由上表可知,修正数据的格式主要分为RTCM SC104和RTCA DO-229C两种。当修正数据用卫星发送时,这种系统就叫SBAS。SBAS使用的协议格式为RTCA DO-229C。
当然,SBAS的功能远不止简单地播发修正数据,它还能监测GPS或其他GNSS卫星的情况以加强信号的可靠性和安全性。图9-24展示了目前几个已投入使用或在建的SBAS系统以及它们的覆盖范围。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/7bea35d9dfdcc73425aae5bb33b4ce61_1218x668.jpg)
图9-24 SBAS系统
图9-24中几个主要的SBAS从左至右分别如下。
* WAAS:美国建造的广域增强系统(Wide Area Augmentation System)。
* EGNOS:欧盟建造的欧洲静地导航覆盖服务(European Geostationary Navigation OverlayService)。
* GAGAN:印度建造的地球同步轨道增强导航系统(GPS And GEO AugmentedNavigation)。
* Beidou:中国建造的北斗导航系统。
* MSAS:日本建造的多功能卫星增强系统(Multifunctional Satellite Augmentation System)。
虽然每个单独的SBAS只能覆盖一定的范围,但通过RTCA DO-229C协议,SBAS之间的数据能够保证兼容性。
**②、AGPS**
AGPS的作用很简单,就是在没有有效星历数据等定位计算所需信息的情况下(或者在GPS信号不好甚至没有GPS信号的环境中),使得GPS接收器能通过别的方式获取所需信息以
加快定位速度。AGPS的原理如图9-25所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/d2a7b60b1d0224f6f1a8d71504ba3547_843x631.jpg)
图9-25 AGPS原理
如图9-25所示,AGPS是一个比较复杂的系统,它首先需要在全球建造一个用于收集GPS卫星数据的参考网络(Global Reference Network)。目前比较知名的参考网络由IGS(International GNSS Service)组织建造,它在全球80多个国家设有300多个GPS卫星跟踪站。IGS的官方网站(http://igscb.jpl.nasa.gov/ )提供GLONASS和GPS的卫星数据下载服务。
AGPS将收集到的这些数据(一般称为辅助数据,Aiding Data)保存在服务器上并对外界提供下载。例如图9-25中,支持AGPS的手机就可以通过移动通信网络发起相关请求以获得这些辅助数据从而加快自己的定位速度。
以上是对AGPS原理非常简略的介绍。在AGPS的实现细节中,还有如下几个比较重要的知识点。首先是User Plane和Control Plane的概念,这两个词源自移动通信领域。
* Control Plane用于在手机和基站间传输控制信息,例如手机收到一个来电信号就是通过Control Plane传输的。
* User Plane主要用来传送数据包(例如TCP/IP、HTTP等)。我们用的3G或GPRS上网就是User Plane。显然,手机通过User Plane来获取Aiding Data这种方式更具通用性和扩展性,例如它能通过TCP或UDP等协议来传输数据。关于CP和UP的区别,见参考资料[26]。
手机如何利用AGPS来进行定位呢?AGPS可分为MSB(Mobile Station Based)和MSA(Mobile Station Assisted)两种运行模式。
* MSB模式下,手机从AGPS位置服务器上下载辅助数据,然后手机再结合GPS卫星信号进行定位计算。这种方式需要手机下载辅助数据,同时它还需要利用其自身的CPU、内存等资源进行最终的定位计算。MSB模式的优点是网络负担小且定位延时小,适合短时间内的连续定位。
* MSA模式下,手机接收并解调GPS卫星信号,然后将这些伪距信息传给AGPS位置服务器。AGPS位置服务器根据手机所发送的数据、自己所保存的卫星数据以及其他一些信息(例如手机当时通信的基站位置)计算出手机所在位置,然后将该信息返回给手机。MSA模式下,手机无须使用自己的CPU等资源来进行定位计算。MSA的优点是对终端的性能要求低,但其定位的延时大,不适合高速行驶情况下的定位。
>[info] 提示 如果接收器只使用GPS卫星信号进行定位,这种工作模式称为Standalone(也叫Autonomous)模式。MSA或MSB都需要手机接收GPS卫星信号。对于无法接收GPS卫星信号的地区(如办公室等)该怎么办呢?这就需要借助其他方法了,例如Cell-ID定位方法,其原理很简单,就是通过获取目标手机所在的蜂窝小区ID来确定其所在的位置。
AGPS涉及的内容非常多,为了更规范地为用户提供AGPS服务,OMA(Open Mobile
Alliance,开放移动联盟)制定了一整套服务和标准,这套服务和标准统称为OMASUPL(
Secure User Plane Location)。了解OMA-SUPL的工作流程非常有助于理解本章下文对
Android平台中AGPS相关的代码分析。
[^①]:根据参考资料[10],这32颗卫星中的31颗处于运行状态,另外一颗不可用。美国一共发射了64颗卫星,未来还有卫星更新计划。美国GPS官方对卫星的说明见参考资料[9]。
[^②]:GPS双频接收机可以同时接收两个不同频率的载波信号。它将利用不同频率的电磁波在大气电离层传输时所造成的延迟时间不一致的原理来减少电离层带来的定位误差。
[^③]:PRN的特点是看起来像随机的噪声,但又不是真正的噪声,它是一种比较复杂的数字编码。
[^4]:注意,表9-2中并未列出全部的星历和历书数据项,感兴趣的读者请参考[16]。
';
9.2.1 卫星导航基本原理
最后更新于:2022-04-02 06:06:06
在这一节中,我们将介绍测距、参考坐标系、时间系统、卫星轨道等四个方面的基础知识。先来介绍测距原理。
**1、测距原理介绍[2]**
GPS(包括其他的GNSS系统)使用的测距原理非常简单。我们用一个图来说明它的工作过程,如图9-1所示。
:-: ![](http://img.blog.csdn.net/20140322221108421?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图9-1 卫星测距原理示意
在图9-1所示的卫星测距原理示意图中:
卫星和地面接收器都各自有一个时钟。假设卫星和接收器的时钟能完美同步(注意这个假设,以后我们还会讲到它)。在0ms时刻,卫星向接收器发送了一串信号。
在67.3ms时,接收器收到了该信号。那么,卫星离接收器的距离就是信号传播速度乘以传播时间。
以数学公式来表示图9-1的卫星测距原理就是:
[公式一]
`D=Δτ*c `
该公式中,c为光速,为信号传输时间。D为距离
有了公式一,我们可以计算接收器到任意一个卫星的距离。不过,距离(Range)和位置(Location)显然是两个不同的概念。那么,如何根据距离得到位置信息呢?
原来,位置需要放在某个坐标系中来考察。下一节将专门讨论坐标系。假设现在已经有一个坐标系了。那么,图9-2所示的内容就能回答刚才提出的问题。
:-: ![](http://img.blog.csdn.net/20140322221126281?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图9-2 二维坐标系中接收器位置计算示意图
和图9-1的测距原理示意比起来,图9-2多了如下一些特点:
- 卫星和接收器的位置都置于一个统一的二维坐标系中来考察。
- 接收器离两个卫星的距离都由公式一计算得到,分别是D1和D2。
- 如果以卫星为圆心,以接收器到卫星的距离为半径,那么我们可以得到图9-2中的两个圆。这两个圆的相交点到卫星1的距离为D1,到卫星2的距离为D2。也就是说,这两个点就是接收器的可能位置。
- 如果接收器的Y坐标值不能高于卫星的Y坐标值,那么接收器的实际位置只能是图9-2中的(Xp,Yp)了。
掌握了二维坐标系中接收器的位置计算方法,只要再增加一颗卫星,我们就很容易推导出接收器在三维坐标系中的位置了。
从理想情况来说,定位(英文为Positioning)计算就这么简单,但现实情况却相当复杂。例如,在上述的讨论中还有两个重要的潜在问题没有解决,这两个问题是:
- 如何选择坐标系?
- 出于成本、便携性等各方面的考虑,接收器的时钟精度远不如卫星的时钟精度,所以在计算信号传输时间时会造成较大的偏差。由于信号传播速度是光速,所以哪怕这个时间偏差为0.1毫秒,距离偏差都会达到30公里。
那么这两个问题是如何解决的呢?下两节将分别介绍坐标系和时间系统。时间偏差的问题则通过引入第四颗GPS卫星参与定位计算来解决(详情见9.2.2中“定位计算相关知识”一节)。
**2、坐标系介绍**
**①、ECI/ECEF/WGS-84介绍[2]**
根据上一节的内容可知,坐标系对于位置计算非常重要。坐标系有很多个,甚至不同的国家都可能会建立更加符合本国实际情况的坐标系。但在GPS中,与它相关的坐标系主要有两个,它们分别是:
- **地心惯性坐标系(英文为Earth Centered Inertial,简称ECI)**:该坐标系用于描述GPS卫星的位置信息。在这种坐标系中,原点为地球的质心,卫星围绕质心运动,并遵守牛顿运动定律。
- **地心地球固连坐标系(英文为Earth Centered,Earth Fixed,简称ECEF)**:该坐标系用于描述地面接收器的位置信息。ECEF最大的特点是它会随着地球旋转而旋转。
>[info] 提示:在GPS的定位计算过程中,我们需要先把卫星在ECI坐标系的位置转换成它在ECEF坐标系的位置。
图9-3展示了ECI和ECEF坐标系。
:-: ![](http://img.blog.csdn.net/20140322221149484?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图9-3 ECI和ECEF坐标系示意图
图9-3中:左图所示为ECI坐标系。该坐标系中,XY平面与地球赤道面重合。X轴指向天球(Celestial Sphere,一种假想的无限大的球,它和地球同心。所以ECI坐标系不受地球旋转的影响)的某个位置。Z轴与XY平面垂直并指向北极。ECI坐标系属于笛卡尔坐标系,故卫星的位置由(x,y,z)表示。
右图所示为ECEF坐标系。该坐标系的原点为地球中心(这就是Earth Centered一词的缘由)。XY平面也与地球赤道面重合。不过其X轴指向0经度方向,Y轴指向东经90度的方向。所以ECEF坐标系实际上是随着地球一起旋转的。ECEF坐标系也属于笛卡尔坐标系,故接收器的位置也由(x,y,z)表示。
ECEF是一个笛卡尔坐标系,而我们实际使用的位置信息却是由经纬度来表示的,那么如何将笛卡尔坐标系中的X,Y,Z值转换成经纬度呢?
该转换工作涉及到另外一个重要的概念,即标准地球模型。GPS参考的地球模型名为WGS-84(英文名为World Geodetic System 1984,由美国国防部建立)。WGS-84模型如图9-4所示:
:-: ![](http://img.blog.csdn.net/20140322221222812?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图9-4 WGS-84模型介绍
图9-4所示的标准大地模型中:
地球被看做是一个椭球体。该椭球体的半长轴(英文为Semi Major Axis,实际长度为6378137.00米)为a,半短轴为b(英文为Semi Minor Axis,实际长度为6356752.31米)。根据a和b的值,该椭球体的偏心率[^1](英文名为Eccentricity)可由公式 计算得到。
图中的Equatorial Plane为赤道面。赤道面和椭球体相交得到的椭圆为赤道(图中的Equator),它就是纬度为0的地方。图中的Greenwhich Meridian为格林尼治子午线,即经度为0的地方。椭球体的表面叫椭球面,即图中的Ellipsoid。
图中的P1点的位置采用了笛卡尔坐标系,其值为(x,y,z),而P点的位置则由椭球坐标系确定,其值为( )。注意,此处的h是P点与椭球面的高度,即GPS概念中的高度。
根据相关的公式[2],椭球坐标系和笛卡尔坐标系能相互转化。
**②、高度计算**
根据上节最后关于椭球坐标系中h坐标值的解释,GPS中的高度是指它和椭球面(Ellipsoid)的距离。但值得特别注意的是,这个高度和日常生活中所说的海拔高度不是同一个概念。日常生活中所说的海拔高度不是基于Ellipsoid,而是基于大地水准面(英文名为Geoid)的。那么,大地水准面是什么呢?
大地水准面是一个重力等位面。简单点说,静止海水在大地水准面上不会因为重力原因而流动。大地水准面和地球的质量分布等有重要关系。相比椭球面而言,大地水准面的数学模型非常复杂,很难用数学公式来描述它。
大地水准面和椭球面之间的区别影响了我们对高度的计算。图9-5所示为GPS高度与海拔高度的区别[3]:
:-: ![](http://img.blog.csdn.net/20140322221247921?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图9-5 高度计算的区别
图9-5中:
地球真实的表面由大海和高山组成,这个表面叫地形(图中的Topography)。
GPS测量的高度为h(也叫大地高,英文为Ellipsoidal Height),而日常所说的海拔高度为H(也叫正高,英文为Orthometric Height)。h和H之间的差为N(也叫大地水准面高,英文为Geoid Height)。
>[info] 注意:对于高精度的测绘需求,我们往往需要把h值转换成H,不过一般情况下二者的差别不大。
了解了GPS的坐标系统,马上来看与GPS相关的另外一个非常重要的系统。
**3、时间系统[^2]**
和GPS相关的时间系统有四种之多,它们分别是国际原子时(英文为International Atomic Time,简写为IAT,注意,其对应的法语名为Temps Atomique International,所以其常用缩写也为TAI。笔者此处采用英文缩写IAT)、协调世界时间(Coordinated Universal Time,简写为UTC)、GPS时间(英文为GPS Time,简写为GPST)和本地时间(英文为Local Time)。笔者在此总结这四种时间系统的特点如下:
**①、IAT**:1967年,人们利用铯原子振荡周期极为规律的特性研制出了高精度的原子钟,并将铯原子能级跃迁辐射9192631770周所经历的时间定为1秒。IAT起始时间从1958年1月1日0时0分0秒开始,其精度能达到每日数纳秒。细心的读者可能会问到,在原子钟出现之前,人们如何定义秒呢?原来,在原子钟出现之前,人们使用基于地球自转的天文测量得到的世界时(Universal Time,简称UT)作为时间计量单位。和原子时比起来,UT会由于地球自转的不稳定(由地球物质分布不均匀和其它星球的摄动力等引起的)而带来时间上的差异,该差异大概在3年内会增加到1秒左右。
**②、UTC**(也叫世界统一时间、世界标准时间):TAI的精度为每日数纳秒,而UT的精度为每日数毫秒。对于这种情况,一种称为协调世界时的折衷时标于1972年面世。UTC以原子秒长为基础,在时刻上尽量接近UT。UT和UTC之间的间隔不能超过0.9秒,所以在有需要的情况下会在UTC内加上正或负闰秒(Leap second)。因此,协调世界时与国际原子时之间会出现若干整数秒的差别,而位于巴黎的国际地球自转事务中央局将决定何时加入闰秒以减少UTC和IAT之间的差别。UTC时间系统用途很广。目前几乎所有国家发播的时号都以UTC为基准。另外,互联网使用的网络时间协议(Network Time Protocol,简称NTP)获取的时间就是UTC。UTC的时间格式为:年(y)月(m)日(d)时(h)分(min)秒(s)。
**③、GPST**:GPST也使用IAT中的原子秒为单位,其时间原点定于1980年1月6日UTC 0时。GPST比IAT慢19秒,而它和UTC时间的差异为整数秒,并且这个差值会随着时间的增加而积累(到2009年,GPST和UTC相差15秒)。GPST时间格式由从GPST原点开始的周数和周内秒数组成。例如2009年7月9号13点08分36秒(转成时分秒格式的GPST)用GPST表示就是第1539周392916秒。参考资料[5]介绍了GPST和UTC的转换方法。
**④、本地时间[6]**:本地时间基于UTC。它将全球分为24个时区,每一时区之中心为相隔15度经线,每一国家都处於一个或以上的时区内。第一时区的中心位於格林尼治子午线(或简称子午线)。该时区以西的地方慢一个小时或以上,而东面则较其快。本地时间表达方法遵循ISO 8601,其格式为“年月日T时分秒Z(或者时区标识)”。例如,20131030T093000Z,表示2013年10月30号09点30分0秒,Z表示是标准时间。如果表示北京时间,那么就是20131030T093000+08,其中“+08”表示东八区。
>[info] 提示:以上是本书和时间系统相关的知识。这部分内容原本非常复杂,还涉及到较多天文方面的概念。在此,笔者建议读者先掌握本节所述内容。
**4、卫星轨道等知识介绍**
本节将介绍卫星轨道等方面的知识。首先是卫星运行所遵循的开普勒三定律。
**①、开普勒三定律介绍**
卫星围绕地球运行时将遵循开普勒三定律。图9-6所示为开普勒第一和第二定律的示意图:
:-: ![](http://img.blog.csdn.net/20140322221308125?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图9-6 开普勒第一和第二定律示意
图9-6中:
左图所示为开普勒第一定律示意。图中的Perigee为近地点,Apogee为远地点。根据开普勒第一定律[^3],卫星将围绕地球做椭圆运动,地球为该椭圆两个焦点中的一个。
右图所示为开普勒第二定律示意。根据开普勒第二定律,在相同的时间内,卫星运行时所扫过的区域的面积相同。即如果图中的时间段Tv_1等于时间段Tv_2的话,面积A_1等于面积A_2。
而根据开普勒第三定律可知,围绕地球椭圆轨道运行的卫星,其椭圆轨道半长轴的立方与运行周期的平方之比为常量。第三定律可用公式表达,如下所示:
[公式二]
k=a*3/T*2
//开普勒第三定律,a为半长轴,T为卫星运行周期。
k为常量,取值为。其中,M为地球的质量,G为万有引力常数
开普勒三定律主要用来计算卫星运行位置等相关参数,例如第三定律常用来计算卫星的轨道高度。这部分内容请读者参考[7]。
**②、卫星轨道及星历等知识介绍**
卫星轨道虽然涉及到很多空间科学方面的知识,但对于本书的读者来说,我们只需掌握卫星运行轨道的几个重要参数和概念即可。图9-7展示了卫星运行轨道及相关参数。
:-: ![](http://img.blog.csdn.net/20140322221324468?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图9-7 卫星运行轨道示意图
图9-7中:
左图中,Equatorial Plane为赤道平面,卫星轨道本身是一个椭圆轨道,它和赤道平面有一个夹角。这个夹角叫轨道倾角(图中的Inclination)。
右图中,假设观察者站在坐标原点观察左上角的卫星,则h代表仰角(Elevation angle),z代表天顶角(Zenith angle),而正北方向离卫星投影点的顺时针角度A为方位角(Azimuth angle)。
>[info] 提示:上述参数是卫星运行轨道中几个非常重要的参数,不过,读者现在只需要记住它们的定义即可。
根据轨道倾角、运行周期等参数,人们将卫星轨道分为如图9-8所示的几大类:
:-: ![](http://img.blog.csdn.net/20140322221353312?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图9-8 卫星轨道分类
**1)地球同步轨道(Geosynchronous Earth Orbit,简写为GEO)**:GEO特点是其轨道高度距离地面大约35786公里,卫星运行周期等于地球自转周期(23小时56分4秒),卫星运行方向和地球自转方向一致。最后,轨道是圆形(即偏心率为0)。根据轨道倾角的不同,地球同步轨道还可细分为静止同步轨道、倾斜同步轨道和极地同步轨道。这三者的特点如下文所述:
1. 静止同步轨道:如果轨道面与地球赤道面重合(即轨道倾角为0),则这种轨道叫静止同步轨道(Geostationary Satellite Orbit,简称GSO)。该轨道的特点是:从地面观察者看到该轨道上的卫星始终位于某一位置,似乎保持静止不动。利用该轨道上的3颗卫星就可以实现除南北极很小一部分地区外的全球通信
2. 倾斜同步轨道(Inclined Geostationary Orbit,简称IGSO):如果轨道倾角大于零并小于90度,则这种轨道叫倾斜同步轨道。
3. 极地同步轨道(Polar Earth Orbit,简称PEO):如果轨道倾角等于90度,则称为极地同步轨道。运行在这种轨道上的卫星能到达南北极区上空,所以那些需要在全球范围内进行观测和应用的气象卫星等多采用这种轨道。
**2)中地球轨道(Medium Earth Orbit,简称MEO**):也叫中圆轨道,它距离地面10000公里,卫星运转周期在2至12小时之间。运行在该轨道上的卫星大部分是导航卫星,例如GPS导航卫星有一部分运行在该轨道上。
**3)低地球轨道(Low Earth Orbit,检查LEO)**:也叫近地轨道或低地轨道,距离地面大约1000公里。由于近地轨道离地面较近,绝大多数对地观测卫星、测地卫星、空间站都采用近地轨道。
**4)高椭圆轨道**:高椭圆轨道是一种具有较低近地点和极高远地点的椭圆轨道,其远地点高度大于静止卫星的高度(36000千米)。根据开普勒定律,卫星在远地点附近区域的运行速度较慢,因此这种极度拉长的轨道的特点是卫星到达和离开远地点的过程很长,而经过近地点的过程极短。这使得卫星对远地点下方的地面区域的覆盖时间可以超过12小时。具有大倾斜角度的高椭圆轨道卫星可以覆盖地球的极地地区,所以对于像俄罗斯这样的高纬度国家而言,高椭圆轨道比同步轨道更有实际作用。
以上是卫星运行轨道的几个重要参数[^4],除此之外,还有两个重要概念需要读者了解:
* 星历表(英文为Ephemeris):星历表本来是用来记录天体特定时刻的位置的。而在GNSS中,星历表则记录了卫星的一些运行参数,它使得我们通过星历表就可以计算出任意时刻的导航卫星的位置和速度。下文我们将见到在GPS中,星历表包含了非常详细的卫星轨道和位置信息,所以其数据量较大,传输时间较长。为了克服这个问题,人们设计了星历表的简化集,即历书。
* 历书(英文为Almanac):历书也包含了卫星的位置等相关信息,不过它是星历数据的简化集,其精度较低。所以,历书数据量较小,传输时间较短。
>[info] 提示:星历和历书对于GPS定位计算来说至关重要。本章后文将介绍二者所包含的参数信息。
到此,本书所涉及到的与卫星导航原理相关的知识介绍就告一段落,这些内容对于讲解本章知识点来说已经足够。但本节所述内容仅仅是卫星导航全部知识的一小部分,那些有志从事卫星导航工作的读者还需要进一步花费时间来学习相关的专业知识。
[^1]:偏心率也叫扁率,对应的英文名为flattening。
[^2]:时间系统相关的知识非常复杂,参考资料[4]介绍得最为简练。
[^3]:开普勒三大定律本来描述的是行星在宇宙空间绕太阳公转所遵循的定律。不过导航卫星围绕地球运行也遵循此定律。所以笔者直接以导航卫星和地球为对象来介绍开普勒三大定律。
[^4]:卫星运行轨道的分类总结由笔者提炼并整理从网上搜索到的相关内容而来。
';
9.2 GPS基础知识
最后更新于:2022-04-02 06:06:04
与GPS相关的知识非常多,市面上也有很多专业的书籍来教授它们。不过,对于本书的读者来说,笔者将挑选并介绍一些比较实用的内容。笔者将这些知识归纳为如下三个部分:
* 卫星导航基本原理:这一节主要介绍卫星导航的一些基础知识。
* GPS工作原理:这一节集中介绍GPS的工作原理和相关的数据格式。
* OMA-SUPL协议:这一节集中介绍OMA-SUPL方面的知识。
注意:如何在大量专业的书籍中选择合适的知识点来向读者介绍是本书编写过程中一项非常重要及困难的工作。以GPS为例,其专业书籍涉及到较多的数学计算和公式推导。显然,这些内容对于当今业已成熟并高度集成化的GPS模块来说太过基础。就笔者总结的工作经验来说,对于一门陌生的技术和专业,初学者首先一定要掌握其基本原理和相关的概念。这些基本原理和概念将是这门技术或专业的主要框架和脉络。只有在掌握专业知识框架的基础上,才能开展更进一步的学习和研究。从这个角度出发,本章的基础知识将综合下文实际代码分析的需求,把主要内容集中在相关的GPS原理和概念上。
';
9.1 概述
最后更新于:2022-04-02 06:06:01
GPS,全称是Global Positioning System,中文译为全球定位系统。GPS源自美国军方的一个项目,其主要目的是为陆海空三大领域提供实时、全天候和全球性的导航服务。和GPS相对应的还有一个词,叫GNSS,它是Global Navigation Satellite System(全球导航卫星系统)的缩写。GPS是GNSS的一种具体实现形式。目前,世界上的GNSS除了美国的GPS外,还有欧盟的GALILEO、俄罗斯的GLONASS以及中国的北斗导航系统。
近几年来,随着iPhone和Android等新一代移动智能平台的普及,支持GPS及其它GNSS系统几乎是当下所有智能手机的标准功能,而在GPS或其他能提供位置信息的服务之上,人们更是构建了一个市场规模达数十亿美金的所谓的基于位置的服务(Location Based Service,简称LBS[^1])。
随着位置信息获取技术的多样化,Android平台在这些技术之上抽象出了一套名为Location Manager(位置管理)的软件架构。当然,作为该框架中最重要的位置提供服务模块,GPS功能由Android系统直接提供。
和本书其他章节类似,本章也会从两个方面来介绍Android平台中LM相关的功能:
* 首先将介绍和GPS相关的一些基础知识。从原理上看,GPS和GLONASS或北斗等其它卫星导航系统类似,所以本章将仅围绕GPS开展相关的知识讲解。而读者在掌握GPS知识的基础上,能轻松将它们运用到其他GNSS系统中。
* 在了解GPS相关原理的基础上,本章将介绍Android平台中位置管理的软件架构及代码实现。
马上来看GPS基础知识介绍。
>[info] 提示:作为《深入理解Android》系列丛书的惯例,笔者在每本书的最后一章都会留下一些内容请读者自行学习和研究。在此,笔者希望读者在本章基础上深入钻研GPS相关知识并能和其他读者分享自己的成果。
[^1]: 根据参考资料[1],LBS源于几起悲剧事件。
';
第9章 深入理解GPS
最后更新于:2022-04-02 06:05:59
本章主要内容
* 介绍GPS基础知识;
* 介绍Android中GPS模块以及LocationManagerService。
本章所涉及的源代码文件名及位置
* LocationActivity.java
~~~
development/samples/training/locationaware/src/com/example/android/location/LocationActivity.java
~~~
* LocationManagerService.java
~~~
framework/base/services/java/com/android/server/LocationManagerService.java
~~~
* LocationProviderProxy.java
~~~
framework/base/services/java/com/android/server/location/LocationProviderProxy.java
~~~
* GpsLocationProvider.java
~~~
framework/base/services/java/com/android/server/location/GpsLocationProvider.java
~~~
* com_android_server_location_GpsLocationProvider.cpp
~~~
framework/base/services/jni/com_android_server_location_GpsLocationProvider.cpp
~~~
* gps.h
~~~
hardware/libhardware/include/hardware/gps.h
~~~
* LocationSettings.java
~~~
packages/apps/Settings/src/com/android/settings/LocationSettings.java
~~~
';
8.5.2 参考资料说明
最后更新于:2022-04-02 06:05:57
**1、概述**
[1] Near Field Communication From Theory to Practice第1章“Executive Summary”和第2章“Towards NFC Era”。
这本书是笔者目前所阅读的关于NFC最为详尽的资料,建议初学者仔细阅读它,尤其是前三章。
**2、NFC概述**
[2] Near Field Communication From Theory to Practice第2章“Towards NFC Era”图2-1,略有修改。
[3] NFC Technology Overview
下载地址为http://www.nfc-forum.org/resources/presentations/NFCForum_Technical_WIMA09.pdf。该资料为NFC Forum官方提供,概要性得介绍了NFC技术。
[4] NFC vs ISO 14443 vs Felica
该文档介绍了目前NFC、ISO 14443和Felica之间的区别,文档下载地址为
http://developer.nokia.com/Community/Blogs/resources/300066/Philips-NFC-vs-ISO14443-vs-Felica-SLIDES.pdf
[5] http://www.nfc-forum.org/specs/spec_list/
该网页介绍了当前NFC Forum官方各个技术文档的主要内容,建议读者下载NFC Forum技术文档前先阅读此网页。
**3、NFC Reader/Write运行模式介绍**
[6] Near Field Communication From Theory to Practice第3章3.5节“Reader/Writer Operating Mode Essentials”
该节对NFC R/W运行模式进行了相关介绍
[7] http://www.nfc-forum.org/resources/white_papers/NXP_BV_Type_Tags_White_Paper-Apr_09.pdf
该文档可在NFC Forum官网上下载,属于NXP公司的一篇介绍NFC Tag Type的白皮书,通熟易懂,建议不熟悉的读者仔细研究它。
**4、NDEF和NFC Record介绍**
[8] NFC Data Exchange Format Technical Specification
[9] NFC Record Type Definition Technical Specification
[10] URI Record Type Definition Technical Specification
[11] Text Record Type Definition Technical Specification
NFC Forum官方文档,难度都比较小。
**5、NFC P2P运行模式介绍**
[12] Logical Link Control Protocol Technical Specification
LLCP的官方协议,建议读者先阅读本章相关章节后再去看它。
[13] NFC Digital Protocol Technical Specification
阅读此规范前,最好看看ISO 18092(http://www.docin.com/p-586980527.html)。
[14] Simple NDEF Exchange Protocol Technical Specification
SNEP官方协议,非常简单。
**6、NFCCE运行模式介绍**
[15] Near Field Communication From Theory to Practice第3章3.7节“Card Emulation Operating Mode Essentials”
[16] Near Field Communication From Theory to Practice第3章3.3节“General Architecture of NFC Enabled Mobile Phones”
这两个资料详细介绍了NFC Enabled Phone和Card Emulation Mode,读者可在阅读完本节基础上再去看它。
[17] http://www.nfc.cc/technology/nxp-nfc-chips/
NXP公司pn65 NFC系列芯片模块图
[18] http://www.chinaz.com/biz/2011/0827/207232.shtml
[19] http://kan.weibo.com/con/3616344461572955
中国市场上运营商和银联这两大利益集团联合推广NFC-SIM卡方案
**7、NCI介绍**
[20] NFC Controller Interface (NCI) Specification
NCI官方文档,长达140多页。不过读者无需了解其细节,只要掌握NCI架构及相关模块的功能即可。
[21] https://github.com/charsyam/linux-kernel-3.8/blob/master/Documentation/networking/nfc.txt
Linux kernel 3.8中关于NFC Subsystem的介绍
**8、NFC规范列表**
[22] Professional NFC Application Development for Android第1章“Overview of NFC”表1-2
此书由[1]同一团队编写,对Android上如何开发NFC应用进行了详细介绍。
**9、NFC CE示例介绍**
[23] http://stackoverflow.com/questions/15065172/nfcee-execution-environment-hardware-or-library-module
这个资料介绍了Android中如何操作NFC EE。读者不妨看看
[24] http://nelenkov.blogspot.jp/2012/08/accessing-embedded-secure-element-in.html
[25] http://nelenkov.blogspot.de/2012/08/android-secure-element-execution.html
上面这两个资料非常详尽得介绍了Android SE方面的知识,文章质量非常高。不过需要翻墙才能看。
**10、NFC HAL层探讨**
[26] http://elinux.org/images/d/d1/Near_Field_Communication_with_Linux.pdf
内容和[21]类似。
关于NFC认证测试,请参考http://www.nfc-forum.org/certification/certification-testing/。
2013年5月起,北京可用支持NFC功能的手机当公交卡乘坐地铁和公交,该措施无疑为NFC的推广起到了积极作用。
>[info] 注意,图8-26对应的Android版本为4.2。 根据审稿专家的意见,NFC Tag Technologies分为supported和optional supported 两种。optional supported表示某些Tag Technology在某些平台上不受支持。笔者此处采用的是Android SDK关于Tag Technology的解释,详情见http://developer.android.com/reference/android/nfc/tech/package-summary.html。
';
8.5.1 本章总结
最后更新于:2022-04-02 06:05:55
本章对NFC进行了详细介绍,主要内容包括:
* NFC理论知识,这部分内容主要围绕NFC三种运行模式进行了相关讨论。
* 介绍了Android系统中NFC模块的结构并通过三个示例程序向读者展示了Android中NFC API的一些知识
* 最后,我们对NFC系统模块NfcService及其它重要成员进行了介绍,并详细分析了NFC Tag分发以及Android Beam的实现细节。
最后,希望读者在本章的基础上,完成下列的一些任务:
* 学习NFC Forum中的其他规范。
* 结合NFC Forum中的Connection Handover规范,分析Android中NFC Handover的实现代码。
';
8.5 本章总结和参考资料说明
最后更新于:2022-04-02 06:05:52
8.4 NFC HAL层讨论
最后更新于:2022-04-02 06:05:50
Android在Hardward目录下为NFC定义了一个nfc.h头文件用于支持NFC HAL操作,但读者如果看过libnfc或libnfc-nci代码会发现,libnfc和libnfc-nci没有太多使用nfc.h定义的接口,而是大量引用各自公司定义的一套API。这种做法无可厚非,但它使得其他更上层的模块很难做到与底层平台或硬件解耦合。相信图8-26已经让读者直观感受到到这种做法恶果了。
>[info] 注意 与NFC这种状况形成鲜明对比,本书前面浓墨重彩介绍的Wi-Fi模块,借助nl80211机制或历史更悠久的wireless extension API解决了上层模块与底层平台或硬件的解耦合问题。
表8-13列举了当前知名的几个NFC HAL层实现。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/76f1702a3b26979401c90b70c137a0ce_1268x376.jpg)
笔者研究了表8-13中的除nfcpy之外的几个NFC HAL层模块代码,感觉和wpa_supplicant比起来还是有一定差距。不过,根据参考资料[21]和[26]的介绍,未来Linux系统中,NFC整个软件架构将变成如图8-42所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/567c5252a9ea5d752192b92bf987ebc6_762x643.jpg)
图8-42 NFC软件架构展望
图8-42中,用户空间运行一个名为neard的NFC Daemon进程,它通过AF_NFC socket以及Generic Netlink机制和内核空间的NFC子系统通信。neard通过不同的插件来支持NFC的协议,例如Handover、NPP、SNEP等。
内核空间中,NFC子系统包括Control Command Handler、LLCP Handler、Raw Data Handler以及Core等核心模块。
不同NFC芯片厂商只要实现相关的NFC驱动即可。至于Core模块如何与NFC驱动交互,则可使用基于NFC Forum定义的NCI规范(抽象为NFC NCI Layer)、HCI规范(抽象为NFC HCILayer),或者直接操作NFC Driver。
>[info] 提示 在此,笔者希望NFC软件架构尽快完善,同时也希望国内的公司能积极参与到这一过程中来以提高我们的话语权。
';
8.3.2 NFC系统模块
最后更新于:2022-04-02 06:05:48
本节开始时介绍,Android平台中,NFC系统模块运行在com.android.nfc进程中,该进程对应的应用程序文件名为Nfc.apk。NFC系统模块包含的组件非常多,所以通过以下几条分析路线来介绍。
* NFC系统模块的核心NfcService和一些重要成员的作用及之间的关系。
* R/W模式下NFC Tag的处理。
* Android Beam的实现。
* CE模式相关的处理。
**1、NfcService介绍**
Nfc.apk源码中包含一个NfcApplication类。当该应用启动时,NfcApplication的onCreate函数将被调用。正是在这个onCreate函数中,NFC系统模块的核心成员NfcService得以创建。我们直接来看NfcService的构造函数。
**NfcService.java::NfcService**
~~~
public NfcService(Application nfcApplication) {
// NFC系统模块重要成员
mNfcTagService = new TagService();// TagService用于和NFC Tag交互
// NfcAdapterService用于和Android系统中其他使用NfcService的客户端交互
mNfcAdapter = new NfcAdapterService();
// NfcAdapterExtrasService用于和Android系统中使用Card Emulation模式的客户端交互
mExtrasService = new NfcAdapterExtrasService();
sService = this; mContext = nfcApplication;
// NativeNfcManager由dhimpl模块实现,用于和具体芯片厂商提供的NFC模块交互
mDeviceHost = new NativeNfcManager(mContext, this);
// HandoverManager处理Connection Handover工作
HandoverManager handoverManager = new HandoverManager(mContext);
// NfcDispatcher用于向客户端派发NFC Tag相关的通知
mNfcDispatcher = new NfcDispatcher(mContext, handoverManager);
// P2pLinkManager用于处理LLCP相关的工作
mP2pLinkManager = new P2pLinkManager(mContext, handoverManager,
mDeviceHost.getDefaultLlcpMiu(),
mDeviceHost.getDefaultLlcpRwSize());
// NativeNfcSecureElement用于和SE交互,它也由dhimpl模块实现
mSecureElement = new NativeNfcSecureElement(mContext);
mEeRoutingState = ROUTE_OFF;
/*
NfceeAccessControl用于判断哪些应用程序有权限操作NFCEE。它将读取/etc/nfcee_access.xml文件的
内容。nfcee_access.xml内容比较简单,请参考Nfc目录下的etc/sample_nfcee_access.xml来学习。
*/
mNfceeAccessControl = new NfceeAccessControl(mContext);
......
// 向系统注册一个“nfc”服务。注意,SERVICE_NAME的值为“nfc”。该服务对应的对象为mNfcAdapter
ServiceManager.addService(SERVICE_NAME, mNfcAdapter);
/*
注册广播事件监听对象。NfcService对屏幕状态、应用程序安装和卸载等广播事件感兴趣。这部分内容请读者
自行研究。
*/
......
// EnableDisableTask为一个AsyncTask,TASK_BOOT用于NfcService其他初始化工作
new EnableDisableTask().execute(TASK_BOOT);
}
~~~
由上述代码可知,NfcService在其构造函数中,首先创建了NFC系统模块的几个核心成员。下文将详细介绍它们的作用及之间的关系。NfcService向Binder系统添加了一个名为"nfc"的服务,该服务对应的Binder对象为mNfcAdapter,类型为NfcAdapter。通过一个AysncTask(代码中的EnableDisableTask)完成NfcService其他初始化工作。
下面马上来看NFC系统模块核心成员。
**①、NfcService核心成员**
图8-35所示为NfcAdapter、TagService等相关成员的类信息。
:-: ![](http://img.blog.csdn.net/20140322214731484?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图8-35 NfcAdapter、TagService及相关类成员结构图
图8-35中,NfcAdapter包含一个类型为INfcAdapter的sService成员变量,该变量通过Android Binder机制来和NfcService内部类NfcAdapter的实例(即上述代码中的mAdapter)交互。NfcService内部的NfcAdapter对象即是注册到Android系统服务中的那个名为"nfc"的Binder服务端对象。
NfcAdapter还包含一个类型为INfcTag的sTagService成员变量,该变量通过Android Binder机制来和NfcService内部类TagService的实例(即上述代码中的mNfcTagService)交互。INfcTag接口封装了对Tag操作相关的函数。注意,前面所示的Nfc客户端示例中并没有直接使用INfcTag接口的地方,但表8-12所列的各种Tech类内部需要通过ITag接口来操作NFC Tag。
NfcAdapterExtras包含一个类型为INfcAdapterExtras的sService成员变量,也通过Android Binder机制来和NfcService内部类NfcAdapterService的实例(即上述代码中的mExtrasService)交互。
接着来看NfcService和NativeNfcManager,它们的类家族如图8-36所示。
:-: ![](http://img.blog.csdn.net/20140322214753375?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图8-36 NativeNfcManager和NfcService类家族
Android NFC系统模块通过接口类DeviceHost和其内部的接口类LlcpServerSocket、DeviceHostListener、LlcpSocket、LlcpConnectionlessSocket、NfcDepEndpoint、TagEndpoint将NFC系统模块中和NFC芯片无关的处理逻辑,以及和芯片相关的处理逻辑进行了有效解耦。图8-36中以"Native"开头的类均定义在packages/app/Nfc/nxp目录下,所以它们和NXP公司的NFC芯片相关。
DeviceHost接口中,DeviceHost类用于和底层NFC芯片交互,TagEndpoint用于和NFC Tag交互,NfcDepEndpoint用于和P2P对端设备交互,LlcpSocket和LlcpServerSocket分别用于LLCP中有链接数据传输服务的客户端和服务器端,LlcpConnectionlessSocket用于LLCP中无连接数据传输服务。另外,DeviceHostListener也非常重要,它用于NativeNfcManager往NfcService传递NFC相关的通知事件。例如其中的onRemoteEndpointDiscovered代表搜索到一个NFC Tag、onLlcpActivited代表本机和对端NFC设备进入Link Activation(链路激活)阶段。
NativeNfcManager实现了DeviceHost接口,以NXP公司的NativeNfcManager为例,它将通过libnfc_jni及libnfc和NXP公司的NFC芯片交互。
NativeNfcSecureElement用来和Secure Element交互。
接下来要出场的是HandoverManager以及P2pLinkManager,它们的家族关系如图8-37所示。
:-: ![](http://img.blog.csdn.net/20140322214814406?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSW5ub3N0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
图8-37 P2pLinkManager家族关系图
图8-37所示的P2pLinkManager家族关系非常复杂,图中的各成员说明如下。
* P2pLinkManager包含三个和传输相关的Server,分别是SnepServer、NdefPushServer以及HandoverServer。每一个Server都定义了相关的回调接口类(如SnepServer.callback),这些回调接口类的实现均由P2pLinkManager的内部类来实现。
* HandoverServer负责处理NFC Connection Handover协议,而具体的数据传输则由HandoverManager来实现。由图中HandoverMangar的mBluetoothAdapter可知,Android默认使用蓝牙来传输数据(提示:这部分内容请读者学完本章后自行研究)。
* P2pEventManager用于处理NFC P2P中和用户交互相关的逻辑。例如当搜索到一个P2P设备时,手机将会震动并发出提示音。
SendUi实现了类似图8-29左图的界面及用户触摸事件处理。在Android平台中,当两个设备LLCP链路被激活时,SendUi将显示出来。其界面组成部分包括两个部分,一个是SendUi界面显示之前手机的截屏图像,另外一个是“触摸即可发送”的文本提示信息。当用户触摸SendUi界面时,数据将被发送出去。
**②、enableInternal函数**
介绍完NfcService的几个核心成员后,马上来看NfcService构造函数中最后创建的EnableDisableTask,由于设置了参数为TASK_BOOT,故最终被执行的函数为enableInternal。
**NfcService.java::EnableDisableTask:enableInternal**
~~~
boolean enableInternal() {
......
// 启动一个WatchDog线程用来监视NFC底层操作是否超时
WatchDogThread watchDog = new WatchDogThread("enableInternal",
INIT_WATCHDOG_MS);
watchDog.start();
try {
mRoutingWakeLock.acquire();
try {
// 初始化NFC底层模块,这部分内容请读者自行阅读
if (!mDeviceHost.initialize()) {......}
} finally {
mRoutingWakeLock.release();
}
} finally {
watchDog.cancel();
}
synchronized(NfcService.this) {
mObjectMap.clear();
// mIsNdefPushEnabled判断是否启用NPP协议,可在Settings中设置
mP2pLinkManager.enableDisable(mIsNdefPushEnabled, true);
updateState(NfcAdapter.STATE_ON);
}
initSoundPool();// 创建SoundPool,用于播放NFC相关事件的通知音
applyRouting(true);// 启动NFC Polling流程,一旦搜索到周围的NFC设备,相关回调将被调用
return true;
}
~~~
我们重点关注上面代码中和P2pLinkManager相关的enableDisable函数,其代码如下所示。
**P2pLinkManager.java::enableDisable**
~~~
public void enableDisable(boolean sendEnable, boolean receiveEnable) {
synchronized (this) {// 假设参数sendEnable和receiveEnable为true
if (!mIsReceiveEnabled && receiveEnable) {
/*
启动SnepServer、NdefPushServer和HandoverServer。
下面这三个成员变量均在P2pLinkManager的构造函数中被创建,这部分内容请读者自行阅读
本章将只分析SnepServer。
*/
mDefaultSnepServer.start();
mNdefPushServer.start();
mHandoverServer.start();
if (mEchoServer != null) // EchoServer用于测试,以后代码分析将忽略它
mHandler.sendEmptyMessage(MSG_START_ECHOSERVER);
}......
mIsSendEnabled = sendEnable;
mIsReceiveEnabled = receiveEnable;
}
}
~~~
**③、SnepServer的start函数**
SnepServer的start函数将创建一个ServerThread线程对象,其run函数代码如下所示。
**SnepServer.java::ServerThread:run**
~~~
public void run() {// 注意:为了方便阅读,此处代码省略了synchronized和try/catch等一些代码逻辑
boolean threadRunning;
threadRunning = mThreadRunning;
while (threadRunning) {
// 创建一个LlcpServerSocket,其中mServiceSap值为0x04,mServiceName为“urn:nfc:sn:sne”
// mMiu和mRwSize为本机NFC LLCP层的MIU和RW大小。1024为内部缓冲区大小,单位为字节
mServerSocket = NfcService.getInstance().createLlcpServerSocket(mServiceSap,
mServiceName, mMiu, mRwSize, 1024);
LlcpServerSocket serverSocket;
serverSocket = mServerSocket;
// 等待客户端的链接
LlcpSocket communicationSocket = serverSocket.accept();
if (communicationSocket != null) {
// 获取客户端设备的MIU
int miu = communicationSocket.getRemoteMiu();
/*
判断分片大小。mFragmentLength默认为-1。MIU非常重要。例如本机的MIU为1024,而
对端设备的MIU为512,那么本机在向对端发送数据时,每次发送的数据不能超过对端
MIU即512字节。
*/
int fragmentLength = (mFragmentLength == -1) ?miu : Math.min(miu, mFragmentLength);
// 每一个连接成功的客户端对应一个ConnectionThread,其内容留待下文详细分析
new ConnectionThread(communicationSocket, fragmentLength).start();
}
}
mServerSocket.close();
}
~~~
NfcService初始化完毕后,手机中的NFC模块就进入工作状态,一旦有Tag或其他设备进入其有效距离,NFC模块即可开展相关工作。
下面先来分析NFC Tag的处理流程。
**2、NFC Tag处理流程分析**
**①、notifyNdefMessageListeners流程**
当NFC设备检测到一个NFC Tag时,NativeNfcManager的notifyNdefMessageListeners函数将被调用(由libnfc_jni在JNI层调用),其代码如下所示。
**NativeNfcManager.java::notifyNdefMessageListeners**
~~~
private void notifyNdefMessageListeners(NativeNfcTag tag) {
/*
mListener指向NfcService,它实现了DeviceHostListener接口。
注意,notifyNdefMessageListeners的参数类型为NativeNfcTag,tag对象由jni层直接创建
并返回给Java层。
*/
mListener.onRemoteEndpointDiscovered(tag);
}
~~~
上述代码中,mListener指向NfcService,它的onRemoteEndPointDiscovered函数代码如下所示。
**NfcService.java::onRemoteEndpointDiscovered**
~~~
public void onRemoteEndpointDiscovered(TagEndpoint tag) {
// 注意,onRemoteEndpointDiscovered的参数类型是TagEndpoint
// 由图8-36可知,NativeNfcTag实现了该接口
sendMessage(NfcService.MSG_NDEF_TAG, tag);// 发送一个MSG_NDEF_TAG消息
}
~~~
NfcService的onRemoteEndpointDiscovered将给自己发送一个MSG_NDEF_TAG消息。
NfcService内部有一个NfcServiceHandler专门用来处理这些消息。其处理函数如下所示。
**NfcService.java::NfcServiceHandler:handleMessage**
~~~
final class NfcServiceHandler extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
......// 其他消息处理
case MSG_NDEF_TAG:
TagEndpoint tag = (TagEndpoint) msg.obj;
playSound(SOUND_START);// 播放一个通知音
/*
从该NFC Tag中读取NDEF消息,NativeNfcTag的findAndReadNdef比较复杂,
其大体工作流程是尝试用该Tag支持的Technology来读取其中的内容。
*/
NdefMessage ndefMsg = tag.findAndReadNdef();
// 注意下面这段代码,无论ndefMsg是否为空,dispatchTagEndpoint都会被调用
if (ndefMsg != null) {
tag.startPresenceChecking();// 检测目标Tag是否还在有效距离内
dispatchTagEndpoint(tag);// 重要函数,详情见下节
} else {
if (tag.reconnect()) {// 重新链接到此NFC Tag
tag.startPresenceChecking();
dispatchTagEndpoint(tag);
} ......
}
break;
......
}
}
}
~~~
由上述代码可知,NfcService先调用TagEndpoint的findAndReadNdef函数来读取Tag中的数据,然后NfcService将调用dispatchTagEndpoint做进一步处理。
>[info] 提示 findAndReadNdef的实现和具体的NFC芯片有关,而NXP公司的实现函数在NativeNfcTag类中,内容比较复杂,感兴趣的读者可以阅读。
**②、dispatchTagEndpoint流程**
代码如下。
**NfcService.java::NfcServiceHandler.dispatchTagEndpoint**
~~~
private void dispatchTagEndpoint(TagEndpoint tagEndpoint) {
// 构造一个Tag对象。前面的示例中已经见过Tag。对客户端来说,它代表目标NFC Tag
Tag tag = new Tag(tagEndpoint.getUid(), tagEndpoint.getTechList(),
tagEndpoint.getTechExtras(), tagEndpoint.getHandle(), mNfcTagService);
registerTagObject(tagEndpoint);// 保存此tagEndpoint对象
// mNfcDispatcher的类型是NfcDispather,调用它的dispatchTag函数来分发Tag
if (!mNfcDispatcher.dispatchTag(tag)) {......}
}
~~~
**NfcDispatcher.java::dispatchTag**
~~~
public boolean dispatchTag(Tag tag) {
NdefMessage message = null;
Ndef ndef = Ndef.get(tag);// 构造一个Ndef对象,Ndef属于Tag Technology的一种
/*
从Ndef获取目标Tag中的NDEF消息。如果目标Tag中保存的是系统支持的NDEF消息,则message不为空。
特别注意:在前面代码中见到的findAndReadNdef函数内部已经根据表8-11进行了相关处理。
*/
if (ndef != null) message = ndef.getCachedNdefMessage();
PendingIntent overrideIntent;
IntentFilter[] overrideFilters;
String[][] overrideTechLists;
// ①构造一个DispatchInfo对象,该对象内部有一个用来触发Activity的Intent
DispatchInfo dispatch = new DispatchInfo(mContext, tag, message);
synchronized (this) {
// 下面三个变量由前台分发系统相关的NfcAdapter enableForegroundDispatch函数设置
overrideFilters = mOverrideFilters;
overrideIntent = mOverrideIntent;
overrideTechLists = mOverrideTechLists;
}
// 恢复App Switch,详情可参考《深入理解Android:卷Ⅱ》6.3.3节关于resume/stopAppSwitches的介绍
resumeAppSwitches();
// 如果前台Activity启用了前台分发功能,则只需要处理前台分发相关工作即可
if (tryOverrides(dispatch, tag, message, overrideIntent,
overrideFilters, overrideTechLists)) return true;
// 处理Handover事件
if (mHandoverManager.tryHandover(message)) return true;
// ②下面是Tag分发系统的处理,首先处理ACTION_NDEF_DISCOVERED
if (tryNdef(dispatch, message)) return true;
// 如果tryNdef处理失败,则接着处理ACTION_TECH_DISCOVERED
if (tryTech(dispatch, tag)) return true;
// 如图tryTech处理失败,则处理ACTION_TAG_DISCOVERED
// 设置DispatchInfo对象的内部Intent对应的ACTION为ACTION_TAG_DISCOVERED
dispatch.setTagIntent();
// 首先从PackageManagerService查询对ACTION_TAG_DICOVERED感兴趣的Activity,如果有则启动它
if (dispatch.tryStartActivity()) return true;
return false;
}
~~~
上述代码中有①②两个重要函数,我们先来看第一个。
**NfcDispatcher.java::DispatchInfo构造函数**
~~~
public DispatchInfo(Context context, Tag tag, NdefMessage message) {
// 这个Intent的内容将派发给NFC的客户端
intent = new Intent();
/*
不论最终Intent的Action是什么,NFC系统模块都会将tag对象和tag的ID包含在Intent中传递
给客户端。
*/
intent.putExtra(NfcAdapter.EXTRA_TAG, tag);
intent.putExtra(NfcAdapter.EXTRA_ID, tag.getId());
// 如果NDEF消息不为空,则把它也保存在Intent中
if (message != null) {
intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[] {message});
ndefUri = message.getRecords()[0].toUri();
ndefMimeType = message.getRecords()[0].toMimeType();
} else {
ndefUri = null;
ndefMimeType = null;
}
/*
rootIntent用来启动目标Activity。NfcRootActivity是Nfc.apk中定义的一个Activity。
目标Activity启动的过程如下。NFC系统模块先启动NfcRootActivity,然后再由NfcRootActivity
启动目标Activity。由于NfcRootActivity设置了启动标志(FLAG_ACTIVITY_NEW_TASK和
FLAG_ACTIVITY_CLEAR_TASK),所以目标Activity将单独运行在一个Task中。关于ActivityManager
这部内容,感兴趣的读者可阅读《深入理解Android:卷Ⅱ》第6章。
*/
rootIntent = new Intent(context, NfcRootActivity.class);
// 将Intent信息保存到rootIntent中。
rootIntent.putExtra(NfcRootActivity.EXTRA_LAUNCH_INTENT, intent);
rootIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
this.context = context;
packageManager = context.getPackageManager();
}
~~~
接着来看第二个关键函数tryNdef,代码如下所示。
**NfcDispatcher.java::tryNdef**
~~~
boolean tryNdef(DispatchInfo dispatch, NdefMessage message) {
if (message == null) return false;
/* setNdefIntent:
设置Dispatcher内部intent对象的Action为ACTION_NDEF_DISCOVERED。
如果Dispatch对象的ndefUri和ndefMimeType都为null,则函数返回null。
*/
Intent intent = dispatch.setNdefIntent();
if (intent == null) return false;
// 如果message中包含了AAR信息,则取出它们。AAR信息就是应用程序的包名
List aarPackages = extractAarPackages(message);
for (String pkg : aarPackages) {
dispatch.intent.setPackage(pkg);
/*
tryStartActivity先检查目标Activity是否存在以及目标Activity的IntenFiltert
是否匹配intent。注意,下面这个tryStartActivity没有参数。
*/
if (dispatch.tryStartActivity()) return true;
}
// 上面代码对目标Activity进行了精确匹配,如果没有找到,则尝试启动AAR指定的应用程序
if (aarPackages.size() > 0) {
String firstPackage = aarPackages.get(0);
PackageManager pm;
/*
下面这段代码用于启动目标应用程序的某个Activity,由于AAR只是指定了应用程序的包名而没有指定
Activity,所以getLaunchIntentForPackage将先检查目标应用程序中是否有Activity的Category
为CATEGORY_INFO或CATEGORY_LAUNCHER,如果有则启动它。
*/
UserHandle currentUser = new UserHandle(ActivityManager.getCurrentUser());
pm = mContext.createPackageContextAsUser("android", 0,currentUser).getPackageManager();
Intent appLaunchIntent = pm.getLaunchIntentForPackage(firstPackage);
if (appLaunchIntent != null && dispatch.tryStartActivity(appLaunchIntent))
return true;
/*
如果上述处理失败,则获得能启动应用市场去下载某个应用程序的的Intent,该Intent的
Data字段取值为“market:// details?id=应用程序包名”。
*/
Intent marketIntent = getAppSearchIntent(firstPackage);
if (marketIntent != null && dispatch.tryStartActivity(marketIntent))
return true;
}
// 处理没有AAR的NDEF消息
dispatch.intent.setPackage(null);
if (dispatch.tryStartActivity())
return true;
return false;
}
~~~
**③、NFC Tag处理流程总结**
NFC Tag的处理流程还算简单,下面总结其中涉及的重要函数调用,如图8-38所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/09824e1d0a1f071ab1e59f4c1bdbff8f_999x692.jpg)
图8-38 NFC Tag处理流程
NFC Tag的处理流程比较简单,其中有的代码逻辑比较复杂,这部分内容主要集中在NativeNfcTag的findAndReadNdef函数中。它和具体NFC芯片厂商的实现有关,故把它留给感兴趣的读者自己来研究。
下面我们将研究Android Beam的工作流程。
**3、Android Beam工作流程分析**
当本机检测到某个NFC设备进入有效距离并且能处理LLCP协议后,将通过notifyLlcpLinkActivation通知我们。本节就从这个函数开始分析。
**①、notifyLlcpLinkActivation流程**
notifyLlcpLinkActivation代码如下所示。
**NativeNfcManager.java::notifyLlcpLinkActivation**
~~~
private void notifyLlcpLinkActivation(NativeP2pDevice device) {
// mListener指向NfcService
// 它的onLlcpLinkActivated函数将发送一个MSG_LLCP_LINK_ACTIVATION消息
mListener.onLlcpLinkActivated(device);
}
~~~
MSG_LLCP_LINK_ACTIVATION消息由NfcService的内部类NfcServiceHandler处理,它将调用llcpActivated函数,代码如下所示。
**NfcService.java::NfcServiceHandler:llcpActivated**
~~~
private boolean llcpActivated(NfcDepEndpoint device) {
/*
NfcDepEndpoint代表对端设备,其真实类型是NativeP2pDevice。不论对端设备是Target还是
Initiator,P2pLinkManager的onLlcpActivated函数都将被调用。
*/
if (device.getMode() == NfcDepEndpoint.MODE_P2P_TARGET) {
if (device.connect()) {// 如果对端是Target,则需要连接上它
if (mDeviceHost.doCheckLlcp()) {
if (mDeviceHost.doActivateLlcp()) {
synchronized (NfcService.this) {
mObjectMap.put(device.getHandle(), device);
}
mP2pLinkManager.onLlcpActivated();
return true;
}......
} else if (device.getMode() == NfcDepEndpoint.MODE_P2P_INITIATOR) {
if (mDeviceHost.doCheckLlcp()) {
if (mDeviceHost.doActivateLlcp()) {
synchronized (NfcService.this) {
mObjectMap.put(device.getHandle(), device);
}
mP2pLinkManager.onLlcpActivated();
return true;
}......
}
return false;
}
}
}
}
~~~
P2pLinkManager的onLlcpActivated函数代码如下所示。
**P2pLinkManager.java::onLlcpActivated**
~~~
public void onLlcpActivated() {
synchronized (P2pLinkManager.this) {
.....
switch (mLinkState) {
case LINK_STATE_DOWN:// 如果之前没有LLCP相关的活动,则mLinkState为LINK_STATE_DOWN
mLinkState = LINK_STATE_UP;
mSendState = SEND_STATE_NOTHING_TO_SEND;
/*
mEventListener指向P2pEventManager,它的onP2pInRange函数中将播放
通知音以提醒用户,同时它还会通过SendUi截屏。请读者自行阅读该函数。
*/
mEventListener.onP2pInRange();
prepareMessageToSend();// ①准备发送数据
if (mMessageToSend != null ||
(mUrisToSend != null && mHandoverManager.isHandoverSupported())) {
mSendState = SEND_STATE_NEED_CONFIRMATION;
// ②显示提示界面,读者可参考图8-29的左图
mEventListener.onP2pSendConfirmationRequested();
}
break;
......
}
}
}
~~~
onLlcpActivated有两个关键函数,我们先来看第一个函数prepareMessageToSend,其代码如下所示。
**P2pLinkManager.java::prepareMessageToSend**
~~~
void prepareMessageToSend() {
synchronized (P2pLinkManager.this) {
......
// 还记得NFC P2P模式示例程序吗?可通过setNdefPushMessageCallback设置回调函数
if (mCallbackNdef != null) {
try {
mMessageToSend = mCallbackNdef.createMessage();// 从回调函数那获取要发送的数据
/*
getUris和NfcAdapter的setBeamPushUrisCallback函数有关,它用于发送file或
content类型的数据。由于这些数据需要借助Handover技术,请读者自己来分析。
*/
mUrisToSend = mCallbackNdef.getUris();
return;
} ......
}
// 如果没有设置回调函数,则系统会尝试获取前台应用进程的信息
List tasks = mActivityManager.getRunningTasks(1);
if (tasks.size() > 0) {
// 获取前台应用进程的包名
String pkg = tasks.get(0).baseActivity.getPackageName();
/*
应用程序可以在其AndroidManifest中设置“android.nfc.disable_beam_default”
标签为false,以阻止系统通过Android Beam来传递与该应用相关的信息。
*/
if (beamDefaultDisabled(pkg)) mMessageToSend = null;
else {
/*
createDefaultNdef将创建一个NDEF消息,该消息包含两个NFC Record。
第一个NFC Record包含URI类型的数据,其内容为“http://play.google.com/store/apps
/details?id=应用程序包名&feature=beam”。
第二个NFC Record为AAR类型,其内容为应用程序的包名。
*/
mMessageToSend = createDefaultNdef(pkg);// 包名为pkg的应用程序
}
}else
mMessageToSend = null;
}
}
~~~
prepareMessageToSend很有意思,其主要工作如下。
* 如果设置回调对象,系统将从回调对象中获取要发送的数据。
* 如果没有回调对象,系统会获取前台应用程序的包名。如果前台应用程序禁止通过
Android Beam分享信息,则prepareMessageToSend直接返回,否则它将创建一个包含了两个NFC Record的NDEF消息。
接下来看第二个关键函数onP2pSendConfirmationRequested,其代码如下所示。
**P2pEventManager.java::onP2pSendConfirmationRequested**
~~~
public void onP2pSendConfirmationRequested() {
// 对于拥有显示屏幕的Android设备来说,mSendUi不为空
if (mSendUi != null)
mSendUi.showPreSend();// 显示类似图8-29左图所示的界面以提醒用户
else
mCallback.onP2pSendConfirmed();
/*
对于那些没有显示屏幕的Android设备来说,直接调用onP2pSendConfirmed。mCallback指向
P2pLinkManager。待会将分析这个函数。
*/
}
~~~
在onP2pSendConfirmationRequested函数中,SendUi的showPreSend函数将绘制一个通知界面,如图8-29所示。至此,notifyLlcpLinkActivation的工作完毕。在继续分析Android Beam之前,先来总结notifyLlcpLinkActivation的工作流程,如图8-39所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/c820becf4a17a05a238748b30efb3c92_1287x483.jpg)
图8-39 notifyLlcpLinkActivation流程
**②、Beam数据发送流程**
SendUi界面将提醒用户触摸屏幕以发送数据,触摸屏幕这一动作将导致SendUi的onTouch函数被调用,其代码如下所示。
**SendUi.java::onTouch**
~~~
public boolean onTouch(View v, MotionEvent event) {
......
mCallback.onSendConfirmed();// mCallback指向P2pEventManager
return true;
}
~~~
**P2pEventManager.java::onSendConfirmed**
~~~
public void onSendConfirmed() {
if (!mSending) {
if (mSendUi != null)
mSendUi.showStartSend();// 显示数据发送动画
mCallback.onP2pSendConfirmed();// mCallback指向P2pLinkManager
}
mSending = true;
}
~~~
**P2pLinkManager.java::onP2pSendConfirmed**
~~~
public void onP2pSendConfirmed() {
synchronized (this) {
......
mSendState = SEND_STATE_SENDING;
if (mLinkState == LINK_STATE_UP)
sendNdefMessage();// 关键函数
}
}
~~~
sendNefMessage将创建一个类型为SendTask的AsyncTask实例以处理数据发送相关的工作,我们来看这个SendTask,相关代码如下所示。
**P2pLinkManager.java::SendTask:doInBackground**
~~~
final class SendTask extends AsyncTask {
public Void doInBackground(Void... args) {
NdefMessage m; Uri[] uris; boolean result;
m = mMessageToSend; uris = mUrisToSend; // 设置要发送的数据
long time = SystemClock.elapsedRealtime();
try {
int snepResult = doSnepProtocol(mHandoverManager, m, uris,
mDefaultMiu, mDefaultRwSize);
......
} catch (IOException e) {
// 如果使用SNEP发送失败,则将利用NPP协议再次尝试发送
if (m != null) // 请读者自行研究和NPP相关的代码
result = new NdefPushClient().push(m);
......
}
time = SystemClock.elapsedRealtime() - time;
if (result) onSendComplete(m, time);// 发送完毕。请读者自行阅读该函数
return null;
}
}
~~~
上述代码中的doSnepProtocol函数内容如下。
**P2pLinkManager.java::SendTask:doSnepProtocol**
~~~
static int doSnepProtocol(HandoverManager handoverManager,
NdefMessage msg, Uri[] uris, int miu, int rwSize) throws IOException {
// 创建一个SnepClient客户端
SnepClient snepClient = new SnepClient(miu, rwSize);
try {
snepClient.connect(); // ①连接远端设备的SnepServer
} ......
try {
if (uris != null) {// 如果uris不为空,需要使用HandoverManager,这部分内容请读者自行阅读
......
} else if (msg != null) {
snepClient.put(msg);// ②利用SNEP的PUT命令发送数据
}
return SNEP_SUCCESS;
} catch ......
finally {
snepClient.close();
}
return SNEP_FAILURE;
}
~~~
重点介绍SnepClient的connect函数以及put函数。connect函数的代码如下所示。
**SnepClient.java::connect**
~~~
public void connect() throws IOException {
......
LlcpSocket socket = null;
SnepMessenger messenger; // SnepMessenger用于处理数据发送和接收
try {
socket = NfcService.getInstance().createLlcpSocket(0, mMiu, mRwSize, 1024);
if (mPort == -1)
socket.connectToService(mServiceName);// 通过服务名来连接服务端
else
socket.connectToSap(mPort); // 通过SAP连接服务端
// 获取远端设备的MIU
int miu = socket.getRemoteMiu();
int fragmentLength = (mFragmentLength == -1) ? miu : Math.min(miu, mFragmentLength);
messenger = new SnepMessenger(true, socket, fragmentLength);
} ......
......
}
~~~
put函数代码如下所示。
**SnepClient.java::put**
~~~
public void put(NdefMessage msg) throws IOException {
SnepMessenger messenger;
messenger = mMessenger;
synchronized (mTransmissionLock) {
try {
/*
获取SNEP PUT命令对应的数据包,然后发送出去。SnepMessenger的sendMessage
函数将完成具体的发送工作。
*/
messenger.sendMessage(SnepMessage.getPutRequest(msg));
messenger.getMessage();
}.....
}
}
~~~
图8-40总结了本节所述的Beam数据发送流程。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/79044d4af1daa7f1c7f9720b4045d5aa_983x562.jpg)
图8-40 Beam数据发送流程
接着来看Beam数据接收流程。
**③、Beam数据接收流程**
8.3.2节中曾介绍,SnepServer每接收一个客户端的连接后均会创建一个ConnectionThread,它的代码如下所示。
**SnepServer.java::ConnectionThread**
~~~
private class ConnectionThread extends Thread {
private final LlcpSocket mSock;
private final SnepMessenger mMessager;
ConnectionThread(LlcpSocket socket, int fragmentLength) {
super(TAG);
mSock = socket;
// 也创建一个SnepMessenger用来处理具体的数据接收
mMessager = new SnepMessenger(false, socket, fragmentLength);
}
public void run() {
......// 省略一些try/catch逻辑
while (running) {
if (!handleRequest(mMessager, mCallback))
break;
......
}
mSock.close();
}
}
~~~
**SnepServer.java::handleRequest**
~~~
static boolean handleRequest(SnepMessenger messenger,
Callback callback) throws IOException {
SnepMessage request;
request = messenger.getMessage();
......
if (((request.getVersion() & 0xF0) >> 4) != SnepMessage.VERSION_MAJOR) {
messenger.sendMessage(SnepMessage.getMessage(
SnepMessage.RESPONSE_UNSUPPORTED_VERSION));
} else if (request.getField() == SnepMessage.REQUEST_GET) {
// 处理GET命令,callback类型为SnepServer的内部类Callback
messenger.sendMessage(callback.doGet(request.getAcceptableLength(),
request.getNdefMessage()));
} else if (request.getField() == SnepMessage.REQUEST_PUT) {
// 处理PUT命令
messenger.sendMessage(callback.doPut(request.getNdefMessage()));
} .....
return true;
}
~~~
来看SnepServer.callback的doPut函数,它由P2pLinkManager的内部类实现,代码如下所示。
**P2pLinkManager.java::SnepServer.Callback**
~~~
final SnepServer.Callback mDefaultSnepCallback = new SnepServer.Callback() {
public SnepMessage doPut(NdefMessage msg) {
onReceiveComplete(msg);
return SnepMessage.getMessage(SnepMessage.RESPONSE_SUCCESS);
}
}
~~~
onReceiveComplete的代码如下所示。
**P2pLinkManager.java::onReceiveComplete**
~~~
void onReceiveComplete(NdefMessage msg) {
// 发送一个MSG_RECEIVE_COMPLETE消息
mHandler.obtainMessage(MSG_RECEIVE_COMPLETE, msg).sendToTarget();
}
~~~
MSG_RECEIVE_COMPLETE消息由P2pLinkManager的handleMessge处理,相关代码逻辑如下所示。
**P2pLinkManager.java::**
~~~
public boolean handleMessage(Message msg) {
switch (msg.what) {
......
case MSG_RECEIVE_COMPLETE:
NdefMessage m = (NdefMessage) msg.obj;
synchronized (this) {
......
mSendState = SEND_STATE_NOTHING_TO_SEND;
mEventListener.onP2pReceiveComplete(true);// 取消本机显示的SendUi界面
// sendMockNdefTag将发送一个MSG_MOCK_NDEF消息给NfcService的NfcServiceHandler
NfcService.getInstance().sendMockNdefTag(m);
}
break;
.....
}
}
~~~
**NfcService.java::NfcServiceHandler:handleMessage**
~~~
final class NfcServiceHandler extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_MOCK_NDEF: {
NdefMessage ndefMsg = (NdefMessage) msg.obj;
Bundle extras = new Bundle();
extras.putParcelable(Ndef.EXTRA_NDEF_MSG, ndefMsg);
extras.putInt(Ndef.EXTRA_NDEF_MAXLENGTH, 0);
extras.putInt(Ndef.EXTRA_NDEF_CARDSTATE, Ndef.NDEF_MODE_READ_ONLY);
extras.putInt(Ndef.EXTRA_NDEF_TYPE, Ndef.TYPE_OTHER);
// 创建一个模拟Tag对象
Tag tag = Tag.createMockTag(new byte[] { 0x00 },
new int[] { TagTechnology.NDEF },new Bundle[] { extras });
// 直接通过分发系统分发这个Tag
boolean delivered = mNfcDispatcher.dispatchTag(tag);
......
break;
}
......
}
}
}
~~~
Beam接收端的处理流程比较巧妙,系统将创建一个模拟的Tag对象,然后利用分发系统来处理它。图8-41总结了Beam接收端的处理流程。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/009b6118f851b65bedd3843b464e16ba_1008x590.jpg)
图8-41 Beam数据接收流程
**4、CE模式**
NFC系统模块对CE模式的支持集中在以下两点。
* 通过NfcAdapterExtras为CE模式的客户端提供INfcAdapterExtras功能实现。这部分内容所涉及的调用流程比较简单,请读者自行研读。当然,我们略过了和芯片相关的部分。
* 当NFC设备进入CE模式后,和它交互的另一端是诸如NFC Reader这样的设备。此时对端发起一些操作,而NativeNfcManagement的一些回调函数将被触发。下面代码展示了和CE相关的这些回调函数。
**NativeNfcManager.java::CE相关回调函数**
~~~
// 根据NFCIP-1协议,该函数表示对端设备发送了DESELECT命令给我们。它属于deactivation阶段的命令
private void notifyTargetDeselected() {// mListern指向NfcService
mListener.onCardEmulationDeselected();
}
/*
用于通知SE上某个应用程序开始和对端设备进行交互。AID为Application ID的缩写,它代表SE上
的某个应用程序(称为Applet)。
*/
private void notifyTransactionListeners(byte[] aid) {
mListener.onCardEmulationAidSelected(aid);
}
// 用于通知SE模块被激活
private void notifySeFieldActivated() {
mListener.onRemoteFieldActivated();
}
// 用于通知SE模块被禁止
private void notifySeFieldDeactivated() {
mListener.onRemoteFieldDeactivated();
}
// 收到对端设备的APDU命令
private void notifySeApduReceived(byte[] apdu) {
mListener.onSeApduReceived(apdu);
}
/*
EMV是EMV标准是由国际三大银行卡组织Europay(欧陆卡,已被万事达收购)、MasterCard(万事达卡)和
Visa(维萨)共同发起制定的银行卡从磁条卡向智能IC卡转移的技术标准,是基于IC卡的金融支付标准,
目前已成为公认的全球统一标准。下面这个函数用于通知EMV Card进入Removal阶段。
该函数只适用于NXP公司的相关芯片。
*/
private void notifySeEmvCardRemoval() {
mListener.onSeEmvCardRemoval();// NfcService如何处理它呢
}
// 用于通知MIFARE SMX Emulation被外部设备访问。该函数只适用于NXP公司的相关芯片
private void notifySeMifareAccess(byte[] block) {
mListener.onSeMifareAccess(block);
}
~~~
NfcService是如何处理这些回调通知的呢?以notifySeEmvCardRemoval中的onSeEmvCardRemoval为例,NfcService将发送一个MSG_SE_EMV_CARD_REMOVAL消息,而这个消息的处理函数代码如下所示。
**NfcService.java::NfcServiceHandler:handleMessage**
~~~
......
case MSG_SE_EMV_CARD_REMOVAL: // CE相关的消息全是类型的处理方式
Intent cardRemovalIntent = new Intent();
cardRemovalIntent.setAction(ACTION_EMV_CARD_REMOVAL);
sendSeBroadcast(cardRemovalIntent); // 发送广播
......
~~~
由上述代码的注释可知,NfcService对CE相关的处理非常简单,就是接收来自底层的通知事件,然后将其转化为广播事件发送给系统中感兴趣的应用程序。
**5、Android中的NFC总结**
本节介绍了Android平台中NFC系统模块NfcService及其他重要组件。从整体上来说,NfcService以及与底层芯片无关的模块难度不大,而与底层芯片相关的模块则难度较大(位于com.android.nfc.dhimpl包中)。本章没有讨论dhimpl包的具体代码,希望感兴趣的读者能结合芯片手册自行研究它们。
另外,本节还对NFC R/W模式及P2P模块的工作流程进行了相关介绍,这部分难度不大,相信读者能轻松掌握。同时,作为课后作业,请读者在本节基础上自行学习Handover相关的处理流程。
最后,本节简单介绍了NFC CE模式的处理流程,NfcService本身对CE相关的处理比较简单,它仅根据CE相关的操作向系统发送不同的广播,而这些广播则会由感兴趣的应用程序来处理,例如Google Wallet。
>[info] 提示 NFC CE模式其实内容相当复杂,涉及很多规范。笔者会在博客上继续介绍NFC CE相关的知识,敬请读者关注。
';
8.3.1 NFC应用示例
最后更新于:2022-04-02 06:05:46
Android平台中,NFC应用的类型和NFC三种运行模式有关,我们先来看一个使用NFCR/W模式读取NFC Tag的示例。
**1、NFC R/W模式示例**
根据前文对NFC基础知识的介绍可知,和R/W模式相关的应用场景就是使用者利用NFC手机(充当NFC Reader的角色)来读取目标NFC Tag中的信息。Android平台为NFC R/W模式设计了“Tag分发系统”(Tag Dispatch System)的机制,描述了NFC系统模块如何向应用进程分发与目标NFC Tag相关的Intent(该Intent中包含了Tag中的数据或是一个代表目标NFC Tag的Tag对象)。
**①、NFC Tag分发系统**
Tag分发系统的工作机制如图8-27所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/0b72aa7ff6d0e575604d15da0a57df6f_983x462.jpg)
图8-27 Tag分发系统的工作机制
Tag分发系统的工作机制如下。
1. 当本机扫描到一个NFC Tag后,NFC系统模块将首先尝试直接读取该Tag中的数据。
2. 如果这些数据封装在NDEF消息中并且能映射成Android系统直接支持的数据类型(目前仅支持MIME和URI这两大类数据类型,详情见表8-11),则NFC系统模块将发送一个ACTION_NDEF_DISCOVERED的Intent给那些注册了对ACTION_NDEF_DISCOVERED通知感兴趣的Activity。如果找到目标Activity,则将此Intent(携带Tag中的NDEF消息和一个代表该NFC Tag的Tag对象)派发给它。如果NFC系统模块没有找到目标Activity,则将尝试发送一个ACTION_TECH_DISCOVERED的Intent(包含一个代表目标NFC Tag的Tag对象)。
3. 如果NFC Tag中的数据不能转换成系统直接支持的类型,或者NFC Tag中的数据没有使用NDEF消息格式或者没有目标Activity对ACTION_NDEF_DISCOVERED通知感兴趣,则NFC系统模块将发送一个ACTION_TECH_DISCOVERED的Intent(包含一个Tag对象)。如果找到对该Intent感兴趣的Activity,则此Intent将派发给它。
4. 如果没有Activity对ACTION_TECH_DISCOVERED感兴趣,则NFC系统模块将最后尝试发送一个ACTION_TAG_DICOVERED的Intent(包含一个Tag对象)。如果有对ACTION_TAG_DISCOVERED感兴趣的Activity,则此Intent将派发给它。
上述的Tag分发系统看起来很复杂,实际上其核心内容可概况成三个步骤。
* **步骤1** 如果目标NFC Tag包含了系统支持的NDEF消息,则NFC系统模块将直接把这个NDEF消息分发给感兴趣的Activity。如果有目标Activity,则直接分发给它,否则转步骤2。分支转换的判断标准是NFC Tag是否包含了系统支持的NDEF消息以及同时是否有目标Activity注册了ACTION_NDEF_DISCOVERED通知。
* **步骤2** 如果目标NFC Tag包含了系统不支持的NDEF消息或者步骤1中没有目标Activity,则NFC系统模块将尝试分发一个ACTION_TECH_DISCOVERED通知。NFC系统模块在分发此通知时,将首先分析目标NFC Tag所支持的Tag Technology(它代表目标NFC Tag所使用的技术,详情见下文分析),然后寻找注册了支持这些Tag Technology的目标Activity并将Intent分发给它。如果没有合适的目标Activity,则转入步骤3。
* **步骤3** NFC系统模块将分发ACTION_TAG_DISCOVERED通知给注册了对该通知感兴趣的目标Activity。
除了Tag分发系统外,Android系统还有一个“前台分发系统”(Foreground Dispatch System)。其规则和Tag分发系统类似,二者区别主要集中在选择目标Activity上。
* Tag分发系统中,Activity在其AndroidManifest.xml中设置Intent分发条件,即设置对应的IntentFilter。在这种分发系统中,不考虑目标Activity是否在前台还是后台。只要找到目标Activity,NFC系统就会启动它。
* 前台分发系统中,当前活跃(即所谓的前台)的Activity在其启动过程中设置Intent分发条件。如果NFC Tag满足前台Activity设置的分发条件,NFC系统模块首先会把Intent分发给前台这个Activity。当该Activity退到后台时,它需要取消前台分发功能,即它不再是目标Activity。
简而言之,前台分发系统只检查当前显示的Activity是否满足分发条件,而Tag分发系统则会搜索系统内所有满足条件的Activity。
**②、Tag分发通知**
下面我们分别来看看这三个不同作用的Tag分发通知。
* **1)ACTION_NDEF_DISCOVERED**:由上文可知,NFC系统模块首先尝试将NFC Tag中的数据映射成系统直接支持的数据格式。表8-11列举了Android系统直接支持的NFC Forum数据格式。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/4bb6c1602f897c3dc287d357339b5f2a_1280x383.jpg)
由表8-11可知,如果NFC Tag中数据格式能映射成功,则NFC系统模块将发送一个ACTION_NDEF_DISCOVERED Intent给目前Activity,而该Intent将包含此NFC Tag中的NDEF消息。
除了NFC Forum定义的数据类型外,Android还新增了一个名为AAR(Android Application Record)的数据类型,它其实是在一个NDEF的消息中封装了某个应用的package名。对AAR来说,分发系统的工作流程如下。
* 分发系统首先尝试使用IntentFilter来寻找目标Activity。如果和IntentFilter匹配的Activity同时和AAR匹配(即二者的package名一样),就启动该Activity。
* 如果Activity跟AAR不匹配,或者是有多个Activity能够处理该Intent,或者是没有能够处理该Intent的Activity,NFC系统模块将启动由AAR指定的应用程序。
* 如果系统中没有安装该AAR对应的应用程序,NFC系统模块将从Google Play下载该应用程序。
AAR的好处是能让某个公司部署的NFC标签只能由该公司开发的客户端(通过在NDEF中设置AAR)来处理。后文代码分析时候读者还将看到上述AAR的工作流程。
* **2)ACTION_TECH_DISCOVERED**:如果系统不能映射NFC Tag中的数据,我们该如何处理呢?
>[info] 提示 ACTION_TECH_DISCOVERED触发的另一个原因是没有Activity对ACTION_NDEF_DISCOVERED感兴趣。
该问题的直观答案就是应用程序自己去读取并解析Tag中的数据。不过,由于NFC Tag的类型有四种之多,甚至同一个厂商还生产了基于不同底层协议的NFC Tag,导致Android系统无法提供一种通用的接口来操作所有种类的NFC Tag。为了解决此问题,Android提供了一个名为"android.nfc.tech"的Java包来帮助应用程序操作对应的NFC Tag。表8-12为android.nfc.tech包
中的几个重要成员类。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/b928447764016555e6a9f02cf63f084c_1264x553.jpg)
NFC系统模块将在ACTION_TECH_DISCOVERED Intent中携带一个Tag对象,应用程序可调用该Tag对象的getTechList来获取该Tag所使用的Technology。注意,一个Tag可能同时支持表8-12中多种Technology。例如图8-28所示为笔者测试北京市公交卡时所得到的Tag Technology信息。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/6a40b4dd7206f1be4b728c426e53d5da_951x310.jpg)
图8-28 北京市公交卡Tag Technology示例
由图8-28可知,北京市公交卡同时支持MifareClassic、NfcA和NdefFormatable这三种类型的Tag Technology。应用程序接着可根据目标NFC Tag所支持的Tag Technology来创建表8-12中的对象来和NFC Tag交互。
>[info] 特别注意 严格来说,表8-12中所列的类不仅仅是用来读写对应类型的NFC Tag,它还支持一些控制操作以至于能在NFC Tag上实现一些特定的协议。以北京市公交卡为例,其内部肯定有一个相关的协议使得应用程序可通过这些协议来完成公交卡充值,付费等操作。“小木公交”软件即可读取多个城市公交卡的信息,读者不妨下载试试。
* **3)ACTION_TAG_DISCOVERED**:如果目标NFC Tag不属于表8-12中的一种,则NFC系统模块将发送ACTION_TAG_DISCOVERED Intent并携带一个Tag对象传递给感兴趣的Activity。Activity将根据Tag的ID(调用Tag的getId函数)或该Tag使用的技术(调用Tag的getTechList)来创建合适的处理对象。
下面通过一个示例来了解上述三种通知的用法。
**③、示例分析**
本节将通过一个前台分发示例来看看应用程序如何处理上述三种Intent。
**ForegroundDispatch.java::onCreate**
~~~
public class ForegroundDispatch extends Activity {// ForegroundDispatch是一个Activity
......// 定义一些成员变量
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
// NFC客户端必须调用下面这个函数以获得一个NfcAdapter对象,该对象用于和NFC系统模块交互
mAdapter = NfcAdapter.getDefaultAdapter(this);
// 构造一个PendingIntent供NFC系统模块派发
mPendingIntent = PendingIntent.getActivity(this, 0,
new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
// 监听ACTION_NDEF_DISOVERED通知,并且设置MIME类型为“*/*”
// 对任何MIME类型的NDEF消息都感兴趣
IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
ndef.addDataType("*/*");
// 我们同时还监听ACTION_TECH_DISSCOVERED和ACTION_TAG_DISCOVERED通知
mFilters = new IntentFilter[] {
ndef,
new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED),
new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED),
};
// 对ACTION_TECH_DISCOVERED通知来说,还需要注册对哪些Tag Technology感兴趣
mTechLists = new String[][] {
new String[] { NfcF.class.getName() },// 假设本例支持NfcF
new String[]{MifareClassic.class.getName()}
};// 假设本例支持MifareClassic
}
}
~~~
在上述onCreate函数中,同时监听了三种Tag Intent通知,最终效果如下。
* 如果目标Tag中包含MIME类型的NDEF消息,则Tag分发系统将给我们传递一个ACTION_NDEF_DISCOVERED Intent。
* 如果目标Tag使用的Tag Technolog为NfcF或MifareClass,则Tag分发系统将给我们传递一个ACTION_TECH_DISCOVERED Intent。
* 最后,Tag分发系统将不满足上述条件的其他所有Tag通过ACTION_TAG_DISCOVEREDIntent传递给我们。
接下来,由于本例使用了NFC的前台分发系统,故需要将onCreate中设置的配置信息传递给NFC系统模块,相关代码如下所示。
**ForegroundDispatch.java::onResume**
~~~
public void onResume() {
super.onResume();
// 调用NfcAdapter的enableForegroundDispatch函数启动前台分发系统
// 同时需要将分发条件传递给NFC系统模块
mAdapter.enableForegroundDispatch(this, mPendingIntent, mFilters,mTechLists);
}
~~~
当NFC系统模块扫描到一个NFC Tag时,前台分发系统通过将触发ForegroundDispatch这个Activity的onNewIntent函数,该函数的代码如下所示。
**ForegroundDispatch.java::onNewIntent**
~~~
public void onNewIntent(Intent intent) {
String action = intent.getAction();
// 处理ACTION_NDEF_DISCOVERED消息
if(action.equals(NfcAdapter.ACTION_NDEF_DISCOVERED)){
NdefMessage[] ndefMsgs = null;
// 获取该Intent中的NdefMessage数组。绝大部分情况下该数组的长度为1
NdefMessage[] ndefMsgs = (NdefMessage[])intent.
getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
} // 处理ACTION_TECH_DISCOVERED通知
else if (action.equals(NfcAdapter.ACTION_TECH_DISCOVERED)){
// 获取该Intent中的Tag对象
Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
String[] techList = detectedTag. getTechList();// 获取该Tag使用的Technology
for(String tech: techList){
if(tech.equals(NfcF.class.getName())){// 假设该Tag支持NfcF
// 创建NfcF对象和该Tag交互
NfcF nfcF = NfcF.get(detectedTag);
nfcF.connect();// 向目标Tag发起I/O操作前需要先连接上它
......// 调用NfcF类的其他函数,例如transceive向NFC Tag发送命令
nfcF.close();// 关闭连接
}......
}
} // 处理ACTION_TAG_DISCOVERED通知
else if(action.equals(NfcAdapter.ACTION_TAG_DISCOVERED)){
Tag tag = (Tag)intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
String[] arry = tag.getTechList();
......// 根据Tag使用的Technology来构造相应的处理对象
}
}
~~~
当ForegroundActivity退出时,需要在onPause函数中停止使用前台分发系统,相关代码如下所示。
~~~
public void onPause() {
super.onPause();
mAdapter.disableForegroundDispatch(this);// 停止前台分发系统
}
~~~
通过上述介绍可知,在R/W模式中:
* 如果应用程序仅用于读取NFC Tag中所包含的数据,则应尽量通过注册ACTION_NDEF_DISCOVERED通知来获取自己感兴趣的数据。
* 如果应用程序希望能和NFC Tag交互以实现自己的一套协议或者希望能直接读写NFCTag,则可通过注册ACTION_TECH_DISCOVERED通知来获得代表NFC Tag的Tag对象。应用程序接着要根据该Tag使用的Technology来构造对应的TagTechnology对象来操作此NFC Tag。
* 如果应用程序处理的NFC Tag不满足表8-12中的一种,则需要监听ACTION_TAG_DISCOVERED通知,然后再构造自己的TagTechnology对象来操作此NFC Tag。
>[info] 提示 关于Android中NFC的分发系统,请读者阅读Android SDK关于NFC的介绍,相关资料位于http://developer.android.com/guide/topics/connectivity/nfc/nfc.html。
**2、NFC P2P模式示例**
Android平台中的NFC P2P模式使用了前文介绍的SNEP协议。在SNEP协议基础上,Android设计了"Android Beam"技术架构,该架构使得NFC客户端程序能非常容易得在两个NFC设备间传递NDEF消息。
>[info] 提示 除了SNEP外,Android还定义了一个与之类似的NPP(Ndef Push Protocol),该协议对应的服务端SAP为0x10,服务名为"com.android.npp"。Android Beam中,系统首先使用SNEP进行传输。如果一些老旧的设备不支持SNEP,则系统将使用NPP。
和R/W模式一样,Android Beam的使用也需要绑定到一个Activity中,下面我们直接通过一例子来看看如何使用Android Beam。
**①、发送端处理**
**Beam.java::onCreate**
~~~
public class Beam extends Activity implements CreateNdefMessageCallback,// 用于从源Activity中得到需要传递的NDEF消息
OnNdefPushCompleteCallback // 用于通知NDEF消息传送完毕
{
NfcAdapter mNfcAdapter;
TextView mInfoText;
private static final int MESSAGE_SENT = 1;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mInfoText = (TextView) findViewById(R.id.textView);
// 得到一个NfcAdapter对象,用于和NFC系统模块交互
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
/*
下面这两个函数调用非常重要。
setNdefPushMessageCallback:设置一个回调对象。如果该回调对象不为空,则NFC系统模块将
为Beam这个Activity启用Android Beam。该回调对象的作用是:当NFC系统模块通过SNEP协议
发现另外一个NFC设备时,系统会弹出如图8-29所示的“数据发送通知框”。如果用户选择本机发送数据,
则NFC系统模块将通过这个回调对象获取需要发送的数据。
setOnNdefPushCompleteCallback:设置一个数据发送完毕通知回调对象,当NFC系统模块发送完
本Activity所设置的NDEF消息时,该回调对象对应的函数将被调用。
*/
mNfcAdapter.setNdefPushMessageCallback(this, this);
mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
}
}
~~~
如图8-29所示,左图为数据发送通知框。在Android平台中,两个互相靠近的NFC设备都会弹出类似左图这样的数据发送通知框。至于最终是谁来发送数据则需要用户点击触摸屏来决定。当对端设备收到示例Beam所发送的NDEF消息后,对端设备的NFC系统模块将解析该NDEF消息然后通过Tag分发系统或前台分发系统找到目标Activity。图8-29右图即为对端设备收到本例中Beam发送的数据后的处理结果。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/ddc28ff52926408086541505abe255ba_767x423.jpg)
图8-29 Beam示例截图
下面来看createNdefMessage函数,它实现了CreateNdefMessageCallback接口类。当用户在图8-29左图中点击触摸屏后,NFC系统模块将通过该函数获取应用程序需要发送的NDEF消息,其代码如下所示。
**Beam.java::createNdefMessage**
~~~
public NdefMessage createNdefMessage(NfcEvent event) {
Time time = new Time();
time.setToNow();
// 设置一些信息
String text = ("Beam me up!\n\n" +"Beam Time: " + time.format("%H:%M:%S"));
// 构造一个MIME Type的NDEF消息
NdefMessage msg = new NdefMessage(NdefRecord.createMime(
"application/com.example.android.beam", text.getBytes()));
return msg;
}
~~~
当本机NFC系统模块成功发送了NDEF消息后,onNdefPushComplete将被调用以通知数据发送的情况。在Beam示例中,该函数的代码如下所示。
**Beam.java::onNdefPushComplete**
~~~
public void onNdefPushComplete(NfcEvent arg0) {
// 发送一个MESSAGE_SENT消息。注意,onNdefPushComplete运行在Binder线程
mHandler.obtainMessage(MESSAGE_SENT).sendToTarget();
}
~~~
**②、接收端处理**
当对端NFC设备接收到此NDEF消息时,将通过Tag分发系统来处理它。Beam示例在其AndroidManifest.xml设置了如图8-30所示的IntentFilter。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/72df4a7ac623fb73e80cf73d1a355911_742x162.jpg)
图8-30 Beam设置的IntentFilter
根据前文对Tag分发系统的介绍,对端设备的Beam将被启动。启动过程中几个重要函数的代码如下所示。
**Beam.java::onNewIntent/onResume/processIntent**
~~~
public void onNewIntent(Intent intent) {
setIntent(intent);// Beam使用了SINGLE_TOP启动模式。setIntent用于保存Intent
} // Beam启动时,onResume将被调用
public void onResume() {
super.onResume();
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
processIntent(getIntent());// getIntent获取setIntent设置的那个Intent对象
}
}
// 处理ACTION_NDEF_DISCOVERED通知
void processIntent(Intent intent) {
// 取出对端Beam发送的NDEF消息
Parcelable[] rawMsgs =
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
NdefMessage msg = (NdefMessage) rawMsgs[0];
// 设置Text控件,最终结果如图8-29右图所示
mInfoText.setText(new String(msg.getRecords()[0].getPayload()));
}
~~~
至此,通过一个示例展示了NFC客户端程序如何使用Android Beam技术。Android Beam的本质是利用NFC P2P模式的SNEP协议在两个NFC设备间传递NDEF消息。除了NfcAdapter的setOnNdefPushCompleteCallback函数外,NfcAdapter还有其他方式能发送NDEF消息。关于这部分内容,请读者务必阅读SDK中的介绍
(http://developer.android.com/guide/topics/connectivity/nfc/nfc.html#p2p)。
另外,Android Beam除了能发送NDEF消息外,它还支持发送URI Scheme为"file"或"content"类型的数据,也就是文件或数据库中的内容。这些数据的量可能比较大,所以Android Beam将使用Handover并选择蓝牙来传输它们。
**3、NFC CE模式示例**
在Android平台中,NFC CE的使用比较特殊,主要体现在两点。
* Android SDK没有直接提供Card Emulation相关的API,但Android系统内部提供了一个名为"com.android.nfc_extras.jar"的Java动态库。在这个动态库中,Android封装了和CE相关的API。应用程序需要主动加载这个nfc_extras库才能使用CE模式。
* 由于CE通常用于支付等方面的工作,所以Android系统在nfc_extras动态库的使用上有着非常严格的权限管理。
下面介绍相关知识。
**①、nfc_extras和nfcee_access.xml**
根据上文所述,应用程序如何才能使用CE模式呢?我们先来看看如何在应用程序中使用nfc_extras动态库。图8-31所示的AndroidManifest.xml指明了必要的做法。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/ef808a03f7308eb4f99d54723ead28ab_597x105.jpg)
图8-31 AndroidManifest.xml设置
使用NFC CE的应用必须通过标签申明"android.permission.NFC"权限。同时还需通过申明使用动态库"com.android.nfc_extras"。这样,当应用程序运行时,系统会为它加载com.android.nfc_extras.jar包。该包对应的文件位于/system/framework目录下。
接着,客户端在需要使用nfc_extras API的Java类文件中通过import语句导入相关的类,如图8-32所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/914a67b63b3c2f5bdb24c76ef06507a7_681x84.jpg)
图8-32 nfc_extras动态库相关类
nfc_extras主要包含三个类,其用法将留待下节的示例代码中再来介绍。
客户端导入相关类后,下一步要解决的问题就是编译。由于Android SDK没有提供这些类,故需要手动解决编译问题。目前有两种方法解决。
一种方法是为应用程序编写Android.mk,然后添加以下内容。
~~~
LOCAL_JAVA_LIBRARIES:=com.android.nfc_extras
~~~
该方法要求在Android源码下编译此应用程序。
另外一种方式是在Eclipse中为应用程序手动添加一个编译路径,如图8-33所示。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/1537d9eb7bc6450bb5c5a144a6e9c1cf_1051x170.jpg)
图8-33 Eclipse设置编译路径
图8-33中,笔者在测试示例中添加了nfc_extras动态库(即classes-full-debug.jar,它是Android系统编译nfc_extras时生成的中间JAR文件包)。
>[info] 注意 无论哪种方法,都需要有Android系统的源码。
通过上述步骤,应用程序可编译成功,但它此时依然没有权限操作NFC CE。这是因为在Android系统中,除了"android.permission.NFC"权限外,NFC系统模块针对NFC CE这种重要运行模式还需要检查另外一个权限,即客户端程序的签名信息。只有拥有系统指定签名的应用程序才能使用NFC CE模式。
Android系统所指定的签名信息都保存在/etc/nfcee_access.xml文件中,图8-34所示为Galaxy Note 2中该文件的内容。
:-: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/5c20a6c08d15d785c2fedf2251074a6b_788x723.jpg)
图8-34 nfcee_access.xml示例
图8-34中所示nfcee_access.xml包含三个签名(由signer标签指定,图中由黑框标示)。
* 第一个签名为Google Wallet相关应用拥有。
* 第二个签名为Samsung Wallet相关应用拥有。
* 第三个签名为笔者测试时用的签名信息。
签名信息检查的工作流程如下。
1. 客户端程序开展CE相关操作前,必须先获得一个NfcAdapterExtras对象。
2. 在获取该对象时,NFC系统模块先检查应用程序是否拥有"android.permission.NFC"权限,接着检查该应用程序的签名信息。只有调用程序的签名信息在nfcee_access.xml有记录,该应用才能得到一个NfcAdapterExtras对象。
>[info] 提示 显然,nfcee_access.xml要么由手机厂商在出厂前设置,要么在root的手机上修改。了解上述知识后,通过一个示例来介绍nfc_extras相关的API及使用方法。
**②、示例分析**
本节使用的示例通过修改AndroidBeamDemo而来。
**示例**
~~~
class NfcEEActivity extends Activity{
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {// 先从PackageManager那获取自己的签名信息
PackageManager pm = getPackageManager();
PackageInfo info = pm.getPackageInfo("com.example.android.beam",
PackageManager.GET_SIGNATURES);
// 将签名信息打印出来,开发者需要将此签名信息保存到/etc/nfcee_access.xml中
Log.e("NfcEE", "signature = " + info.signatures[0].toCharsString());
mContext = this;
// 调用NfcAdatperExtras.get函数获取一个NfcAdapterExtras对象
mAdapterExtras = NfcAdapterExtras.get(NfcAdapter.getDefaultAdapter(
mContext));
/*
获取与NFC芯片中Execution Environment模块交互的对象。注意:此处的EE一般情况下就是指
Secure Element。以图8-21 NXP pn65芯片模块图为例,SE可以是其内部的SmartMX模块,
也可以是外部的UICC。对UICC来说,不是所有手机都支持将UICC连接到NFC芯片。除此之外,
通过NFC操作UICC还需要相关驱动的支持。
mEe的类型为NfcExecutionEnvironment,通过它可以和SE交互。
*/
mEe = mAdapterExtras.getEmbeddedExecutionEnvironment();
}......
// 创建一个新的线程,相关测试工作放在此线程中进行
Thread testThread = new Thread(){
public void run() {
/*
使用CE模式前需要先设置Route,系统目前有ROUTE_ON_WHEN_SCREEN_ON(屏幕打开
时启用CE)和ROUTE_OFF(关闭CE)这两种选项。
*/
mAdapterExtras.setCardEmulationRoute(
new CardEmulationRoute(CardEmulationRoute.ROUTE_ON_WHEN_SCREEN_ON, mEe));
// 创建和SE交互的通道。该通道创建后,NFC其他功能将被禁止(如R/W或P2P)
mEe.open();
/*
发送命令给EE去执行。SELECT_CARD_MANAGER_COMMAND存储了相关的命令信息。
注意:EE命令的格式遵循ISO 7816-4规范。不同应用需要具体芯片的情况使用对应的命令,这部分是
CE模式的难点。
*/
byte[] out = mEe.transceive(SELECT_CARD_MANAGER_COMMAND);
......
mEe.close();// 关闭与SE交互的通道
// 关闭Card Emulation功能
mAdapterExtras.setCardEmulationRoute(
new CardEmulationRoute(CardEmulationRoute.ROUTE_OFF, null));
}
};
testThread.start(); // 启动工作线程
}
......// 其他代码
}
~~~
从上述示例代码来看,nfc_extras的API似乎比较简单。但对一个实际应用程序而言,其最大难度却在于处理相关命令上。由于不同NFC芯片以及所使用的SE不同,其定义的命令也不尽相同。关于SE命令的格式,读者可参考ISO 7816-4规范。
至此,我们对NFC CE进行了一些简单介绍,并围绕nfc_extras动态库的使用进行了相关讨论。根据笔者的研究,CE模式的内容远比R/W及P2P模式复杂。为此,强烈建议读者继续阅读参考资料[23]、[24]和[25]。
';