1. 程式人生 > >APN---Telephony data Part I

APN---Telephony data Part I

關於Telephony data 準備寫三篇,本文是第一部分APN,根據Android O原始碼,簡單總結了Android上APN相關的知識點。分以下三部分:
1. 預置APN資料載入
2. APN欄位
3. APN的顯示和編輯


APN(access point name)決定了我們手機接入網路的方式,一般我們手機都做了預設配置,也可以根據需要進行手動配置。APN儲存在手機的“/data/user_de/0/com.android.providers.telephony/databases/”路徑下的telephony.db中(多使用者的情況下,只有owner才有這份資料),對應表是carriers。

1. 預置APN資料載入
關於db和表的建立,Subscription文章 TelephonyProvider部分已經寫了,這裡就不再贅述了。下面說下預置APN資料的載入過程。
我們預置的APN資料也是在db建立的過程中從配置檔案讀取然後載入到db的。
在db建立的過程中,DatabaseHelper.OnCreate方法會呼叫DatabaseHelper.initDatabase方法,該方法可以從配置檔案中載入我們要預置的apn資料。

        /**
         *  This function adds APNs from xml file(s) to db. The db may or may not be empty to begin
         *  with.
         */
private void initDatabase(SQLiteDatabase db) { if (VDBG) log("dbh.initDatabase:+ db=" + db); // Read internal APNS data Resources r = mContext.getResources(); XmlResourceParser parser = r.getXml(com.android.internal.R.xml.apns);//載入frameworks/base/core/res/res/xml/apns.xml 中的配置
int publicversion = -1; try { XmlUtils.beginDocument(parser, "apns"); publicversion = Integer.parseInt(parser.getAttributeValue(null, "version")); loadApns(db, parser);//呼叫loadApns方法完成載入任務。 } catch (Exception e) { loge("Got exception while loading APN database." + e); } finally { parser.close(); } // Read external APNS data (partner-provided) XmlPullParser confparser = null; //下面的部分獲取指定路徑下的APN配置檔案。 //getApnConfFile方法用於獲取配置檔案,常用路徑有三個: //1. etc/apns-conf.xml //2. telephony/apns-conf.xml //3. misc/apns-conf.xml //但是這三個檔案並不會全部載入, 只加載最新修改的那個。 File confFile = getApnConfFile(); FileReader confreader = null; if (DBG) log("confFile = " + confFile); try { confreader = new FileReader(confFile); confparser = Xml.newPullParser(); confparser.setInput(confreader); XmlUtils.beginDocument(confparser, "apns"); // Sanity check. Force internal version and confidential versions to agree int confversion = Integer.parseInt(confparser.getAttributeValue(null, "version")); if (publicversion != confversion) { log("initDatabase: throwing exception due to version mismatch"); throw new IllegalStateException("Internal APNS file version doesn't match " + confFile.getAbsolutePath()); } loadApns(db, confparser);//呼叫loadApns方法完成載入任務。 } catch (FileNotFoundException e) { // It's ok if the file isn't found. It means there isn't a confidential file // Log.e(TAG, "File not found: '" + confFile.getAbsolutePath() + "'"); } catch (Exception e) { loge("initDatabase: Exception while parsing '" + confFile.getAbsolutePath() + "'" + e); } finally { // Get rid of user/carrier deleted entries that are not present in apn xml file. // Those entries have edited value USER_DELETED/CARRIER_DELETED. if (VDBG) { log("initDatabase: deleting USER_DELETED and replacing " + "DELETED_BUT_PRESENT_IN_XML with DELETED"); } //到這裡, 需要預置的APN已經全部載入到db中了, 但是db中可能存入了不必要的資料, //下面的code是清除,更新db裡的資料。 // Delete USER_DELETED db.delete(CARRIERS_TABLE, IS_USER_DELETED + " or " + IS_CARRIER_DELETED, null); //下面的code用於清除為了解決插入資料時的衝突而加了特殊標記的記錄。 // Change USER_DELETED_BUT_PRESENT_IN_XML to USER_DELETED ContentValues cv = new ContentValues(); cv.put(EDITED, USER_DELETED); db.update(CARRIERS_TABLE, cv, IS_USER_DELETED_BUT_PRESENT_IN_XML, null); // Change CARRIER_DELETED_BUT_PRESENT_IN_XML to CARRIER_DELETED cv = new ContentValues(); cv.put(EDITED, CARRIER_DELETED); db.update(CARRIERS_TABLE, cv, IS_CARRIER_DELETED_BUT_PRESENT_IN_XML, null); if (confreader != null) { try { confreader.close(); } catch (IOException e) { // do nothing } } // Update the stored checksum setApnConfChecksum(getChecksum(confFile)); } if (VDBG) log("dbh.initDatabase:- db=" + db); }

getApnConfFile方法用於獲取配置檔案,常用路徑有三個,但是這三個檔案並不會全部載入, 只加載最新修改的那個。
1. etc/apns-conf.xml
2. telephony/apns-conf.xml
3. misc/apns-conf.xml

        private File getApnConfFile() {
            // Environment.getRootDirectory() is a fancy way of saying ANDROID_ROOT or "/system".
            File confFile = new File(Environment.getRootDirectory(), PARTNER_APNS_PATH);//etc/apns-conf.xml
            File oemConfFile =  new File(Environment.getOemDirectory(), OEM_APNS_PATH);//telephony/apns-conf.xml
            File updatedConfFile = new File(Environment.getDataDirectory(), OTA_UPDATED_APNS_PATH);//misc/apns-conf.xml
            confFile = getNewerFile(confFile, oemConfFile);
            confFile = getNewerFile(confFile, updatedConfFile);
            return confFile;
        }

2. APN欄位

APN欄位的定義在Telephony.java中的靜態內部類Carriers中,總結如下:

欄位名稱 描述
NAME APN記錄的名字; 我們手機裡內建的運營APN會使用運營商的名字,例如”中國移動(China Mobile)Net”
APN APN的名字
PROXY 代理地址
PORT 代理埠
MMSPROXY MMS代理地址
MMSPORT MMS代理埠
SERVER 伺服器地址
USER APN使用者名稱
PASSWORD APN使用者名稱對應的密碼
MMSC 多媒體訊息業務中心, 即彩信業務中心
MCC 移動國家碼
MNC 行動網路碼
NUMERIC 運營商的數字程式碼,一般是MCC+MNC的形式,例如46001
AUTH_TYPE 鑑權型別,一般使用的鑑權是PAP或CHAP
TYPE APN的型別,如mms,dun,ims等; 一個APN配置可以支援多種APN type, 不同型別用”,”分割
PROTOCOL 連線APN所使用的協議,例如IP,IPV6,IPV4V6,PPP等
ROAMING_PROTOCOL 漫遊時所使用的協議
CURRENT 新增新APN時,如果新APN和當前SIM的mcc,mnc相同,這個欄位會被設定為1。 沒有看到其他地方使用這個欄位, 所以不太清楚這個欄位的作用
CARRIER_ENABLED 用於標識這個APN是否可用
BEARER Radio Access Technology 資訊, 當前可用的有LTE(14)和eHRPD(13)
BEARER_BITMASK Radio Access Technology bitmask, 用於標明當前APN可以包含的RAT; 0表示所有的RAT都可以,否則bitmask和RAT的關係是(1 << (RAT - 1))
MVNO_TYPE 移動虛擬網路運營商(Mobile virtual network operator)的型別; 可用的資料有spn, IMSI和GID(Group Identifier Level 1)
MVNO_MATCH_DATA MVNO_TYPE資料, 這個值是和MVNO_TYPE對應的。 例如SPN: A MOBILE, BEN NL, …; IMSI: 302720x94, 2060188, …; GID: 4E, 33, …
SUBSCRIPTION_ID 用於表明這個APN屬於哪個subscription, 這個值是從siminfo表中獲取的
PROFILE_ID profile是modem側儲存資訊的方式, 這個值將APN和modem側的profile聯絡起來
MODEM_COGNITIVE 用於標明這個APN是否會在modem側設定(沒用過這個欄位)
MAX_CONNS 該APN支援的最大連線數量
WAIT_TIME 使用該APN進行資料連線時, 如果失敗, retry要等待的時間
MAX_CONNS_TIME
MTU 使用該APN建立的連線,可以傳輸的最大單元
EDITED 該APN是否可以編輯
USER_VISIBLE 是否可見,如果不可見, 那麼我們在APN選單裡是看不到的

3. APN的顯示和編輯

一般在手機的”settings->More->Mobile networks->Access point name”選單中可以看到當前手機可用的Apn資訊,也可以編輯新增Apn。進入選單後的選單如下圖:
這裡寫圖片描述
圖例中顯示了兩個apn,點選任意一個可以進入編輯介面; 點選右上角的“+”號,可以新增新apn; 右上角的選項選單用於將apn恢復成預設配置。
圖片所示介面對應的程式碼是ApnSettings.java類,這是一個PreferenceFragment子類,中間有多層繼承關係。ApnSettings作為一個fragment子類有自己的生命週期; 在啟動時,OnResume()方法會呼叫fillList()放法,該方法完成了從db中查詢Apn資料的任務,所以如果要完成一些APN的顯示定製需求,可以在這個函式中修改sql語句。

    private void fillList() {
        final TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
        //mSubscriptionInfo是在onCreate()方法中根據intent中的引數從SubscriptionManager中獲取的。
        //即指定SIM卡對應的Subscription資訊。
        final int subId = mSubscriptionInfo != null ? mSubscriptionInfo.getSubscriptionId()
                : SubscriptionManager.INVALID_SUBSCRIPTION_ID;
        //獲取mccmnc資訊,這是查詢APN資訊的重要依據。
        final String mccmnc = mSubscriptionInfo == null ? "" : tm.getSimOperator(subId);
        Log.d(TAG, "mccmnc = " + mccmnc);
        StringBuilder where = new StringBuilder("numeric=\"" + mccmnc +
                "\" AND NOT (type='ia' AND (apn=\"\" OR apn IS NULL)) AND user_visible!=0");
        //IMS APN是否需要隱藏可以在CarrierConfig中配置,
        //mHideImsApn是在OnCreate方法中從CarrierConfigManager(CarrierConfigManager.KEY_HIDE_IMS_APN_BOOL)中獲取的值
        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) {
                //獲取SIM卡對應的IcccRecords記錄, 後面會用到。
                r = mUiccController.getIccRecords(
                        SubscriptionManager.getPhoneId(subId), UiccController.APP_FAM_3GPP);
            }
            PreferenceGroup apnList = (PreferenceGroup) findPreference("apn_list");
            apnList.removeAll();

            //下面建立了四個ArrayList<ApnPreference>物件, 用於儲存不同型別的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>();

            mSelectedKey = getSelectedApnKey();
            cursor.moveToFirst();
            while (!cursor.isAfterLast()) {
                String name = cursor.getString(NAME_INDEX);//APN記錄的名字
                String apn = cursor.getString(APN_INDEX);//APN名字
                String key = cursor.getString(ID_INDEX);//這個是資料庫裡每項記錄的主鍵ID
                String type = cursor.getString(TYPES_INDEX);//型別
                String mvnoType = cursor.getString(MVNO_TYPE_INDEX);//MVNO 型別
                String mvnoMatchData = cursor.getString(MVNO_MATCH_DATA_INDEX);//MVNO 資料

                ApnPreference pref = new ApnPreference(getPrefContext());//建立ApnPreference物件儲存APN資料

                pref.setKey(key);
                pref.setTitle(name);
                pref.setSummary(apn);
                pref.setPersistent(false);
                pref.setOnPreferenceChangeListener(this);
                pref.setSubId(subId);

                boolean selectable = ((type == null) || !type.equals("mms"));//將mms型別的APN和非mms型別的APN分開處理
                pref.setSelectable(selectable);
                if (selectable) {
                    if ((mSelectedKey != null) && mSelectedKey.equals(key)) {
                        pref.setChecked();
                    }
                    //addApnToList函式負責將不同型別的APN放進前面建立的四個不同ArrayList<ApnPreference>物件中。
                    addApnToList(pref, mnoApnList, mvnoApnList, r, mvnoType, mvnoMatchData);
                } else {
                    addApnToList(pref, mnoMmsApnList, mvnoMmsApnList, r, mvnoType, mvnoMatchData);
                }
                cursor.moveToNext();
            }
            cursor.close();

            //如果有MVNO型別的APN, mvnoApnList和mnoMmsApnList就分別重新指向儲存了MVNO型別APN的mvnoApnList
            //和mvnoMmsApnList, 結果是mvnoApnList和mnoMmsApnList之前儲存的APN記錄不會被顯示出來。
            if (!mvnoApnList.isEmpty()) {
                mnoApnList = mvnoApnList;
                mnoMmsApnList = mvnoMmsApnList;

                // Also save the mvno info
            }

            for (Preference preference : mnoApnList) {//新增顯示mnoApnList中的APN記錄
                apnList.addPreference(preference);
            }
            for (Preference preference : mnoMmsApnList) {//新增顯示mnoMmsApnList中的APN記錄
                apnList.addPreference(preference);
            }
        }
    }

addApnToList函式負責將不同型別的APN放進前面建立的四個不同ArrayList<ApnPreference>物件中; 下面是具體程式碼,相關邏輯比較簡單, 在IccRecords,MVNO_type和MVNO_data資料都非空的時候呼叫ApnSetting.mvnoMatches去比較IccRecords和MVNO_data中的資料,如果匹配,那麼這個APN是有效的,會被放進List中。

    private void addApnToList(ApnPreference pref, ArrayList<ApnPreference> mnoList,
                              ArrayList<ApnPreference> mvnoList, IccRecords r, String mvnoType,
                              String mvnoMatchData) {
        if (r != null && !TextUtils.isEmpty(mvnoType) && !TextUtils.isEmpty(mvnoMatchData)) {
            if (ApnSetting.mvnoMatches(r, mvnoType, mvnoMatchData)) {
                mvnoList.add(pref);
                // Since adding to mvno list, save mvno info
                mMvnoType = mvnoType;
                mMvnoMatchData = mvnoMatchData;
            }
        } else {
            mnoList.add(pref);
        }
    }

新增,編輯APN都是從ApnSettings跳轉到ApnEditor,並由ApnEditor.validateAndSave方法將資料寫入資料庫。
當新增新APN進入ApnEditor時,ApnEditor.onCreate方法根據intent的action(ACTION_INSERT),先在資料庫中插入了一條空資料,並將包含資料記錄ID的uri儲存在mUri中,以便在ApnEditor.validateAndSave中使用。所以ApnEditor.validateAndSave將資料寫入資料庫時可以使用update方法,這樣也同時滿足了編輯操作的需求。

“Restore to default”是什麼實現的?
當點選”Restore to defualt”選單時,ApnSettings.onOptionsItemSelected 方法會呼叫
ApnSettings.restoreDefaultApn方法。

    private boolean restoreDefaultApn() {
        showDialog(DIALOG_RESTORE_DEFAULTAPN);//顯示一個友好的提示框
        mRestoreDefaultApnMode = true;//標識變數

        if (mRestoreApnUiHandler == null) {
            mRestoreApnUiHandler = new RestoreApnUiHandler();//這個是主執行緒中的handler
        }

        if (mRestoreApnProcessHandler == null ||
            mRestoreDefaultApnThread == null) {
            mRestoreDefaultApnThread = new HandlerThread(//建立了一個HandlerThread
                    "Restore default APN Handler: Process Thread");
            mRestoreDefaultApnThread.start();//啟動新建立的HandlerThread
            mRestoreApnProcessHandler = new RestoreApnProcessHandler(
                    mRestoreDefaultApnThread.getLooper(), mRestoreApnUiHandler);//使用HandlerThread的looper建立了一個handler, mRestoreApnUiHandler用於通知UI。
        }

        mRestoreApnProcessHandler
                .sendEmptyMessage(EVENT_RESTORE_DEFAULTAPN_START);// 傳送EVENT_RESTORE_DEFAULTAPN_START,開始恢復資料。
        return true;
    }

RestoreApnProcessHandler會處理EVENT_RESTORE_DEFAULTAPN_START,操作比較簡單,只是通過ContentResolver呼叫delete去刪除資料。下面我們看看Uri的內容,已經TelephonyProvider如何做的處理。
Uri處理是通過getUriForCurrSubId(Uri uri)方法,該方法是將SubId和傳入的Uri拼接到一塊; 而傳入的引數是ApnSettings定義好的常量,如下:

    public static final String RESTORE_CARRIERS_URI =
        "content://telephony/carriers/restore";
    ...
    private static final Uri DEFAULTAPN_URI = Uri.parse(RESTORE_CARRIERS_URI);

所以完整的Uri是”content://telephony/carriers/restore/subId/*“。
下面的TelephonyProvider中Macher的定義:

    static {
        s_urlMatcher.addURI("telephony", "carriers", URL_TELEPHONY);
        s_urlMatcher.addURI("telephony", "carriers/current", URL_CURRENT);

         ----------省略------------

        /*restore 對應的Uri*/
        s_urlMatcher.addURI("telephony", "carriers/restore/subId/*", URL_RESTOREAPN_USING_SUBID);
        ...
        s_currentSetMap = new ContentValues(1);
        s_currentSetMap.put(CURRENT, "1");
    }

從上面程式碼中可以看出,ApnSettings中的Uri對應的是URL_RESTOREAPN_USING_SUBID,下面截取了delete方法中的相關code:

    @Override
    public synchronized int delete(Uri url, String where, String[] whereArgs)
    {
        int count = 0;
        int subId = SubscriptionManager.getDefaultSubscriptionId();
        String userOrCarrierEdited = ") and (" +
                EDITED + "=" + USER_EDITED +  " or " +
                EDITED + "=" + CARRIER_EDITED + ")";
        String notUserOrCarrierEdited = ") and (" +
                EDITED + "!=" + USER_EDITED +  " and " +
                EDITED + "!=" + CARRIER_EDITED + ")";
        ContentValues cv = new ContentValues();
        cv.put(EDITED, USER_DELETED);

        checkPermission();

        SQLiteDatabase db = getWritableDatabase();
        int match = s_urlMatcher.match(url);
        switch (match)
        {
            ----------省略------------
            case URL_RESTOREAPN_USING_SUBID: {
                String subIdString = url.getLastPathSegment();//解析subId資料
                try {
                    subId = Integer.parseInt(subIdString);//轉換成int型別
                } catch (NumberFormatException e) {
                    loge("NumberFormatException" + e);
                    throw new IllegalArgumentException("Invalid subId " + url);
                }
                if (DBG) log("subIdString = " + subIdString + " subId = " + subId);
                // FIXME use subId in query
            }
            case URL_RESTOREAPN: {
                count = 1;
                restoreDefaultAPN(subId);//呼叫restoreDefaultAPN(int subId)方法完成restore操作
                break;
            }
            ----------省略------------
        }
        ----------省略------------
    }

restoreDefaultAPN方法會直接刪除carrier表和 shared preferences 中的資料,然後呼叫DatabaseHelper.initDatabase方法重新初始資料。也就是說APN相關資料全部刪除,重新從配置檔案中初始化。

    private void restoreDefaultAPN(int subId) {
        SQLiteDatabase db = getWritableDatabase();

        try {
            db.delete(CARRIERS_TABLE, null, null);//直接刪除carrier表中的全部資料
        } catch (SQLException e) {
            loge("got exception when deleting to restore: " + e);
        }

        //下面的code刪除了shared preferences檔案
        // delete preferred apn ids and preferred apns (both stored in diff SharedPref) for all
        // subIds
        SharedPreferences spApnId = getContext().getSharedPreferences(PREF_FILE_APN,
                Context.MODE_PRIVATE);
        SharedPreferences.Editor editorApnId = spApnId.edit();
        editorApnId.clear();
        editorApnId.apply();

        SharedPreferences spApn = getContext().getSharedPreferences(PREF_FILE_FULL_APN,
                Context.MODE_PRIVATE);
        SharedPreferences.Editor editorApn = spApn.edit();
        editorApn.clear();
        editorApn.apply();

        initDatabaseWithDatabaseHelper(db);//該方法會呼叫DatabaseHelper.initDatabase方法。
    }

結束!