目的:
为了访问网络,手机必须设置合适的APN参数。Android中的apn是配置在apns-conf.xml文件中,由手机开机时加载到TelephonyProvider中。然后供设置查看和编辑,供框架使用来进行数据拨号。本文旨在描述这APN加载、显示和编辑的过程。
版本
Android 6.0
前言
APN的英文全称是Access Point Name,中文全称叫接入点,是您在通过手机上网时必须配置的一个参数,它决定了您的手机通过哪种接入方式来访问网络。从运营商角度看,APN就是一个逻辑名字,APN一般都部署在GGSN设备上或者逻辑连接到GGSN上,用户使用GPRS上网时同,都通过GGSN代理出去到外部网络。
GGSN (Gateway GPRS Support Node) 网关GPRS支持节点,GGSN(Gateway GSN,网关GSN)主要是起网关作用,它可以和多种不同的数据网络连接,如ISDN、PSPDN和LAN等。GGSN可以把GSM网中的GPRS分组数据包进行协议转换,从而可以把这些分组数据包传送到远端的TCP/IP或X.25网络。GGSN具有网络控制的信息屏蔽功能,可以选择哪些分组能够进入GPRS网络,以便保证GPRS网络的安全。
一. TelephonyProvider
TelephonyProvider继承自ContentProvider,在android中的代码路径为packages/providers/TelephonyProvider。
public class TelephonyProvider extends ContentProvider
从上面的代码可以看出TelephonyProvider继承自ContentProvider,因此为了了解TelephonyProvider的启动过程,最好先明白ContentProvider是如何加载的。
1-ContentProvider加载方式简要介绍
在ContentProvider对应的AndroidManifest.xml文件中,我们可以通过android:sharedUserId和android:process来指定ContentProvider运行的进程。如果不指定这些参数,那么该ContentProvider运行在定义它的独立进程中(或者说定义它的APK对应的进程中)。
- ContentProvider和某个进程同属一个进程时,当该进程启动时,会搜索属于该进程的所有ContentProvider,并加载。
- ContentProvider属于独立的一个进程时,只有需要用到该ContentProvider时,才会去加载。
当一个进程想要操作一个ContentProvider时,先需要获取该ContentProvider的对象,系统是这样处理的:
- 如果该ContentProvider属于当前主叫进程,因为在进程启动时就已经加载过了,所以系统会直接返回该ContentProvider的对象。
- 如果该ContentProvider不属于当前主叫进程,那么系统会进行相关处理(由ActivityManagerService进行,以下简称为AMS,所有已加载的ContentProvider信息都已保存在AMS中):
当需要获取某个ContentProvider的对象时,AMS会先判断该ContentProvider是否已被加载。
如果已被加载,且该ContentProvider和当前主叫进程不属一个进程,但是该ContentProvider设置了multiprocess的属性,并且该ContentProvider属于系统级ContentProvider,那么就在当前主叫进程内部新生成该ContentProvider的对象;否则就需要通过IPC机制进行调用。
如果还未被加载,且该ContentProvider和当前主叫进程不属一个进程,但是该ContentProvider设置了multiprocess的属性,并且该ContentProvider属于系统级ContentProvider,那么就在当前主叫进程内部新生成该ContentProvider的对象;否则就需要先创建该ContentProvider所在的进程,然后再通过IPC机制进行调用。
2-TelephonyProvider的加载
我们来截取一段TelephonyProvider对应AndroidManifest.xml的内容:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.providers.telephony"
coreApp="true"
android:sharedUserId="android.uid.phone">
......
<application android:process="com.android.phone"
android:allowClearUserData="false"
android:allowBackup="false"
android:label="@string/app_label"
android:icon="@mipmap/ic_launcher_phone"
android:usesCleartextTraffic="true">
<provider android:name="TelephonyProvider"
android:authorities="telephony"
android:exported="true"
android:singleUser="true"
android:multiprocess="false" />
从这段代码,我们可以看出TelephonyProvider是运行在phone进程中的,同事其multiprocess的值为false,也就意味着若其它进程要访问TelephonyProvider,必须使用IPC机制进行调用。
由于phone进程是开机就启动的,因此TelephonyProvider在开机的时候,就会被加载到AMS中。
3- TelephonyProvider的主要行为
我们首先来看看TelephonyProvider的onCreate函数。
@Override
public boolean onCreate() {
//首先需要创建出数据库
mOpenHelper = new DatabaseHelper(getContext());
......
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
// Update APN db on build update
String newBuildId = SystemProperties.get("ro.build.id", null);
if (!TextUtils.isEmpty(newBuildId)) {
// Check if build id has changed
SharedPreferences sp = getContext().getSharedPreferences(BUILD_ID_FILE,
Context.MODE_PRIVATE);
String oldBuildId = sp.getString(RO_BUILD_ID, "");
if (!newBuildId.equals(oldBuildId)) {
// Get rid of old preferred apn shared preferences
SubscriptionManager sm = SubscriptionManager.from(getContext());
if (sm != null) {
List<SubscriptionInfo> subInfoList = sm.getAllSubscriptionInfoList();
for (SubscriptionInfo subInfo : subInfoList) {
SharedPreferences spPrefFile = getContext().getSharedPreferences(
PREF_FILE + subInfo.getSubscriptionId(), Context.MODE_PRIVATE);
if (spPrefFile != null) {
//版本发生改变后,清楚旧的记录信息
SharedPreferences.Editor editor = spPrefFile.edit();
editor.clear();
editor.apply();
}
}
}
// Update APN DB
updateApnDb();
} else {
if (VDBG) log("onCreate: build id did not change: " + oldBuildId);
}
sp.edit().putString(RO_BUILD_ID, newBuildId).apply();
} else {
if (VDBG) log("onCreate: newBuildId is empty");
}
if (VDBG) log("onCreate:- ret true");
return true;
}
从上面的代码,我们知道TelephonyProvider初始化时的主要工作包括:1. 创建出数据库;2. 根据build_id的值,判断是否需要清楚旧有的存储信息,并更新数据库。
可以看出TelephonyProvider的主要工作,就是围绕数据库的操作展看的。
从上面的代码可以看出,与常见的方式类似,TelephonyProvider也是通过定义一个SQLiteOpenHelper,即DatabaseHelper来封装底层对数据的直接操作。
private static class DatabaseHelper extends SQLiteOpenHelper {
......
@Override
public void onCreate(SQLiteDatabase db) {
if (DBG) log("dbh.onCreate:+ db=" + db);
//创建SIM卡信息对应table
createSimInfoTable(db);
//创建运营商信息对应table
createCarriersTable(db, CARRIERS_TABLE);
//初始化数据库
initDatabase(db);
if (DBG) log("dbh.onCreate:- db=" + db);
}
这里需要注意的是,在创建两个table时,其实对某些列定义了默认值。因此,即使apn的配置文件中没有定义一些字段,这些字段也是有值的。
举个例子,在创建运营商的table时,会将user_visible的值默认置为1,read_only的值默认置为0等。
........
"user_visible BOOLEAN DEFAULT 1," +
"read_only BOOLEAN DEFAULT 0," +
接下来我们可以看看,initDatabase初始化数据库的主要过程:
private void initDatabase(SQLiteDatabase db) {
// Read internal APNS data
Resources r = mContext.getResources();
//获取xml
XmlResourceParser parser = r.getXml(com.android.internal.R.xml.apns);
.........
//解析xml,并将xml中的信息加入到数据库中
loadApns(db, parser);
.........
// Read external APNS data (partner-provided)
XmlPullParser confparser = null;
//找到外部配置的apns-conf.xml文件
...........
loadApns(db, confparser);
............
}
真实的过程可能比这个稍微复杂一下,但上面的代码应该包含了最主要的逻辑,其实就是找到apns-config.xml文件,然后解析这个xml文件,然后调用loadApns将xml中定义的数据,插入到TelephonyProvider底层的数据库中。
我们可以再简单看一看loadApns的过程:
private void loadApns(SQLiteDatabase db, XmlPullParser parser) {
if (parser != null) {
try {
db.beginTransaction();
XmlUtils.nextElement(parser);
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
//getRow读取每xml中,每一个APN块的内容,并将这些内容以键值对的形式,插入到ContentValues中
ContentValues row = getRow(parser);
if (row == null) {
throw new XmlPullParserException("Expected 'apn' tag", parser, null);
}
//每次解析完一个APN块,都将其插入到数据库中
insertAddingDefaults(db, row);
XmlUtils.nextElement(parser);
}
db.setTransactionSuccessful();
getRow和insertAddingDefaults的内容过于单纯,就不再进一步分析。
分析到这里,我们应该还剩最后一个疑问,apn对应的xml到底在哪里,长什么样子?
实际上android自带的内部APN配置文件,定义于frameworks/base/core/res/res/xml/apns.xml中,其实是个空文件。
于是,android中可用的APN配置文件,为外部定义的文件。
在Android源码build目录下,通过搜索apns-conf.xml可以找到在各个board中分别有配置:
device/generic/goldfish/data/etc/apns-conf.xml:system/etc/apns-conf.xml
在编译该product时会将device/generic/goldfish/data/etc/apns-conf.xml文件拷贝到system/etc/目录下,最后打包到system.img中。
上面是通常的解释,但实际上各个厂商实际上采用了一种Overlay机制,在编译的时候可以替换资源文件。不同厂商新建了自己的apns-conf.xml文件,放在自己指定的目录下,例如vendor/xxxx/xxxx/xxxx/etc/apns-conf.xml,然后编译时将改路径下的apns-conf.xml文件编入system.img,这才是实际使用的APN xml。
在这一部分的最后,我们举例来看看apns-conf.xml中的内容的形式:
<apn carrier="中国移动彩信 (China Mobile)"
mcc="460"
mnc="00"
apn="cmwap"
proxy="10.0.0.172"
port="80"
mmsc="http://mmsc.monternet.com"
mmsproxy="10.0.0.172"
mmsport="80"
type="mms"
protocol="IPV4V6"
/>
这就是apns-conf.xml中,中国移动发送彩信时对应的APN配置。
二. APN显示、修改和新建
PART-I ApnSettings
在上一部分,我们知道在开机时,Android启动Phone进程后,会加载Phone进程中的TelephonyProvider。而TelephonyProvider在创建时,会将apns-conf.xml中的数据添加的到数据库。
以上这些都是看不见的操作。对于用户而言,只能通过设置界面才能看到当前使用的APN,并进行新建和修改的操作。
在Android中,设置是一个系统级的APK。不同厂商的ROM中,设置的界面被组织成不同的形式。因此,我们仅来分析一下,接近原生的比较共性的流程。
一般而言,Settings.apk中有许多activity、fragment。与Telephony有关的界面,将在Setting.apk的AndroidManifest.xml通过android:process字段,指定其运行在Phone进程中。
public class ApnSettings extends SettingsPreferenceFragment implements Preference.OnPreferenceChangeListener {
与Apn操作有关的具体的界面是一个Fragment,当然它也是运行在phone进程中的。在这里,我们不分析太多细节,主要分析一下我们比较关注的显示、新建和编辑过程。
@Override
public void onCreate(Bundle icicle) {
........
//现在很多手机是支持双卡的,每个卡都有对应的ApnSettings,于是在这里加载卡对应的subId
mSubId = activity.getIntent().getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, SubscriptionManager.INVALID_SUBSCRIPTION_ID);
......
//根据subId,加载卡信息
mSubscriptionInfo = SubscriptionManager.from(activity).getActiveSubscriptionInfo(mSubId);
.......
//新建APN的按钮
mAddNewApn = new Preference(getActivity());
mAddNewApn.setTitle(R.string.menu_new);
mAddNewApn.setOrder(0);
........
可以看到ApnSettings的onCreate中,仅完成部分初始化的工作,并没有与之前的数据库关联起来。
我们接着来看一下,该界面的onResume函数。
@Override
public void onResume() {
.......
fillList();
.......
}
其中实际的工作,由fillList来完成。
private void fillList() {
........
//根据前面获得的卡信息,得到卡对应运营商的mcc mnc,这部分信息将成为,查询数据库使用的,where语句的一部分;
final String mccmnc = getOperatorNumeric();
........
//根据where信息,获得cursor对象(不同的rom有不同的选取,这里就不举例了,主要还是以mnc和mcc为主,毕竟一个卡对应所有APN信息,均应该显示)
........
//可能有多个可用的APN,估需要PreferenceGroup
PreferenceGroup apnList = (PreferenceGroup) findPreference("apn_list");
.........
//从数据库读出上一次选中的apn(即上一次使用的apn)的key值,对这个APN,UI界面一般有一些特殊的效果,同时这个APN一般是作为拨号使用的APN
mSelectedKey = getSelectedApnKey();
.........
//从数据库读出APN对应字段信息(数据库中包括xml里的,也包括用户之前新建的)
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);
//记得么,TelephonyProvider初始化创建运营商对应的表时,默认readOnly字段值为0,也就是说如果xml中apn不配置该字段时,改apn被加载后,默认是只读的
boolean readOnly = (cursor.getInt(RO_INDEX) == 0);
..........
//创建每个APN对应的Preference
ApnPreference pref = new ApnPreference(getActivity());
//在该Preference上,显示基本的APN信息
pref.setSubId(mSubId);
//这个位置调用了ApnPreference的setApnReadOnly函数,会使得从apns-conf.xml中加载的apn,无法编辑
pref.setApnReadOnly(readOnly);
pref.setWidgetLayoutResource (R.layout.leui_widget_arrow);
pref.setKey(key);
pref.setTitle(name);
pref.setSummary(apn);
pref.setPersistent(false);
pref.setOnPreferenceChangeListener(this);
........
cursor.moveToNext();
}
cursor.close();
.........
//在界面上增加新建APN的按钮
if (mAddNewApn != null) {
mAddNewApn.setOrder(1);
apnList.addPreference(mAddNewApn);
}
.......
}
看完fillList,我们终于将UI和底层的数据库关联起来了。现在我们知道,当ApnSetting界面被加载时,对应卡可用APN的主要信息将以Preference的形式显示在界面上。
我们现在可以点击ApnPreference看到更加详细的信息,也可以点击新建APN自己编辑APN信息,也可以选择Prefer APN(拨号时,优先选择的APN)。
在ApnSettings中,onPreferenceChange决定了优选APN。
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (newValue instanceof String) {
//某个APN preference被选中时,将其设为selected APN
setSelectedApnKey((String) newValue);
}
return true;
}
private void setSelectedApnKey(String key) {
mSelectedKey = key;
ContentResolver resolver = getContentResolver();
//优选APN的信息,也会被写入到数据库中
ContentValues values = new ContentValues();
values.put(APN_ID, mSelectedKey);
resolver.update(getUri(PREFERAPN_URI), values, null, null);
}
在ApnSettings的onPreferenceTreeClick函数中,定义了新建Apn时的操作。
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
....................
} else if (preference == mAddNewApn) {
//点击新建APN按钮
addNewApn();
}
return true;
}
private void addNewApn() {
//注意新建APN时,对应的action是insert
Intent intent = new Intent(Intent.ACTION_INSERT, getUri(Telephony.Carriers.CONTENT_URI));
.........
startActivity(intent);
}
在ApnPreference的onClick函数中,定义了点击ApnPreference的操作。
public void onClick(android.view.View v) {
.......
Uri url = ContentUris.withAppendedId( Telephony.Carriers.CONTENT_URI, pos);
//注意点击ApnPreference时,对应的action是ACTION_EDIT
Intent intent = new Intent(Intent.ACTION_EDIT, url);
intent.putExtra(PhoneConstants.SUBSCRIPTION_KEY, mSubId);
//注意这个字段,意味着默认从apns-conf.xml中读出的apn,DISABLE_EDITOR的值为true
intent.putExtra("DISABLE_EDITOR", mApnReadOnly);
...........
context.startActivity(intent);
..........
}
从上面的代码我们知道,不论是点击APN preference,还是点击新建APN按钮,我们都将进入到ApnEditor见面。
之所以确认是进入ApnEditor界面,主要是从Settings.apk的AndroidManifest.xml看出来的。
<activity android:name="ApnEditor" ....>
......
<intent-filter>
.......
<action android:name="android.intent.action.EDIT" />
......
<data android:mimeType = "vnd.android.cursor.item/telephony-carrier" />
</intent-filter>
<intent-filter>
<action android:name = "android.intent.action.INSERT" />
..........
<data android:mimeType = "vnd.android.cursor.dir/telephony-carrier" />
</intent-filter>
</activity>
PART-II ApnEditor
public class ApnEditor extends Activity
可以看到ApnEditor是一个Activity。
我们首先来关注它的onCreate方法。
protected void onCreate(Bundle icicle) {
.........
//初始化界面上各个域,这些域对应于APN中的字段,供显示和编辑使用
mName = getEditText(R.id.apn_name);
mName.setInputType(InputType.TYPE_CLASS_TEXT);
mApn = getEditText(R.id.apn_apn);
.........
//从Intent中读出DISABLE_EDITOR字段的值,若该值为true,则禁掉对应的editor。于是,默认读出apn无法编辑
mDisableEditor = intent.getBooleanExtra( "DISABLE_EDITOR", false);
if (mDisableEditor) {
for (int i = 0; i < EDIT_TEST_IDS.length; i++) {
getEditText( EDIT_TEST_IDS[i]).setEnabled(false);
}
for (int i = 0; i < LIST_IDS.length; i++) {
findViewById(LIST_IDS[i]).setEnabled(false);
}
Log.d(TAG, "ApnEditor form is disabled.");
}
............
if (action.equals(Intent.ACTION_EDIT)) {
//编辑APN的情况
mUri = intent.getData();
} else if (action.equals(Intent.ACTION_INSERT)) {
//也会取出Uri
.........
//新建APN的情况,将mNewApn置为true
mNewApn = true;
...............
//取出数据库对应的cursor。不论显示和编辑,都依赖与cursor对象
mCursor = getContentResolver().query(mUri, sProjection, null, null, null);
..........
//以数据库中的内容,填充UI界面
fillUi(intent.getStringExtra( ApnSettings.OPERATOR_NUMERIC_EXTRA));
//.............
}
private void fillUi(String defaultOperatorNumeric) {
.........
//从数据库中读出数据显示到界面上
String type = mCursor.getString(TYPE_INDEX);
String numeric = mTelephonyManager. getIccOperatorNumericForData(mSubId);
mName.setText(mCursor.getString(NAME_INDEX));
mApn.setText(mCursor.getString(APN_INDEX));
mProxy.setText(mCursor.getString(PROXY_INDEX));
mPort.setText(mCursor.getString(PORT_INDEX));
...........
}
从上面的代码,我们知道ApnEditor的初始化,就是决定自己能否进行实际的编辑工作,同时将数据库中的数据现实到UI界面上。
然后,如果该APN可编辑的话,用户就可以进行修改,然后点击保存了。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case com.android.internal.R.id.home:
//新建APN时,未保存就点击home键,将清空对应数据库
if (mNewApn) {
getContentResolver().delete(mUri, null, null);
}
finish();
return true;
case MENU_SAVE:
//点击保存,会将界面修改的信息写入数据库
if (validateAndSave(false)) {
finish();
}
return true;
}
return super.onOptionsItemSelected(item);
}
private boolean validateAndSave(boolean force) {
//对编辑界面的信息做一些检查
String name = checkNotSet(mName.getText().toString());
String apn = checkNotSet(mApn.getText().toString());
String mcc = checkNotSet(mMcc.getText().toString());
String mnc = checkNotSet(mMnc.getText().toString());
...........
//满足要求后,存储UI数据
ContentValues values = new ContentValues();
values.put(Telephony.Carriers.NAME, name.length() < 1 ? getResources().getString(R.string.untitled_apn) : name);
values.put(Telephony.Carriers.APN, apn);
values.put(Telephony.Carriers.PROXY, checkNotSet( mProxy.getText().toString()));
............
//将数据插入到数据库中
getContentResolver().update(mUri, values, null, null);
return true;
}
结束语
至此我们分析了与APN相关的主要流程,接下来APN的工作主要是用于数据业务了,以后用到再分析。