目的:
为了访问网络,手机必须设置合适的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函数。

@Overridepublic boolean onCreate() {//首先需要创建出数据库mOpenHelper = new DatabaseHelper(getContext());......SQLiteDatabase db = mOpenHelper.getReadableDatabase();// Update APN db on build updateString newBuildId = SystemProperties.get("ro.build.id", null);if (!TextUtils.isEmpty(newBuildId)) {// Check if build id has changedSharedPreferences 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 preferencesSubscriptionManager 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 DBupdateApnDb();} 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 {......@Overridepublic void onCreate(SQLiteDatabase db) {if (DBG) log("dbh.onCreate:+ db=" + db);//创建SIM卡信息对应tablecreateSimInfoTable(db);//创建运营商信息对应tablecreateCarriersTable(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 dataResources r = mContext.getResources();//获取xmlXmlResourceParser 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进程中的。在这里,我们不分析太多细节,主要分析一下我们比较关注的显示、新建和编辑过程。

@Overridepublic void onCreate(Bundle icicle) {........//现在很多手机是支持双卡的,每个卡都有对应的ApnSettings,于是在这里加载卡对应的subIdmSubId =  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函数。

@Overridepublic void onResume() {.......fillList();.......}

其中实际的工作,由fillList来完成。

private void fillList() {........//根据前面获得的卡信息,得到卡对应运营商的mcc mnc,这部分信息将成为,查询数据库使用的,where语句的一部分;final String mccmnc = getOperatorNumeric();........//根据where信息,获得cursor对象(不同的rom有不同的选取,这里就不举例了,主要还是以mnc和mcc为主,毕竟一个卡对应所有APN信息,均应该显示)........//可能有多个可用的APN,估需要PreferenceGroupPreferenceGroup apnList = (PreferenceGroup) findPreference("apn_list");.........//从数据库读出上一次选中的apn(即上一次使用的apn)的key值,对这个APN,UI界面一般有一些特殊的效果,同时这个APN一般是作为拨号使用的APNmSelectedKey = 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对应的PreferenceApnPreference 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 APNsetSelectedApnKey((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是insertIntent 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_EDITIntent intent = new Intent(Intent.ACTION_EDIT, url);intent.putExtra(PhoneConstants.SUBSCRIPTION_KEY, mSubId);//注意这个字段,意味着默认从apns-conf.xml中读出的apn,DISABLE_EDITOR的值为trueintent.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置为truemNewApn = 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的工作主要是用于数据业务了,以后用到再分析。

Android6.0 APN相关推荐

  1. [Android6.0][RK3399] PCIe 接口 4G模块 EC20 调试记录

    原址 Platform: RK3399  OS: Android 6.0  Kernel: 4.4  Version: v2017.04  4G Module: EC20-CE 一基本概念 USB 部 ...

  2. Android6.0 EC20 R2.1 4G模块移植

    摘要:   本文主要针对MTK MT8317 Android6.0 代码进行4G模块移植,使用pppd拨号的方式实现上网,并没有按照移远提供的文档GobiNet方式上网. 1.内核部分的移植 1.1 ...

  3. android 蓝牙找不到电脑,Android6.0 蓝牙搜索不到设备原因

    原因: 为提供更高的数据保护 Android6.0版本上增加了关于Wifi和蓝牙的权限,以下是官方文档说明: 图1 修改方法: 在AndroidManifest 中添加权限 或者 注意 如果targe ...

  4. 编译可在Nexus5上运行的CyanogenMod13.0 ROM(基于Android6.0)

    编译可在Nexus5上运行的CyanogenMod13.0 ROM (基于Android6.0) 作者:寻禹@阿里聚安全 前言 下文中无特殊说明时CM代表CyanogenMod的缩写. 下文中说的&q ...

  5. android6.0麦克风权限,android 6.0权限检测以及6.0以下,麦克风、相机权限判断

    android 6.0以上权限 android 6.0以上权限,我是通过PermissionsDispatcher进行申请,操作的,具体使用方法,见PermissionsDispatcher,Andr ...

  6. nexus5 刷 Android6.0+Xposed

    不得不说现在的刷机工具都做的方便快捷,只需要简单的几天命令就解决了.以前用windows刷的时候还需要装一堆驱动软件啥的. 原文链接: nexus5 刷 Android6.0+Xposed 0x00 ...

  7. Android6.0执行时权限解析,RxPermissions的使用,自己封装一套权限框架

    Android6.0执行时权限解析,RxPermissions的使用.自己封装一套权限框架 在Android6.0中,新添加了一个执行时的权限,我相信非常多人都已经知道了.预计也知道怎么用了,这篇博客 ...

  8. Android6.0以上的权限问题

    Android6.0以上的权限问题 可以在使用权限的页面oncreate方法中进行如下的动态的申请 if (ContextCompat.checkSelfPermission(this, Manife ...

  9. Android6.0 Log的工作机制

    Android6.0log新机制 Android6.0后Android 日志系统做了很大的改变,但是对于应用层改变是透明的,原因是由于日志系统只是针对底层做了相应改变.之前的系统通过读写设备文件的方式 ...

最新文章

  1. 使用Hugo搭建自己的个人博客网站
  2. c++ 类文件的动态库生成及调用例子
  3. Python闭包及其作用域
  4. ASP.NET2.0 - ASP.net MVC和ASP.net Web Forms
  5. datacombo重复值的处理_Pandas入门【S1E3】缺失值和重复值处理
  6. 腾讯云数据库2020年度盛典等你来
  7. Vscode----热门插件超实用插件汇总(史上最全)
  8. 为什么 Java 在 25 年之后依旧如此年轻:一个架构师的看法
  9. Android学习笔记-隐藏app图标
  10. tomcat7 性能优化
  11. 【Rustdesk】最友好的开源远程桌面软件——Rustdesk 实现 Windows、Linux、MacOS 之间远程连接桌面
  12. MIT6.824 2022 Fault-tolerant Key/Value Service
  13. 常见的网站攻击与防御,道高一尺,魔高一丈
  14. Java开发短连接分享功能
  15. 020.验证二叉搜索树
  16. 图片字节数组的获取,字节数组图片的保存
  17. python爬虫利用线程池下载视频
  18. springboot学习(七十一)解决问题:the URL contained a potentially malicious String “;“
  19. 这几款摸鱼神器,让我惊了!
  20. qt文件逐行读取_QT平台文件逐行读取和字符串规律输出练习

热门文章

  1. service启动流程
  2. svga-前端如何使用svga动画
  3. python scrapy详细解析文档
  4. python基本语法—列表
  5. 《投名状》是数年来最成功的商业大片
  6. 【踩坑专栏】Test测试类Class Not Found
  7. 【UE4】从零开始学习使用UE4制作FPS游戏02(血量和护甲HUD篇)
  8. 03-Java解决应用程序被安全阻止
  9. zlib-Deflate压缩算法
  10. 简单的理解deflate算法