要实现电话拦截,肯定要先对电话的状态进行监听,当有来电时,检测该来电是否需要进行拦截。
监听
首先,是对电话的状态进行监听,有两种方式:
- 实现广播接收者,监听
android.intent.action.PHONE_STATE和android.intent.action.NEW_OUTGOING_CALL这两个 Action - 给
TelephonyManager设置PhoneStateListener监听,重写PhoneStateListener.onCallStateChanged(int state, String incomingNumber)方法。
通过接收广播的方式
经过我在模拟器中拨打了无数次电话,终于搞清楚 PHONE_STATE 和 NEW_OUTGOING_CALL 这两个 Action 分别是在什么时候触发:
外拨情况(有 2 种状态):
1’ 外拨电话时,会收到两次广播(onReceive()被执行2次):先接收NEW_OUTGOING_CALL,再接收PHONE_STATE。
x’(对方接通时,本机没有任何状态的改变。)
2’ 任何一方挂断时,接收到PHONE_STATE。来电情况(有 3 种状态):
1’ 响铃时,接收到PHONE_STATE。
2’ 通话时,接收到PHONE_STATE。(不接听 or 对方直接挂断时,没有这一步)
3’ 任何一方挂断时,接收到PHONE_STATE。
对于这种状态的变化,可以想象成有一台固定电话(括号中数字对应上面的状态):
当要拨打电话时,拿起话筒(外拨1),不管对方接不接电话,本机都是“拿起话筒”的状态(外拨x,无变化),当任何一方挂断电话(外拨2),状态才发生改变。
来电时,响铃(来电1)是一种状态,本机接通电话,拿起话筒(来电2),通话结束后放下话筒(来电3)或者对方直接挂断(来电3),这些情况下状态就会发生变化。
结论:当 Action 为 PHONE_STATE 时,只能监听状态发生变化,但却不知道当前是哪个状态,究竟是从哪个状态变化到哪个状态?根本没办法知道!
可以获取当前状态,使用 intent.getStringExtra(TelephonyManager.EXTRA_STATE) 即可,状态为:TelephonyManager.EXTRA_STATE_IDLE(值为"IDLE")TelephonyManager.EXTRA_STATE_RINGING(值为"RINGING")TelephonyManager.EXTRA_STATE_OFFHOOK(值为"OFFHOOK")
其中之一。
|
|
需要权限:
通过设置监听的方式
设置监听的方法相比接收广播有个好处,就是监听是优先于广播的(可以分别在onReceive() 和 onCallStateChanged() 中输出 Log 验证)
设置监听很简单,先获取 TelephonyManager 对象,然后调用 listen(PhoneStateListener listener, int events) 方法:
第一个参数是 PhoneStateListener 对象,需要继承该类重写 onCallStateChanged() 方法,如果需要监听多个事件,还要重写其他方法;
第二个参数是需要监听的事件,如果需要监听多个事件,使用 | “或”一下就行。
监听的状态有三种,分别是TelephonyManager.CALL_STATE_IDLE(值为0,空闲状态,挂断电话时的状态)TelephonyManager.CALL_STATE_RINGING(值为1,响铃状态,有来电时的状态)TelephonyManager.CALL_STATE_OFFHOOK(值为2,通话状态,拨打电话、接听电话时的状态)
onCallStateChanged(int state, String incomingNumber) 方法有 2 个参数,state 表示当前的状态,incomingNumber 表示来电的号码(监听的方式无法获取外拨电话的号码)
incomingNumber 不是任何时候都有值的,经过我在模拟器中拨打了无数次的电话,发现一下规律(状态跟 PHONE_STATE 广播是一致的):
外拨:2种状态都没有incomingNumber(这不是废话么!)
来电(看图):
|
|
需要注意的地方
从上面的代码可以看到,在 onDestroy 中又给 TelephonyManager 设置了一次 listen,监听的事件是 LISTEN_NONE。
对系统服务设置监听,在不需要的时候,一定要注销掉,否则会一直占用系统资源。既然 TelephonyManager 也是一个系统服务,又可以对其设置监听,那应该也是有提供注销的方式。而注销 TelephonyManager 监听的方式,就是这个 LISTEN_NONE。
这个 LISTEN_NONE 就是一个用于取消监听的事件,官方文档描述如下:
To unregister a listener, pass the listener object and set the events argument to LISTEN_NONE (0).
在做此功能之前,Google 了一下,发现网上大多数文章,发现了两点严重的问题:
- 都说什么不是外拨就是来电(估计很多都是相互抄袭的),其实上面也说了,外拨是会发送两种广播的,所以当接收到
PHONE_STATE时,它可能是一个外拨电话的事件。 - 都是采取了在
onReceive()中设置监听的方式,这种方式非常恶心,下面是我的测试代码:
|
|
测试结果:
看看测试结果:
比较长的那行,每次都状态改变,都执行一次。但是!onCallStateChanged 每次都会多执行一次。
然后再看看每个对象的情况:TelephonyManager: 输出了4次,每次输出的 hashCode 都是 b3faecc0,说明每次都是同一个对象BroadcastReceiver: 输出了4次,每次输出的 hashCode 都不一样,说明每次都是一个新的对象,一共有4个PhoneStateListener: 输出了10次,仔细观察每一次输出的 hashCode,发现每次都会有一个新的对象加入到输出的队伍,一共有4个
解释一下:TelephonyManager: 系统服务在每个进程中都是单例的,所以每次 getSystemService 得到的都是同一个 TelephonyManager。BroadcastReceiver: 每次接收广播时,都是一个新的对象。PhoneStateListener: 就更不用说了,每次都 new 一个新的,然后投入到 TelephonyManager 的温暖的怀抱,老大 TelephonyManager 日益壮大起来。
还有些文章,将 PhoneStateListener 作为 BroadcastReceiver 的成员变量,但是,这有区别么??结果却还是一样的,因为每次接受到的广播,都是一个新的 BroadcastReceiver 对象。
最终解决方案
然而,这两种方式都是有局限性的。广播执行次数太多了,而且无法判断究竟是什么状态。服务需要开启,还有被 kill 的可能性。
终上所述,决定两种方式相结合。
由于 PHONE_STATE 不管是外拨还是来电,都会被接收多次。所以当 Action 是 PHONE_STATE 时,相对来说比较适合开启拦截服务(多次startService()并无影响,只回调onStartCommand())。
在来电时开启服务,服务中实现了监听,一直在后台运行。除了第一次需要创建服务,会比服务会较晚执行,可能会听到一瞬间的铃声响起之外,之后只要服务没有被 kill,服务中的监听肯定是比广播先执行,所以只要被拦截,就不会再听到来电的铃声了。
代码基本上都在上面给出来了,就不再重复了。
拦截
ITelephony 接口
要实现拦截,在 ITelephony 接口中有一个 endCall() 方法,但是很遗憾,很久很久以前(不知道哪个版本,听说是 Android 1.5)开始,这个方法就不能直接调用啦。
不过!这个方法一直都存在的,至于为什么不能直接调用,那是因为 Google 出于安全考虑,给隐藏起来啦。
但是!在Java中没什么是不可能滴。因为我们有终极武器——反射!只要你存在,不管你在哪,都可以把你挖出来!!哈哈哈!!!
首先,要知道 ITelephony 这个接口在哪里。呃。。这个在SDK中是没有的啦,要到 Android 的源码库去找(庆幸 Android 是开源的)。ITelephony 是一个 AIDL 文件,在 这里(Google源码) 或者 这里(Github镜像) 可以得到。
如果使用 Android Studio,在项目中的 main 文件夹里新建一个 aidl 文件夹,然后再在 aidl 文件夹中,按照包名 com.android.internal.telephony 依次创建文件夹,再把 ITelephony.aidl 文件放进去。(其实直接在 AS 中使用图形化界面创建会更快更方便,先创建 aidl 文件夹,再创建包,再复制文件)
如果使用 Eclipse,好像是不用新建 aidl 文件夹的,直接在 src 文件夹下创建包的路径,方法同 AS。
获取 ITelephony 对象
由于 endCall() 方法是在 ITelephony 接口中的,要调用此方法,就必须先得到实现 ITelephony 接口的对象。
获取的方法(过程)有很多,但是归根到底都是围绕着一个方法,就是 TelephonyManager 中有一个 getITelephony() 的方法,通过这个方法可以得到一个 ITelephony 的对象。但是,这是一个隐藏的、private 的方法,外部无法直接调用。
|
|
我们可以通过反射去调用这个 getITelephony() 方法,也可以通过直接实现方法体内的业务逻辑。
直接通过反射调用 getITelephony()
推荐
直接实现业务逻辑
不太推荐
参考: How to import com.android.internal.telephony.ITelephony to the Android application
要拦截电话,还需要一下权限:
裁剪 ITelephony.aidl
ITelephony.aidl 中提供了很多很多方法,而方法越多,需要 import 的类也就越多,目前最新的 ITelephony.aidl (2015年9月10日更新)有 1000 多行,还 import 了多个其它接口(都是隐藏的),这些接口都要加到 aidl 文件夹中,难免给编译带来了不少麻烦,其实这些都是可以删掉的。
如果只用到 endCall() 方法,可以删掉其他,只剩下一个 endCall() 的,这完全是没问题的。
有些文章中说,除了 ITelephony.aidl ,还要添加一个 NeighboringCellInfo.aidl,如果使用的是高版本(没研究,也许是 API Level 19 以上,我现在用的是 23 的)的 SDK,是不需要的,因为这个 API 已经不再是隐藏的了。
如果裁剪过的话,只需要一个 ITelephony.aidl 就行,其他什么都不需要。
P.S. 别忘了 import 也要删掉,要不然 import 找不到类,还是会报错的。
|
|