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方法。
}