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<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時,不復用任何連接即可。
這種方案是目前最簡單有效的。

由於一些保密的原因,具體修改的代碼此處就不附上了,按照上面的思路還是很好實現的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章