Android L 漫游浅析

       这篇文章主要是分析在Android L 源代码中对手机漫游的处理。当然我这里所说的漫游指的是国际漫游。通常我们判断手机是否在国际漫游,第一个想法就是比较网络上获取的MCC+MNC是否与手机中的IMSI相同,如果不同就判断为漫游了。如果是漫游的话,手机上最直观的可以看到就是两个地方了:

a . 手机的屏幕的状态拦上手机信号角标的左下方是否有”R”显示。

技术分享


b . Setting --->About phone --->Status --->Roming

技术分享


      当然这是最粗略的比较方法,通常全球的运营商对于漫游有互相签订协议,所以单纯用上面的方法是不够细致的,google 为了解决这个特殊化定制的问题,在Android L 上使用了一个机制来判断手机是否漫游,下面就从解析代码的角度来分析这个机制。

 

手机注网是一个比较复杂的过程,当然漫游就在这个过程中,在这里,我重点分析漫游这个点,注网的话,后面再另发文。

首先需要看的一个类就是:

Android_L/frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java

 

在这个类中有方法:

protected void handlePollStateResult (int what, AsyncResult ar) { ...... }

      什么时候会调用这个方法呢?RIL层在完成ServiceStateTracker对象发起的查询最新网络服务的状态后,通过ServiceStateTracker创建的Message对象发起的Callback回调。在ServiceStateTracker对象中会调用handlePollStateResult 和 pollStateDone 方法,将查询得来的最新信息保存在ServiceStateTracker的多个属性中 。由于GsmServiceStateTracker extends ServiceStateTracker,GsmServiceStateTracker 中的handlePollStateResult 方法会覆盖ServiceStateTracker中的方法,下面是handlePollStateResult 方法的实现:

/**
     * Handle the result of one of the pollState()-related requests
     */
    @Override
    protected void handlePollStateResult (int what, AsyncResult ar) {
        int ints[];
        String states[];

        // Ignore stale requests from last poll
        if (ar.userObj != mPollingContext) return;

        if (ar.exception != null) {
            CommandException.Error err=null;

            if (ar.exception instanceof CommandException) {
                err = ((CommandException)(ar.exception)).getCommandError();
            }

            if (err == CommandException.Error.RADIO_NOT_AVAILABLE) {
                // Radio has crashed or turned off
                cancelPollState();
                return;
            }

            if (!mCi.getRadioState().isOn()) {
                // Radio has crashed or turned off
                cancelPollState();
                return;
            }

            if (err != CommandException.Error.OP_NOT_ALLOWED_BEFORE_REG_NW) {
                loge("RIL implementation has returned an error where it must succeed" +
                        ar.exception);
            }
        } else try {
            switch (what) {
                case EVENT_POLL_STATE_REGISTRATION: {
                    states = (String[])ar.result;
                    int lac = -1;
                    int cid = -1;
                    int type = ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN;
                    int regState = ServiceState.RIL_REG_STATE_UNKNOWN;
                    int reasonRegStateDenied = -1;
                    int psc = -1;
                    if (states.length > 0) {
                        try {
                            regState = Integer.parseInt(states[0]);
                            if (states.length >= 3) {
                                if (states[1] != null && states[1].length() > 0) {
                                    lac = Integer.parseInt(states[1], 16);
                                }
                                if (states[2] != null && states[2].length() > 0) {
                                    cid = Integer.parseInt(states[2], 16);
                                }

                                // states[3] (if present) is the current radio technology
                                if (states.length >= 4 && states[3] != null) {
                                    type = Integer.parseInt(states[3]);
                                }
                            }
                            if (states.length > 14) {
                                if (states[14] != null && states[14].length() > 0) {
                                    psc = Integer.parseInt(states[14], 16);
                                }
                            }
                        } catch (NumberFormatException ex) {
                            loge("error parsing RegistrationState: " + ex);
                        }
                    }

                    mGsmRoaming = regCodeIsRoaming(regState);
                    mNewSS.setState(regCodeToServiceState(regState));
                    mNewSS.setRilVoiceRadioTechnology(type);

                    boolean isVoiceCapable = mPhoneBase.getContext().getResources()
                            .getBoolean(com.android.internal.R.bool.config_voice_capable);
                    if ((regState == ServiceState.RIL_REG_STATE_DENIED_EMERGENCY_CALL_ENABLED
                         || regState == ServiceState.RIL_REG_STATE_NOT_REG_EMERGENCY_CALL_ENABLED
                         || regState == ServiceState.RIL_REG_STATE_SEARCHING_EMERGENCY_CALL_ENABLED
                         || regState == ServiceState.RIL_REG_STATE_UNKNOWN_EMERGENCY_CALL_ENABLED)
                         && isVoiceCapable) {
                        mEmergencyOnly = true;
                    } else {
                        mEmergencyOnly = false;
                    }

                    // LAC and CID are -1 if not avail
                    mNewCellLoc.setLacAndCid(lac, cid);
                    mNewCellLoc.setPsc(psc);
                    break;
                }

                case EVENT_POLL_STATE_GPRS: {
                    states = (String[])ar.result;

                    int type = 0;
                    int regState = ServiceState.RIL_REG_STATE_UNKNOWN;
                    mNewReasonDataDenied = -1;
                    mNewMaxDataCalls = 1;
                    if (states.length > 0) {
                        try {
                            regState = Integer.parseInt(states[0]);

                            // states[3] (if present) is the current radio technology
                            if (states.length >= 4 && states[3] != null) {
                                type = Integer.parseInt(states[3]);
                            }
                            if ((states.length >= 5 ) &&
                                    (regState == ServiceState.RIL_REG_STATE_DENIED)) {
                                mNewReasonDataDenied = Integer.parseInt(states[4]);
                            }
                            if (states.length >= 6) {
                                mNewMaxDataCalls = Integer.parseInt(states[5]);
                            }
                        } catch (NumberFormatException ex) {
                            loge("error parsing GprsRegistrationState: " + ex);
                        }
                    }
                    int dataRegState = regCodeToServiceState(regState);
                    mNewSS.setDataRegState(dataRegState);
                    mDataRoaming = regCodeIsRoaming(regState);
                    mNewSS.setRilDataRadioTechnology(type);
                    if (DBG) {
                        log("handlPollStateResultMessage: GsmSST setDataRegState=" + dataRegState
                                + " regState=" + regState
                                + " dataRadioTechnology=" + type);
                    }
                    break;
                }

                case EVENT_POLL_STATE_OPERATOR: {
                    String opNames[] = (String[])ar.result;

                    if (opNames != null && opNames.length >= 3) {
                         mNewSS.setOperatorName (opNames[0], opNames[1], opNames[2]);
                    }
                    break;
                }

                case EVENT_POLL_STATE_NETWORK_SELECTION_MODE: {
                    ints = (int[])ar.result;
                    mNewSS.setIsManualSelection(ints[0] == 1);
                    break;
                }
            }

        } catch (RuntimeException ex) {
            loge("Exception while polling service state. Probably malformed RIL response." + ex);
        }
        mPollingContext[0]--;

        if (mPollingContext[0] == 0) {
            /**
             * Since the roaming state of gsm service (from +CREG) and
             * data service (from +CGREG) could be different, the new SS
             * is set to roaming when either is true.
             *
             * There are exceptions for the above rule.
             * The new SS is not set as roaming while gsm service reports
             * roaming but indeed it is same operator.
             * And the operator is considered non roaming.
             *
             * The test for the operators is to handle special roaming
             * agreements and MVNO's.
             */
           <span style="color:#FF0000;"> boolean roaming = (mGsmRoaming || mDataRoaming);</span>
            if ((mGsmRoaming && isSameNamedOperators(mNewSS)
                        && !isSameNamedOperatorConsideredRoaming(mNewSS))
                    || isOperatorConsideredNonRoaming(mNewSS)) {
                roaming = false;
            }
            mNewSS.setRoaming(roaming);
            mNewSS.setEmergencyOnly(mEmergencyOnly);
            pollStateDone();
        }
    }

这个方法主要完成了三件事情:

1,RIL返回的查询结果异常处理;

2,根据返回的4种不同网络服务查询类型,作不同处理;

3,调用pollDtateDone方法完成后面的工作。


在这里我们只关注 Roaming , 看方法中对于漫游定义:

boolean roaming = (mGsmRoaming || mDataRoaming);

那么mGsmRoaming 和 mDataRoaming 分别是什么呢?在类的最前面对于变量的定义中有:

    /**
     * GSM roaming status solely based on TS 27.007 7.2 CREG. Only used by
     * handlePollStateResult to store CREG roaming result.
     */
    private boolean mGsmRoaming = false;

    /**
     * Data roaming status solely based on TS 27.007 10.1.19 CGREG. Only used by
     * handlePollStateResult to store CGREG roaming result.
     */
    private boolean mDataRoaming = false;

根据Google加的注释可以看到,我们应该需要参考3GPP文档TS 27.007的相关章节,大家可以去3GPP官网下载该文档看看。

对上面两个变量的赋值,主要是在:

mGsmRoaming = regCodeIsRoaming(regState);

mDataRoaming = regCodeIsRoaming(regState);

这里传入的参数 regState 网络状态编码 是一个很重要的参数,手机当前状态的很多属性都是根据这个参数来判断的。同样是在TS 27.007文档的7.2节有定义对应关系。

Defined values
<n>:
0 disable network registration unsolicited result code
1 enable network registration unsolicited result code +CREG: <stat>
2 enable network registration and location information unsolicited result code +CREG:
<stat>[,<lac>,<ci>[,<AcT>]]
<stat>: circuit mode registration status
0 not registered, MT is not currently searching a new operator to register to
1 registered, home network
2 not registered, but MT is currently searching a new operator to register to
3 registration denied
4 unknown
<span style="color:#FF0000;">5 registered, roaming</span>
<lac>: string type; two byte location area code or tracking are a code in hexadecimal format (e.g. "00C3" equals
195 in decimal)
<ci>: string type; four byte GERAN/UTRAN/E-UTRAN cell ID in hexadecimal format
<AcT>: access technology of the registered network

从上面的定义可以看出,code为5的时候是漫游状态,找到方法regCodeIsRoaming()

    /**
     * code is registration state 0-5 from TS 27.007 7.2
     * returns true if registered roam, false otherwise
     */
    private boolean regCodeIsRoaming (int code) {
        return ServiceState.RIL_REG_STATE_ROAMING == code;
    }

从代码中可以看到,当code为ServiceState.RIL_REG_STATE_ROAMING 时,返回值为true。找到这个常量的定义:

Android_L/frameworks/base/telephony/java/android/telephony/ServiceState.java

  /**
     * RIL level registration state values from ril.h
     * ((const char **)response)[0] is registration state 0-6,
     *              0 - Not registered, MT is not currently searching
     *                  a new operator to register
     *              1 - Registered, home network
     *              2 - Not registered, but MT is currently searching
     *                  a new operator to register
     *              3 - Registration denied
     *              4 - Unknown
     *              5 - Registered, roaming
     *             10 - Same as 0, but indicates that emergency calls
     *                  are enabled.
     *             12 - Same as 2, but indicates that emergency calls
     *                  are enabled.
     *             13 - Same as 3, but indicates that emergency calls
     *                  are enabled.
     *             14 - Same as 4, but indicates that emergency calls
     *                  are enabled.
     * @hide
     */
    public static final int RIL_REG_STATE_NOT_REG = 0;
    /** @hide */
    public static final int RIL_REG_STATE_HOME = 1;
    /** @hide */
    public static final int RIL_REG_STATE_SEARCHING = 2;
    /** @hide */
    public static final int RIL_REG_STATE_DENIED = 3;
    /** @hide */
    public static final int RIL_REG_STATE_UNKNOWN = 4;
    /** @hide */
  <span style="color:#FF0000;">  public static final int RIL_REG_STATE_ROAMING = 5;</span>
    /** @hide */
    public static final int RIL_REG_STATE_NOT_REG_EMERGENCY_CALL_ENABLED = 10;
    /** @hide */
    public static final int RIL_REG_STATE_SEARCHING_EMERGENCY_CALL_ENABLED = 12;
    /** @hide */
    public static final int RIL_REG_STATE_DENIED_EMERGENCY_CALL_ENABLED = 13;
    /** @hide */
    public static final int RIL_REG_STATE_UNKNOWN_EMERGENCY_CALL_ENABLED = 14;

        可以看到代码中这些定义的code值与名称的对应关系,RIL_REG_STATE_ROAMING常量的值为5 。所以当传入的参数regState为5的时候,为漫游。

现在我们关注下面这段代码:

if (mPollingContext[0] == 0) {
            /**
             * Since the roaming state of gsm service (from +CREG) and
             * data service (from +CGREG) could be different, the new SS
             * is set to roaming when either is true.
             *
             * There are exceptions for the above rule.
             * The new SS is not set as roaming while gsm service reports
             * roaming but indeed it is same operator.
             * And the operator is considered non roaming.
             *
             * The test for the operators is to handle special roaming
             * agreements and MVNO's.
             */
            boolean roaming = (mGsmRoaming || mDataRoaming);
            if ((mGsmRoaming && isSameNamedOperators(mNewSS)
                        && !isSameNamedOperatorConsideredRoaming(mNewSS))
                    || isOperatorConsideredNonRoaming(mNewSS)) {
                roaming = false;
            }
            mNewSS.setRoaming(roaming);
            mNewSS.setEmergencyOnly(mEmergencyOnly);
            pollStateDone();
        }

可以先看看google加在前面的注释,大概的意思是:

        由于GSM服务和数据服务的漫游状态可能不同,所以只要这二者其中之一是漫游就将New SS(最新的ServiceState)设置为漫游。

对于上面的规则有一个说明。

当GSM服务被认为是漫游但事实上他们是同一个运营商,且时运营商决定不漫游,这个时候new SS不会设置为漫游。

对于运营商的测试是为了处理特殊的漫游协议和移动虚拟网络运营商。

下面分析下if条件中的几个方法,在这里传入的参数都是mNewSS,简单的说一下这个对象:

ServiceState意思是服务状态,手机插入SIM卡成功启动后,BP Modem会读取SIM卡中的IMSI信息完成SIM卡中信息的验证和运营商移动网络的注册,这样手机才能正常使用运营商提供的服务,代码中ServiceState保存SIM卡注册成功后运营商网络的一些基本服务信息,具体可以看这个类中常量的定义,显然,漫游也在其中。

(1)isSameNamedOperators():从注释中知道如果运营商网络的MCC和SIM卡的MCC一样,同时ons(查看相关的协议文档)和spn(spn是写在sim卡中的值,具体请查找文档)不同则设置为漫游状态,即返回值为true。

 /**
     * Set roaming state if operator mcc is the same as sim mcc
     * and ons is different from spn
     *
     * @param s ServiceState hold current ons
     * @return true if same operator
     */
    private boolean isSameNamedOperators(ServiceState s) {
        String spn = SystemProperties.get(TelephonyProperties.PROPERTY_ICC_OPERATOR_ALPHA, "empty");    //获得SimCard中的spn,如果没有,返回“empty”
        String onsl = s.getOperatorAlphaLong();     //获得当前注册的运营商网络的长名
        String onss = s.getOperatorAlphaShort();    //获取当前注册的运营商网络的短名

        boolean equalsOnsl = onsl != null && spn.equals(onsl);  //onsl不为空,且spn和onsl相同时,equalsOnsl为true
        boolean equalsOnss = onss != null && spn.equals(onss);  //onss不为空,且spn和onss相同时,equalsOnsl为true

        return currentMccEqualsSimMcc(s) && (equalsOnsl || equalsOnss);
    }

         看看这个返回值的逻辑。只有当equalsOnsl或者equalsOnss其中一个为true且currentMccEqualsSimMcc()返回值为true时,上面这个方法才返回true,下面看看

currentMccEqualsSimMcc()这个方法,该方法用来比较SIM卡的MCC和网络上的MCC,即比较simNumeric和operatorNumeric的前三位,相同则返回true。

    /**
     * Compare SIM MCC with Operator MCC
     *
     * @param s ServiceState hold current ons
     * @return true if both are same
     */
    private boolean currentMccEqualsSimMcc(ServiceState s) {
        String simNumeric = SystemProperties.get(         //获得SIM Number                  
                TelephonyProperties.PROPERTY_ICC_OPERATOR_NUMERIC, "");
        String operatorNumeric = s.getOperatorNumeric();  //获得Operator Number
        boolean equalsMcc = true;

        try {
            equalsMcc = simNumeric.substring(0, 3).
                    equals(operatorNumeric.substring(0, 3));   
        } catch (Exception e){
        }
        return equalsMcc;
    }

下面同时看看isSameNamedOperatorConsideredRoaming()和isOperatorConsideredNonRoaming()这两个方法。

    private boolean isSameNamedOperatorConsideredRoaming(ServiceState s) {
        String operatorNumeric = s.getOperatorNumeric();
        String[] numericArray = mPhone.getContext().getResources().getStringArray(
                    <span style="color:#FF0000;">com.android.internal.R.array.config_sameNamedOperatorConsideredRoaming</span>);

        if (numericArray.length == 0 || operatorNumeric == null) {
            return false;
        }

        for (String numeric : numericArray) {
            if (operatorNumeric.startsWith(numeric)) {
                return true;
            }
        }
        return false;
    }

 /**
     * Do not set roaming state in case of oprators considered non-roaming.
     *
     + Can use mcc or mcc+mnc as item of config_operatorConsideredNonRoaming.
     * For example, 302 or 21407. If mcc or mcc+mnc match with operator,
     * don't set roaming state.
     *
     * @param s ServiceState hold current ons
     * @return false for roaming state set
     */
    private boolean isOperatorConsideredNonRoaming(ServiceState s) {
        String operatorNumeric = s.getOperatorNumeric();
        String[] numericArray = mPhone.getContext().getResources().getStringArray(
                    <span style="color:#FF0000;">com.android.internal.R.array.config_operatorConsideredNonRoaming</span>);

        if (numericArray.length == 0 || operatorNumeric == null) {
            return false;
        }

        for (String numeric : numericArray) {
            if (operatorNumeric.startsWith(numeric)) {
                return true;
            }
        }
        return false;
    }

通过比较发现这两个方法的代码逻辑中只有getStringArray()这个方法中传递的参数不同,那我们下意识的跟进这个方法,来到:

Android L/frameworks/base/core/java/android/content/res/Resources.java

    /**
     * Return the string array associated with a particular resource ID.
     *
     * @param id The desired resource identifier, as generated by the aapt
     *           tool. This integer encodes the package, type, and resource
     *           entry. The value 0 is an invalid identifier.
     *
     * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
     *
     * @return The string array associated with the resource.
     */
    public String[] getStringArray(int id) throws NotFoundException {
        String[] res = mAssets.getResourceStringArray(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String array resource ID #0x"
                                    + Integer.toHexString(id));
    }
从注释来看,返会的是一个与特有资源ID相关联的字符数组,我们继续往下跟进getResourceStringArray()这个方法,来到:

Android L/frameworks/base/core/java/android/content/res/AssetManager.java

    /**
     * Retrieve the string array associated with a particular resource
     * identifier.
     * @param id Resource id of the string array
     */
    /*package*/ final String[] getResourceStringArray(final int id) {
        String[] retArray = getArrayStringResource(id);
        return retArray;
    }
那AssetManager.java是一个怎样的类呢,注意到代码中对于该类有一个注释。

/**
 * Provides access to an application's raw asset files; see {@link Resources}
 * for the way most applications will want to retrieve their resource data.
 * This class presents a lower-level API that allows you to open and read raw
 * files that have been bundled with the application as a simple stream of
 * bytes.
 */
public final class AssetManager {
      ......
}
        大概的意思是说这个类为应用提供一个通往原始资源文件的通道,通过这种方式应用可以重新获得它们的资源文件。这个类提供了一种轻量级的API,

能够让你打开和读那些已经与应用绑定在一起作为简单字节流的原始资源文件。这个翻译起来比较绕口,楼主英语也是渣,所以就直接看效果了,

再看看getArrayStringResource()这个方法的定义:

private native final String[] getArrayStringResource(int arrayRes);

注意到这是一个native方法,那JNI是如何实现的呢?来到:

/home/simon/Android L/frameworks/base/core/jni/android_util_AssetManager.cpp

代码中有如下函数:

static jobjectArray android_content_AssetManager_getArrayStringResource(JNIEnv* env, jobject clazz,
                                                                        jint arrayResId)
{
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return NULL;
    }
    const ResTable& res(am->getResources());

    const ResTable::bag_entry* startOfBag;
    const ssize_t N = res.lockBag(arrayResId, &startOfBag);
    if (N < 0) {
        return NULL;
    }

    jobjectArray array = env->NewObjectArray(N, g_stringClass, NULL);
    if (env->ExceptionCheck()) {
        res.unlockBag(startOfBag);
        return NULL;
    }

    Res_value value;
    const ResTable::bag_entry* bag = startOfBag;
    size_t strLen = 0;
    for (size_t i=0; ((ssize_t)i)<N; i++, bag++) {
        value = bag->map.value;
        jstring str = NULL;

        // Take care of resolving the found resource to its final value.
        ssize_t block = res.resolveReference(&value, bag->stringBlock, NULL);
#if THROW_ON_BAD_ID
        if (block == BAD_INDEX) {
            jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!");
            return array;
        }
#endif
        if (value.dataType == Res_value::TYPE_STRING) {
            const ResStringPool* pool = res.getTableStringBlock(block);
            const char* str8 = pool->string8At(value.data, &strLen);
            if (str8 != NULL) {
                str = env->NewStringUTF(str8);
            } else {
                const char16_t* str16 = pool->stringAt(value.data, &strLen);
                str = env->NewString(str16, strLen);
            }

            // If one of our NewString{UTF} calls failed due to memory, an
            // exception will be pending.
            if (env->ExceptionCheck()) {
                res.unlockBag(startOfBag);
                return NULL;
            }

            env->SetObjectArrayElement(array, i, str);

            // str is not NULL at that point, otherwise ExceptionCheck would have been true.
            // If we have a large amount of strings in our array, we might
            // overflow the local reference table of the VM.
            env->DeleteLocalRef(str);
        }
    }
    res.unlockBag(startOfBag);
    return array;
}
         那上面的函数是什么作用呢,简单来说就是根据之前传入的ID到Android L/frameworks/base/core/res/res目录下获得相应数组,先看下这个目录下是什么文件

技术分享

           会根据MCC和MNC(如果有的话)去找到相应的目录,比如MCC=234,MNC=34,那么就去找到values-mcc234-mnc34这个目录,并去读取这个目录下的配置文件,

<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2013, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
**     http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <!-- Don't use roaming icon for considered operators -->
    <string-array translatable="false" name="<span style="color:#FF0000;">config_operatorConsideredNonRoaming</span>">
        <item>23430</item>
        <item>23431</item>
        <item>23432</item>
        <item>23433</item>
        <item>23434</item>
        <item>23486</item>
    </string-array>
</resources>
       我们之前所说的isSameNamedOperatorConsideredRoaming()和isOperatorConsideredNonRoaming()这两个方法里对于得到数组传入的参数不同,就体现在当前配置文件中的"name"字段,上面JNI函数返回的是一个数组,数组里的值就是读取的"item",这里的item是可以根据运营商的需求去手动配置的,这个就体现了个性化定制。在得到数组

后,isSameNamedOperatorConsideredRoaming()和isOperatorConsideredNonRoaming()方法中的处理逻辑都是会遍历数组,同时与operatorNumeric作比较,如果相同则

返回true.

       在一一分析了上面这些方法后,再回头去看handlePollStateResult()方法最后if中的处理逻辑就一目了然了,在这里就不多说了,读者可以简单推理一下。这篇博客的题目

为漫游浅析,那么实际上手机厂商对于漫游的处理不一定会采取google原生的方案,通常芯片厂商会有自己的解决方案,所以真正对于漫游的处理可能会复杂点,如果读者有机会参与手机ROM开发,也许会接触到更多这方面的知识,当然android也在不断生长中,第一次写这么多内容,不对的地方望指正。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。