Android 7.0 插卡后APN信息的加载流程、UI界面编辑APN的流程及Android中APN配置相关的漏洞

本文详细探讨了Android 7.0中插卡后APN的选择流程,包括DcTracker的事件处理、APN数据库加载以及创建可用APN的过程。此外,还分析了用户在UI界面修改APN的步骤,如切换、新建和重置操作。最后,文章揭示了一个APN配置相关的漏洞,当APN的type字段为空时,可能导致数据连接无法断开,从而在特定情况下让后台应用继续消耗流量。提出了三种解决方案,包括修正APN类型解析、确保IMS连接优先和针对IMS的特殊处理。
摘要由CSDN通过智能技术生成

终端中有一个apns-config.xml文件,负责定义各个运营商规定的默认APN参数。
开机后,终端启动Phone进程时,会加载运行在Phone进程中的TelephonyProvider。
TelephonyProvider负责解析apns-config.xml文件,将其中定义的APN参数写入到数据库中。
Android 7.0中这一部分的流程,与Android 6.0基本类似,可以参考Android6.0 APN

在这边博客中我们重点看看:
1、插卡后,手机选择可以使用的APN的流程;
2、终端UI界面,修改(新建)APN的流程;
3、Android中APN配置相关的漏洞——在某些场景下,数据连接断开失败。

一、插卡后APN选择流程
在这篇博客中,我们不分析终端完整的检卡流程,仅关注与APN相关的部分。
首先来看一下DcTracker的构造函数:

public DcTracker(Phone phone) {
    .......
    //每个Phone对象有自己DcTracker
    //每个DcTracker加载各自卡可用的APN
    mPhone = phone;
    .......
    //1、监听卡载入
    mUiccController = UiccController.getInstance();
    mUiccController.registerForIccChanged(this, DctConstants.EVENT_ICC_CHANGED, null);
    .......
    //2、监听卡信息变化
    mSubscriptionManager = SubscriptionManager.from(mPhone.getContext());
    mSubscriptionManager.addOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener);
    .......
    //监听APN数据库变化
    mApnObserver = new ApnChangeObserver();
    phone.getContext().getContentResolver().registerContentObserver(
            Telephony.Carriers.CONTENT_URI, true, mApnObserver);
    .............
    //初始化不同APN类型对应的网络能力,后文介绍
    initApnContexts();
    .............
    // Add Emergency APN to APN setting list by default to support EPDN in sim absent cases
    initEmergencyApnSetting();
    addEmergencyApnSetting();
    ...............
}

在这一部分,我们先研究一下卡相关的内容。
APN数据库变化触发的流程,放在下一部分介绍。

1、EVENT_ICC_CHANGED
根据DcTracker的构造函数,我们知道DcTracker注册成为UiccController的观察者,监听Icc Changed事件。
当UiccController通知DcTracker时,将触发DcTracker发送DctConstants.EVENT_ICC_CHANGED给自己处理。

在DcTracker的handleMessage函数中:

public void handleMessage (Message msg) {
    .........
    case DctConstants.EVENT_ICC_CHANGED: {
        onUpdateIcc();
        break;
    }
    .........
}

跟进一下onUpdateIcc函数:

private void onUpdateIcc() {
    ..........
    //利用UiccController得到当前Phone对应的iccRecord
    IccRecords newIccRecords = getUiccRecords(UiccController.APP_FAM_3GPP);

    //旧有的IccRecord
    IccRecords r = mIccRecords.get();
    if (r != newIccRecords) {
        //移除对旧有信息的记录
        if (r != null) {
            log("Removing stale icc objects.");
            r.unregisterForRecordsLoaded(this);
            mIccRecords.set(null);
        }

        if (newIccRecords != null) {
            if (SubscriptionManager.isValidSubscriptionId(mPhone.getSubId())) {
                log("New records found.");
                mIccRecords.set(newIccRecords);
                //向IccRecord注册,观察卡信息是否载入完成
                //收到通知后,DcTracker将发送EVENT_RECORDS_LOADED信息给自己处理
                newIccRecords.registerForRecordsLoaded(
                        this, DctConstants.EVENT_RECORDS_LOADED, null);

                //这里应该是7.0新加入的,将SIM卡置为SIM_PROVISIONED状态,表示卡是激活的
                SubscriptionController.getInstance().setSimProvisioningStatus(
                        SubscriptionManager.SIM_PROVISIONED, mPhone.getSubId());
            }
        } else {
            //处理卡被移除的情况
            onSimNotReady();
        }
    }
}

我们再来看看handleMessage中对EVENT_RECORDS_LOADED函数的处理:

public void handleMessage (Message msg) {
    .........
    case DctConstants.EVENT_RECORDS_LOADED:
        int subId = mPhone.getSubId();
        if (SubscriptionManager.isValidSubscriptionId(subId)) {
            //卡信息载入完成后,调用onRecordsLoadedOrSubIdChanged函数进行处理
            onRecordsLoadedOrSubIdChanged();
        } else {
            log("Ignoring EVENT_RECORDS_LOADED as subId is not valid: " + subId);
        }
        break;
    .........
}

从上面的代码可以看出,卡信息加载完成后,DcTracker将调用onRecordsLoadedOrSubIdChanged函数进行处理。
这个函数等下再做进一步介绍。

2、Subscriptions Changed
从DcTracker的构造函数,可以看出DcTracker还向SubscriptionManager注册了一个Listener。
当SubscriptionManager发现卡信息变化时,也会通过该Listener进行回调。
看看这个Listener的实现:

private final OnSubscriptionsChangedListener mOnSubscriptionsChangedListener =
        new OnSubscriptionsChangedListener() {
            public final AtomicInteger mPreviousSubId =
                    new AtomicInteger(SubscriptionManager.INVALID_SUBSCRIPTION_ID);

            public void onSubscriptionsChanged() {
                .............
                int subId = mPhone.getSubId();
                if (SubscriptionManager.isValidSubscriptionId(subId)) {
                    //监听一些数据库变化
                    registerSettingsObserver();
                    /* check if sim is un-provisioned */
                    //7.0新增的,主要是根据卡对应的SubscriptionInfo判断卡是否激活
                    //如果卡从激活变成未激活,就要断开数据连接(如果存在),并进行通知
                    applyUnProvisionedSimDetected();
                }
                //subId发生变化,说明卡变化了
                if (mPreviousSubId.getAndSet(subId) != subId &&
                        SubscriptionManager.isValidSubscriptionId(subId)) {
                    onRecordsLoadedOrSubIdChanged();
                }
            }
        };

从Listener的代码不难看出,当SubscriptionManager回调其接口onSubscriptionsChanged时,若检测到卡发生变化,也会调用onRecordsLoadedOrSubIdChanged函数。

现在我们就可以明白onRecordsLoadedOrSubIdChanged函数命名的由来:不论是检测到卡信息载入完成,还是卡的SubId发生变化,该函数均会被调用。

3、onRecordsLoadedOrSubIdChanged
现在我们看看onRecordsLoadedOrSubIdChanged函数:

private void onRecordsLoadedOrSubIdChanged() {
    ..............
    //1、创建当前卡可用的APN
    createAllApnList();

    //2、设置初始使用的APN
    setInitialAttachApn();

    if (mPhone.mCi.getRadioState().isOn()) {
        if (DBG) log("onRecordsLoadedOrSubIdChanged: notifying data availability");
        notifyOffApnsOfAvailability(Phone.REASON_SIM_LOADED);
    }

    //卡变化也会触发拨号流程;不过若此时数据开关未开,那么拨号是不会成功的
    setupDataOnConnectableApns(Phone.REASON_SIM_LOADED);
}

从上面的代码可以看出,插卡或卡发生变化后,就要创建当前卡可用的APN,同时设置初始时使用的APN。

接下来,我们分别看看这两个流程。

3.1、createAllApnList
首先看看创建卡对应APN的过程:

private void createAllApnList() {
    //表示mvno是否匹配
    //mvno也是APN的一种属性,代表该APN适用于虚拟运营商,目前用的比较少
    mMvnoMatched = false;

    //用于保存结果
    mAllApnSettings = new ArrayList<ApnSetting>();

    //得到当前卡的信息
    IccRecords r = mIccRecords.get();
    //得到卡对应的MCC/MNC
    String operator = (r != null) ? r.getOperatorNumeric() : "";
    if (operator != null) {
        //构造SQL语句
        String selection = "numeric = '" + operator + "'";
        String orderBy = "_id";
        ...............
        //查询MCC/MNC对应的APN
        Cursor cursor = mPhone.getContext().getContentResolver().query(
                Telephony.Carriers.CONTENT_URI, null, selection, null, orderBy);

        if (cursor != null) {
            if (cursor.getCount() > 0) {
                //1、利用数据创建APN
                mAllApnSettings = createApnList(cursor);
            }
            cursor.close();
        }
    }

    //2、添加emergencyApnSettings
    addEmergencyApnSetting();

    //3、去除重复的APN
    dedupeApnSettings();

    if (mAllApnSettings.isEmpty()) {
        mPreferredApn = null;
    } else {
        //4、得到用户偏爱的APN (用户在UI界面主动选择的)
        mPreferredApn = getPreferredApn();
        if (mPreferredApn != null && !mPreferredApn.numeric.equals(operator) {
            mPreferredApn = null;
            //用户偏爱的与当前卡不匹配,删除数据库中对应信息
            setPreferredApn(-1);
        }
    }

    //5、在需要的情况下,构造APN文件发送给modem
    setDataProfilesAsNeeded();
}

以上是创建卡对应APN的整个过程,细节还是挺多的。
不过主干的思路还是很清晰:TelephonyProvider在初始时,加载了apns-conf.xml中所有信息;同时,如果用户自己新建过APN,那么对应的信息也会存入到数据库中。
此时,就是根据MCC/MNC取出所有对应的APN信息。

3.1.1 createApnList

private ArrayList<ApnSetting> createApnList(Cursor cursor) {
    //Framework是用的APN数据结构为ApnSetting
    ArrayList<ApnSetting> mnoApns = new ArrayList<ApnSetting>();
    ArrayList<ApnSetting> mvnoApns = new ArrayList<ApnSetting>();
    IccRecords r = mIccRecords.get();

    if (cursor.moveToFirst()) {
        do {
            //利用数据创建ApnSetting
            ApnSetting apn = makeApnSetting(cursor);
            if (apn == null) {
                continue;
            }

            //判断APN是否有mvnoType和mvnoMatchData
            if (apn.hasMvnoParams()) {
                //卡与mvno相关的信息匹配
                if (r != null && ApnSetting.mvnoMatches(r, apn.mvnoType, apn.mvnoMatchData)) {
                    mvnoApns.add(apn);
                }
            } else {
                mnoApns.add(apn);
            }
        } while (cursor.moveToNext());
    }

    //从下面结果的选择的代码来看
    //一张卡要么支持普通运营商,要么支持虚拟运营商
    ArrayList<ApnSetting> result;
    if (mvnoApns.isEmpty()) {
        result = mnoApns;
        mMvnoMatched = false;
    } else {
        result = mvnoApns;
        //虚拟运营商时,mMvnoMatched置为true
        mMvnoMatched = true;
    }
}

以上过程还是比较简单的,我们跟进一下makeApnSetting:

private ApnSetting makeApnSetting(Cursor cursor) {
    String[] types = parseTypes(
            cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.TYPE)));
    //从数据库中读取各种信息,共同构造ApnSetting
    ApnSetting apn = new ApnSetting(
            cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID)),
            ................);
    return apn;
}

这里的代码没什么疑点,就是利用数据库的信息,调用ApnSetting的构造函数。
我们进入parseTypes看看:

private String[] parseTypes(String types) {
    String[] result;
    // If unset, set to DEFAULT.
    if (types == null || types.equals("")) {
        result = new String[1];
        result[0] = PhoneConstants.APN_TYPE_ALL;
    } else {
        //一个APN可以包含多个type
        result = types.split(",");
    }
    return result;
}

这段代码是解析APN的type字段。APN的type域,决定了它提供的网络能力。

关于type,我们可以参考前面提到的DcTracker构造函数中的initApnContexts函数:

private void initApnContexts() {
    ..........
    // Load device network attributes from resources
    String[] networkConfigStrings = mPhone.getContext().getResources().getStringArray(
            com.android.internal.R.array.networkAttributes);

    for (String networkConfigString : networkConfigStrings) {
        NetworkConfig networkConfig = new NetworkConfig(networkConfigString);
        ApnContext apnContext = null;

        switch (networkConfig.type) {
            case ConnectivityManager.TYPE_MOBILE:
                //ApnContext是拨号时使用的数据结构
                //这里创建ApnContext时,将Network Config与APN type关联起来了
                apnContext = addApnContext(PhoneConstants.APN_TYPE_DEFAULT, networkConfig);
                break;
            case ConnectivityManager.TYPE_MOBILE_MMS:
                apnContext = addApnContext(PhoneConstants.APN_TYPE_MMS, networkConfig);
                break;
            ................
        }
    }
    ................
}

结合parseTypes和initApnContexts,我们就能知道APN type对应的具体网络能力。
例如:APN type包含default时,利用这个APN建立的网络就具有Mobile能力,即能够用数据网络访问Internet;
当APN type包含mms时,利用这个APN建立的网络就具有发送彩信的能力。

从parseTypes函数可以看出,当APN的type为空时,即没有配置时,APN的type被定义为APN_TYPE_ALL。
利用APN_TYPE_ALL建立的网络,将具有全部的网络能力。

正常情况下,这种设计是合理的:
运营商会不同的服务定义不同的网络,于是通过APN的type域进行区分;
但是,有的运营商在某些地区会用同一个网络支持所有的功能(例如在非洲的一些国家),此时将APN的type域写成”default, mms, supl, dun, hipri, fota, ims…….”是件繁琐的事,
于是,就规定APN的type域为”“时,可以支持所有网络能力。

然而,这种设计成为了Android的一个漏洞,在某些场景下,将带来数据连接无法断开的问题。
关于这个问题的成因,我们在最后分析。

3.1.2 addEmergencyApnSetting
接下来,我们看看addEmergencyApnSetting中的内容:

private void addEmergencyApnSetting() {
    if(mEmergencyApn != null) {
        if(mAllApnSettings == null) {
            mAllApnSettings = new ArrayList<ApnSetting>();
        } else {
            boolean hasEmergencyApn = false;
            for (ApnSetting apn : mAllApnSettings) {
                if (ArrayUtils.contains(apn.types, PhoneConstants.APN_TYPE_EMERGENCY)) {
                    hasEmergencyApn = true;
                    break;
                }
            }

            if(hasEmergencyApn == false) {
                //将mEmergencyApn插入到当前卡可用的Apn List中
                mAllApnSettings.add(mEmergencyApn);
            } else {
                log("addEmergencyApnSetting - E-APN setting is already present");
            }
        } 
    }
}

插入卡后,我们从数据库中取出与该卡MCC/MNC匹配的数据,构建对应的ApnSetting。
但是每个卡还需要支持紧急拨号对应网络,因此在加载完数据库中匹配数据后,将mEmergencyApn也写入到mAllApnSettings中。

mEmergencyApn在DcTracker的构造函数中调用initEmergencyApnSetting得到:

private 
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值