安卓代替系统默认电话应用 (Android 6.0+) 与电话状态监听

Procrastinator's Brain

Android 6.0 之后开始支持修改默认电话应用,刚好最近有个相关的需求,于是记录下自己探索之旅。

00 Android Telecom framework

从 API 21 开始,谷歌添加了 TelecomManager 用于提供对电话通讯状态的监听,而 API 23 之后,又开放了 Telecom framework,这个框架允许第三方应用开发者编写应用来替换系统默认电话应用,而其中大部分接口就添加在 android.telecom 包下。

Telecom framework 其实提供了两个方面的 API,一个是 ConnectionService 用于实现通讯(比如通过电信服务商提供的电话连接服务),另一个就是我们这次的需要用到的 InCallService,它主要负责提供 UI 来管理电话。一般系统自带的电话应用同样也是实现这套 API 来提供电话通信的交互界面的。

01 替代系统默认电话应用

整个实现过程其实分为两步,首先是新增一个 Service 继承 InCallService 并实现其中你感兴趣的方法,然后再添加一个 Activity 用于提供用户界面。

首先我们来看下 Service 部分。

实现 InCallService

继承 InCallService 后我们需要实现两个方法 onCallAddedonCallRemoved,分别代表电话进来与断开时会被调用,一般我们会在 onCallAdded 中注册电话状态监听,并在 onCallRemoved 中解除监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class PhoneCallService extends InCallService {

private Call.Callback callback = new Call.Callback() {
@Override
public void onStateChanged(Call call, int state) {
super.onStateChanged(call, state);
switch (state) {
case Call.STATE_ACTIVE: {
break; // 通话中
}
case Call.STATE_DISCONNECTED: {
break; // 通话结束
}
}
}
};

@Override
public void onCallAdded(Call call) {
super.onCallAdded(call);

call.registerCallback(callback);
}

@Override
public void onCallRemoved(Call call) {
super.onCallRemoved(call);

call.unregisterCallback(callback);
}
}

当然,对于通话 Service 在 menifest 中注册的方法肯定也和普通的 service 有点区别:

1
2
3
4
5
6
7
8
9
10
<service
android:name=".PhoneCallService"
android:permission="android.permission.BIND_INCALL_SERVICE">
<meta-data
android:name="android.telecom.IN_CALL_SERVICE_UI"
android:value="true" />
<intent-filter>
<action android:name="android.telecom.InCallService" />
</intent-filter>
</service>

代替系统默认通话应用并不需要添加特殊的权限,但是要在你实现的 InCallService 上声明 android.permission.BIND_INCALL_SERVICE 权限,另外还要加上 <meta-data> 用于表明我们的应用提供了接听电话的 UI,android.telecom.InCallService 的 <intent-filter> 当然就是用于在电话拨出或打进的时候,让系统发送的广播能够直接启动我们的电话服务。

实现电话接听的 UI

这部分比较简单,只要创建一个 Activity 然后在 menifest 中注册就可以了,不过注册时至少需要添加这两个 <intent-filter>:

1
2
3
4
5
6
7
8
9
<!-- provides ongoing call UI -->
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent-filter>
<!-- provides dial UI -->
<intent-filter>
<action android:name="android.intent.action.DIAL" />
</intent-filter>

看起来好像有点奇怪,因为好像这两个 <intent-filter> 几乎一样,但其实第一个 <intent-filter> 是用来提供打电话UI的 ,而第二个用于提供拨号功能,至于为什么要分开呢?可以参考 DefaultDialerManager,这是安卓源码里的一个隐藏的类。方法 getInstalledDialerApplications() 上的注释写的很清楚,想要让系统检测到可供拨号的应用就必须至少要添加这两个 <intent-filter>。

管理电话的接打

接下来我们需要定义一个接打电话的管理类 PhoneCallManager,用于给 activity 提供一些电话相关的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PhoneCallManager {

public static Call call;

/**
* 接听电话
*/
public void answer() {
if (call != null) {
call.answer(VideoProfile.STATE_AUDIO_ONLY);
}
}

/**
* 断开电话,包括来电时的拒接以及接听后的挂断
*/
public void disconnect() {
if (call != null) {
call.disconnect();
}
}
}

可以看到,PhoneCallManger 包含接听和断开电话的方法调用,我们要做的只是在合适的地方将 call 对象传进去,最合适的地方当然是在 InCallService 里添加与移除 Call 的时候了。

另外,在添加电话的时候,我们要记得启动我们作为电话界面的 activity 去提供接打的操作,这里我们可以根据 call 的 state 来判断电话是打进的还是拨出的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
public void onCallAdded(Call call) {
super.onCallAdded(call);

call.registerCallback(callback);
PhoneCallManager.call = call; // 传入call

CallType callType = null;

if (call.getState() == Call.STATE_RINGING) {
callType = CallType.CALL_IN;
} else if (call.getState() == Call.STATE_CONNECTING) {
callType = CallType.CALL_OUT;
}

if (callType != null) {
Call.Details details = call.getDetails();
String phoneNumber = details.getHandle().toString().substring(4)
.replaceAll("%20", ""); // 去除拨出电话中的空格
PhoneCallActivity.actionStart(this, phoneNumber, callType);
}
}

@Override
public void onCallRemoved(Call call) {
super.onCallRemoved(call);

call.unregisterCallback(callback);
PhoneCallManager.call = null;
}

还记得之前的我们创建的 call 的回调吗,我们监听了电话状态的改变,所以可以在电话断开的时候退出电话界面:

1
2
3
4
5
case Call.STATE_DISCONNECTED: {
// 通话结束
ActivityStack.getInstance().finishActivity(PhoneCallActivity.class);
break;
}

至于 UI 界面如何实现,取决于你的需求了,这里我只是提供了接听和挂断的按钮用于演示,来看下效果图:

In Call Demo

最后,既然要替换系统默认电话应用,当然要提醒用户将我们的应用设置为默认应用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Android M 以上的系统则发起将本应用设为默认电话应用的请求
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent intent = new Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER);
intent.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME,
getPackageName());
startActivity(intent);
}
}
}

这段代码会在我们的 Actiivty 中启动这样一个 Dialog,如果用户点击确定后我们的应用就变成了默认电话应用啦╭(๑•̀ㅂ•́)و✧

请求将App设置为默认应用

02 电话状态监听

以上实现的功能只能在 Android M 以上的系统才有效,那么 Android M 以下怎么办呢?其实我们可以通过监听电话状态然后做一些操作。

之前说过从 API 21 开始 Google 添加了 TelecomManager 类,所以我们可以借助这个类来监听电话状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void initPhoneStateListener() {
phoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
super.onCallStateChanged(state, incomingNumber);

switch (state) {
case TelephonyManager.CALL_STATE_IDLE: // 待机,即无电话时,挂断时触发
break;
case TelephonyManager.CALL_STATE_RINGING: // 响铃,来电时触发
break;
case TelephonyManager.CALL_STATE_OFFHOOK: // 摘机,接听或打电话时触发
break;
default:
break;
}
}
};

telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
if (telephonyManager != null) {
// 设置来电监听
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
}
}

我们可以把监听放到 Service 中,然后再注册一个电话变化的广播接收器来启动这个 Service,这样无论何时电话状态发生变化我们都可以接收到并做出一些操作。比如这里我监听了来电并弹出一个顶级弹框覆盖默认的电话应用,然后提供一个按钮用于打开App。

电话状态的监听

具体代码请看我写的 Demo,Github 地址:aJIEw/PhoneCallApp

Demo 包括两部分,代替默认电话应用以及电话监听。

这里记录一个小坑,一般情况下,Android M 以下只要在 manifest 中声明权限就可以显示顶级弹框了,但是在某些国产 Rom 上顶级弹框默认是禁止的,需要在悬浮窗管理中允许后才可以。所以这里有个三方库就非常值得推荐了:czy1121/SettingsCompact

用法也相当简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 检查是否开启了权限
if (!SettingsCompat.canDrawOverlays(this)) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android M 以上引导用户去系统设置中打开允许悬浮窗
// 使用反射是为了用尽可能少的代码保证在大部分机型上都可用
try {
Context context = this;
Class clazz = Settings.class;
Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION");
Intent intent = new Intent(field.get(null).toString());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(Uri.parse("package:" + context.getPackageName()));
context.startActivity(intent);
} catch (Exception e) {
Toast.makeText(this, "请在悬浮窗管理中打开权限", Toast.LENGTH_LONG).show();
}
} else {
// 6.0 以下则直接使用 SettingsCompat 中提供的接口
SettingsCompat.manageDrawOverlays(act);
}
}

03 后记

这篇文章拖了一个礼拜才写完,论拖延的功底也是没谁了。其实本来应该上周日发的,但是后来把时间花在了研究各种录屏软件的使用,然后为了一个视频截取的功能又开始研究各种视频软件。推荐一个TED视频:拖延症人群的内心世界,可以说把拖延症人群的心理描述得相当到位了。拖延不可怕,可怕的是拖出习惯了,完全不拖延的人不存在,所以我们每个人都应该问问自己:我有没有在那些会影响到自己未来的事上拖延?


参考文章:
  1. Android 之来电秀实现(一)

  2. Android 之来电秀实现(二)