Android6.0 APN

来源:互联网 发布:windows安装git 编辑:程序博客网 时间:2024/06/05 01:11

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

从上面的代码可以看出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" />
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

从这段代码,我们可以看出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;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

从上面的代码,我们知道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);        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这里需要注意的是,在创建两个table时,其实对某些列定义了默认值。因此,即使apn的配置文件中没有定义一些字段,这些字段也是有值的。 
举个例子,在创建运营商的table时,会将user_visible的值默认置为1,read_only的值默认置为0等。

........"user_visible BOOLEAN DEFAULT 1," +"read_only BOOLEAN DEFAULT 0," +
  • 1
  • 2
  • 3

接下来我们可以看看,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);    ............       }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

真实的过程可能比这个稍微复杂一下,但上面的代码应该包含了最主要的逻辑,其实就是找到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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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
  • 1

在编译该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"  />
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这就是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 {
  • 1

与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);        ........
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

可以看到ApnSettings的onCreate中,仅完成部分初始化的工作,并没有与之前的数据库关联起来。

我们接着来看一下,该界面的onResume函数。

@Override    public void onResume() {        .......        fillList();        .......    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

其中实际的工作,由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);    }    .......}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

看完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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在ApnSettings的onPreferenceTreeClick函数中,定义了新建Apn时的操作。

@Overridepublic boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {    ....................    } else if (preference == mAddNewApn) {        //点击新建APN按钮        addNewApn();    }        return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
private void addNewApn() {    //注意新建APN时,对应的action是insert    Intent intent = new Intent(Intent.ACTION_INSERT, getUri(Telephony.Carriers.CONTENT_URI));    .........    startActivity(intent);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在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);    ..........}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

从上面的代码我们知道,不论是点击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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

PART-II ApnEditor

public class ApnEditor extends Activity
  • 1

可以看到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));    //.............}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
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));    ...........}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

从上面的代码,我们知道ApnEditor的初始化,就是决定自己能否进行实际的编辑工作,同时将数据库中的数据现实到UI界面上。

然后,如果该APN可编辑的话,用户就可以进行修改,然后点击保存了。

@Overridepublic 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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

结束语 
至此我们分析了与APN相关的主要流程,接下来APN的工作主要是用于数据业务了,以后用到再分析。



原文地址:http://blog.csdn.net/gaugamela/article/details/52238454

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 小米五指纹解锁失灵怎么办 小米note3指纹解锁失灵怎么办 小米4s手机屏幕失灵怎么办 vivo手机没有otg功能怎么办 头戴耳机压头发怎么办 小米4c很卡怎么办 小米4c玩王者怎么办 小米4s屏幕乱跳怎么办 小米4s手机后壳碎了怎么办 小米5spius开不了机怎么办 小米5s无限重启怎么办 小米5s外屏坏了怎么办 小米5s内屏碎了怎么办 小米4充电没反应怎么办 小米5手机变卡了怎么办 小米5变卡了怎么办 小米手机充电无反应怎么办 小米6相机卡顿怎么办 华为手机玩游戏发热怎么办 华为手机变慢了怎么办 华为p10手机变慢怎么办 华为手机账户密码忘记了怎么办 QQ浏览器无法加载插件怎么办 电脑开了机黑屏怎么办 扫描仪打不开运单扫描怎么办 打印机不支持64位系统怎么办 xp系统dnf闪退怎么办 w10电脑所有程序都打不开怎么办 安卓手机太卡怎么办 系统装到f盘了怎么办 虚拟机占c盘内存怎么办 外机连无线虚拟机显示受限怎么办 使用msdn下载解压后怎么办 路由80端口被占用怎么办 c盘拒绝粘贴文件怎么办 oracle数据库密码忘了怎么办 电脑开机时不显示用户名怎么办? xp系统忘记开机密码怎么办 电脑开机密码忘了怎么办 c盘满了怎么办win10 win10电脑开机密码忘了怎么办