目的:
为了访问网络,手机必须设置合适的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对应的进程中)。

  1. ContentProvider和某个进程同属一个进程时,当该进程启动时,会搜索属于该进程的所有ContentProvider,并加载。
  2. ContentProvider属于独立的一个进程时,只有需要用到该ContentProvider时,才会去加载。

当一个进程想要操作一个ContentProvider时,先需要获取该ContentProvider的对象,系统是这样处理的:

  1. 如果该ContentProvider属于当前主叫进程,因为在进程启动时就已经加载过了,所以系统会直接返回该ContentProvider的对象。
  2. 如果该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的工作主要是用于数据业务了,以后用到再分析。