android 7.0APN信息加载设置流程

博文原址 :https://blog.csdn.net/gaugamela/article/details/53199141

终端中有一个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 createApnList(Cursor cursor) {
    //Framework是用的APN数据结构为ApnSetting
    ArrayList mnoApns = new ArrayList();
    ArrayList mvnoApns = new ArrayList();
    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 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();
        } 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 dps = new ArrayList();
        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 mnoApnList = new ArrayList();
        ArrayList mvnoApnList = new ArrayList();
        ArrayList mnoMmsApnList = new ArrayList();
        ArrayList mvnoMmsApnList = new ArrayList();

        //从数据库中得到原来用户选择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时,不复用任何连接即可。
这种方案是目前最简单有效的。

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

你可能感兴趣的:(源码分析,apn设置)