难题:识别独立安卓设备

今天碰巧朋友问到我怎么识别独立的安卓手机,就花了一些时间琢磨了一下。
其实这个问题可以秒答,就是IMEI。


TelephonyManager.getDeviceId();

这需要一个权限:


<uses-permission android:name="android.permission.READ_PHONE_STATE" />

问题如果这么简单就好了,问题在于:

因此其实问题从这里才开始。网上能够搜索到的解决方案有以下几点:

– WIFI MAC

一个方案是优先采用IMEI,当IMEI相同时,再比较WIFI的MAC地址。但如果手机没有WIFI功能或者WIFI功能没有开启(飞行模式),则无法获取到MAC地址。更加让人惆怅的是,我国大山寨厂商实在是懒透了,无线网卡的MAC地址居然也不修改,不少自刷机的也是这病情(例如这个这个还有这个)。至于说蓝牙MAC地址就更别说了。IMEI重复的病因,与MAC地址相同的其实是一个原因,都是刷机或山寨,所以这个wifi MAC地址的方案其实算不上互补了,必须另谋途径。

– Serial NO

另一个方案是用serial NO。这个值仅在android 2.3版本以上才提供支持。通过adb可以这样查看:


adb shell getprop ro.serialno

代码中可使用系统变量android.os.Build.SERIAL访问。如果这个值能够取到这是仅次于IMEI的最好方法了。缺点是这个值在2.2及以下版本的android系统不支持。不过好在如今的安卓世界2.2及以下的占有率已经越来越低了,翻新速度很快,因此这个值很值得一试。

– Android id

其实安卓系统提供了Settings.Secure.ANDROID_ID来获取唯一设备号。


import android.provider.Settings.Secure;

private String android_id = Secure.getString(
                            getContext().getContentResolver(),
                            Secure.ANDROID_ID); 

但同样2.2以前的系统支持得不好。这个值是系统初次启动后生成的,因此恢复出厂设置后这个值会变,导致观测到的设备数虚高。在手机刷机、重置频繁的环境,这个值是不靠谱的。另外某大厂生产的设备居然有个BUG,这个值是会重复的(DROID2),因此这个值还是别用为好。

– generated UUID

最后还有一个自己生成UUID的办法,保存这个ID,每次访问服务器时上传,自己告诉系统自己是谁。这个方法比所有硬件方法都更不靠谱,因为只需卸载软件和清理数据就会导致这个值被删除,从而产生新的UUID,造成观测到的设备数量虚高。更别说刷机和恢复出厂设置了。这个方法非常适合用来统计软件安装次数,而非独立设备数。

结论

综上所述,目前为止我还没找到非常完美的统计独立设备的方案,尤其是在中国这个水货、硬解、山寨、刷机市场泛滥和不规范的安卓世界,更是难上加难。

但是反过来想,是不是一台完全刷新,安装了全新的ROM的手机,就已经不是原来的那台手机了呢?统计独立设备数的目的究竟是什么?如果要的是独立活跃设备数,其实用自己生成的UUID已经足够,因为原来使用的那个UUID已经失去活性,可以忽略了。对于APP开发者而言这个情况与用户换了一台手机,完全弃用旧手机的情况其实是一样的。假如是采取活跃UUID的方式,则即使是使用ANDROID_ID或者自己生成的UUID都是可取的做法了。

本文参考

Symbian 测试框架调研笔记

symbian没有合适的自动测试框架。不是java的东西就是麻烦。

摘自Nokia wiki, RWindow::Construct

Construct ( const RWindowTreeNode &, TUint32 )

IMPORT_C TInt Construct ( const RWindowTreeNode & parent,
TUint32 aHandle
)

Completes the construction of the window handle.

This method should be called after the RWindow() constructor, before any other functions are performed on the window. It creates a window in the window server corresponding to the RWindow object. The window is initialised to inherit the size and extent of its parent window, given by the first parameter. If its parent is a group window then it will be full screen.

This function always causes a flush of the window server buffer.

 

Parameter Description
parent The window’s parent.
aHandle Client handle for the window. This is an integer value chosen by the client that must be unique within the current server session. The usual way of doing this is to cast the address of the object that owns the window to a TUint32; this allows event handlers which are given a window handle to obtain a reference to the window an event is intended for. For example, CCoeControl uses this technique when it constructs a window. Note that in GUI applications, every window is created and owned by a control. Therefore it is rare for 3rd party code to ever need to call a window’s Construct() function directly.

Returns: KErrNone if successful, otherwise one of the system-wide error codes.

这里的解释很重要,The usual way of doing this is to cast the address of the object that owns the window to a TUint32; this allows event handlers which are given a window handle to obtain a reference to the window an event is intended for. For example, CCoeControl uses this technique when it constructs a window. Note that in GUI applications, every window is created and owned by a control. 也就是说,RWindow的handle可以立即强转为一个CCoeControl*,并且就是构造他的控件对象。

摘自Nokia wiki, RWindowTreeNode::ClientHandle()

ClientHandle ( )

IMPORT_C TUint32 ClientHandle ( ) const

Gets the window’s client handle

The return value is the client’s integer handle that was passed as an argument to the window’s Construct() function: see RWindow::Construct() for a description of the client handle.

This function always causes a flush of the window server buffer.

 

See also: RWindow::Construct()

Returns: Handle ID for the window.

也就是说,通过这个函数可以获取到对应的控件指针。

摘自Nokia wiki, RWindowTreeNode::Child()

Child ( )

IMPORT_C TUint32 Child ( ) const

Gets the first child of the node.

This function always causes a flush of the window server buffer.

 

Returns: The client handle of the child node that currently has ordinal position 0. This is 0 if there isn’t a child.

获取TreeNode的孩子节点。使用NextSibling继续向下遍历。

注意到RWindowGroup也是继承自RWindowTreeNode的。一个测试思路就是,获取到当前WindowGroup,然后遍历整棵树,获取每个节点对应的CCoeControl。暂时不知道Symbian系统上能不能做Dynamic Cast。理论上来说配合RTTI可以了解到每个节点的类型,并对特定的类型调用特定的方法进一步处理。不过跨进程做这种事情恐怕还是有问题。不知能不能把测试程序作为一个dll植入被测程序去运行。这就可以实现Android上Hierarchy Viewer的功能了。

简单记录以作备忘。哪位兄弟知道成熟的symbian测试框架也请不吝赐教。

Symbian 系统学习-活动对象和其他

最近做了一个android monkey test到symbian的移植。算是草草学习了一下symbian系统吧。写一些东西做个记录,方便以后自己查阅。

symbian系统最有意思的设计应该是这个Active Object的设计模型了。他使用非抢占的多任务模型,固定优先级。这个非抢占我一开始理解得不够好。后来做monkey的时候,用的console程序。想在里面用CActive做进程监视,发现他的RunL函数永远都无法进入。跟公司的symbian大牛也讨论了好久都没有解决问题。最后还是自己悟了。这个所谓的非抢占,就是说你console的主函数不退出的话,这个Active scheduler是不会调度的。之所以那位大牛写的demo可以运行起来,是因为他用的是窗口工程,在一个窗口回调函数里面调用的SetActive。窗口回调函数退出之后自然而然Active scheduler就会调度了。而且估计这个窗口消息的处理,symbian也是采用Active Object来实现的。

我本来以为,只要我在主函数里面调用一个引发调度的函数就好了。我想当然地调用了sleep函数,然完全没有效果。还有后来改用User::After函数,也完全没有用。我后来想,论坛上经常有人提醒说,这个User::After函数是同步的。究竟有多同步,这我才知道了。就是说他也是不会引发AO调度的。这样一想也是可以理解的。因为他说的是非抢占嘛,就是说如果你睡眠了把CPU让给别人,等你到了该睡醒的时间,别人正在运行,那你就没有机会再把这个CPU抢占回来了。假如说做成双方都可以互相协作让CPU的,那基本上就算是协程了。这里的AO,如同网上一些讨论一样,应该算是“纤程”吧,可惜还没腾出时间去仔细研究他。本来想仔细去研究一下AO调度,究竟哪些函数(确切的说应该是系统调用)是可以引发AO调度的,但毕竟还是公司的任务优先吧,这个没有去细查,用了另外一个进程,乖乖写成窗口程序来做了。有机会的话以后再来改吧。

Symbian另外一个有意思的设定就是process崩溃之后不会立即退出。而是会继续保留一段时间,之后系统会把这些死进程统一回收。一开始还不是很理解他为什么要这么做,可是后来做进程监视的时候就发现,这个设定非常好用。你可以在进程崩溃之后,再去遍历系统的进程表,然后找到有问题的进程(他的退出码会是负数),然后找到具体的错误原因。其实这个工作就叫做“验尸”,而且symbian也提供了一些验尸工具去帮助调试。这样就给了系统调试很大的自由空间。否则的话,必须在一开始就确定监视哪些进程。假如有些进程,例如我现在在调试的这个(哔—),他是随着用户操作过程中而时而启动时而退出的,你很难去一直捕获着他,去监视他的运行情况。

还有一个就是本来对于symbian程序员来说是司空见惯的东西吧,就是这个清除栈CleanupStack,还有对象的所谓二次构造,以及symbian自有的一套类似于try catch的机制,是用的Leave和trap来实现。这些东西应该就是symbian的系统特点了吧,其他系统上是绝对见不到这些东西的。从异常安全的角度来说,这个二次构造真的是合理的设计吗?我觉得也不见得。不过他配合清除栈的使用,确实能够保证内存泄漏。这在内存紧缺同时开机时间甚至可能长达数年的嵌入式产品来说,的的确确是至关重要的。否则即使是非常微小的内存泄漏,假如连续开机数年(这里肯定不是手机啦,比如说是电冰箱咖啡壶什么的),还是会引起系统崩溃的。

最后就是监视到进程崩溃如何获取调用栈信息的问题。本来的想法甚至是去获取CPU寄存器的值,然后根据可以得到的线程列表中获取到的堆栈地址的信息,配合编译过程中生成的.exe.map,进行函数地址对比,从而得出调用信息。后来发现这种事情不用自己去做(而且在用户这个层次应该是很难获取到CPU寄存器的值的吧)。symbian提供了一个很好的内核调试工具,d_exc.exe,可以在系统中有任何进程崩溃时记录调用栈信息。需要的只是去把这个程序在测试开始之前运行起来就好了。至于symbian的内核端调试,nokia wiki上有一篇很好的文章,我还没找时间好好研究,这里也先记在这里kernel side debug

好啦今天的笔记就做到这里。并非是API介绍,假如说有人有兴趣想知道具体的内容的话,这里特别推荐的是nokia官方论坛的wiki,内容非常充实,对于我这样的初学者来说,非常有帮助。
例如这篇process monitor,还有这篇process and threads, how to find them等等。

手机的相关知识记录

虽然做了挺长时间手机开发了,对手机的最基本的功能完全没有了解。今天听了公司里面的一个讲座,讲STK的,顺便把SIM卡的功能之类的也一起做了research, 一起贴出来做个总结吧。网上很多资料是有关破解啦之类的邪恶内容,就不记录了,主要以了解大体的功能和原理为主。

手机的基础结构是一个叫做Mobile Equipment(ME)的芯片组和一片SIM卡构成的。它们两个合起来叫做Mobile station(MS)。智能手机处理复杂应用的那个部分,大多数情况使用了另外一片叫做Terminal Equipment(TE)的芯片,计算能力应该比ME来的要强一些。给我的感觉TE的构造就更加近似于PC的CPU了,此外多数智能手机还会有一片显卡芯片。

SIM卡里面也有一个简单的芯片,但是计算功能非常弱,它的功能仅是用于做一些加密计算(稍后提到)和一些简单的逻辑判断。因此基本上来说SIM卡就是一个纯粹的存储装置,没有任何计算和通信功能。打电话发短信上网等等功能,基本上都是ME配合无线设施完成的。

但是为何没有SIM卡就不能打电话发短信呢?这是因为SIM卡里面有一些与手机基站交互的必要信息。手机开机时,会对SIM卡做合法检测。SIM卡内有ICCID和IMSI两个明文信息和ki作为信令加密的密钥。ICCID包含了SIM卡的一些基本信息比如使用的服务类型,国家地区等等,而IMSI是SIM卡的全球唯一标识符。在手机运营商那里,这个标识符跟一个手机号码唯一对应。手机开机时,要对ICCID和IMSI做合法检查,确定SIM卡的合法性。同时将IMSI发送到基站,基站接收到该IMSI后,临时生成一个随机数RAND,发回手机,手机将该RAND传送给SIM卡,SIM卡使用存储在卡中的ki和硬连线在SIM卡中的加密算法,生成一长一短两个密文。分别使用的加密算法称作A3和A8。A3生成的短密文称作SRES,发送回基站,与基站生成的密文进行匹配确定是合法的SIM卡。A8生成的长密文当作接下来通信使用的加密密钥。通信加密将使用A5算法。注意到ki, A3, A8都是硬连线在SIM卡上的,并且运营商对这些信息保密,以防止SIM卡被破解和盗用。而A5算法是在ME上运行的,由于整个通信过程都要持续计算,这个算法必须非常简单,并且为了保证手机可以跨国漫游,这个算法其实是全球统一的。而A3, A8两个算法仅是定义了输入和输出的位数,没有要求加密的规范。各个运营商理论上可以采用各种不同的算法来保证安全性。但是事实上这些算法也有些通用的标准,导致某些SIM卡可以被破解。

由于运营商的惰性,导致事实上采取的A3A5A8算法都有通用算法。虽然这些算法被保密,但A3算法的一部分仍然泄漏,并在研究人员使用SIM卡读卡器,对比已知的明文和密文进行测试,推算出了未泄漏的其余部分。使用已知的算法,使用巧妙设计的输入并分析输出,就有可能破解任何SIM卡上的ki密钥。因此有些SIM卡一卡多号,或者例如iphone手机可以破解后加入到移动网络,就是利用这个漏洞。由于v1版本的A3算法被破解,国外后来又开发了v2版本的加密算法,目前没有泄漏,这种SIM卡就是无法破解的了。但是由于这种算法刚刚研发出来,技术上不稳定,因此国内运营商采取的办法是在v1算法上稍作修改,称作v0算法。此算法避开了原先v1算法被破解的某些漏洞,因此更难破解。但由于原理基本相同,实际上地下仍然有某些破解的办法。对于SIM卡破解的方法,此处不做任何讨论和记录。仅对SIM卡曾被破解的事实作为历史注记在此说明记录。

除此之外,手机启动入网时系统也需要对本机进行检查,手机上有一个设备码IMEI用于发至基站以检测该手机的合法性。

手机与基站通讯,首先手机定时与基站通信,使得基站确定手机的位置,判断手机应该属于哪个服务小区。这里涉及到一个很有趣的算法问题即如何生成Voronoi diagram,即beach-line算法。不在本文议题范围之内。

手机第一次与基站通讯,应该会得到基站的一个RAND并用ki计算出今后加密信道使用的密钥。此外基站会临时生成一个TMSI发送给手机并存储在SIM卡中。此后就不再传输IMSI而仅传输TMSI,避免其他组织个人监听信道频道获取个人信息。TMSI会定时更换以保证安全性(当然这些加密措施仅能阻止第三方的监听,很显然运营商当然能获取所有的通信资料以及手机所在的服务小区,因此有些运营商也提供API允许其他第三方获得这些数据,当然了,这就是出卖你的隐私吧,用手机是没有隐私可言的)。

ME与SIM卡通信采用规定的编码格式,除可从SIM卡中获取相关数据之外,还可通过SIM Application Tool Kit(STK)做其他的应用。比如动感地带SIM卡上自带的动感地带的一些服务,就是通过STK完成的。GSM 2G中的STK协议在GSM 11.14中定义。3G中STK被USIM Application Toolkit(USAT)取代。

由于负责计算和消息传输的都是ME,SIM卡本身存储的都是出场时预先写好的一些目录项和每个目录项对应的功能:例如深入一个子目录,或者接受一个输入框,或者弹出一段文字等。这些功能中如果有对应与基站通讯的,ME就将相应的消息发送的基站,并接受响应。这些相应在智能手机上,可通过TE来显示,可通过各种UI来展示。

好吧,能做的记录就是这些了。接下来还需要研究的内容有单片机的工作原理,ME的结构,手机与基站通信的物理机制,手机GPRS上网的机制,以及GMS,TDMA,CDMA,WCDMA这些网络的异同等问题。以后再慢慢查找资料慢慢做笔记吧。

下面列出引用的网址:wiki的链接已经在文中给出于是不再重列
http://www.moon-soft.com/program/bbs/readelite125680.htm
http://www.52rd.com/Blog/Detail_RD.Blog_xuanny_10418.html
http://ido.thethirdmedia.com/article/frame.aspx?…
http://www.mscbsc.com/27829/viewspace-7105.html