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

来源:互联网 发布:淘宝信用等级怎么提升 编辑:程序博客网 时间:2024/06/05 10:10

终端中有一个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();    ...............}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在这一部分,我们先研究一下卡相关的内容。 
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;    }    .........}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

跟进一下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();        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

我们再来看看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;    .........}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

从上面的代码可以看出,卡信息加载完成后,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();                }            }        };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

从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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

从上面的代码可以看出,插卡或卡发生变化后,就要创建当前卡可用的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();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

以上是创建卡对应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;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

以上过程还是比较简单的,我们跟进一下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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里的代码没什么疑点,就是利用数据库的信息,调用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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这段代码是解析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;            ................        }    }    ................}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

结合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");            }        }     }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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

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

private void initEmergencyApnSetting() {    // Operator Numeric is not available when sim records are not loaded.    // Query Telephony.db with APN type as EPDN request does not    // require APN name, plmn and all operators support same APN config.    // DB will contain only one entry for Emergency APN    String selection = "type=\"emergency\"";    Cursor cursor = mPhone.getContext().getContentResolver().query(            Telephony.Carriers.CONTENT_URI, null, selection, null, null);    if (cursor != null) {        if (cursor.getCount() > 0) {            if (cursor.moveToFirst()) {                mEmergencyApn = makeApnSetting(cursor);            }        }        cursor.close();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

根据之前分析的内容,很容易看出mEmergencyApn也是通过查询数据库后构造的。 
对于厂商而言,emergency是要保证写入到apns-conf.xml中的。

3.1.3 dedupeApnSettings 
看过apns-conf.xml的人就知道,由于一些人为的原因,其中可能有很多APN是重复的。 
dedupeApnSettings就是负责移除mAllApnSettings中重复的APN (或者说叫合并同类项)。

我们看看对应的代码:

private void dedupeApnSettings() {    ..........    // coalesce APNs if they are similar enough to prevent    // us from bringing up two data calls with the same interface    int i = 0;    while (i < mAllApnSettings.size() - 1) {        ApnSetting first = mAllApnSettings.get(i);        ApnSetting second = null;        int j = i + 1;        while (j < mAllApnSettings.size()) {            second = mAllApnSettings.get(j);            //判断APN是否相似            //type可以不一致,其它主要参数一致时,则认为两个APN类似            //例如carrier名不一样,但其它参数一致时,这两个APN就是一致的            //具体可看代码,此处不再深入            if (apnsSimilar(first, second)) {                //合并相似的APN,主要是合并type                ApnSetting newApn = mergeApns(first, second);                mAllApnSettings.set(i, newApn);                first = newApn;                mAllApnSettings.remove(j);            } else {                j++;            }        }        i++;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

上面的代码就是逐一比较当前卡可用的APN,找出其中相似的,并进行合并。

3.1.4 getPreferredApn 
用户使用一张卡时,可能手动选择过使用的APN。 
因此,当用户再次插拔卡后,getPreferredApn用于找出用户之前选择的APN。 
我们看看对应的代码:

private ApnSetting getPreferredApn() {    if (mAllApnSettings == null || mAllApnSettings.isEmpty()) {        ..............        return null;    }    //一张卡与其subId一一对应    String subId = Long.toString(mPhone.getSubId());    //从这里可以看出,用户选择的APN还是保留在数据库中    //每个subId有其对应的prefer APN    Uri uri = Uri.withAppendedPath(PREFERAPN_NO_UPDATE_URI_USING_SUBID, subId);    Cursor cursor = mPhone.getContext().getContentResolver().query(            uri, new String[] { "_id", "name", "apn" },            null, null, Telephony.Carriers.DEFAULT_SORT_ORDER);    if (cursor != null) {        mCanSetPreferApn = true;    } else {        mCanSetPreferApn = false;    }    if (mCanSetPreferApn && cursor.getCount() > 0) {        int pos;        cursor.moveToFirst();        pos = cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID));        for(ApnSetting p : mAllApnSettings) {            ............            //当前卡可用的APN中,包含用户之前选择的prefer APN            //同时这个APN可以支持default type时,才能作为prefer APN            if (p.id == pos && p.canHandleType(mRequestedApnType)) {                .............                cursor.close();                return p;            }        }    }    if (cursor != null) {        cursor.close();    }    .......    return null;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

从上面的代码可以看出perfer APN的选择,还是依赖于TelephonyProvider管理的数据库。 
我们看看TelephonyProvider中查询Prefer APN的相关流程:

public synchronized Cursor query(Uri url, String[] projectionIn, String selection,        String[] selectionArgs, String sort) {    ..........    int match = s_urlMatcher.match(url);    switch (match) {        ...........        case URL_PREFERAPN:        case URL_PREFERAPN_NO_UPDATE: {            //利用getPreferredApnId得到subId对应prefer APN的位置信息            qb.appendWhere("_id = " + getPreferredApnId(subId, true));            break;        }        ...........    }    .........    SQLiteDatabase db = mOpenHelper.getReadableDatabase();    Cursor ret = null;    try {        ...........        //构造对应的Cursor        ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);    } catch (SQLException e) {        loge("got exception when querying: " + e);    }    ........    return ret;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

我们看看这里涉及到的getPreferredApnId函数:

private long getPreferredApnId(int subId, boolean checkApnSp) {    //TelephonyProvider维持自己的SharedPreference    SharedPreferences sp = getContext().getSharedPreferences(PREF_FILE_APN,            Context.MODE_PRIVATE);    //先从SP中获取prefer APN的位置信息    long apnId = sp.getLong(COLUMN_APN_ID + subId, INVALID_APN_ID);    if (apnId == INVALID_APN_ID && checkApnSp) {        //SP中无法取到时,才从数据库中进一步查询        apnId = getPreferredApnIdFromApn(subId);        //查询成功后,将prefer APN信息保存到SP中         if (apnId != INVALID_APN_ID) {            setPreferredApnId(apnId, subId);            deletePreferredApn(subId);        }    }    return apnId;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

从上面的代码,我们知道TelephonyProvider为了加快prefer APN的检索速度,专门引入了SharedPreference单独保存每个卡对应的Prefer APN位置信息。

3.1.5 setDataProfilesAsNeeded

这一部分是将APN信息发往modem。

private void setDataProfilesAsNeeded() {    ................    if (mAllApnSettings != null && !mAllApnSettings.isEmpty()) {        ArrayList<DataProfile> dps = new ArrayList<DataProfile>();        for (ApnSetting apn : mAllApnSettings) {            //modemCognitive是从配置文件得到的            if (apn.modemCognitive) {                DataProfile dp = new DataProfile(apn,                        mPhone.getServiceState().getDataRoaming());                //判断是否重复                boolean isDup = false;                for(DataProfile dpIn : dps) {                    if (dp.equals(dpIn)) {                        isDup = true;                        break;                    }                }            }        }        if(dps.size() > 0) {            //将全部的DataProfile发往modem            mPhone.mCi.setDataProfile(dps.toArray(new DataProfile[0]), null);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

modemCognitive参数默认是没有配置的,实际的APN也很少配置这个选项。 
因此,上面的过程一般情况下并没有进行。

在厂商实际的代码流程中,框架在具体的场景中使用到APN时,才会向modem发送对应的DataProfile。 
例如,我看过Qualcomm底层的实现代码,逻辑大致是: 
在数据拨号时,拨号参数中携带了APN的参数。 
在qcril_data_netctrl中通过QMI查询modem是否有对应的DataProfile,如果没有的话,就在QCRIL层构造dataProfile发送给modem。 
modem将根据dataProfile的内容,来进行实际的网络接入。

3.2、setInitialAttachApn 
createAllApnList结束后,我们已经得到了当前卡可以使用的APN, 
如果用户之前选择过APN的话,我们还得到了Prefer APN。

有了这些信息后,我们就可以选择初始时使用的APN了。 
终端初始时,数据卡应该是利用这个Initial Attach Apn注册到数据网络的。

我们看看setInitialAttachApn函数:

private void setInitialAttachApn() {    ApnSetting iaApnSetting = null;    ApnSetting defaultApnSetting = null;    ApnSetting firstApnSetting = null;    if (mAllApnSettings != null && !mAllApnSettings.isEmpty()) {        //排第一个的,就是firstApn        firstApnSetting = mAllApnSettings.get(0);        //以下是找到可用APN中第一个出现的IA类型的APN,或default类型的APN        for (ApnSetting apn : mAllApnSettings) {            if (ArrayUtils.contains(apn.types, PhoneConstants.APN_TYPE_IA) &&                    apn.carrierEnabled) {                iaApnSetting = apn;                break;            } else if ((defaultApnSetting == null)                    && (apn.canHandleType(PhoneConstants.APN_TYPE_DEFAULT))){                defaultApnSetting = apn;            }        }    }    // The priority of apn candidates from highest to lowest is:    //   1) APN_TYPE_IA (Initial Attach)    //   2) mPreferredApn, i.e. the current preferred apn    //   3) The first apn that than handle APN_TYPE_DEFAULT    //   4) The first APN we can find.    ApnSetting initialAttachApnSetting = null;    if (iaApnSetting != null) {        initialAttachApnSetting = iaApnSetting;    } else if (mPreferredApn != null) {        initialAttachApnSetting = mPreferredApn;    } else if (defaultApnSetting != null) {        initialAttachApnSetting = defaultApnSetting;    } else if (firstApnSetting != null) {        initialAttachApnSetting = firstApnSetting;    }    if (initialAttachApnSetting == null) {        ..........    } else {        .........        //将InitialAttachApn发给modem        mPhone.mCi.setInitialAttachApn(initialAttachApnSetting.apn,                initialAttachApnSetting.protocol, initialAttachApnSetting.authType,                initialAttachApnSetting.user, initialAttachApnSetting.password, null);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

上面的逻辑比较简单。 
这里需要注意的是:当插着两张卡时,每个Phone应该都会下发对应的InitialAttachApn。 
当终端在framework选择完可用的数据卡后,对应的Phone下发RIL_REQUEST_ALLOW_DATA, 
于是modem就用数据Phone的InitialAttachApn注册数据网络。

至此,插卡后APN相关的主要流程介绍完毕,整个逻辑还是比较简单的,类似于下图: 

现在我们看看在UI界面修改APN时,相关的流程。

二、UI界面修改APN的流程 
在这一部分我们主要看看3个主要的操作: 
1、对于同一张卡,进行切换APN的操作; 
2、新建一个APN的操作; 
3、重置APN的操作。

1、切换APN的操作 
在原生代码中,ApnSettings界面负责显示一个卡可以使用的所有APN,该文件定义于 packages/apps/settings/src/com/android/settings中。 
这里我们不深究界面显示相关问题,仅看看APN相关的主要内容:

public void onResume() {    super.onResume();    ...........    if (!mRestoreDefaultApnMode) {        //负责加载可用APN对应的Preference        fillList();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在ApnSettings界面的onResume函数中,利用fillList加载当前卡对应的APN Preference。 
我们跟进一下fillList函数:

private void fillList() {    final TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);    //同样是构造数据库查询字段,注意到界面不显示IA和IMS类型的APN    final String mccmnc = mSubscriptionInfo == null ? ""            : tm.getSimOperator(mSubscriptionInfo.getSubscriptionId());    StringBuilder where = new StringBuilder("numeric=\"" + mccmnc +            "\" AND NOT (type='ia' AND (apn=\"\" OR apn IS NULL)) AND user_visible!=0");    if (mHideImsApn) {        where.append(" AND NOT (type='ims')");    }    //查询数据库    Cursor cursor = getContentResolver().query(Telephony.Carriers.CONTENT_URI, new String[] {            "_id", "name", "apn", "type", "mvno_type", "mvno_match_data"}, where.toString(),            null, Telephony.Carriers.DEFAULT_SORT_ORDER);    if (cursor != null) {        //得到卡信息        IccRecords r = null;        if (mUiccController != null && mSubscriptionInfo != null) {            r = mUiccController.getIccRecords(SubscriptionManager.getPhoneId(                    mSubscriptionInfo.getSubscriptionId()), UiccController.APP_FAM_3GPP);        }        //得到界面的组件        PreferenceGroup apnList = (PreferenceGroup) findPreference("apn_list");        apnList.removeAll();        //分别保存普通APN和彩信APN,界面将分开显示(这里还区分了普通运营商和虚拟运营商)        ArrayList<ApnPreference> mnoApnList = new ArrayList<ApnPreference>();        ArrayList<ApnPreference> mvnoApnList = new ArrayList<ApnPreference>();        ArrayList<ApnPreference> mnoMmsApnList = new ArrayList<ApnPreference>();        ArrayList<ApnPreference> mvnoMmsApnList = new ArrayList<ApnPreference>();        //从数据库中得到原来用户选择prefer id        mSelectedKey = getSelectedApnKey();        cursor.moveToFirst();        while (!cursor.isAfterLast()) {            String name = cursor.getString(NAME_INDEX);            String apn = cursor.getString(APN_INDEX);            String key = cursor.getString(ID_INDEX);            String type = cursor.getString(TYPES_INDEX);            String mvnoType = cursor.getString(MVNO_TYPE_INDEX);            String mvnoMatchData = cursor.getString(MVNO_MATCH_DATA_INDEX);            //构造Apn对应的Preference            ApnPreference pref = new ApnPreference(getPrefContext());            //ApnPreference上只显示一些简单信息,即APN的name和apn字段            pref.setKey(key);            pref.setTitle(name);            pref.setSummary(apn);            pref.setPersistent(false);            pref.setOnPreferenceChangeListener(this);            //type仅为MMS时,selectable为false            boolean selectable = ((type == null) || !type.equals("mms"));            pref.setSelectable(selectable);            if (selectable) {                if ((mSelectedKey != null) && mSelectedKey.equals(key)) {                    pref.setChecked();                }                addApnToList(pref, mnoApnList, mvnoApnList, r, mvnoType, mvnoMatchData);            } else {                //MMS加入到mmsAPN对应的list中                addApnToList(pref, mnoMmsApnList, mvnoMmsApnList, r, mvnoType, mvnoMatchData);            }            cursor.moveToNext();        }        cursor.close();        //前面第一部分提过,一个卡要么支持普通运营商,要么是虚拟运营商        if (!mvnoApnList.isEmpty()) {            mnoApnList = mvnoApnList;            mnoMmsApnList = mvnoMmsApnList;        }        //将ApnPreference显示到界面上        for (Preference preference : mnoApnList) {            apnList.addPreference(preference);        }        for (Preference preference : mnoMmsApnList) {            apnList.addPreference(preference);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89

这一部分的内容比较简单,就是利用数据库得到当前卡对应的APN信息,然后构造对应的Preference显示到界面上。

ApnSettings加载完当前卡可用APN对应的Preference后,用户就可以手动进行点击和切换操作了。

当用户进行点击操作时,将进入到ApnEditor的界面,加载更加详细的APN信息:

public boolean onPreferenceTreeClick(Preference preference) {    int pos = Integer.parseInt(preference.getKey());    Uri url = ContentUris.withAppendedId(Telephony.Carriers.CONTENT_URI, pos);    //注意此时的Action为ACTION_EDIT    startActivity(new Intent(Intent.ACTION_EDIT, url));    return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们先不深入ApnEditor界面,后面介绍新建APN的流程时,会再遇到这个类。

当用户进行切换操作后,ApnSettings的onPreferenceChange函数将被调用:

public boolean onPreferenceChange(Preference preference, Object newValue) {    ..........    if (newValue instanceof String) {        setSelectedApnKey((String) newValue);    }    return true;}private void setSelectedApnKey(String key) {    mSelectedKey = key;    ContentResolver resolver = getContentResolver();    //更新数据库设置新的prefer APN    //该APN将保存到TelephonyProvider的SP和数据库中    ContentValues values = new ContentValues();    values.put(APN_ID, mSelectedKey);    resolver.update(PREFERAPN_URI, values, null, null);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

前面已经提到过,在DcTracker的构造函数中监听了数据库变化,代码如下:

........mApnObserver = new ApnChangeObserver();phone.getContext().getContentResolver().registerContentObserver(        Telephony.Carriers.CONTENT_URI, true, mApnObserver);........
  • 1
  • 2
  • 3
  • 4
  • 5

当APN的数据库发生变化时,ApnChangeObserver的onChange函数将被调用,发送DctConstants.EVENT_APN_CHANGED触发onApnChanged函数:

private void onApnChanged() {    ..........    createAllApnList();    setInitialAttachApn();    //手动切换prefer APN后,如果是数据卡,可能会断开当前的数据连接    cleanUpConnectionsOnUpdatedApns(!isDisconnected);    if (mPhone.getSubId() == SubscriptionManager.getDefaultDataSubscriptionId()) {        //当前卡为数据卡,重新拨号        setupDataOnConnectableApns(Phone.REASON_APN_CHANGED);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

容易看出,当用户切换APN后,DcTracker将重新调用createAllApnList和setInitialAttachApn。 
此时,DcTracker将用户选择的APN指定为prefer APN。 
如果是数据卡的话,还会用新的perfer APN进行拨号。

1.1、cleanUpConnectionsOnUpdatedApns 
这里我们跟进一下cleanUpConnectionsOnUpdatedApns函数,看看修改prefer APN后,在什么情况下会触发断开连接的操作。

private void cleanUpConnectionsOnUpdatedApns(boolean tearDown) {    ...............    if (mAllApnSettings.isEmpty()) {        cleanUpAllConnections(tearDown, Phone.REASON_APN_CHANGED);    } else {        for (ApnContext apnContext : mApnContexts.values()) {            .............            boolean cleanUpApn = true;            //取出当前使用的waitApns            ArrayList<ApnSetting> currentWaitingApns = apnContext.getWaitingApns();            //注意到apnContenxt的状态必须不是断开的,即已经连接或正在连接            if ((currentWaitingApns != null) && (!apnContext.isDisconnected())) {                int radioTech = mPhone.getServiceState().getRilDataRadioTechnology();                //由于我们更新了prefer APN,因此可能生成新的waitingApns                ArrayList<ApnSetting> waitingApns = buildWaitingApns(                        apnContext.getApnType(), radioTech);                .............                if (waitingApns.size() == currentWaitingApns.size()) {                    cleanUpApn = false;                    for (int i = 0; i < waitingApns.size(); i++) {                        //waitingApns的size发生改变或者内容发生改变时,cleanUpApn就是true                        if (!currentWaitingApns.get(i).equals(waitingApns.get(i))) {                            cleanUpApn = true;                            apnContext.setWaitingApns(waitingApns);                            break;                        }                    }                }            }            if (cleanUpApn) {                apnContext.setReason(Phone.REASON_APN_CHANGED);                //进行断开连接的操作;对于数据卡才有实际的意义                cleanUpConnection(true, apnContext);            }        }    }    ................}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

不同的ApnContext对应着不同的网络类型。 
当修改APN数据库,使得一个ApnContext可用于拨号waitingApns发生变化时,就会断开当前ApnContext的连接,之后再进行重拨。

1.2 buildWaitingApns 
现在我们来看看构造waitingApns相关的buildWaitingApns函数:

private ArrayList<ApnSetting> buildWaitingApns(String requestedApnType, int radioTech) {    ................    ArrayList<ApnSetting> apnList = new ArrayList<ApnSetting>();    //APN_TYPE_DUN特殊处理,不用管这个    if (requestedApnType.equals(PhoneConstants.APN_TYPE_DUN)) {        ApnSetting dun = fetchDunApn();        if (dun != null) {            apnList.add(dun);            return apnList;        }    }    IccRecords r = mIccRecords.get();    String operator = (r != null) ? r.getOperatorNumeric() : "";    boolean usePreferred = true;    try {        //config_dontPreferApn默认为false,因此有prefer APN时,优先使用prefer APN        usePreferred = ! mPhone.getContext().getResources().getBoolean(com.android.                internal.R.bool.config_dontPreferApn);    } catch (Resources.NotFoundException e) {        ............        usePreferred = true;    }    if (usePreferred) {        //前面介绍过,从数据库中取出prefer APN        mPreferredApn = getPreferredApn();    }    ..............    //prefer APN要能处理当前的requestApnType    //即default类型的prefer APN,只能影响default类型的ApnContext    if (usePreferred && mCanSetPreferApn && mPreferredApn != null &&            mPreferredApn.canHandleType(requestedApnType)) {        //preferApn必须与当前卡匹配        if (mPreferredApn.numeric.equals(operator)) {            //能支持当前的无线传输技术            if (ServiceState.bitmaskHasTech(mPreferredApn.bearerBitmask, radioTech)) {                //一但prefer APN可用,就会返回prefer APN                apnList.add(mPreferredApn);                ........                return apnList;            } else {                setPreferredApn(-1);                mPreferredApn = null;            }        } else {            setPreferredApn(-1);            mPreferredApn = null;        }    }    ............    //否则从卡当前可用APN中取出类型合适的    if (mAllApnSettings != null) {        ............        for (ApnSetting apn : mAllApnSettings) {            if (apn.canHandleType(requestedApnType)) {                if (ServiceState.bitmaskHasTech(apn.bearerBitmask, radioTech)) {                    .........                    apnList.add(apn);                } else {                    ........                }            }         }    } else {        .........    }    return apnList;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

buildWaitingApns的代码看起来比较复杂,其实上就是有可用的prefer APN时,选择prefer APN; 
没有可用的prefer APN时,从现有卡对应的APN中,取出支持当前网络类型和无线技术的APN。

结合buildWaitingApns和cleanUpConnectionsOnUpdatedApns函数,我们可以知道: 
1、对于数据卡而言,在连网状态下,当手动切换prefer APN时,如果这个prefer APN支持default类型,那么必然会断开原有连接,建立新的连接 (因为default ApnContext的waitingApns发生了变化,size或者内容发生改变)。 
2、对于数据卡而言,在连网状态下,当我们新建或删除一个default类型的APN时,如果这个卡当前没有可用的prefer APN,那么也会断开原有连接,建立新的连接 (因为default ApnContext的waitingApns的size发生了变化)。

2、新建APN的操作 
现在我们回过头来看看,APN界面相关的第二部分,即新建APN的操作。 
原生代码中,新建APN的按键定义于ApnSettings界面的menu中,我们看看对应的代码:

public boolean onOptionsItemSelected(MenuItem item) {    switch (item.getItemId()) {        case MENU_NEW:            addNewApn();            return true;        .......    }    return super.onOptionsItemSelected(item);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

跟进一下addNewApn函数:

private void addNewApn() {    //此时的action是Intent.ACTION_INSERT    Intent intent = new Intent(Intent.ACTION_INSERT, Telephony.Carriers.CONTENT_URI);    int subId = mSubscriptionInfo != null ? mSubscriptionInfo.getSubscriptionId()            : SubscriptionManager.INVALID_SUBSCRIPTION_ID;    intent.putExtra(SUB_ID, subId);    //加入虚拟运营商相关的内容    if (!TextUtils.isEmpty(mMvnoType) && !TextUtils.isEmpty(mMvnoMatchData)) {        intent.putExtra(MVNO_TYPE, mMvnoType);        intent.putExtra(MVNO_MATCH_DATA, mMvnoMatchData);    }    //拉起ApnEditor界面    startActivity(intent);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

随着流程,我们进入到了ApnEditor。先来看看ApnEditor的onCreate函数:

public void onCreate(Bundle icicle) {    super.onCreate(icicle);    addPreferencesFromResource(R.xml.apn_editor);    //找到xml中定义的组件    sNotSet = getResources().getString(R.string.apn_not_set);    mName = (EditTextPreference) findPreference("apn_name");    mApn = (EditTextPreference) findPreference("apn_apn");    mProxy = (EditTextPreference) findPreference("apn_http_proxy");    ................    mRes = getResources();    //取出拉起ApnEditor的Intent中的内容    final Intent intent = getIntent();    final String action = intent.getAction();    mSubId = intent.getIntExtra(ApnSettings.SUB_ID,    SubscriptionManager.INVALID_SUBSCRIPTION_ID);    //初始时mFirstTime为true    mFirstTime = icicle == null;    if (action.equals(Intent.ACTION_EDIT)) {        Uri uri = intent.getData();        .......        //前面提到过,直接点击已经加载的ApnPreference,将发送Intent.ACTION_EDIT拉起ApnEditor,显示更详细的APN信息        //这里就是保存对应Uri,利用该Uri访问数据库,加载对应的数据        mUri = uri;    } else if (action.equals(Intent.ACTION_INSERT)){        if (mFirstTime || icicle.getInt(SAVED_POS) == 0) {            Uri uri = intent.getData();            ..........            //向数据库中插入数据,不过此时还未保存实际的APN信息            mUri = getContentResolver().insert(uri, new ContentValues());        } else {            ..............        }        mNewApn = true;        ......................    } else {        finish();        return;    }    //查询mUri对应的数据    mCursor = getActivity().managedQuery(mUri, sProjection, null, null);    mCursor.moveToFirst();    .........    //利用查询的数据填充界面    fillUi();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

当ApnEditor界面被拉起来后,如果是查看已有的APN信息,那么fillUi函数会利用数据库中的信息填充界面; 
如果是新建APN,此时UI界面的各字段就是空白的,等待用户进行填充。

需要注意的是,apns-conf.xml中加载的APN,默认是仅可读的,用户只能查看该类型的APN。 
对于自己建立的APN,则可以在界面上进行编辑操作。

在ApnEditor的界面上,通过menu来保存或者删除APN,对应的代码如下:

public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {    super.onCreateOptionsMenu(menu, inflater);    // If it's a new APN, then cancel will delete the new entry in onPause    // 在onCreate中已经看到了,新建APN时,mNewApn为true    // 因此没有delete图标,只有save和cancel的按键    if (!mNewApn) {        menu.add(0, MENU_DELETE, 0, R.string.menu_delete)                .setIcon(R.drawable.ic_menu_delete);    }    menu.add(0, MENU_SAVE, 0, R.string.menu_save)            .setIcon(android.R.drawable.ic_menu_save);    menu.add(0, MENU_CANCEL, 0, R.string.menu_cancel)            .setIcon(android.R.drawable.ic_menu_close_clear_cancel);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

我们看看按键对应的处理代码:

public boolean onOptionsItemSelected(MenuItem item) {    switch (item.getItemId()) {        case MENU_DELETE:            deleteApn();            return true;        case MENU_SAVE:            //调用validateAndSave保存新建的APN            if (validateAndSave(false)) {                finish();            }            return true;        case MENU_CANCEL:            if (mNewApn) {                //对于新建的APN,直接删除数据库中对应数据                getContentResolver().delete(mUri, null, null);            }            finish();            return true;    }    return super.onOptionsItemSelected(item);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

我们主要关注保存新建APN使用的validateAndSave函数:

private boolean validateAndSave(boolean force) {    //检查用户的一些基本信息是否填写    String name = checkNotSet(mName.getText());    String apn = checkNotSet(mApn.getText());    String mcc = checkNotSet(mMcc.getText());    String mnc = checkNotSet(mMnc.getText());    //getErrorMsg将判断填写的信息是否有误    //如果有错误信息的话,将弹出dialog    if (getErrorMsg() != null && !force) {        ErrorDialog.showError(this);        return false;    }    // If it's a new APN and a name or apn haven't been entered, then erase the entry    //在ApnEditor的onCreate函数中,新建APN创建了一个空的ContentValue    //如果本次的编辑有问题,则删除该ContentValue    if (force && mNewApn && name.length() < 1 && apn.length() < 1) {        getContentResolver().delete(mUri, null, null);        return false;    }    ContentValues values = new ContentValues();    //将界面的信息保存到ContentValue中    values.put(Telephony.Carriers.NAME,            name.length() < 1 ? getResources().getString(R.string.untitled_apn) : name);    ................    //更新onCreate中插入数据库的APN信息    getContentResolver().update(mUri, values, null, null);    return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

至此,新建APN的流程基本介绍完毕,界面相关的工作其实还是比较简单的。 
从上面的代码可以看出,最终新建的APN信息还是会被保存到数据库,因此也会触发DcTracker的流程。

3、重置APN的操作 
在描述APN UI界面这一部分的最后,我们看看重置APN相关的操作。 
重置APN就是将手机的APN恢复到出厂设置的状态,即移除所有用户添加的APN和当前卡的prefer APN相关的信息。

在ApnSettings界面的menu中提供了重置APN的按键,我们看看相关的处理函数:

public boolean onOptionsItemSelected(MenuItem item) {    switch (item.getItemId()) {    ............    case MENU_RESTORE:        restoreDefaultApn();        return true;    }    ...........}private boolean restoreDefaultApn() {    //这个dialog会一直持续,直到APN恢复到出厂设置    showDialog(DIALOG_RESTORE_DEFAULTAPN);    mRestoreDefaultApnMode = true;    if (mRestoreApnUiHandler == null) {        //创建一个主线程的UiHandler,用于接收重置完成的消息        mRestoreApnUiHandler = new RestoreApnUiHandler();    }    if (mRestoreApnProcessHandler == null ||            mRestoreDefaultApnThread == null) {        //创建单独的线程进行数据库操作        mRestoreDefaultApnThread = new HandlerThread(                "Restore default APN Handler: Process Thread");        mRestoreDefaultApnThread.start();        //ProcessHandler运行在单独的线程中进行工作        //参数中传入了Ui Handler,用于给主线程发送消息        mRestoreApnProcessHandler = new RestoreApnProcessHandler(                mRestoreDefaultApnThread.getLooper(), mRestoreApnUiHandler);    }    //发送消息,开始进行重置工作    mRestoreApnProcessHandler            .sendEmptyMessage(EVENT_RESTORE_DEFAULTAPN_START);    return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

我们看看RestoreApnProcessHandler中的处理EVENT_RESTORE_DEFAULTAPN_START的函数:

public void handleMessage(Message msg) {    switch (msg.what) {        case EVENT_RESTORE_DEFAULTAPN_START:            ContentResolver resolver = getContentResolver();            //删除数据库中的信息            resolver.delete(DEFAULTAPN_URI, null, null);            //向Ui Handler发送完成的信息            mRestoreApnUiHandler                    .sendEmptyMessage(EVENT_RESTORE_DEFAULTAPN_COMPLETE);            break;   }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

我们先看看TelephonyProvider收到删除数据库消息的处理来流程:

public synchronized int delete(Uri url, String where, String[] whereArgs) {    ............    SQLiteDatabase db = mOpenHelper.getWritableDatabase();    int match = s_urlMatcher.match(url);    switch (match)    {        ..........        case URL_RESTOREAPN: {            count = 1;            restoreDefaultAPN(subId);            break;        }        ......    }    ..........}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

跟着流程进入到TelephonyProvider的restoreDefaultAPN函数:

private void restoreDefaultAPN(int subId) {    SQLiteDatabase db = mOpenHelper.getWritableDatabase();    try {        //整个数据库均被删除了        db.delete(CARRIERS_TABLE, null, null);    } catch (SQLException e) {        loge("got exception when deleting to restore: " + e);    }    //前面已经提到过,TelephonyProvider在SharedPreference和数据库中均记录了subId对应的prefer APN    //上面删除了数据库,此处将SP中的信息也删除    setPreferredApnId((long) INVALID_APN_ID, subId);    //重新加载数据库,仅载入apns-conf.xml中记录的APN信息    mOpenHelper.initDatabase(db);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

当数据库相关的工作执行完毕后,ApnSettings中的RestoreApnUiHandler开始处理EVENT_RESTORE_DEFAULTAPN_COMPLETE消息:

public void handleMessage(Message msg) {    switch (msg.what) {        case EVENT_RESTORE_DEFAULTAPN_COMPLETE:            Activity activity = getActivity();            if (activity == null) {                mRestoreDefaultApnMode = false;                return;            }            //重新利用数据库中的资源填充界面            fillList();            getPreferenceScreen().setEnabled(true);            mRestoreDefaultApnMode = false;            //移除界面上的Dialog            removeDialog(DIALOG_RESTORE_DEFAULTAPN);            .........    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

整体来看,APN界面相关的工作还是比较简单的,主要的工作量还是集中在操作数据库上。

三、Android中APN配置相关的漏洞 
在这篇博客的最后,我们来看看目前原生代码中APN配置相关的漏洞。这个漏洞将使得在某些场景下,用户明明关闭了数据业务开关,但后台应用仍然可以利用数据业务上网,消耗用户的流量。

前面我们已经提到过,初始时及APN界面改变时,DcTracker将创建当前卡可以使用的APN。 
从前面的分析,我们知道DcTracker创建一个具体ApnSetting的函数为makeApnSetting,其中解析APN type的函数为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 {        result = types.split(",");    }    return result;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

当一个APN的type没有定义时,framework认为这个APN的类型为APN_TYPE_ALL。 
因此,当一个用户新建一个APN,但不设置type域,或者apns-conf.xml中某个APN的type域没有配置时,这个APN的type就是APN_TYPE_ALL。

现在,我们看看framework拨号的部分流程:

private boolean onTrySetupData(String reason) {    ............    setupDataOnConnectableApns(reason);    return true;}private void setupDataOnConnectableApns(String reason) {    setupDataOnConnectableApns(reason, RetryFailures.ALWAYS);}private void setupDataOnConnectableApns(String reason, RetryFailures retryFailures) {    .........    for (ApnContext apnContext : mPrioritySortedApnContexts) {        ArrayList<ApnSetting> waitingApns = null;        .............        //判断ApnContext是否激活        if (apnContext.isConnectable()) {            ..........            //激活后,开始拨号            trySetupData(apnContext, waitingApns);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

这里我们仅截取重点的部分,关于ApnContext激活及数据业务拨号的详细过程可以参考: 
Android7.0 数据拨号前的准备工作 
Android7.0 数据业务长连接拨号过程

我们跟进一下trySetupData函数:

private boolean trySetupData(ApnContext apnContext, ArrayList<ApnSetting> waitingApns) {    ................    //判断拨号的条件是否满足    if (apnContext.isConnectable() && (isEmergencyApn ||            (isDataAllowed && isDataAllowedForApn(apnContext) &&            isDataEnabled(checkUserDataEnabled) && !isEmergency())) && !mColdSimDetected ) {        ...............        int radioTech = mPhone.getServiceState().getRilDataRadioTechnology();        ..............        if (apnContext.getState() == DctConstants.State.IDLE) {            if (waitingApns == null) {                //正常数据拨号,apnContext.getApnType的值为APN_TYPE_DEFAULT                //buildWaitingApns我们前面已经分析过,就是从当前卡的可用ApnSetting中选出满足条件的APN                //现在,我们假设用户选择的prefer APN的type是空的;或者用户没有选择prefer APN,但卡有一个type为空的APN可用                //由于type为空的APN对应与APN_TYPE_ALL,因此该APN可以处理APN_TYPE_DEFAULT的需求                //会被加入到waitingApns中                waitingApns = buildWaitingApns(apnContext.getApnType(), radioTech);            }            if (waitingApns.isEmpty()) {                ............            } else {                //waitingApns被保存到apnContext中                apnContext.setWaitingApns(waitingApns);                ...........            }            //继续拨号流程            boolean retValue = setupData(apnContext, radioTech);            ............        }    } else {        .........    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

按照注释中的假设,我们知道当用户的数据卡包含一个type域为空的APN时,这个空APN可能会被加入到拨号使用的apnContext中,对应的type变为APN_TYPE_ALL。

我们跟进一下setupData函数:

private boolean setupData(ApnContext apnContext, int radioTech) {    ...........    //从apnContext中取出拨号使用的ApnSetting,假设我们就取出了APN_TYPE_ALL的APN    apnSetting = apnContext.getNextApnSetting();    ...........    int profileId = apnSetting.profileId;    if (profileId == 0) {        //APN_TYPE_ALL对应的profileId是RILConstants.DATA_PROFILE_DEFAULT,就是0        profileId = getApnProfileID(apnContext.getApnType());    }    //注意这个地方,我们等会儿还会再分析    //这里是从当前可用的DataConnection中,找到可以复用的    //也就说如果已经存在一个DataConnection能够支持当前的apnContext,那么就尽量使用这个DataConnection    if (apnContext.getApnType() != PhoneConstants.APN_TYPE_DUN ||            teardownForDun() == false) {        //具体的匹配规则等下分析        dcac = checkForCompatibleConnectedApnContext(apnContext);        if (dcac != null) {            // Get the dcacApnSetting for the connection we want to share.            ApnSetting dcacApnSetting = dcac.getApnSettingSync();            if (dcacApnSetting != null) {                // Setting is good, so use it.                //使用该dataConnection的apnSetting                apnSetting = dcacApnSetting;            }        }    }    //假设我们之前没有建立数据连接,这是第一次,于是进入到以下分支    if (dcac == null) {        //有些无线技术仅支持单一的无线连接        if (isOnlySingleDcAllowed(radioTech)) {            //禁保留最高优先级的dataConnection            ..............        }        //这里不是复用,只是取用空闲的资源        dcac = findFreeDataConnection();        if (dcac == null) {            dcac = createDataConnection();        }        ...........    }    final int generation = apnContext.incAndGetConnectionGeneration();    //apnContext如它的名字一样,类似于一个运行环境,保存相应的信息    apnContext.setDataConnectionAc(dcac);    apnContext.setApnSetting(apnSetting);    apnContext.setState(DctConstants.State.CONNECTING);    Message msg = obtainMessage();    msg.what = DctConstants.EVENT_DATA_SETUP_COMPLETE;    msg.obj = new Pair<ApnContext, Integer>(apnContext, generation);    //开始拨号了    dcac.bringUp(apnContext, profileId, radioTech, msg, generation);    ....................}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

现在我们使用拨号的APN的type为APN_TYPE_ALL,到目前为止没有任何问题。

我们跟进DataConnection的流程:

private class DcInactiveState extends State {    ..........    public boolean processMessage(Message msg) {        ..........        switch (msg.what) {            .......            case EVENT_CONNECT:                .............                ConnectionParams cp = (ConnectionParams) msg.obj;                //初始化拨号参数                if (initConnection(cp)) {                    //发送消息给modem                    onConnect(mConnectionParams);                    transitionTo(mActivatingState);                } else {                    .........                }            break;        }        .........    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这里我们跟进以下initConnection:

private boolean initConnection(ConnectionParams cp) {    ApnContext apnContext = cp.mApnContext;    //注意mApnSetting为null时,才从cp中取出    if (mApnSetting == null) {        // Only change apn setting if it isn't set, it will        // only NOT be set only if we're in DcInactiveState.        mApnSetting = apnContext.getApnSetting();    }    .............    //注意这个位置,DataConnection保存了拨号apnContext    mApnContexts.put(apnContext, cp);    .............}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

现在假设我们数据拨号成功,modem成功返回结果,DataConnection进入到自己的Active状态:

private class DcActiveState extends State {    public void enter() {        .........        boolean createNetworkAgent = true;        .........        if (createNetworkAgent) {            //创建NetworkAgent注册到ConnectivityService            //ConnectivityService利用NetworkManagementService配置网络路由后就可以上网了            mNetworkAgent = new DcNetworkAgent(getHandler().getLooper(), mPhone.getContext(),                    "DcNetworkAgent", mNetworkInfo, makeNetworkCapabilities(), mLinkProperties,                    50, misc);            }    }    ...........}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里我们看看makeNetworkCapabilities函数:

private NetworkCapabilities makeNetworkCapabilities() {    NetworkCapabilities result = new NetworkCapabilities();    result.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);    if (mApnSetting != null) {        //ApnSetting可以有多个Type,因此这里用来for循环        //这也是为什么很多厂商在配置APN时,会把一个支持default和mms的APN,拆分成两个        //主要是避免发送彩信时,建立起一个支持internet能力的连接,引起潜在的流量消耗        for (String type : mApnSetting.types) {            switch (type) {                    case PhoneConstants.APN_TYPE_ALL: {                        //type_all具备了各种能力                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_SUPL);                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_FOTA);                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS);                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_CBS);                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_IA);                        break;                    }                    case PhoneConstants.APN_TYPE_DEFAULT: {                        //default才有internet能力,终端可以用这个dataConnection访问网络                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);                        break;                    }                    case PhoneConstants.APN_TYPE_MMS: {                        //mms的连接只能发彩信,不能上网                        result.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS);                        break;                    }                .............            }        }        ........    }    .......}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

从上面的代码,我们可以看出,当用户利用type域为空的APN建立长连接时,该连接对应的网络支持各种能力。 
在一般的情况下,这或许没有问题。但当用户使用的卡支持IMS功能,同时用户激活了IMS功能时,问题就来了。

IMS和MMS一样,都需要建立自己的DataConnection。 
不同的是:MMS完成业务后,会断开建立的DataConnection。 
但IMS建立的DataConnection将长时间存在,只是对应的网络没有访问Internet的能力罢了。

现在假设APN_TYPE_ALL的DataConnection已经建立成功了,手机卡支持IMS功能,需要建立一条IMS连接。 
IMS建立DataConnection的流程与前面基本一致,不同的地方在setupData中,我们重新看看这部分代码:

private boolean setupData(ApnContext apnContext, int radioTech) {    ........    if (apnContext.getApnType() != PhoneConstants.APN_TYPE_DUN ||            teardownForDun() == false) {        //检查是否有可复用的DataConnection        //当APN_TYPE_ALL已经被用于建立dataConnection后        //这里就会返回一个有效的dcac        dcac = checkForCompatibleConnectedApnContext(apnContext);        if (dcac != null) {            // Get the dcacApnSetting for the connection we want to share.            ApnSetting dcacApnSetting = dcac.getApnSettingSync();            if (dcacApnSetting != null) {                // Setting is good, so use it.                //apnSetting被替换为APN_TYPE_ALL的apnSetting                apnSetting = dcacApnSetting;            }        }    }    if (dcac == null) {        .........    }    .............    //向APN_TYPE_ALL的dataConnection发送CONNECT消息    dcac.bringUp(apnContext, profileId, radioTech, msg, generation);    .............}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

我们跟进一下checkForCompatibleConnectedApnContext函数:

private DcAsyncChannel checkForCompatibleConnectedApnContext(ApnContext apnContext) {    String apnType = apnContext.getApnType();    .............    DcAsyncChannel potentialDcac = null;    ApnContext potentialApnCtx = null;    for (ApnContext curApnCtx : mApnContexts.values()) {        DcAsyncChannel curDcac = curApnCtx.getDcAc();        if (curDcac != null) {            //APN_TYPE_ALL对应的ApnSetting被取出            ApnSetting apnSetting = curApnCtx.getApnSetting();            if (dunSetting != null) {                ..........            } else if (apnSetting != null && apnSetting.canHandleType(apnType)) { //APN_TYPE_CALL可以处理TYPE_IMS                switch (curApnCtx.getState()) {                    case CONNECTED:                        .........                        return curDcac;                    case RETRYING:                    case CONNECTING:                        potentialDcac = curDcac;                        potentialApnCtx = curApnCtx;                    default:                        // Not connected, potential unchanged                        break;                }            }        } else {            ......        }    }    if (potentialDcac != null) {        ........        return potentialDcac;    }    .........}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

从上面的代码不难看出,如果APN_TYPE_ALL对应的apn先于IMS APN被用于建立DataConnection,那么IMS APN将复用APN_TYPE_ALL对应的DataConnection。 
我们再来回顾一下DataConnection的代码,此时DataConnection已经在DcActiveState:

private class DcActiveState extends State {    ..........    public boolean processMessage(Message msg) {        ........        switch (msg.what) {            //APN_TYPE_ALL的dataConnection此时已经完成拨号            case EVENT_CONNECT: {                ConnectionParams cp = (ConnectionParams) msg.obj;                // either add this new apn context to our set or                // update the existing cp with the latest connection generation number                //DataConnection的mApnContexts再次保留了参数                //mApnContexts是一个hashMap                //此时已经有两个键值对了,一个是default APN context,对应APN_TYPE_ALL的参数                //另一个是IMS APN context,对应的参数也被替换为APN_TYPE_ALL                mApnContexts.put(cp.mApnContext, cp);                notifyConnectCompleted(cp, DcFailCause.NONE, false);                retVal = HANDLED;                break;            }            ..........        }        ......    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

至此,IMS完全复用了APN_TYPE_ALL的dataConnection,因此IMS服务也将通过长连接对应的网络进行。 
前面已经分析过,该网络是支持IMS服务的,因此没什么问题。

现在假设用户不愿意使用数据业务了,于是用户关闭了数据业务开关,这部分详细的流程可以参考Android7.0 数据业务长连接去拨号过程,此处我们仅关心APN相关的部分:

//此时关闭的default类型的apnContext,即APN_TYPE_ALL对应的private void cleanUpConnection(boolean tearDown, ApnContext apnContext) {    ...........    DcAsyncChannel dcac = apnContext.getDcAc();    ...........    if (tearDown) {        if (apnContext.isDisconnected()) {            .........        } else {            if (dcac != null) {                if (apnContext.getState() != DctConstants.State.DISCONNECTING) {                    boolean disconnectAll = false;                    ........                    Message msg = obtainMessage(DctConstants.EVENT_DISCONNECT_DONE, pair);                    if (disconnectAll) {                        ..........                    } else {                        //发送消息给DataConnection的DcActiveState处理                        apnContext.getDcAc()                                .tearDown(apnContext, apnContext.getReason(), msg);                    }                    apnContext.setState(DctConstants.State.DISCONNECTING);                    mDisconnectPendingCount++;                }            } else {                .........            }        }    } else {        ........    }    .........}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

看看DataConnection的DcActiveState处理流程:

private class DcActiveState extends State {    ..........    public boolean processMessage(Message msg) {        ........        switch (msg.what) {            ........            case EVENT_DISCONNECT: {                DisconnectParams dp = (DisconnectParams) msg.obj;                ...........                if (mApnContexts.containsKey(dp.mApnContext)) {                    ..........                    //注意这里,size等于1时才会断开dataConnection                    //同时离开dcActiveState                    if (mApnContexts.size() == 1) {                        mApnContexts.clear();                        mDisconnectParams = dp;                        mConnectionParams = null;                        dp.mTag = mTag;                        tearDownData(dp);                        transitionTo(mDisconnectingState);                    } else {                        //否则只是移除存储信息而已                        mApnContexts.remove(dp.mApnContext);                        notifyDisconnectCompleted(dp, false);                } else {                    ...............                }                .............            }        }        ........    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

从上面的代码可以看出,当IMS复用APN_TYPE_ALL的dataConnection时,即使用户关闭数据业务开关,dataConnection也不会断开,仍然保留在DcActiveState。

public void exit() {    ........    mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED,            reason, mNetworkInfo.getExtraInfo());    if (mNetworkAgent != null) {        mNetworkAgent.sendNetworkInfo(mNetworkInfo);        mNetworkAgent = null;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面是DcActiveState的exit函数,从中可以看出只有DataConnection离开DcActiveState时,对应的NetworkAgent才会变成DISCONNECTED状态。 
当dataConnection的NetworkAgent断开时,ConnectivityService才会清除对应的网络信息,例如路由之类的。

这一部分的内容比较长,我们再来回顾总结一下: 
假设用户使用的是支持IMS网络的卡,同时激活了IMS能力。 
用户选用了一个APN type域为空的APN作为prefer APN。 
假设数据开关开机时是开启状态。

现在手机重启了,由于数据能力设置完成后,就会进行拨号操作, 
因此长连接可能优先于IMS连接被建立成功。

在这种场景下,由于长连接对应的APN type被解析为APN_TYPE_ALL,因此IMS连接可能复用长连接。 
当用户关闭数据开关后,由于IMS仍然需要使用该长连接,因此该长连接不会被断开。 
ConnectivityService就不会通知应用网络断开,也不会清除网络对应的路由等信息。

因此,最终表现的结果就是数据开关明明是关闭状态,但后台还是在使用数据流量。

需要注意的是: 
在分析时,我假设了许多条件,给人感觉好像这是个凭空想象出的问题。 
实际上,这确实是国外用户上报的真实问题。 
个人觉得,可能国外IMS普及率高,同时流量便宜(或者他们太土豪了),使得数据开关一直处于开启状态,导致这个问题会概率性发生。 
基本上,只要APN的type域为空,同时数据连接先于IMS连接被建立起来,问题就会发生。 
尽管我是用原生代码来分析,但Qualcomm和MTK都没有修复这个问题,主要依赖于具体的厂商来修复了。

总结 
本篇博客主要分析了APN相关的常用流程,以及实际存在的APN相关的漏洞。

APN加载和编辑的内容比较单一,容易理解。 
但APN相关漏洞这一部分,与数据业务拨号流程关联性比较大,需要先对数据业务有一定理解才能掌握。

最后说一下这个漏洞如何解决: 
其实这个漏洞简单讲就是:APN的type为空时,建立的数据连接具有Internet和IMS能力; 
IMS APN建立的连接将复用该数据连接后,导致该数据连接不受数据开关控制了。

解决方案大致分为三种: 
1、parse Apn type时,对于用户建立的APN,如果type域为空时,不将其解析成APN_TYPE_ALL,而是解析成default。 
只有apns-conf.xml中的默认APN的type为空时,才解析成APN_TYPE_ALL。 
这样可以避免用户操作带来的问题,但apns-conf.xml配置有问题的话,该漏洞依然存在。

2、保证IMS连接优先于建立。 
IMS可以复用APN_TYPE_ALL的连接;但APN_TYPE_ALL无法复用IMS的连接。 
因此,若能保证IMS网络先完成注册,再进行数据拨号,那么该漏洞就不存在了。 
但IMS网络的注册难以有效保证,这个思路可行,实际操作几乎是不可能的。

3、在判断是否复用DataConnection时,对IMS特殊处理一下。 
正常情况下,使用default type的APN建立dataConnection,IMS本来就是无法复用的。 
因此只需要修改DcTracker的setupData函数,判断当前的拨号的APN类型为IMS时,不复用任何连接即可。 
这种方案是目前最简单有效的。

由于一些保密的原因,具体修改的代码此处就不附上了,按照上面的思路还是很好实现的。



原文地址:http://blog.csdn.net/gaugamela/article/details/53199141

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 旧鞋穿着磨脚了怎么办 皮鞋磨脚怎么办小窍门 拉链从下面开了怎么办 高帮足球鞋松了怎么办 橡筋裤头太紧了怎么办 内增高鞋跟太高怎么办 电脑增高架高了怎么办 银行取钱走后回来说少了怎么办 运动t桖太大了怎么办 袖口松紧太紧了怎么办 衣服穿着就皱了怎么办 麻料的衣服很皱怎么办 麻料衣服皱了怎么办 棉麻裤子皱了怎么办 裙子屁股坐皱了怎么办 真丝衣服洗皱了怎么办 粘纤的衣服皱了怎么办 硅胶手机壳粘灰怎么办 橡筋裤子买大了怎么办 橡筋裤子腰小了怎么办 地垫粘瓷砖上怎么办 汽车围裙锈透了怎么办 万能胶水沾到手上怎么办 圆领体恤领口容易皱怎么办 上衣剪了个洞怎么办 上衣破了个洞怎么办 鸟屎腐蚀车漆怎么办 毛风衣叠久了怎么办 黑色的衣服沾毛怎么办 雪纺裙子弄上油怎么办 内衣买小了怎么办妙招 长裤衬衫裙邹了怎么办 100棉衬衣皱了怎么办? 短袖t恤袖口大了怎么办 短袖底下卷边了怎么办 棉质短袖衫缩水怎么办 纯棉t恤缩水了怎么办 t恤缩水变小了怎么办 衣服掉在雨棚上怎么办 车衣密码锁忘记密码怎么办 衣服的铁拉链弯怎么办