来电去电流程

1 阅读8分钟

我来分析一下这个仓库中的来电和去电流程。让我查看相关代码:

现在让我查看来电处理和通话状态轮询的逻辑:

好的,现在我已经收集了足够的信息。让我整理一下来电和去电的完整流程:


Android Telephony 来电去电流程详解 ☎️📞

基于 frameworks-opt-telephony 代码分析,电话呼叫处理涉及多个层次的复杂交互。


核心架构

应用层 (Telecom/Dialer)
    ↓
Phone/GsmCdmaPhone
    ↓
CallTracker (GsmCdmaCallTracker / ImsPhoneCallTracker)
    ↓
Call (GsmCdmaCall / ImsCall)
    ↓
Connection (GsmCdmaConnection / ImsConnection)
    ↓
RIL → Modem

一、去电流程(Mobile Originated Call - MO) 📤

流程架构图

用户拨号
    ↓
Dialer App → Telecom Framework
    ↓
Phone.dial()
    ↓
GsmCdmaCallTracker.dialGsm() / dialCdma()
    ↓
创建 GsmCdmaConnection (mPendingMO)
    ↓
RIL.dial() → Modem
    ↓
轮询通话状态 (pollCallsWhenSafe)
    ↓
呼叫建立 (ALERTING → ACTIVE)

详细步骤

步骤 1: 用户发起拨号

应用通过 Telecom 框架发起呼叫:

TelecomManager.placeCall(Uri.parse("tel:10086"), ...)

步骤 2: Phone.dial() 处理拨号请求

GsmCdmaPhone.dial()ImsPhone.dial() 根据网络类型选择:

  • CS 域(电路域):传统 2G/3G 语音
  • IMS 域(VoLTE/VoWiFi):4G/5G 语音

步骤 3: CallTracker 创建连接

public synchronized Connection dialGsm(String dialString, DialArgs dialArgs)
        throws CallStateException {
    int clirMode = dialArgs.clirMode;
    UUSInfo uusInfo = dialArgs.uusInfo;
    Bundle intentExtras = dialArgs.intentExtras;
    boolean isEmergencyCall = dialArgs.isEmergency;
    if (isEmergencyCall) {
        clirMode = CommandsInterface.CLIR_SUPPRESSION;
        if (Phone.DEBUG_PHONE) log("dial gsm emergency call, set clirModIe=" + clirMode);

    }

    // note that this triggers call state changed notif
    clearDisconnected();

    // Check for issues which would preclude dialing and throw a CallStateException.
    checkForDialIssues(isEmergencyCall);

    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() == GsmCdmaCall.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
        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
        // TODO: setup duration metrics won't capture this
        try {
            Thread.sleep(500);
        } 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() != GsmCdmaCall.State.IDLE) {
        //we should have failed in !canDial() above before we get here
        throw new CallStateException("cannot dial in current state");
    }

    mPendingMO = new GsmCdmaConnection(mPhone, dialString, this, mForegroundCall,
            dialArgs);

    if (intentExtras != null) {
        Rlog.d(LOG_TAG, "dialGsm - emergency dialer: " + intentExtras.getBoolean(
                TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
        mPendingMO.setHasKnownUserIntentEmergency(intentExtras.getBoolean(
                TelecomManager.EXTRA_IS_USER_INTENT_EMERGENCY_CALL));
    }
    mHangupPendingMO = false;

    mMetrics.writeRilDial(mPhone.getPhoneId(), mPendingMO, clirMode, uusInfo);
    mPhone.getVoiceCallSessionStats().onRilDial(mPendingMO);

    if ( mPendingMO.getAddress() == null || mPendingMO.getAddress().length() == 0
            || mPendingMO.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0) {
        // Phone number is invalid
        mPendingMO.mCause = DisconnectCause.INVALID_NUMBER;

关键操作:

  1. clearDisconnected(): 清理已断开的连接
  2. checkForDialIssues(): 检查是否允许拨号
    • 检查是否有活动呼叫阻止新呼叫
    • 检查飞行模式
    • 检查服务状态
  3. switchWaitingOrHoldingAndActive(): 如果有活动通话,先置为保持
  4. 创建 mPendingMO: 待处理的移动始发连接
  5. 记录指标: TelephonyMetrics.writeRilDial()

步骤 4: 发送拨号命令到 Modem

继续 dialGsm() 方法:

// 发送拨号命令到 RIL
mCi.dial(mPendingMO.getAddress(), mPendingMO.isEmergencyCall(), 
         mPendingMO.getEmergencyNumberInfo(),
         mPendingMO.hasKnownUserIntentEmergency(), clirMode, uusInfo, 
         obtainCompleteMessage());

通过 RIL 发送:

  • GSM: RIL_REQUEST_DIAL
  • CDMA: RIL_REQUEST_CDMA_DIAL
  • IMS: 通过 IMS 栈处理

步骤 5: 通话状态轮询

Modem 开始建立呼叫后,会主动上报状态变化:

RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED

GsmCdmaCallTracker 收到通知后触发轮询:

EVENT_CALL_STATE_CHANGE → pollCallsWhenSafe()

pollCallsWhenSafe() 流程:

pollCallsWhenSafe()
    ↓
mCi.getCurrentCalls(EVENT_POLL_CALLS_RESULT)
    ↓
RIL_REQUEST_GET_CURRENT_CALLS
    ↓
handlePollCalls(AsyncResult ar)

步骤 6: 处理轮询结果

if (conn == null && dc != null) {
    // 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);
        mPendingMO = null;

        // Someone has already asked to hangup this call
        if (mHangupPendingMO) {
            mHangupPendingMO = false;

            // Re-start Ecm timer when an uncompleted emergency call ends
            if (!isPhoneTypeGsm() && mPhone.isEcmCanceledForEmergency()) {
                mPhone.handleTimerInEmergencyCallbackMode(
                        GsmCdmaPhone.RESTART_ECM_TIMER);
            }

            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 {
        if (Phone.DEBUG_PHONE) {
            log("pendingMo=" + mPendingMO + ", dc=" + dc);
        }

        mConnections[i] = new GsmCdmaConnection(mPhone, dc, this, i);
        log("New connection is not mPendingMO. Creating new GsmCdmaConnection,"
                + " objId=" + System.identityHashCode(mConnections[i]));

        Connection hoConnection = getHoConnection(dc);
        if (hoConnection != null) {
            log("Handover connection found.");
            // Single Radio Voice Call Continuity (SRVCC) completed
            mConnections[i].migrateFrom(hoConnection);
            // Updating connect time for silent redial cases (ex: Calls are transferred
            // from DIALING/ALERTING/INCOMING/WAITING to ACTIVE)
            if (hoConnection.mPreHandoverState != GsmCdmaCall.State.ACTIVE &&
                    hoConnection.mPreHandoverState != GsmCdmaCall.State.HOLDING &&
                    dc.state == DriverCall.State.ACTIVE) {
                mConnections[i].onConnectedInOrOut();
            } else {
                mConnections[i].onConnectedConnectionMigrated();
            }

            mHandoverConnections.remove(hoConnection);

            if (isPhoneTypeGsm()) {
                for (Iterator<Connection> it = mHandoverConnections.iterator();
                     it.hasNext(); ) {
                    Connection c = it.next();
                    Rlog.i(LOG_TAG, "HO Conn state is " + c.mPreHandoverState);
                    if (c.mPreHandoverState == mConnections[i].getState()) {
                        Rlog.i(LOG_TAG, "Removing HO conn "
                                + hoConnection + c.mPreHandoverState);
                        it.remove();
                    }
                }
            }

            mPhone.notifyHandoverStateChanged(mConnections[i]);
        } else {
            // find if the MT call is a new ring or unknown connection
            log("New connection is not mPendingMO nor a pending handover.");
            newRinging = checkMtFindNewRinging(dc,i);
            if (newRinging == null) {
                unknownConnectionAppeared = true;
                if (isPhoneTypeGsm()) {
                    newUnknownConnectionsGsm.add(mConnections[i]);
                } else {
                    newUnknownConnectionCdma = mConnections[i];
                }
            } else if (hangupWaitingCallSilently(i)) {
                return;
            }
        }
    }
    hasNonHangupStateChanged = true;
}

处理逻辑:

  • mPendingMO 转换为正式的 mConnections[i]
  • 更新连接状态
  • 处理 SRVCC(VoLTE → 2G/3G 切换)场景

步骤 7: 呼叫状态转换

去电状态机:

IDLE → DIALING → ALERTING → ACTIVE
public enum State {
    @UnsupportedAppUsage IDLE,
    ACTIVE,
    @UnsupportedAppUsage HOLDING,
    @UnsupportedAppUsage DIALING,
    @UnsupportedAppUsage ALERTING,
    @UnsupportedAppUsage INCOMING,
    @UnsupportedAppUsage WAITING,
    @UnsupportedAppUsage DISCONNECTED,
    @UnsupportedAppUsage DISCONNECTING;

    @UnsupportedAppUsage
    public boolean isAlive() {
        return !(this == IDLE || this == DISCONNECTED || this == DISCONNECTING);
    }

    @UnsupportedAppUsage
    public boolean isRinging() {
        return this == INCOMING || this == WAITING;
    }

    public boolean isDialing() {
        return this == DIALING || this == ALERTING;
    }
}
  • DIALING: 拨号中(本地发起,网络未响应)
  • ALERTING: 对方振铃中(网络已建立,等待接听)
  • ACTIVE: 通话接通

步骤 8: 通知上层

  • Phone.notifyPreciseCallStateChanged()
  • 广播到 Telecom 框架
  • 更新通话 UI
  • 开始计费

二、来电流程(Mobile Terminated Call - MT) 📥

流程架构图

网络推送来电
    ↓
Modem → RIL_UNSOL_CALL_RING
    ↓
GsmCdmaCallTracker 收到通知
    ↓
pollCallsWhenSafe()
    ↓
handlePollCalls() 检测新来电
    ↓
创建 GsmCdmaConnection (mRingingCall)
    ↓
通知上层 (onNewRingingConnection)
    ↓
显示来电界面
    ↓
用户接听/拒接

详细步骤

步骤 1: Modem 上报来电

网络推送来电信号,Modem 通过 RIL 上报:

RIL_UNSOL_CALL_RING
RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED

对于 CDMA 还会收到:

RIL_UNSOL_CDMA_CALL_WAITING

步骤 2: CallTracker 注册监听

mCi = phone.mCi;
mCi.registerForCallStateChanged(this, EVENT_CALL_STATE_CHANGE, null);
mCi.registerForOn(this, EVENT_RADIO_AVAILABLE, null);

收到事件后:

case EVENT_CALL_STATE_CHANGE:
    pollCallsWhenSafe();
    break;

步骤 3: 轮询获取通话列表

mCi.getCurrentCalls(obtainCompleteMessage(EVENT_POLL_CALLS_RESULT))

Modem 返回当前所有通话的 DriverCall 列表,包括:

  • 通话索引
  • 通话状态(INCOMING/WAITING/ACTIVE等)
  • 号码信息
  • 是否为来电(isMT = true)

步骤 4: 识别新来电

handlePollCalls() 中:

newRinging = checkMtFindNewRinging(dc, i);

checkMtFindNewRinging() 逻辑:

  • 检查 DriverCall 的 isMT 标志
  • 检查状态是否为 INCOMINGWAITING
  • 如果是新的来电,创建新的 Connection 对象

步骤 5: 创建来电连接

mConnections[i] = new GsmCdmaConnection(mPhone, dc, this, i);

将新连接添加到 mRingingCall:

if (mConnections[i].getCall() == mRingingCall) {
    newRinging = mConnections[i];
}

步骤 6: 通知上层新来电

mPhone.notifyNewRingingConnection(newRinging);

通知链:

GsmCdmaCallTracker
    ↓
GsmCdmaPhone.notifyNewRingingConnectionP()
    ↓
CallManager.registerForNewRingingConnection()
    ↓
TelephonyConnectionService (Telecom 框架)
    ↓
InCallService (来电界面)

步骤 7: 显示来电界面

Telecom 框架接收到新来电通知后:

  1. 检查来电权限和来电显示设置
  2. 查询联系人数据库匹配号码
  3. 启动 InCallUI 显示来电界面
  4. 播放铃声/振动

步骤 8: 用户响应

接听电话:

public void acceptCall() throws CallStateException {
    // FIXME if SWITCH fails, should retry with ANSWER
    // in case the active/holding call disappeared and this
    // is no longer call waiting

    if (mRingingCall.getState() == GsmCdmaCall.State.INCOMING) {
        Rlog.i("phone", "acceptCall: incoming...");
        // Always unmute when answering a new call
        setMute(false);
        mPhone.getVoiceCallSessionStats().onRilAcceptCall(mRingingCall.getConnections());
        mCi.acceptCall(obtainCompleteMessage());
    } else if (mRingingCall.getState() == GsmCdmaCall.State.WAITING) {
        if (isPhoneTypeGsm()) {
            setMute(false);
        } else {
            GsmCdmaConnection cwConn = (GsmCdmaConnection)(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 GsmCdmaCall.detach
            // triggered by updateParent.
            cwConn.updateParent(mRingingCall, mForegroundCall);
            cwConn.onConnectedInOrOut();
            updatePhoneState();
        }
        switchWaitingOrHoldingAndActive();
    } else {
        throw new CallStateException("phone not ringing");
    }
}

处理逻辑:

  • INCOMING: 直接接听 → mCi.acceptCall()
  • WAITING: 呼叫等待场景 → 切换通话 switchWaitingOrHoldingAndActive()

发送到 RIL:

RIL_REQUEST_ANSWER (GSM)
RIL_REQUEST_CDMA_FLASH (CDMA)

拒接电话:

public void rejectCall() throws CallStateException {
    // AT+CHLD=0 means "release held or UDUB"
    // so if the phone isn't ringing, this could hang up held
    if (mRingingCall.getState().isRinging()) {
        mCi.rejectCall(obtainCompleteMessage());
    } else {
        throw new CallStateException("phone not ringing");
    }
}

发送到 RIL:

RIL_REQUEST_HANGUP (挂断)
RIL_REQUEST_UDUB (User Determined User Busy)

步骤 9: 状态更新

接听后状态转换:

INCOMING/WAITING → ACTIVE

Modem 上报状态变化 → 再次轮询 → 更新连接状态


三、通话状态管理 📊

Call 对象分类

GsmCdmaCallTracker 维护三个 Call 对象:

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public GsmCdmaCall mRingingCall = new GsmCdmaCall(this);
// A call that is ringing or (call) waiting
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public GsmCdmaCall mForegroundCall = new GsmCdmaCall(this);
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public GsmCdmaCall mBackgroundCall = new GsmCdmaCall(this);
  • mRingingCall: 来电/呼叫等待
  • mForegroundCall: 前台活动通话
  • mBackgroundCall: 后台保持通话

连接数组

GsmCdmaConnection[] mConnections;
- GSM: 最多 19 个连接(7个CS + 12个IMS SRVCC)
- CDMA: 最多 8 个连接

状态轮询机制

触发轮询的事件:

  1. EVENT_CALL_STATE_CHANGE: Modem 主动通知
  2. EVENT_RADIO_AVAILABLE: Radio 重新可用
  3. EVENT_REPOLL_AFTER_DELAY: 延迟重新轮询

轮询作用:

  • 同步 Framework 和 Modem 的通话状态
  • 检测新来电
  • 检测通话断开
  • 更新通话属性(时长、状态等)

四、关键功能

1. 通话保持与恢复

switchWaitingOrHoldingAndActive()
  • GSM: RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE
  • CDMA: 发送 Flash 命令

2. 多方通话(会议)

conference()
  • 将保持通话和活动通话合并
  • GSM: RIL_REQUEST_CONFERENCE
  • CDMA: Flash 操作

3. 通话转移

explicitCallTransfer()
  • 将通话转移给第三方
  • RIL_REQUEST_EXPLICIT_CALL_TRANSFER

4. 挂断通话

hangup(Connection conn)
hangupForegroundResumeBackground()
hangupWaitingOrBackground()
  • 挂断指定连接
  • 挂断前台,恢复后台
  • 挂断等待或后台通话

五、IMS 通话处理

对于 VoLTE/VoWiFi 通话,使用 ImsPhoneCallTracker

关键差异

  1. 不使用轮询: IMS 通过事件驱动
  2. ImsCall 对象: 通过 IMS 栈管理
  3. 更丰富的功能:
    • 视频通话
    • 通话升降级
    • Wi-Fi 切换
    • RTT(实时文本)

SRVCC 切换

Single Radio Voice Call Continuity

  • VoLTE → 2G/3G 无缝切换
  • ImsPhoneCallTrackerGsmCdmaCallTracker
  • 保持通话不中断

六、时序图总结

去电时序

Dialer App
    ↓ placeCall()
Telecom
    ↓ 
Phone.dial()
    ↓
CallTracker.dialGsm()
    ↓ 创建 mPendingMO
RIL.dial()
    ↓
Modem 拨号
    ↓
RIL_UNSOL_CALL_STATE_CHANGED
    ↓
pollCallsWhenSafe()
    ↓ getCurrentCalls()
Modem 返回通话列表
    ↓
handlePollCalls()
    ↓ 识别 mPendingMO → mConnections[i]
DIALING → ALERTING → ACTIVE
    ↓
notifyPreciseCallStateChanged()
    ↓
Telecom 更新 UI

来电时序

网络来电
    ↓
Modem 检测
    ↓
RIL_UNSOL_CALL_RING
    ↓
CallTracker.EVENT_CALL_STATE_CHANGEpollCallsWhenSafe()
    ↓ getCurrentCalls()
Modem 返回通话列表(包含新来电)
    ↓
handlePollCalls()
    ↓ checkMtFindNewRinging()
创建新 Connection → mRingingCall
    ↓
notifyNewRingingConnection()
    ↓
Telecom 收到通知
    ↓
启动 InCallUI
    ↓ 播放铃声
用户接听
    ↓
acceptCall()
    ↓ RIL.acceptCall()
Modem 接听
    ↓
RIL_UNSOL_CALL_STATE_CHANGED
    ↓
pollCallsWhenSafe()
    ↓
INCOMING → ACTIVE
    ↓
通话建立

七、关键日志标签

  • GsmCdmaCallTracker: CS 域通话追踪
  • ImsPhoneCallTracker: IMS 域通话追踪
  • GsmCdmaCall: 通话对象
  • GsmCdmaConnection: 连接对象
  • RIL: RIL 层命令和响应

这就是 Android Telephony 中完整的来电去电流程!整个系统通过轮询机制保持 Framework 和 Modem 状态同步,支持 GSM/CDMA/IMS 多种网络类型,并提供了丰富的通话管理功能。