安卓代替系统默认电话应用(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 中解除监听。

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 有点区别:

<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>:

<!-- 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 提供一些电话相关的操作:

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 来判断电话是打进的还是拨出的。

@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 的回调吗,我们监听了电话状态的改变,所以可以在电话断开的时候退出电话界面:

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

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

In Call Demo

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

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 类,所以我们可以借助这个类来监听电话状态。

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

用法也相当简单:

// 检查是否开启了权限
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 之来电秀实现(二)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,275评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,566评论 18 139
  • 丑奴儿 少年不识愁滋味,爱上层楼。爱上层楼,为赋新词强说愁。 而今识尽愁滋味,欲说还休。欲说还休,却道天好个秋。 ...
    faa101ab89b1阅读 261评论 0 0
  • 其实,赵冉对林枫是用了真心的。虽然也许一开始,他是习惯性的猎艳。 有的人读书越多越迂腐,有的人则可以融会贯通,把做...
    一条小鱼两条小鱼阅读 508评论 0 1
  • 最近有越起越晚的趋势,今天醒来快七点了,生物钟紊乱,明天要靠闹钟了。 晚起了一小时,整天都很被动,那一小时最近都用...
    顾鸣芬阅读 226评论 0 2