利用RIL代理接口实现短信的操作之短信的接收
作者:吴春雷
QQ:819543772
Email:wuchunlei@163.com
提到在Windows Mobile客户端对短信进行操作,几乎所有人都会在第一时间想起CEMAPI接口,诚然cemapi接口是目前为止使用最多,也是最为成熟的技术,利用Cemapi接口可以很方便的实现短信的发送、接收、删除等相关操作,而无需关注繁琐的编码解码问题,但Cemapi也有自己的缺点,比如cemapi中接口完全基于tmail实现,短信截获、发送、到达通知等操作最终也是由tmail来实现的,这就意味着,程序中短信操作的模块需要完全依赖于tmail的执行状态,例如,当tmail程序没有启动时,客户端执行submitmessage操作后短信并不会被马上发出,必须要等到tmail启动后,短信才会被发送,由于该接口的返回值仅仅指明与短信数据库进行操作是否成功,因此在程序中无法获知短信是否已经成功发送。
那么,除了Cemapi接口以外,还有没办法在更加底层的位置对短信进行操作呢?答案是直接访问RIL层。但是很不幸,RIL层的驱动程序每个厂家的实现都有可能不同,并且作为操作系统的核心程序,被烧到了ROM中,由于不知道手机厂家具体是怎么实现的RIL层驱动,因此重新编译了rilgsm.dll模块替换原有模块的是很难实现的。但是,微软提供了标准的RIL层的代理接口,通过一系列的代理接口可以使客户端程序在有限的条件下对RIL层进行控制(不过这部分源代码微软没有公开,所以说是有条件的对RIL层进行控制)。
本文利用RIL代理接口实现了对到达短信的接收,并将到达的短信内容通知给客户端程序,由于RIL层所接收到的数据会同时通知给所有注册过RIL代理接口的程序,因此,即便系统已经有实现IMailRuleClient接口的拦截程序,你的程序仍然可以通过RIL代理接口接收到该短信。
RIL层代理使用非常简单,通过RIL_Initialize函数进行初始化,然后就可以通过初始化时设置的回调函数得到RIL端口获取到的数据了。在初始化的时候,有两个回调函数需要被设置,其一是数据回调函数,当RIL端口接收到数据后,会通过该回调将数据通知给客户端程序,其二是结果回调函数,当客户端像RIL端口请求某些数据和操作后,RIL代理会通过该回调将处理和请求结果通知给客户端,个人感觉,这种数据和结果分别用回调函数通知的技术还是很值得学习的。
RIL_Initialize函数的原型如下:
HRESULT RIL_Initialize(DWORD dwIndex,
RILRESULTCALLBACK pfnResult,
RILNOTIFYCALLBACK pfnNotify,
DWORD dwNotificationClasses,
DWORD dwParam,
HRIL* lphRil);
函数返回S_OK表示初始化成功,否则失败。参数意义如下:
dwIndex:RIL端口号,例如,如果端口为RIL1: 则dwIndex=1
pfnResult:请求和处理结果的回调
pfnNotify:数据到达通知的回调
dwNotifycationClasses:用于确定将那些类型的通知发送给本客户端
dwParam: 某种约定的标志,一般为55AA55AA,搞低层的朋友应该了解
RILRESULTCALLBACK回调定义如下:
void CALLBACK ResultCallback(DWORD dwCode, HRESULT hrCmdID, const void *lpData, DWORD cbData, DWORD dwParam)
参数的定义:
dwCode:结果代码
hrCmdID:产生该回调的命令的ID
lpData:结果数据的字符串
dwParam:与初始化时的dwParam参数相同,一般为55AA55AA
RILNOTIFYCALLBACK定义如下:
typedef void (CALLBACK *RILNOTIFYCALLBACK)(
DWORD dwCode,
const void* lpData,
DWORD cbData,
DWORD dwParam
);
参数定义为:
dwCode:通知的代码
lpData:到达的数据
cbData:lpData指向的结构体对象的size
dwParam:55AA55AA
注册成功后,当有数据到达RIL端口的时候,RILNOTIFYCALLBACK回调就会被触发,参数dwCode指示了当前通知的类型,lpData中的内容到为RIL端口接收到的原始数据。对GSM编码有了解的朋友应该知道,到达RIL端口的数据是不能直接被阅读的,因为这些数据在发送时被编了码。常见的编码方式有两种,第一种被称作7位码,原理是将8位的ASCII码中没有用到的最高位去掉,然后按照某种规则将8位ASCII码的字符串流转换为7位ASCII码的字符串流,这样对于每个字符能够节省1个bit的空间,对于GSM这种大数据量(指服务器端)的交换有着重要的意义,但是这种编码方式的缺点显而易见,那就是无法编码中文,于是出现了第二种编码方式,也就双字节编码,这种方式把字符流编码成unicode格式,其优点是编解码简单,缺点也很明显,任何一个非中文字符也被编码为2个字节,相同长度的数据将增加一倍的Size.一般来讲手机会对这两种编码方式同时支持,其原则是,只要短信中存在中文字符,哪怕只有一个,该短信的编码方式就是双字节编码,否则编码方式就采用7位码的方式。
简单的介绍一下两种编码方式的算法。
一.双字节编码(UCS2):
这种编码方式比较简单,编码时,我们先把要发送的字符串转换成unicode格式的数据流,然后,将数据流中偶数字节和奇数字节所对应的数据相交换即可,也就是说,将每个unicode码的字符前后两个字节交换。解码时,只需再对数据流做一次奇偶字节的交换,就是unicode码的字符串。
例如:
源字符串:我是一个兵
Unicode:11 62 2f 66 0 4e 2a 4e 75 51
双字节编码:62 11 66 2f 4e 00 4e 2a 51 75
二.7位码
这种编码方式略相对与双字节编码稍微复杂些。下面分别看一下7位码的编解码算法。
编码:将字符串转换成2进制流,每8位分为一组,去掉每组的最高位bit8。将第二组的最低位bit0移动到第一组的bit8位上形成一个字节,然后将第二组左移2位,将第三组的最低两位bit1,bit0移动到第二组的最高两位上,将第三组左移3位,将第四组的低三位bit2,bit1,bit0移动到第三组的高三位上,bit8,bit7,bit6,以此类推。最后一组如果不够8位,则在高位补零。
例如:1234
二进制流:00110001 00110010 00110011 00110100
编码后:00110001 11011001 10001100 00000110
解码:与编码的过程相反。将待解码的数据转换为二进制流,每8位编为一组。取第一组的最高为bit8,并将最高位置0。取第二组的高两位bit8,bit7,并将高两位置0,左移1位,将从第一组取出的最高位bit8放置在第二组末尾,在第二组的最高位补0凑齐一个字节(8位)。取第三组的高三位bit8,bit7,bit6,将高三位置0,左移2位,将第二组的高两位bit8,bit7放置在第三组的末尾,在第三组最高位补0凑齐一个字节(8位),以此类推。
例如:
接收到: 00110001 11011001 10001100 00000110
解码后: 00110001 00110010 00110011 00110100
本文中只需要对接收到的数据进行解码,因此这里只给出两种编码方式中解码的函数,有关编码的部分,有兴趣的朋友可以自己编写。
//将接受到的双字节码转换为Unicode
void ConvertDataToUnicode(BYTE *psData,int nLength,wchar_t *ppszRet)
{
BYTE btTemp=0;
int i=0;
for(i=0;i<nLength;i++) //将前后两位交换,组成UNICODE格式的BYTE流
{
btTemp=psData[i*2];
psData[i*2]=psData[i*2+1];
psData[i*2+1]=btTemp;
}
ppszRet=(wchar_t*)psData;
}
//将接收到的7位码转换为ASCII
void Convert7BitsToASCII(BYTE *psData, int nLength,char **ppsRet)
{
int m=1; //取字符前几位
int n=0; //左移多少位
BYTE btPrev=0; //用于保存从左侧取出的数据
BYTE bt=0;
BYTE *p=psData;
//计算还原后的长度,每位原始数据还原为位数据
int nBC=nLength/7;
*ppsRet=new char[nBC+nLength+1]; //分配空间
memset(*ppsRet,0x00,nBC+nLength+1);
int nCount=0;
int i=0;
while(i<nLength-1)
{
if(m==8)
{
(*ppsRet)[nCount++]=(*(p-1))>>1;
m=1;n=0;
//p++;
}
else
{
//当前字符将前n+1位置零然后左移n位
//将上一个字符的前m位放置到上一步结果的后m位上
int n1=0xff>>(n+1);
int n2=*(p+1)&n1;
int n3=n2<<n;
if(m==1) //每轮次的第一个字符
{
(*ppsRet)[nCount++]=*p & 0x7F;
}
else
{
bt=(*(p-1))>>(8-m+1);
(*ppsRet)[nCount++]=bt | ((*p & (0xFF>>(n+1)))<<n);
}
m++;
n++;
p++;
i++;
}
}
(*ppsRet)[nCount]='\0';
return ;
}
有了上面的内容做基础,我们就可以利用RILNOTIFYCALLBACK回调中的数据来获取的到达手机的短信了。当有短消息到达的时候,通知回调函数中的dwCode值与RIL_NCLASS_ALL做”与”操作如果为RIL_NCLASS_MESSAGE,则到达的数据将与短信相关。然后通过dwCode的值,可以判断当前接收到的消息的类型。当dwCode为RIL_NOTIFY_MESSAGE的时候表示接收到的消息为短信,这时lpData指向一个结构体RILMESSAGE,该结构体封装从RIL接收到的数据和数据的属性,RILMESSAGE由若干个结构体组成,本文只关注msgInDeliver,该结构体中封装了接收到的短消息数据,其中raOrigAddress中可以获取到来源电话,rmdDataCoding成员中的dwAlphabet字段可以用来判断当前数据的编码格式,dwAlphabet=3时为双字节编码(UCS2),dwAlphabet=1的时候为7位码。rgbMsg中保存了接收到的原始数据,ccbMsgLength为原始数据长度。将rgbMsg作为参数传递给前面给出的解码函数中,即可获得短消息正文。RILNOTIFYCALLBACK函数的源代码如下:
void CALLBACK Notify (DWORD dwCode, const void *lpData,
DWORD cbData, DWORD dwParam)
{
if((dwCode & RIL_NCLASS_ALL)== RIL_NCLASS_MESSAGE) {
{
if(dwCode==RIL_NOTIFY_MESSAGE)
{
RILMESSAGE *prm = (RILMESSAGE *)lpData;
if(prm->dwType==RIL_MSGTYPE_IN_DELIVER)
{
char *psRet=NULL;
memset(psz,0x00,sizeof(wchar_t)*prmMsg->msgInDeliver.cchMsgLength);
Convert7BitsToASCII(prmMsg->msgInDeliver.rgbMsg, prmMsg->msgInDeliver.cchMsgLength,&psRet);
}
}
}