最近项目遇到短信拦截的问题,新短信到达以后,项目app希望可以提示用户未读短信的数据,并且可以将短信置为已读。
这个可能在不同系统中都遇到了问题,并且和其他短信访问的app,例如微信电话本发生冲突,下面内容主要针对Android4.4以下系统,和部分被修改的操作系统,例如小米
首先就说和微信电话本冲突的情况,由于微信电话本也要实现上述功能,可是它有一个坑就是,它收到短信以后,就将短信置为已读(完全搞不明白为什么要这样做啊喂!)
由于它在系统中的优先级比较高,所以当我的App读取短信的时候,就没有未读短信了!
下面就这个内容,来说一下短信接收优先级的问题。
众所周知,监听短信有两种方式
1,是注册一个BroastReceiver,监听系统的短信到底的广播
public class SmsReceiver extends BroadcastReceiver{
public SmsReceiver() {
Log.i("cky", "new SmsReceiver");
}
@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
Log.i("cky", "接收到广播");
}
}
2,是使用ContentObserver来监听系统短信数据库的变化
public class SmsObserver extends ContentObserver{
public SmsObserver(Context context, Handler handler) {
super(handler);
mContext = context;
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
Log.i("cky","短信数据库变化了");
}
}
那么问题来了,如果我两个方法都使用了,到底谁会先接收到短信到达的通知呢?
经过测试,虽然没有真正去看源码,发现应该是这样的:
1,当短信到底,首先BroastReceiver会接收到通知,此时该新短信,会被系统记录进系统的短信数据库
如果我们在BroastReceiver里面查询短信数据库,有可能查询不到新短信,因为数据库写入需要一定时间
经过测试,如果onReceive()方法里面,我暂停1秒以后再去查短信数据库,才能查到新短信的信息
2,当系统数据库被写入后,马上会通知ContentObserver,调用其onChange()方法
由于我的app是使用ContentObserver去探测短信的,所以当然要比微信电话本慢,测试结果是,
在4.4系统以下,和4.4的小米系统,当我的onChange()方法去查询数据库的时候,果然查询到新短信记录,但是很快就被微信电话本清除了。。
在google原生6.0系统,onChange()方法查询数据库时,没有查询到新短信,说明在这之前,记录就被微信电话本清除了。。。
那么解决办法是什么呢?
我们要想办法抢在微信电话本前面收到短信,然后把短信广播给拦截了(短信广播是有序广播,在4.4以下系统,是可以阻止其传播的)
但是问题是我的app不是短信应用啊,我这样做的话,用户就收不到短信了,体验太差。
而且问题来了,就是我怎么样才能抢在微信电话本前面呢,我们能做的事情,微信电话本都能做。
所以结局悲剧了(>﹏<),这个bug没有办法修改!
不过我还有要说一下短信拦截的机制,让我们的app第一个接收到短信到底的消息
首先我们要注册BroastReceiver,根据源码,广播接收优先级有以下规律:
静态广播接收器 由PackageManagerService负责,当手机启动时(或者新安装了应用),PackageManagerService负责扫描手机中所有已安装的APP应用(题外话,确定不再使用的APP需要卸载了),将AndroidManifest.xml中 有关注册广播的信息 解析出来,存储至一个全局静态变量当中mReceivers。
需要注意的是:
1 PackageManagerService扫描目录的顺序如下:
system/framework
system/app
vendor/app
data/app
drm/app-private
2 当处于同一目录下时:按照file.list()的返回顺序。(题外话:因为在data/app下的应用都是用户安装的,并且都是以com.xxx.xxx-1.apk 的形式出现,所以如果打算做手机管家之类的应用,需要好好的研究下包名,争取在file.list()的独木桥下抢的头筹---优先接收开机启动完成的广播。)
3 在此处并未对 接收顺序做完整的排序。(注意修饰词完整的,毕竟先扫描的当然会有一定优先级)
动态广播接收器 由ActivityManagerService负责,当APP的服务或者进程起来之后,执行了注册广播接收的代码逻辑,即进行加载,最后会存储在一个全局静态变量
mReceiverResolver中。
需要注意的是:
1 这个并非是一成不变的,当程序被杀死之后, 已注册的动态广播接收器也会被移出mReceiverResolver,直到下次程序启动,再进行动态广播的注册,当然这里面的顺序也已经变更了一次。
2 这里也并没完整的进行广播的排序,只记录的注册的先后顺序,并未有结合优先级的处理。
当有广播发出时,接收顺序如下:
在ActivityManagerService处理广播,当广播为有序广播时,将动态广播接收器和动态广播接收器合并起来,形成最终的有序广播接收顺序。
上述的规则1排序为:
1 优先级高的先接收
2 同优先级的动静态广播接收器,动态优先于静态
3 同优先级的动态广播接收器 或者同优先级的静态广播接收器,按照图1 的流程注册顺序。
即静态:先扫描的大于后扫描的,动态:先注册的大于后注册的。
当广播为普通广播时,规则2排序为:
1 无视优先级,动态广播接收器优先于静态广播接收器
2 同规则1排序的第3点
接下来请看代码以片段:
private final int broadcastIntentLocked(ProcessRecord callerApp,
String callerPackage, Intent intent, String resolvedType,
IIntentReceiver resultTo, int resultCode, String resultData,
Bundle map, String requiredPermission,
boolean ordered, boolean sticky, int callingPid, int callingUid) {
…………
…………
// 静态广播接收器list
List receivers = null;
// 动态广播接收器List
List<BroadcastFilter> registeredReceivers = null;
// 获取静态广播接收器mReceivers
try {
if (intent.getComponent() != null) {
// Broadcast is going to one specific receiver class...
ActivityInfo ai = AppGlobals.getPackageManager().
getReceiverInfo(intent.getComponent(), STOCK_PM_FLAGS);
if (ai != null) {
receivers = new ArrayList();
ResolveInfo ri = new ResolveInfo();
ri.activityInfo = ai;
receivers.add(ri);
}
} else {
// Need to resolve the intent to interested receivers...
if ((intent.getFlags()&Intent.FLAG_RECEIVER_REGISTERED_ONLY)
== 0) {
receivers =
AppGlobals.getPackageManager().queryIntentReceivers(
intent, resolvedType, STOCK_PM_FLAGS);
}
// 获取动态广播接收器mReceiverResolver
registeredReceivers = mReceiverResolver.queryIntent(intent, resolvedType, false);
}
} catch (RemoteException ex) {
// pm is in same process, this will never happen.
}
final boolean replacePending =
(intent.getFlags()&Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
……
// 如果接收到的广播 是普通广播。
if (!ordered && NR > 0) {
// If we are not serializing this broadcast, then send the
// registered receivers separately so they don't wait for the
// components to be launched.
BroadcastRecord r = new BroadcastRecord(intent, callerApp,
callerPackage, callingPid, callingUid, requiredPermission,
registeredReceivers, resultTo, resultCode, resultData, map,
ordered, sticky, false);
// 很明显接收到普通广播之后,在这只处理了动态广播 registeredReceivers,对于普通广播而言,动态广播接收器要优先于静态广播接收器 无关设置的优先级
boolean replaced = false;
if (replacePending) {
for (int i=mParallelBroadcasts.size()-1; i>=0; i--) {
if (intent.filterEquals(mParallelBroadcasts.get(i).intent)) {
if (DEBUG_BROADCAST) Slog.v(TAG,
"***** DROPPING PARALLEL: " + intent);
mParallelBroadcasts.set(i, r);
replaced = true;
break;
}
}
}
if (!replaced) {
mParallelBroadcasts.add(r);
scheduleBroadcastsLocked();
}
//将registeredReceivers置为null,后面只处理静态广播接收器,所以不会有冲突。
registeredReceivers = null;
NR = 0;
}
//如果是有序广播,将静态广播接收器和动态广播接收器组合成一个最终的顺序
int ir = 0;
if (receivers != null) {
...
//合并的过程,注意顺序
int NT = receivers != null ? receivers.size() : 0;
int it = 0;
ResolveInfo curt = null;
BroadcastFilter curr = null;
while (it < NT && ir < NR) {
if (curt == null) {
curt = (ResolveInfo)receivers.get(it);
}
if (curr == null) {
curr = registeredReceivers.get(ir);
}
//如果动态广播接收器优先级高于或者等于静态广播接收器,那么就插到前面
//很明显动态的要在静态的前面
if (curr.getPriority() >= curt.priority) {
// Insert this broadcast record into the final list.
receivers.add(it, curr);
ir++;
curr = null;
it++;
NT++;
} else {
// Skip to the next ResolveInfo in the final list.
it++;
curt = null;
}
}
}
OK,了解规律以后,我们来考虑具体怎么实现:
首先我们要动态注册广播,那么我们怎么样才能在开机之后,马上就动态注册呢?
显然开机广播只能使用静态注册的方式来监听,所以我注册一个广播用于监听开机广播,然后在该onReceive()方法中,创建一个service,再在这个service重新动态注册一个Receiver,优先级为2147483647
这里注意两个问题,1是保证我们的应用首先接收到开机广播,根据上述原则,只能让我们的应用最先安装(这个当然没有办法保证),并且文件包排在最前面(这个要修改包名,所以我们做这类应用的时候,也要注意包名)
2是保证我们的service长期存活,因为一旦service死掉,会造成动态注册的广播也死掉
源码下载地址:
接收广播的最高优先级
根据上面的方法,原则上我们就能保证我们的app是第一个接收到短信广播的,当然这是在4.4系统以下,如果是4.4系统,请看下一篇文章!
Android短信拦截机制适配的坑(下)--4.4以上系统,主要是6.0