telephony framework通话状态维护流程

frameworks/opt/telephony/src/java/com/android/internal/telephony/CallTracker.java

CallTracker同字面意思就是依据ril上报消息,维护framework层通话数据结构的类。子类有多个,例如GsmCallTracker,CdmaCallTracker

GsmCallTracker

frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm/GsmCallTracker.java

    static final int MAX_CONNECTIONS = 7;   // only 7 connections allowed in GSM
    static final int MAX_CONNECTIONS_PER_CALL = 5; // only 5 connections allowed per call
两个常量,可以看出Gsm的通话中最多可以有7个Connection,而每一个Call最多可以有5个Connection

    //***** Instance Variables
    GsmConnection mConnections[] = new GsmConnection[MAX_CONNECTIONS];
mConnections数组,保存所有的Connection

    GsmCall mRingingCall = new GsmCall(this);
            // A call that is ringing or (call) waiting
    GsmCall mForegroundCall = new GsmCall(this);
    GsmCall mBackgroundCall = new GsmCall(this);
拥有三个Call的实例,分别代表了来电,前台通话和后台通话。一个Call可以有1个或者多个Connection,但是也是有限制的,RingingCall就能只有1个Connection,而且只有一个Call是会议通话即最多5个Connection,另一个Call只能有1个Connection。那么之前7个Connection的情况,就是前台和后台分别有5个和1个(1个和5个),来电一个,加起来是7个。

 GsmConnection mPendingMO;
成员变量mPendingMo,拨号的时候会用到。
synchronized Connection
    dial (String dialString, int clirMode, UUSInfo uusInfo, Bundle intentExtras)
            throws CallStateException {
        // note that this triggers call state changed notif
        clearDisconnected();

        if (!canDial()) {   //多种条件判断当前是否可以拨号,例如当前正在拨号未接通就不可以拨号
            throw new CallStateException("cannot dial in current state");
        }

        String origNumber = dialString;
        dialString = convertNumberIfNecessary(mPhone, dialString);

        // The new call must be assigned to the foreground call.
        // That call must be idle, so place anything that's
        // there on hold
        if (mForegroundCall.getState() == GsmCall.State.ACTIVE) { //已有前台通话的情形
            // this will probably be done by the radio anyway
            // but the dial might fail before this happens
            // and we need to make sure the foreground call is clear
            // for the newly dialed connection

            /// M: CC015: CRSS special handling @{
            mWaitingForHoldRequest.set();
            /// @}

            switchWaitingOrHoldingAndActive(); //先切换前台通话到后台
            // This is a hack to delay DIAL so that it is sent out to RIL only after
            // EVENT_SWITCH_RESULT is received. We've seen failures when adding a new call to
            // multi-way conference calls due to DIAL being sent out before SWITCH is processed
            try {
                Thread.sleep(500);   //等待modem切换,modem操作要一定时间的
            } catch (InterruptedException e) {
                // do nothing
            }

            // Fake local state so that
            // a) foregroundCall is empty for the newly dialed connection
            // b) hasNonHangupStateChanged remains false in the
            // next poll, so that we don't clear a failed dialing call
            fakeHoldForegroundBeforeDial();
        }

        if (mForegroundCall.getState() != GsmCall.State.IDLE) {
            //we should have failed in !canDial() above before we get here
            throw new CallStateException("cannot dial in current state");
        }

        mPendingMO = new GsmConnection(mPhone.getContext(), checkForTestEmergencyNumber(dialString),
                this, mForegroundCall);  //创建一个新的GsmConnection,赋值给mPendingMo
        mHangupPendingMO = false;

        if ( mPendingMO.getAddress() == null || mPendingMO.getAddress().length() == 0
                || mPendingMO.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0
        ) {//拨号号码为空或者有*字符,为非法号码,不拨号
            // Phone number is invalid
            mPendingMO.mCause = DisconnectCause.INVALID_NUMBER;

            /// M: CC015: CRSS special handling @{
            mWaitingForHoldRequest.reset();
            /// @}

            // handlePollCalls() will notice this call not present
            // and will mark it as dropped.
            pollCallsWhenSafe();
        } else {
            // Always unmute when initiating a new call
            setMute(false);//拨号会取消静音

            /// M: CC015: CRSS special handling @{
            if (!mWaitingForHoldRequest.isWaiting()) {
                /// M: CC010: Add RIL interface @{
                if (PhoneNumberUtils.isEmergencyNumber(mPhone.getSubId(), dialString)
                        && !PhoneNumberUtils.isSpecialEmergencyNumber(dialString)) {
                    int serviceCategory = PhoneNumberUtils.getServiceCategoryFromEcc(dialString);
                    mCi.setEccServiceCategory(serviceCategory);
                    mCi.emergencyDial(mPendingMO.getAddress(), clirMode, uusInfo,
                            obtainCompleteMessage(EVENT_DIAL_CALL_RESULT)); //紧急号码拨号
                /// @}
                } else {
                    mCi.dial(mPendingMO.getAddress(), clirMode, uusInfo,
                            obtainCompleteMessage(EVENT_DIAL_CALL_RESULT)); //普通拨号
                }
            } else {
                mWaitingForHoldRequest.set(mPendingMO.getAddress(), clirMode, uusInfo);
            }
            /// @}
        }

        if (mNumberConverted) {
            mPendingMO.setConverted(origNumber);
            mNumberConverted = false;
        }

        updatePhoneState();
        mPhone.notifyPreciseCallStateChanged();

        return mPendingMO;
    }
流程看我写的注释,拨号时传递了EVENT_DIAL_CALL_RESULT消息。

            case EVENT_DIAL_CALL_RESULT:
                ...
                operationComplete();
消息处理调用operationComplete,这个也是通话操作(交换通话,主动挂断,通话中断等)完毕后收到ril响应时的标准操作

    private void
    operationComplete() {
        mPendingOperations--; //操作数-1,表示又完成了一个操作,这个数量是在obtainCompleteMessage中加1的

        if (DBG_POLL) log("operationComplete: pendingOperations=" +
                mPendingOperations + ", needsPoll=" + mNeedsPoll);

        if (mPendingOperations == 0 && mNeedsPoll) {
            mLastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT);
            mCi.getCurrentCalls(mLastRelevantPoll); //向ril发起状态查询
        } else if (mPendingOperations < 0) {
            // this should never happen
            Rlog.e(LOG_TAG,"GsmCallTracker.pendingOperations < 0");
            mPendingOperations = 0;
        }
    }
ril的getCurrentCalls函数发起的通话状态查询,这个是维护通话相关数据结构Call、Connection的起点

 case EVENT_POLL_CALLS_RESULT:
       ...
       handlePollCalls((AsyncResult)msg.obj);
       ...    
收到回应消息后调用handlePollCalls,这个函数比较长,分为三段来看:

 handlePollCalls(AsyncResult ar) {
        List polledCalls;

        ...
        polledCalls = (List)ar.result;  //ril上报的数据是个list
        ...
首先获取了ril上报的数据

for (int i = 0, curDC = 0, dcSize = polledCalls.size()
                ; i < mConnections.length; i++) {  //循环次数是Connection数组的个数,对gsm来说也就是7
            GsmConnection conn = mConnections[i];  //从数组中获取对应索引的每一个Connection
            DriverCall dc = null;

            // polledCall list is sparse
            if (curDC < dcSize) {
                dc = (DriverCall) polledCalls.get(curDC);  //强制转换为DiriverCall,这时可以看出polledCalls列表的每一项的类型了
                //DriverCall是Connection的数据来源
                if (dc.index == i+1) {
                    curDC++;
                } else {
                    dc = null;
                }
            }

            if (DBG_POLL) log("poll: conn[i=" + i + "]=" +
                    conn+", dc=" + dc);
            //这里大概也能推断出这个循环的作用:依据每个DriverCall更新对应的Connection
            if (conn == null && dc != null) {
                //conn为null,dc不为null,那么用dc填充conn
                /* M: CC part start */
                if (DBG_POLL) log("case 1 : new Call appear");

                ...
                // Connection appeared in CLCC response that we don't know about
                if (mPendingMO != null && mPendingMO.compareTo(dc)) {
                    //拨号的情况
                    if (DBG_POLL) log("poll: pendingMO=" + mPendingMO);

                    ...
                    // It's our pending mobile originating call
                    mConnections[i] = mPendingMO;
                    mPendingMO.mIndex = i;
                    mPendingMO.update(dc); //依据dc更新mPendingMO状态
                    mPendingMO = null;    //前台通话已经有mPendingMO的引用,所以这里mPendingMO可以置空了

                    // Someone has already asked to hangup this call
                    if (mHangupPendingMO) { //拨号后还没成功,就马上点击挂断,会走到这里
                        mHangupPendingMO = false;
                        try {
                            if (Phone.DEBUG_PHONE) log(
                                    "poll: hangupPendingMO, hangup conn " + i);
                            hangup(mConnections[i]);
                        } catch (CallStateException ex) {
                            Rlog.e(LOG_TAG, "unexpected error on hangup");
                        }

                        // Do not continue processing this poll
                        // Wait for hangup and repoll
                        return;
                    }
                } else {
                    //其它情况,一般是来电
                    ...
                    mConnections[i] = new GsmConnection(mPhone.getContext(), dc, this, i);

                    ...
                    } else if (checkFlag && (mConnections[i].getCall() == mRingingCall)) { // it's a ringing call
                        newRinging = mConnections[i]; //来电
                    } else if (checkFlag) {
                        ...
                        newUnknown = mConnections[i];

                        unknownConnectionAppeared = true; //这种情况很罕见,但是我还是见过的,是stk程序中的拨号或者phone进程crash后会走到这里
                        //stk拨号是直接发送At命令,不通过telephony framework的代码;phone crash后会马上重启,这个时候相关数据结构当然是空的
                    }
                    /* M: CC part end */
                }
                hasNonHangupStateChanged = true;
            } else if (conn != null && dc == null) {
                //conn不为null,而dc为null,对应通话挂断
                /* M: CC part start */
                if (DBG_POLL) log("case 2 : old Call disappear");

                /// M: CC019: Convert state from WAITING to INCOMING @{
                if (((conn.getCall() == mForegroundCall && mForegroundCall.mConnections.size() == 1 && mBackgroundCall.isIdle()) ||
                     (conn.getCall() == mBackgroundCall && mBackgroundCall.mConnections.size() == 1 && mForegroundCall.isIdle())) &&
                    mRingingCall.getState() == GsmCall.State.WAITING) {
                    mRingingCall.mState = GsmCall.State.INCOMING;  //本来是通话中来电,然后前台或者后台通话挂断了,就变成了第一路来电了
                }
                /// @}

                // Connection missing in CLCC response that we were tracking.
                mDroppedDuringPoll.add(conn); //加入conn到mDroppedDuringPoll中
                // Dropped connections are removed from the CallTracker
                // list but kept in the GsmCall list
                mConnections[i] = null; //置空
                ...

            } else if (conn != null && dc != null && !conn.compareTo(dc)) {
                //conn和dc都不为null,但是conn和dc不匹配
                /* M: CC part start */
                if (DBG_POLL) log("case 3 : old Call replaced");

                // Connection in CLCC response does not match what
                // we were tracking. Assume dropped call and new call

                mDroppedDuringPoll.add(conn); //加入conn到mDroppedDuringPoll中

                /// M: CC010: Add RIL interface @{
                // give CLIP ALLOW default value, it will be changed on CLIP URC
                dc.numberPresentation = PhoneConstants.PRESENTATION_ALLOWED;
                /// @}
                /* M: CC part end */

                mConnections[i] = new GsmConnection (mPhone.getContext(), dc, this, i); //创建新的Connection代替

                if (mConnections[i].getCall() == mRingingCall) {
                    newRinging = mConnections[i];  //有可能是新的来电
                } // else something strange happened
                hasNonHangupStateChanged = true;
            } else if (conn != null && dc != null) { /* implicit conn.compareTo(dc) */
                //conn和dc都不为null,且conn和dc匹配,此时更新conn即可
                /* M: CC part start */
                if (DBG_POLL) log("case 4 : old Call update");

                ...

                boolean changed;
                changed = conn.update(dc);
                hasNonHangupStateChanged = hasNonHangupStateChanged || changed;
            }

            ...
        } //循环完毕
再次更新GsmPhone中mConnections数组和GsmCallTracker的一些变量值

// This is the first poll after an ATD.
        // We expect the pending call to appear in the list
        // If it does not, we land here
        //正常情况下mPendingMO所指向的Connection已经由前台通话维护了,这里应该为null。如果不为null,这里特殊处理
        if (mPendingMO != null) {
            Rlog.d(LOG_TAG,"Pending MO dropped before poll fg state:"
                            + mForegroundCall.getState());

            mDroppedDuringPoll.add(mPendingMO);  //丢弃mPendingMO,其实号码已经实际拨出,后续拨号成功后会作为unknown connection出现的
            mPendingMO = null;
            mHangupPendingMO = false;
        }

        if (newRinging != null) { 
            if (DBG_POLL) log("notifyNewRingingConnection");
            mPhone.notifyNewRingingConnection(newRinging); //向上层app通知来电
            ...
        }

        // clear the "local hangup" and "missed/rejected call"
        // cases from the "dropped during poll" list
        // These cases need no "last call fail" reason
        log("dropped during poll size = " + mDroppedDuringPoll.size());
        for (int i = mDroppedDuringPoll.size() - 1; i >= 0 ; i--) { //处理之前要丢弃的Connection
            GsmConnection conn = mDroppedDuringPoll.get(i);

            /// M: CC012: Set as DisconnectCause.LOCAL if conn is disconnected due to Radio Off @{
            if (isCommandExceptionRadioNotAvailable(ar.exception)) {
                conn.onHangupLocal();
            }
            /// @}

            if (conn.isIncoming() && conn.getConnectTime() == 0) {
                // Missed or rejected call
                int cause;
                if (conn.mCause == DisconnectCause.LOCAL) { //这里可以看出来电未接的两种挂断类型其实是代码设置的,不属于网络上报挂断类型
                    cause = DisconnectCause.INCOMING_REJECTED;   //主动拒接来电
                } else {
                    cause = DisconnectCause.INCOMING_MISSED;   //未接来电
                }

                if (Phone.DEBUG_PHONE) {
                    log("missed/rejected call, conn.cause=" + conn.mCause);
                    log("setting cause to " + cause);
                }
                mDroppedDuringPoll.remove(i);
                hasAnyCallDisconnected |= conn.onDisconnect(cause);
            } else if (conn.mCause == DisconnectCause.LOCAL
                    || conn.mCause == DisconnectCause.INVALID_NUMBER) {

                log("local hangup or invalid number");
                mDroppedDuringPoll.remove(i);
                hasAnyCallDisconnected |= conn.onDisconnect(conn.mCause);
            }
        }

        ...


        // Any non-local disconnects: determine cause
        if (mDroppedDuringPoll.size() > 0 &&
                /// M: For 3G VT only @{
                !hasPendingReplaceRequest) { //不是local类型的挂断,向ril请求获取挂断类型
                /// @}
            mCi.getLastCallFailCause(
                obtainNoPollCompleteMessage(EVENT_GET_LAST_CALL_FAIL_CAUSE));
        }

        if (needsPollDelay) {
            pollCallsAfterDelay();  //如有需要就再次调用ril的getCurrentCalls
        }

        // Cases when we can no longer keep disconnected Connection's
        // with their previous calls
        // 1) the phone has started to ring
        // 2) A Call/Connection object has changed state...
        //    we may have switched or held or answered (but not hung up)
        if (newRinging != null || hasNonHangupStateChanged || hasAnyCallDisconnected) {
            internalClearDisconnected(); //清理所有Call中已经挂断的Connection
        }

        updatePhoneState();   //更新并发送phone state

        if (unknownConnectionAppeared) {
            if (DBG_POLL) log("notifyUnknownConnection");
            mPhone.notifyUnknownConnection(newUnknown); //通知上层有unknown connection出现
        }

        if ((hasNonHangupStateChanged || newRinging != null || hasAnyCallDisconnected)
            /// M: CC015: CRSS special handling @{
            && !mHasPendingSwapRequest) {
            /// @}
            if (DBG_POLL) log("notifyPreciseCallStateChanged");
            mPhone.notifyPreciseCallStateChanged(); //通知上层有Call状态的变化。
        }

        ...
更新后的处理。具体分析见代码中加的注释。
拨号的obtainCompleteMessage(EVENT_DIAL_CALL_RESULT)发起了获取通话状态请求,代码中另一处发起该请求的是pollCallsWhenSafe,该方法定义在基类CallTracker中

    protected void pollCallsWhenSafe() {
        mNeedsPoll = true;

        if (checkNoOperationsPending()) {
            mLastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT);
            mCi.getCurrentCalls(mLastRelevantPoll);
        }
    }
该方法在EVENT_CALL_STATE_CHANGE消息处理中被调用

            case EVENT_REPOLL_AFTER_DELAY:
            case EVENT_CALL_STATE_CHANGE:
                pollCallsWhenSafe();
该消息是在GsmCallTracker构造方法中注册到ril中的

      mCi.registerForCallStateChanged(this, EVENT_CALL_STATE_CHANGE, null);
那么对应于obtainCompleteMessage是主动请求,EVENT_CALL_STATE_CHANGE消息就是被动的,在通话状态有变化的时候上报,然后修正当前通话状态相关数据结构。

CdmaCallTracker

frameworks/opt/telephony/src/java/com/android/internal/telephony/cdma/CdmaCallTracker.java

cdma的大体流程和gsm的类似,下面讲讲区别的地方

    static final int MAX_CONNECTIONS = 8;
    static final int MAX_CONNECTIONS_PER_CALL = 1; // only 1 connection allowed per call
cdma的有变化,每个Call的最大数量是1,connection数量最大是8。这个是我手头mtk6.0的代码,不过我印象中我n久之前看过的源码中MAX_CONNECTIONS=2,8肯定是不对的。因为只有三个Call,一个Call一个Connection也算不出是8呀,顶多是3。其实3也不对,cdma最多是2,不信的话可以可以拿起电信手机试试,已有两路通话下是打不进去电话的。

        mCi.registerForCallWaitingInfo(this, EVENT_CALL_WAITING_INFO_CDMA, null);
        /// M: For CDMA call accepted @{
        mCi.registerForCallAccepted(this, EVENT_CDMA_CALL_ACCEPTED, null);

构造函数中的这两个是特有的,一个是监听通话中来电,gsm的来电就是一个事件,而cdma的是两个事件。

另一个是监听cdma拨号后接通。CdmaCall是没有DIALING和ALERTING这两个状态的,Call的State定义见

frameworks/opt/telephony/src/java/com/android/internal/telephony/Call.java

 public enum State {
        IDLE, ACTIVE, HOLDING, DIALING, ALERTING, INCOMING, WAITING, DISCONNECTED, DISCONNECTING;
        ...
        public boolean isDialing() {
            return this == DIALING || this == ALERTING;
        }
        ...
}
电信手机一拨出就是ACTIVE的状态,然后计时也开始走,接通后会重置计时,这个重置事件就是EVENT_CDMA_CALL_ACCEPTED。

 Connection
    dial (String dialString, int clirMode) throws CallStateException {
        ...
        if (mForegroundCall.getState() == CdmaCall.State.ACTIVE) {
            return dialThreeWay(dialString);
        }
        ...
}
拨号大体类似,但是第二路拨号就不同了,会走dialThreeWay,这里和监听通话中来电是单独事件类似,是cdma的特殊性。
   private Connection
    dialThreeWay (String dialString) {
   ...
                       mCi.sendCDMAFeatureCode(mPendingMO.getAddress(),
                        obtainMessage(EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA));
   ...
 }
    
调用ril的sendCDMAFeatureCode拨号,不是ril的dial,这个比较奇怪。继续看怎么接听电话:

  void
    acceptCall() throws CallStateException {
        if (mRingingCall.getState() == CdmaCall.State.INCOMING) {
            Rlog.i("phone", "acceptCall: incoming...");
            // Always unmute when answering a new call
            setMute(false);
            mCi.acceptCall(obtainCompleteMessage());
        } else if (mRingingCall.getState() == CdmaCall.State.WAITING) {
            CdmaConnection cwConn = (CdmaConnection)(mRingingCall.getLatestConnection());

            // Since there is no network response for supplimentary
            // service for CDMA, we assume call waiting is answered.
            // ringing Call state change to idle is in CdmaCall.detach
            // triggered by updateParent.
            cwConn.updateParent(mRingingCall, mForegroundCall);
            cwConn.onConnectedInOrOut();
            updatePhoneState();
            switchWaitingOrHoldingAndActive();
        } else {
            throw new CallStateException("phone not ringing");
        }
    }

   void
    switchWaitingOrHoldingAndActive() throws CallStateException {
        // Should we bother with this check?
        if (mRingingCall.getState() == CdmaCall.State.INCOMING) {
            throw new CallStateException("cannot be in the incoming state");
        } else if (mForegroundCall.getConnections().size() > 1) {
            flashAndSetGenericTrue();
        } else {
            // Send a flash command to CDMA network for putting the other party on hold.
            // For CDMA networks which do not support this the user would just hear a beep
            // from the network. For CDMA networks which do support it will put the other
            // party on hold.
            mCi.sendCDMAFeatureCode("", obtainMessage(EVENT_SWITCH_RESULT));
        }
    }
   private void flashAndSetGenericTrue() {
        mCi.sendCDMAFeatureCode("", obtainMessage(EVENT_SWITCH_RESULT));

        // Set generic to true because in CDMA it is not known what
        // the status of the call is after a call waiting is answered,
        // 3 way call merged or a switch between calls.
        mForegroundCall.setGeneric(true);
        mPhone.notifyPreciseCallStateChanged();
    }

接听第一路和第二路又是不一样的分支,而且接听第二路居然使用的是switchWaitingOrHoldingAndActive,这个方法不是交换通话时用的吗?而switchWaitingOrHoldingAndActive最后用的还是sendCDMAFeatureCode。还可以看下合并通话的的方法:

    void
    conference() {
        // Should we be checking state?
        flashAndSetGenericTrue();
    }

一样是最终使用sendCDMAFeatureCode。

这个其实就是cdma的特殊之处,和gsm差异很大的。还记得小时候央视有个联通的广告是这样的:一个小年轻说自己头疼,这时老妈跳出来说你这是天天用手机通话时间长的原因(明着说移动),然后掏出一个联通手机说用cdma好啊,信令少,打多久都不头疼。这个信令少确实说的对,从代码中可见cdma手机进入多方通话状态的时候,所有的命令其实都是一个命令,无论是接听来电、拨号、交换通话还是合并通话。交换通话和合并通话是一个命令是这样的:当第二路通话是拨号的时候,只有合并通话的功能;第二路通话是来电的时候,只有交换通话的功能。那么挂断第二路通话的时候怎么办?和接通电话不冲突吗?见代码

挂断通话中来电:
   /*package*/ void
    hangup (CdmaConnection conn) throws CallStateException {
       ...
        } else if ((conn.getCall() == mRingingCall)
                && (mRingingCall.getState() == CdmaCall.State.WAITING)) {
            /// M: @{
            log("hangup waiting call");
            /// @}
            // Handle call waiting hang up case.
            //
            // The ringingCall state will change to IDLE in CdmaCall.detach
            // if the ringing call connection size is 0. We don't specifically
            // set the ringing call state to IDLE here to avoid a race condition
            // where a new call waiting could get a hang up from an old call
            // waiting ringingCall.
            //
            // PhoneApp does the call log itself since only PhoneApp knows
            // the hangup reason is user ignoring or timing out. So conn.onDisconnect()
            // is not called here. Instead, conn.onLocalDisconnect() is called.
            conn.onLocalDisconnect();
            updatePhoneState();
            mPhone.notifyPreciseCallStateChanged();
            return;
        ...
    }
并没有向ril发送什么命令,只是把framework层的来电结构release了,这样app就以为电话是挂断的。

针对cdma的这种特点,在进入多方通话的状态后会运行下面这句代码:

 mForegroundCall.setGeneric(true);
这句代码就在上面的flashAndSetGenericTrue方法里,这个定义在Call.java中

   /**
     * To indicate if the connection information is accurate
     * or not. false means accurate. Only used for CDMA.
     */
    public boolean isGeneric() {
        return mIsGeneric;
    }
可见这个是专门针对cdma而设置的,标记当前是否已经进入多方通话。当mIsGeneric为true的时候,app UI不会像gsm一样精确的显示每个Connection信息。cdma也没有gsm的会议管理功能,所以也就没有会议管理中分离Connection的功能和挂断Connection的功能。mIsGeneric为true的时候也不会接收到对方传来的挂断消息,只能自己主动挂断,同时主动挂断会挂断所有的通话。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值