#############需求#############

最近我的一位同事大概是利用下面的方法监听电话的状态

        https://www.pocketdigi.com/20110725/417.html 

          

        相关demo下载链接:

本来我是将系统的ITelephony.aidl复制进去,但是编译会报not fount the import class xx错误,于是看了一下上面的demo,对aidl作了简化:

interface ITelephony {
    boolean endCall();

    void answerRingingCall();
    boolean enableDataConnectivity();

    boolean disableDataConnectivity();

    boolean isDataConnectivityPossible();
}

        aidl文件及目录建好,重新rebuild工程,就会在对应的build目录下生成对应的java文件

        

android 拨号键盘键盘_android 拨号键盘键盘

    

然后我同事说遇到两个问题

        1.电话接通之后,无法挂断

        2.通话状态无法正常显示(准确的说是:通话中的状态不显示)   

我写了个测试demo

        挂断方法:

public void hangup_call(View view) {
        Log.e(TAG,"hangup_call START");
        ...
    }

无响应,日志也没有打。                

        我隐隐约约感觉线程阻塞了,果然接听加上线程就解决了问题。上面的问题就是接听电话阻塞了主线程,所以挂断按钮点击不了,广播也阻塞了。

        

#############源码研究#############

以下源码基于android6.0.1

拨号源码详解:

       

拨号源码架构:

        https://www.jianshu.com/p/ca4ab4e9817f/     (一个电话应用,实际由多个代码模块组成。由dialer下的Android.mk可以看出)

          (介绍各个模块的作用)

一、拨号

      假如我不想调用系统的INTENT来打电话(最直接的方法就是调用ITelephony,文章开头已经介绍过),那么就得对系统打电话的INTENT做一番探索了。

        

      通过调用Intent.ACTION_CALL,会打开系统的拨号引用。  

        向外拨号,发现当前显示的activity是(通过sdk工具

android 拨号键盘键盘_android 拨号键盘键盘_02

):

InCallActivity.java

        现在,我们需求是想自己实现拨号级来电接听,不走系统UI 。

先确定Intent.ACTION_CALL是被谁处理了,怎么处理的,如何调起InCallActivity,InCallActivity又干了啥。

  •   InCallActivity被谁打开的 在xref全局搜索,发现InCallPresenter.java这个类通过官方的解释,可以知道此类正是管理通话状态, 开启InCallActivity。搜索InCallPresenter 又会被很多类调用,可见此类是集中处理通话状态的核心类。其中有 InCallServiceImpl,InCallServiceImpl注册在dialer的清单文件里:

、屏蔽原始UI

              1.

     电话的UI都是通过InCallActivity添加Fragment实现的,所以,只要把其UI屏蔽掉就可以了。(修改源码要一针见血,找到最佳修改位置。这样既能减少工作量又能避免修改错误)

                

              2.但是来电时显示的那个悬浮窗是如何出现的呢?如何屏蔽掉?

              

              

android 拨号键盘键盘_UI_03

              首先,得从“来电”来出发寻找,其实这个UI就叫做StatusBarNotifier

              在InCallPresenter里找到一些蛛丝马迹:

               

android 拨号键盘键盘_拨打电话_04

                看看官方对此类的解释:

                

android 拨号键盘键盘_android 拨号键盘键盘_05

                准备把下面的方法注释掉,应该就不会显示通知栏

                

android 拨号键盘键盘_android_06

                经测试,注释上面的方法,确实可以屏蔽来电时显示的通知栏。

  

、主动拨号之后无法挂断的问题

            在(二.1),屏蔽IncallUI之后,发现主动往外打电话之后,其他的按钮没法点击。

public void call(View view) {
        Log.d(TAG, "call: ");
/*        //取得一个Callintent
        final Intent intent = CallUtil.getCallIntent("13121116227");

        //交给DialerUtils去处理
        DialerUtils.startActivityWithErrorToast(this, intent);*/

        callThread = new Thread(){
            @Override
            public void run() {
                try {
                    ITelephony iTelephony = PhoneUtils.getITelephony((TelephonyManager)MainActivity.this.getSystemService(
                            Context.TELEPHONY_SERVICE));
                    iTelephony.call(getPackageName(),"17316107592");
                } catch (Exception e) {
                    Log.e(TAG, "[Broadcast]Exception111="+e.getMessage(), e);
                }
            }
        };
        callThread.start();
    }

        所以我猜想有2种原因:

        1)打电话时InCallActivity弹出来了(眼睛看不见),挡住了我的Activity

        2)系统电话的call方法阻塞了UI线程

        

        是否是系统的UI挡住了我的Activity:

              将上面的InCallActivity的布局显示出来,果然,是有个系统的拨打电话的界面。

               

android 拨号键盘键盘_android 拨号键盘键盘_07

             那么如何隐藏并这个activity界面并屏蔽它的所有时间

             1)将InCallActivity的theme变成透明

                  

android 拨号键盘键盘_android 拨号键盘键盘_08

↓↓↓

                    

android 拨号键盘键盘_android_09

             2)屏蔽InCallActivity的点击事件

                     在InCallActivity的onCreate方法里加上下面代码

this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);

             3)屏蔽InCallActivity对状态栏的改变

                             

      

        对系统电话的call方法发起追踪研究:

        1.找service

        

android 拨号键盘键盘_UI_10

↓↓↓

          

android 拨号键盘键盘_android_11

↓↓↓

            注意Context.TELEPHONY_SERVICE的值是“phone”

            

android 拨号键盘键盘_UI_12

“phone”可以找到ITelephony的Service类/packages/services/Telephony/src/com/android/phone/PhoneInterfaceManager.java:

             可以看到这个service

             

android 拨号键盘键盘_UI_13

             上面的init方法在PhoneGlobals的onCreate方法里被调用

              

android 拨号键盘键盘_android 拨号键盘键盘_14

           2.分析service的相应call方法

android 拨号键盘键盘_UI_15

↓↓↓

                

android 拨号键盘键盘_拨打电话_16

               可以看到最终也是通过Intent去打开拨号页面的。

、主动拨号之后对方没有接听直接挂断,ITelephony没有回调的问题。

               此种情况虽然ITelephony没有回调,但是系统会有个无人接听的提示语音,可以在此处修改系统源码,人工发一个空闲状态广播。

      

、去电,PhoneListener的状态瞎回调的问题。

错误的回调。需要人为的去处理,可以根据时间来过滤判断,但是具有不可靠性。

通话中状态。但有时候不会回调。

                

  • 空闲的状态很好解决:

              点击拨打电话的按钮时,将Constants.isPhoneOut设置为true。

case TelephonyManager.CALL_STATE_IDLE: // 空闲 0
			Log.d(tag, "--------------------------CALL_STATE_IDLE00---------------------isPhoneOut = " + Constants.isPhoneOut);
			
			if(Constants.isPhoneOut == true){
				//拨打电话时,系统会误发一个挂断的广播,需要自己区分处理
				Constants.isPhoneOut = false;
				return;
			}

             

            XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

通话中状态回调不准确的问题则不好解决,但是经测试,发现系统电话应用关于电话状态的判断则十分准确,只有从源码入手了。

              网络上搜到的关于通话中状态判断的解决方案:

                 

事实证明此方法行不通,拨打电话还没有通话就回调了。

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

              

          突然我脑洞大开,查看了一下电话应用com.android.dialer的日志,发现,在InCallPresenter类里,在来电接听会有系统日志:

                Phone switching state: INCOMING -> INCALL

          去电时会有系统日志:

                Phone switching state: OUTGOING -> INCALL

          而且这个日志不会重复,可以在上面发送两个自定义广播,就能准确监听到通话中状态了。

          在测试中发现  OUTGOING -> INCALL,这个不一定只有去电通话中才会回调,需要结合前一个状态变化联合判断:

          所以修改InCallPresenter类,缓存上一次的通话状态变化,然后对比方能准确判断通话中的状态

InCallState newState = getPotentialStateFromCallList(callList);
        InCallState oldState = mInCallState;
        Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState);
        newState = startOrFinishUi(newState);
        Log.d(this, "onCallListChange newState changed to " + newState);

        String oldStateLast = phone_state_cacheSp.getString("oldState", "");
        String newStateLast = phone_state_cacheSp.getString("newState", "");

        // Set the new state before announcing it to the world
        Log.i(this, "Phone switching state22222: " + oldState + " -> " + newState);
        SharedPreferences.Editor editor = phone_state_cacheSp.edit();
        editor.putString("oldState",oldState.toString());
        editor.putString("newState",newState.toString());
        editor.commit();

		if(mInCallActivity != null){
				if("INCOMING".equals(oldState.toString()) && "INCALL".equals(newState.toString())){
            mInCallActivity.sendBroadcast(new Intent("CZ_IN"));
		}else if("OUTGOING".equals(oldState.toString()) && "INCALL".equals(newState.toString())){
			if("PENDING_OUTGOING".equals(oldStateLast) && "OUTGOING".equals(newStateLast)){
                Log.i(this, "Phone switching state22222: " + "这是个假的通话中状态,舍弃");
			    return;
            }
            mInCallActivity.sendBroadcast(new Intent("CZ_OUT"));

        }
		}
	
        mInCallState = newState;

    

                 

      

转载于:https://blog.51cto.com/4259297/2372997