开关飞模流程

8 阅读6分钟

我来分析 Android 开关飞行模式的流程。

好的,现在我已经收集到足够的信息。让我为用户详细分析 Android 开关飞行模式的完整流程。

Android 开关飞行模式流程详解

飞行模式(Airplane Mode)是 Android 中一个重要的系统功能,用于同时关闭所有无线通信功能(蜂窝、Wi-Fi、蓝牙等)。下面详细分析其实现原理。


一、飞行模式的触发入口

1. 用户界面触发

用户可以通过以下方式开关飞行模式:

  • 快捷设置面板(下拉通知栏)
  • 设置应用 → 网络与互联网 → 飞行模式
  • 长按电源键 → 全局操作菜单

2. 核心操作:修改 Settings 数据库

所有触发入口最终都会执行两个关键操作:

        Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
        intent.putExtra("state", on);
        mContext.sendBroadcastAsUser(intent, UserHandle.ALL);

步骤:

  1. 修改 Settings 数据库

    Settings.Global.putInt(
        getContentResolver(),
        Settings.Global.AIRPLANE_MODE_ON,
        on ? 1 : 0
    );
    
  2. 发送系统广播

    Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
    intent.putExtra("state", on);  // true=开启, false=关闭
    context.sendBroadcastAsUser(intent, UserHandle.ALL);
    

二、电话模块的处理流程

1. ServiceStateTracker 监听初始化

        mCr = phone.getContext().getContentResolver();
        // system setting property AIRPLANE_MODE_ON is set in Settings.
        int airplaneMode = Settings.Global.getInt(mCr, Settings.Global.AIRPLANE_MODE_ON, 0);
        int enableCellularOnBoot = Settings.Global.getInt(mCr,
                Settings.Global.ENABLE_CELLULAR_ON_BOOT, getDefaultEnableCellularOnBoot());
        mDesiredPowerState = (enableCellularOnBoot > 0) && ! (airplaneMode > 0);
        if (!mDesiredPowerState) {
            mRadioPowerOffReasons.add(TelephonyManager.RADIO_POWER_REASON_USER);
        }
        mRadioPowerLog.log("init : airplane mode = " + airplaneMode + " enableCellularOnBoot = " +
                enableCellularOnBoot);

ServiceStateTracker 在初始化时:

  • 读取飞行模式状态 (AIRPLANE_MODE_ON)
  • 计算射频的期望状态 mDesiredPowerState = !airplaneMode
  • 如果飞行模式开启,则添加关闭原因 RADIO_POWER_REASON_USER

2. 注册 Settings 监听(通过 ContentObserver)

其他模块(如 CarrierActionAgent)会监听 Settings 变化:

                    mSettingsObserver.observe(
                            Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON),
                            EVENT_APM_SETTINGS_CHANGED);

当飞行模式设置变化时,触发 EVENT_APM_SETTINGS_CHANGED 事件。


三、开启飞行模式流程(关闭射频)

流程图

用户操作
  ↓
Settings.Global.AIRPLANE_MODE_ON = 1
  ↓
发送 ACTION_AIRPLANE_MODE_CHANGED 广播
  ↓
ServiceStateTracker 处理(通过 ContentObserver 或广播)
  ↓
setRadioPower(false) 
  ↓
mDesiredPowerState = false
mRadioPowerOffReasons.add(RADIO_POWER_REASON_USER)
  ↓
setPowerStateToDesired()
  ↓
powerOffRadioSafely()
  ↓
【关键】先断开数据连接
  ↓
hangupAndPowerOff()
  ↓
mCi.setRadioPower(false) → HAL → Modem
  ↓
射频关闭完成

关键代码分析

1. setRadioPowerForReason 方法

    public void setRadioPowerForReason(boolean power, boolean forEmergencyCall,
            boolean isSelectedPhoneForEmergencyCall, boolean forceApply, int reason) {
        log("setRadioPower power " + power + " forEmergencyCall " + forEmergencyCall
                + " forceApply " + forceApply + " reason " + reason);

        if (power) {
            if (forEmergencyCall) {
                clearAllRadioOffReasons();
            } else {
                mRadioPowerOffReasons.remove(reason);
            }
        } else {
            mRadioPowerOffReasons.add(reason);
        }
        if (power == mDesiredPowerState && !forceApply) {
            log("setRadioPower mDesiredPowerState is already " + power + " Do nothing.");
            return;
        }
        if (power && !mRadioPowerOffReasons.isEmpty()) {
            log("setRadioPowerForReason " + "power: " + power + " forEmergencyCall= "
                    + forEmergencyCall + " isSelectedPhoneForEmergencyCall: "
                    + isSelectedPhoneForEmergencyCall + " forceApply " + forceApply + " reason: "
                    + reason + " will not power on the radio as it is powered off for the "
                    + "following reasons: " + mRadioPowerOffReasons + ".");
            return;
        }

        mDesiredPowerState = power;
        setPowerStateToDesired(forEmergencyCall, isSelectedPhoneForEmergencyCall, forceApply);
    }

重点:

  • 使用 mRadioPowerOffReasons 集合管理关闭原因(可能有多个,如 USER、CARRIER、THERMAL 等)
  • 只有当 mRadioPowerOffReasons 为空时才允许开机

2. setPowerStateToDesired 方法

    protected void setPowerStateToDesired(boolean forEmergencyCall,
            boolean isSelectedPhoneForEmergencyCall, boolean forceApply) {
        if (DBG) {
            String tmpLog = "setPowerStateToDesired: mDeviceShuttingDown=" + mDeviceShuttingDown
                    + ", mDesiredPowerState=" + mDesiredPowerState
                    + ", getRadioState=" + mCi.getRadioState()
                    + ", mRadioPowerOffReasons=" + mRadioPowerOffReasons
                    + ", IMS reg state=" + mImsRegistrationOnOff
                    + ", pending radio off=" + hasMessages(EVENT_POWER_OFF_RADIO_IMS_DEREG_TIMEOUT);
            log(tmpLog);
            mRadioPowerLog.log(tmpLog);
        }

        if (mDesiredPowerState && mDeviceShuttingDown) {
            log("setPowerStateToDesired powering on of radio failed because the device is " +
                    "powering off");
            return;
        }

        // If we want it on and it's off, turn it on
        if (mDesiredPowerState && mRadioPowerOffReasons.isEmpty()
                && (forceApply || mCi.getRadioState() == TelephonyManager.RADIO_POWER_OFF)) {
            mCi.setRadioPower(true, forEmergencyCall, isSelectedPhoneForEmergencyCall, null);
        } else if ((!mDesiredPowerState || !mRadioPowerOffReasons.isEmpty()) && mCi.getRadioState()
                == TelephonyManager.RADIO_POWER_ON) {
            if (DBG) log("setPowerStateToDesired: powerOffRadioSafely()");
            powerOffRadioSafely();
        } else if (mDeviceShuttingDown
                && (mCi.getRadioState() != TelephonyManager.RADIO_POWER_UNAVAILABLE)) {
            // !mDesiredPowerState condition above will happen first if the radio is on, so we will
            // see the following: (delay for IMS dereg) -> RADIO_POWER_OFF ->
            // RADIO_POWER_UNAVAILABLE
            mCi.requestShutdown(null);
        }
        // Cancel any pending timeouts because the state has been re-evaluated.
        cancelDelayRadioOffWaitingForImsDeregTimeout();
    }

判断逻辑:

  • 开机条件: mDesiredPowerState=true && mRadioPowerOffReasons 为空 && 当前射频关闭
  • 关机条件: (mDesiredPowerState=false || mRadioPowerOffReasons 非空) && 当前射频开启

3. powerOffRadioSafely 安全关闭

    public void powerOffRadioSafely() {
        synchronized (this) {
            SatelliteController.getInstance().onCellularRadioPowerOffRequested();
            if (DomainSelectionResolver.getInstance().isDomainSelectionSupported()) {
                EmergencyStateTracker.getInstance().onCellularRadioPowerOffRequested();
            }
            if (!mPendingRadioPowerOffAfterDataOff) {
                // hang up all active voice calls first
                if (mPhone.isPhoneTypeGsm() && mPhone.isInCall()) {
                    mPhone.mCT.mRingingCall.hangupIfAlive();
                    mPhone.mCT.mBackgroundCall.hangupIfAlive();
                    mPhone.mCT.mForegroundCall.hangupIfAlive();
                }

                for (Phone phone : PhoneFactory.getPhones()) {
                    if (!phone.getDataNetworkController().areAllDataDisconnected()) {
                        log("powerOffRadioSafely: Data is active on phone " + phone.getSubId()
                                + ". Wait for all data disconnect.");
                        mPendingRadioPowerOffAfterDataOff = true;
                        phone.getDataNetworkController().registerDataNetworkControllerCallback(
                                mDataDisconnectedCallback);
                    }
                }

                // Tear down outside of the disconnected check to prevent race conditions.
                mPhone.getDataNetworkController().tearDownAllDataNetworks(
                        DataNetwork.TEAR_DOWN_REASON_AIRPLANE_MODE_ON);

                if (mPendingRadioPowerOffAfterDataOff) {
                    sendEmptyMessageDelayed(EVENT_SET_RADIO_POWER_OFF,
                            POWER_OFF_ALL_DATA_NETWORKS_DISCONNECTED_TIMEOUT);
                } else {
                    log("powerOffRadioSafely: No data is connected, turn off radio now.");
                    hangupAndPowerOff();
                }
            }
        }
    }

安全关闭步骤:

  1. 通知 SatelliteControllerEmergencyStateTracker
  2. 挂断所有进行中的电话 (避免通话中断)
  3. 断开所有数据连接 (tearDownAllDataNetworks)
  4. 等待数据完全断开(超时机制)
  5. 调用 hangupAndPowerOff()mCi.setRadioPower(false)

四、关闭飞行模式流程(开启射频)

流程图

用户操作
  ↓
Settings.Global.AIRPLANE_MODE_ON = 0
  ↓
发送 ACTION_AIRPLANE_MODE_CHANGED 广播
  ↓
ServiceStateTracker 处理
  ↓
setRadioPower(true)
  ↓
mRadioPowerOffReasons.remove(RADIO_POWER_REASON_USER)
mDesiredPowerState = true
  ↓
setPowerStateToDesired()
  ↓
mCi.setRadioPower(true) → HAL → Modem
  ↓
EVENT_RADIO_STATE_CHANGED
  ↓
setPowerStateToDesired() (确认状态)
  ↓
pollStateInternal(true) (开始网络搜索)
  ↓
射频开启完成,开始驻网

关键代码

            case EVENT_RADIO_STATE_CHANGED:
                RadioPowerStateStats.onRadioStateChanged(mCi.getRadioState());
                // fall through, the code above only logs metrics when radio state changes
            case EVENT_PHONE_TYPE_SWITCHED:
                if(!mPhone.isPhoneTypeGsm() &&
                        mCi.getRadioState() == TelephonyManager.RADIO_POWER_ON) {
                    handleCdmaSubscriptionSource(mCdmaSSM.getCdmaSubscriptionSource());
                }
                // This will do nothing in the 'radio not available' case
                setPowerStateToDesired();
                // These events are modem triggered, so pollState() needs to be forced
                pollStateInternal(true);
                break;

关闭飞行模式后:

  1. Modem 上电后发送 UNSOL_RESPONSE_RADIO_STATE_CHANGED 通知
  2. 触发 EVENT_RADIO_STATE_CHANGED 事件
  3. 调用 setPowerStateToDesired() 确认状态
  4. 调用 pollStateInternal(true) 开始网络搜索,进入驻网流程

五、其他无线模块的处理

飞行模式不仅影响蜂窝网络,还会影响:

1. Wi-Fi

  • 监听 ACTION_AIRPLANE_MODE_CHANGED 广播
  • 根据 Settings.Global.AIRPLANE_MODE_RADIOS 判断是否需要关闭
  • 如果在 AIRPLANE_MODE_TOGGLEABLE_RADIOS 中,则允许在飞行模式下独立开关

2. Bluetooth

  • 类似 Wi-Fi,通过 AIRPLANE_MODE_RADIOS 配置
  • 默认在飞行模式下关闭

3. NFC / UWB

  • 根据配置决定是否受飞行模式影响

六、特殊场景处理

1. 紧急呼叫中开启飞行模式

            if (tm != null && tm.isInEmergencyCall()) {
                // Switch airplane mode back to off.
                ConnectivityManager cm =
                        (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
                cm.setAirplaneMode(false);
                Toast.makeText(this, R.string.radio_off_during_emergency_call, Toast.LENGTH_LONG)
                        .show();
                Log.i(LOG_TAG, "Ignoring airplane mode: emergency call. Turning airplane off");

特殊处理:

  • 如果正在进行紧急呼叫,自动关闭飞行模式
  • 显示 Toast 提示用户

2. IMS 注销延迟

  • 关闭射频前会等待 IMS 注销完成
  • 超时机制: EVENT_POWER_OFF_RADIO_IMS_DEREG_TIMEOUT

七、底层 HAL 调用

RadioModemProxy.setRadioPower

    public void setRadioPower(int serial, boolean powerOn, boolean forEmergencyCall,
            boolean preferredForEmergencyCall) throws RemoteException {
        if (isEmpty()) return;
        if (isAidl()) {
            mModemProxy.setRadioPower(serial, powerOn, forEmergencyCall, preferredForEmergencyCall);
        } else if (mHalVersion.greaterOrEqual(RIL.RADIO_HAL_VERSION_1_6)) {
            ((android.hardware.radio.V1_6.IRadio) mRadioProxy).setRadioPower_1_6(serial, powerOn,
                    forEmergencyCall, preferredForEmergencyCall);
        } else if (mHalVersion.greaterOrEqual(RIL.RADIO_HAL_VERSION_1_5)) {
            ((android.hardware.radio.V1_5.IRadio) mRadioProxy).setRadioPower_1_5(serial, powerOn,
                    forEmergencyCall, preferredForEmergencyCall);
        } else {
            mRadioProxy.setRadioPower(serial, powerOn);
        }
    }

根据 HAL 版本选择接口:

  • AIDL HAL: IRadioModem.setRadioPower
  • HIDL 1.6+: setRadioPower_1_6 (支持紧急呼叫标志)
  • HIDL 1.5: setRadioPower_1_5
  • 旧版本: setRadioPower

八、总结

关键组件

  1. Settings.Global.AIRPLANE_MODE_ON: 系统数据库存储状态
  2. Intent.ACTION_AIRPLANE_MODE_CHANGED: 系统广播通知各模块
  3. ServiceStateTracker: 电话模块核心控制器
  4. mRadioPowerOffReasons: 关闭原因集合(支持多重原因)

核心流程

开启飞行模式:
  修改 Settings → 广播 → 挂断电话 → 断开数据 → setRadioPower(false) → Modem 关闭

关闭飞行模式:
  修改 Settings → 广播 → setRadioPower(true) → Modem 开启 → pollState → 驻网

设计亮点

  1. 安全关闭: 先断开数据和电话再关闭射频,避免数据丢失
  2. 多重原因管理: 通过 Set<Integer> 管理多个关闭原因,任一原因存在都不能开机
  3. 紧急呼叫保护: 通话中强制阻止飞行模式开启
  4. IMS 友好: 等待 IMS 注销完成再关闭射频
  5. 状态机驱动: 通过 mDesiredPowerState 和实际状态对比,自动修正

这套机制确保了飞行模式在各种场景下的安全性和可靠性。