解决Android模拟器中修改IMSI后无法上网问题

0x00 前言

百度百科中对IMSI的介绍如下:

国际移动用户识别码(IMSI:International Mobile Subscriber Identification Number)是区别移动用户的标志,储存在SIM卡中,可用于区别移动用户的有效信息。其总长度不超过15位,同样使用0~9的数字。其中MCC是移动用户所属国家代号,占3位数字,中国的MCC规定为460;MNC是移动网号码,由两位或者三位数字组成,中国移动的移动网络编码(MNC)为00;用于识别移动用户所归属的移动通信网;MSIN是移动用户识别码,用以识别某一移动通信网中的移动用户。

通过IMSI可以知道移动用户所在的国家。在Android中可以通过以下方法获取设备的IMSI号:

TelephonyManager telephonyManager= (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
String android_imsi = telephonyManager.getSubscriberId();

Android模拟器中默认使用的IMSI是:310260000000000。其中,310代表美国,260代表T-Mobile US。事实上,我们期望在模拟器上获取的IMSI应当以460开头(460代表中国)。

0x01 问题定位

但是,这串数字是硬编码在模拟器中的,路径是external/qemu/android/telephony/modem.c,只能通过修改模拟器源码来实现。

#define  OPERATOR_HOME_MCC   310
#define  OPERATOR_HOME_MNC   260
#define  OPERATOR_HOME_MCCMNC  STRINGIFY(OPERATOR_HOME_MCC) \
                               STRINGIFY(OPERATOR_HOME_MNC)
{ "+CIMI", OPERATOR_HOME_MCCMNC "0000000000", NULL },   /* request internation subscriber identification number */

将以上代码改为:

#define  OPERATOR_HOME_MCC   460 //中国
#define  OPERATOR_HOME_MNC   00  //移动
#define  OPERATOR_HOME_MCCMNC  STRINGIFY(OPERATOR_HOME_MCC) \
                               STRINGIFY(OPERATOR_HOME_MNC)
{ "+CIMI", OPERATOR_HOME_MCCMNC "00000000000", NULL }

重新编译,运行,使用getSubscriberId获取的值的确变成了我们期望的值:460000000000000
,但是出现了新的问题:模拟器不能上网了。一番Google之后,发现也有别人遇到了这个问题,但是也没有找到好的解决方法。

于是,决定自己寻找原因,从TelephonyManager一路翻下去,最终到ril层,也没有看出问题出在哪儿。但是,直觉告诉我,问题应当出在APN上。

Android系统中APN的配置信息是在/system/etc/apns-conf.xml中。下面是模拟器中默认的APN配置。

<!-- use empty string to specify no proxy or port -->
<!-- This version must agree with that in apps/common/res/apns.xml -->
<apns version="8">
    <apn carrier="T-Mobile US"
         mcc="310"
         mnc="260"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
    />

    <apn carrier="T-Mobile US 250"
         mcc="310"
         mnc="250"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 660"
         mcc="310"
         mnc="660"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 230"
         mcc="310"
         mnc="230"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 310"
         mcc="310"
         mnc="310"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 580"
         mcc="310"
         mnc="580"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 240"
         mcc="310"
         mnc="240"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 800"
         mcc="310"
         mnc="800"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 210"
         mcc="310"
         mnc="210"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 160"
         mcc="310"
         mnc="160"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 270"
         mcc="310"
         mnc="270"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 200"
         mcc="310"
         mnc="200"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 220"
         mcc="310"
         mnc="220"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <apn carrier="T-Mobile US 490"
         mcc="310"
         mnc="490"
         apn="epc.tmobile.com"
         user="none"
         server="*"
         password="none"
         mmsc="http://mms.msg.eng.t-mobile.com/mms/wapenc"
            />

    <!-- T-Mobile Europe -->
    <apn carrier="T-Mobile UK"
         mcc="234"
         mnc="30"
         apn="general.t-mobile.uk"
         user="t-mobile"
         password="tm"
         server="*"
         mmsproxy="149.254.201.135"
         mmsport="8080"
         mmsc="http://mmsc.t-mobile.co.uk:8002"
    />

    <apn carrier="T-Mobile D"
         mcc="262"
         mnc="01"
         apn="internet.t-mobile"
         user="t-mobile"
         password="tm"
         server="*"
         mmsproxy="172.028.023.131"
         mmsport="8008"
         mmsc="http://mms.t-mobile.de/servlets/mms"
    />

    <apn carrier="T-Mobile A"
         mcc="232"
         mnc="03"
         apn="gprsinternet"
         user="t-mobile"
         password="tm"
         server="*"
         mmsproxy="010.012.000.020"
         mmsport="80"
         mmsc="http://mmsc.t-mobile.at/servlets/mms"
         type="default,supl"
    />

    <apn carrier="T-Mobile A MMS"
         mcc="232"
         mnc="03"
         apn="gprsmms"
         user="t-mobile"
         password="tm"
         server="*"
         mmsproxy="010.012.000.020"
         mmsport="80"
         mmsc="http://mmsc.t-mobile.at/servlets/mms"
         type="mms"
    />

    <apn carrier="T-Mobile CZ"
         mcc="230"
         mnc="01"
         apn="internet.t-mobile.cz"
         user="wap"
         password="wap"
         server="*"
         mmsproxy="010.000.000.010"
         mmsport="80"
         mmsc="http://mms"
         type="default,supl"
    />

    <apn carrier="T-Mobile CZ MMS"
         mcc="230"
         mnc="01"
         apn="mms.t-mobile.cz"
         user="mms"
         password="mms"
         server="*"
         mmsproxy="010.000.000.010"
         mmsport="80"
         mmsc="http://mms"
         type="mms"
    />

    <apn carrier="T-Mobile NL"
         mcc="204"
         mnc="16"
         apn="internet"
         user="*"
         password="*"
         server="*"
         mmsproxy="010.010.010.011"
         mmsport="8080"
         mmsc="http://t-mobilemms"
         type="default,supl"
    />

    <apn carrier="T-Mobile NL MMS"
         mcc="204"
         mnc="16"
         apn="mms"
         user="tmobilemms"
         password="tmobilemms"
         server="*"
         mmsproxy="010.010.010.011"
         mmsport="8080"
         mmsc="http://t-mobilemms"
         type="mms"
    />
</apns>

可以看出,这里是没有国内运营商的配置的。我从小米手机中提取出的国内运营商配置。

<apn carrier="中国移动 (China Mobile) GPRS"
    mcc="460"
    mnc="00"
    apn="cmnet"
    type="default,supl"
/>

<apn carrier="中国移动 (China Mobile) WAP"
    mcc="460"
    mnc="00"
    apn="cmwap"
    port="80"
    proxy="10.0.0.172"
    type="default,supl"
/>

<apn carrier="中国移动彩信 (China Mobile)"
    mcc="460"
    mnc="00"
    apn="cmwap"
    mmsc="http://mmsc.monternet.com"
    mmsport="80"
    mmsproxy="10.0.0.172"
    port="80"
    proxy="10.0.0.172"
    type="mms"
/>

<apn carrier="沃3G连接互联网 (China Unicom)"
    mcc="460"
    mnc="01"
    apn="3gnet"
    type="default,supl"
/>

<apn carrier="沃3G手机上网 (China Unicom)"
    mcc="460"
    mnc="01"
    apn="3gwap"
    port="80"
    proxy="10.0.0.172"
    type="default,supl"
/>

<apn carrier="联通彩信 (China Unicom)"
    mcc="460"
    mnc="01"
    apn="3gwap"
    mmsc="http://mmsc.myuni.com.cn"
    mmsport="80"
    mmsproxy="10.0.0.172"
    type="mms"
/>

<apn carrier="中国移动 (China Mobile) GPRS"
    mcc="460"
    mnc="02"
    apn="cmnet"
    type="default,supl"
/>

<apn carrier="中国移动 (China Mobile) WAP"
    mcc="460"
    mnc="02"
    apn="cmwap"
    port="80"
    proxy="10.0.0.172"
    type="default,supl"
/>

<apn carrier="中国移动彩信 (China Mobile)"
    mcc="460"
    mnc="02"
    apn="cmwap"
    mmsc="http://mmsc.monternet.com"
    mmsport="80"
    mmsproxy="10.0.0.172"
    port="80"
    proxy="10.0.0.172"
    type="mms"
/>

添加到apns-conf.xml中,重启模拟器,能识别出“中国移动”了,但依然不能上网。

此时,我想到通过对比默认情况和修改后的radio日志,来分析原因。

使用

adb logcat -b radio

命令可以查查看ril相关的日志。接着,就发现了如下两条日志:

D/DCT ( 1739): [0]createAllApnList: selection=numeric = ‘460000’
D/DCT ( 1739): [0]createAllApnList: No APN found for carrier: 460000

注意到这里的460000,正确的应该是46000,多了一个0。由于美国的MNC是3个字符,而中国的MNC是2个字符,所以导致这里多了一个字符。来看下源码在这里是怎么实现的。

/frameworks/opt/telephony/src/java/com/android/internal/telephony/dataconnection/DcTracker.java

/**
 * Based on the sim operator numeric, create a list for all possible
 * Data Connections and setup the preferredApn.
 */
private void createAllApnList() {
    mAllApnSettings = new ArrayList<ApnSetting>();
    IccRecords r = mIccRecords.get();
    String operator = (r != null) ? r.getOperatorNumeric() : "";
    if (operator != null) {
        String selection = "numeric = '" + operator + "'";
        // query only enabled apn.
        // carrier_enabled : 1 means enabled apn, 0 disabled apn.
        // selection += " and carrier_enabled = 1";
        if (DBG) log("createAllApnList: selection=" + selection);

        Cursor cursor = mPhone.getContext().getContentResolver().query(
                Telephony.Carriers.CONTENT_URI, null, selection, null, null);

        if (cursor != null) {
            if (cursor.getCount() > 0) {
                mAllApnSettings = createApnList(cursor);
            }
            cursor.close();
        }
    }

    addEmergencyApnSetting();

    dedupeApnSettings();

    if (mAllApnSettings.isEmpty()) {
        if (DBG) log("createAllApnList: No APN found for carrier: " + operator);
        mPreferredApn = null;
        // TODO: What is the right behavior?
        //notifyNoData(DataConnection.FailCause.MISSING_UNKNOWN_APN);
    } else {
        mPreferredApn = getPreferredApn();
        if (mPreferredApn != null && !mPreferredApn.numeric.equals(operator)) {
            mPreferredApn = null;
            setPreferredApn(-1);
        }
        if (DBG) log("createAllApnList: mPreferredApn=" + mPreferredApn);
    }
    if (DBG) log("createAllApnList: X mAllApnSettings=" + mAllApnSettings);

    setDataProfilesAsNeeded();
}

getOperatorNumeric的实现是在/frameworks/opt/telephony/src/java/com/android/internal/telephony/uicc/SIMRecords.java中。

@Override
public String getOperatorNumeric() {
    if (mImsi == null) {
        log("getOperatorNumeric: IMSI == null");
        return null;
    }
    if (mMncLength == UNINITIALIZED || mMncLength == UNKNOWN) {
        log("getSIMOperatorNumeric: bad mncLength");
        return null;
    }

    // Length = length of MCC + length of MNC
    // length of mcc = 3 (TS 23.003 Section 2.2)
    return mImsi.substring(0, 3 + mMncLength);
}

可见,numeric的长度是受mMncLength控制的,而mMncLength的值是从SIM卡中读出来的。

case EVENT_GET_AD_DONE:
    try {
        isRecordLoadResponse = true;

        ar = (AsyncResult)msg.obj;
        data = (byte[])ar.result;

        if (ar.exception != null) {
            break;
        }

        log("EF_AD: " + IccUtils.bytesToHexString(data));

        if (data.length < 3) {
            log("Corrupt AD data on SIM");
            break;
        }

        if (data.length == 3) {
            log("MNC length not present in EF_AD");
            break;
        }

        mMncLength = data[3] & 0xf;
        log("setting4 mMncLength=" + mMncLength);

        if (mMncLength == 0xf) {
            mMncLength = UNKNOWN;
            log("setting5 mMncLength=" + mMncLength);
        }
    }

最终定位到模拟器中的external/qemu/android/telephony/sim_card.c中。

    { "+CRSM=192,28589,0,0,15", "+CRSM: 144,0,000000046fad04000aa0aa01020000" },
    { "+CRSM=176,28589,0,0,4",  "+CRSM: 144,0,00000003" },

00000003改成00000002,重新编译,运行,终于可以正常上网了。

0x02 解决更新问题

此时,对于新创建的模拟器已经正常了,但是对于存量模拟器,由于telephony.db数据库中的carriers表中的数据没有更新,因此重启后还是不能上网。

查看carriers表中的内容可以在adb shell中执行命令:
content query --uri content://telephony/carriers

/packages/providers/TelephonyProvider/src/com/android/providers/telephony/TelephonyProvider.java

@Override
public void onCreate(SQLiteDatabase db) {
    if (DBG) log("dbh.onCreate:+ db=" + db);
    createSimInfoTable(db);
    createCarriersTable(db);
    initDatabase(db);
    if (DBG) log("dbh.onCreate:- db=" + db);
}

private void initDatabase(SQLiteDatabase db) {
    if (VDBG) log("dbh.initDatabase:+ db=" + db);
    // Read internal APNS data
    Resources r = mContext.getResources();
    XmlResourceParser parser = r.getXml(com.android.internal.R.xml.apns);
    int publicversion = -1;
    try {
        XmlUtils.beginDocument(parser, "apns");
        publicversion = Integer.parseInt(parser.getAttributeValue(null, "version"));
        loadApns(db, parser);
    } catch (Exception e) {
        loge("Got exception while loading APN database." + e);
    } finally {
        parser.close();
    }

    // Read external APNS data (partner-provided)
    XmlPullParser confparser = null;
    // Environment.getRootDirectory() is a fancy way of saying ANDROID_ROOT or "/system".
    File confFile = new File(Environment.getRootDirectory(), PARTNER_APNS_PATH);
    File oemConfFile =  new File(Environment.getOemDirectory(), OEM_APNS_PATH);
    if (oemConfFile.exists()) {
        // OEM image exist APN xml, get the timestamp from OEM & System image for comparison
        long oemApnTime = oemConfFile.lastModified();
        long sysApnTime = confFile.lastModified();
        if (DBG) log("APNs Timestamp: oemTime = " + oemApnTime + " sysTime = "
                + sysApnTime);

        // To get the latest version from OEM or System image
        if (oemApnTime > sysApnTime) {
            if (DBG) log("APNs Timestamp: OEM image is greater than System image");
            confFile = oemConfFile;
        }
    } else {
        // No Apn in OEM image, so load it from system image.
        if (DBG) log("No APNs in OEM image = " + oemConfFile.getPath() +
                " Load APNs from system image");
    }

    FileReader confreader = null;
    if (DBG) log("confFile = " + confFile);
    try {
        confreader = new FileReader(confFile);
        confparser = Xml.newPullParser();
        confparser.setInput(confreader);
        XmlUtils.beginDocument(confparser, "apns");

        // Sanity check. Force internal version and confidential versions to agree
        int confversion = Integer.parseInt(confparser.getAttributeValue(null, "version"));
        if (publicversion != confversion) {
            throw new IllegalStateException("Internal APNS file version doesn't match "
                    + confFile.getAbsolutePath());
        }

        loadApns(db, confparser);
    } catch (FileNotFoundException e) {
        // It's ok if the file isn't found. It means there isn't a confidential file
        // Log.e(TAG, "File not found: '" + confFile.getAbsolutePath() + "'");
    } catch (Exception e) {
        loge("Exception while parsing '" + confFile.getAbsolutePath() + "'" + e);
    } finally {
        try { if (confreader != null) confreader.close(); } catch (IOException e) { }
    }
    if (VDBG) log("dbh.initDatabase:- db=" + db);

}

private DatabaseHelper mOpenHelper;

private void restoreDefaultAPN(int subId) {
    SQLiteDatabase db = mOpenHelper.getWritableDatabase();

    try {
        db.delete(CARRIERS_TABLE, null, null);
    } catch (SQLException e) {
        loge("got exception when deleting to restore: " + e);
    }
    setPreferredApnId((long)-1, subId);
    mOpenHelper.initDatabase(db);
}

可以看出,在restoreDefaultAPN的时候可以重新初始化CARRIERS_TABLE。也就说,只要进入APN界面,点击右上角菜单 => 重置为默认设置,就可以解决存量设备的上网问题了。

0x03 解决方法总结

  1. 修改模拟器源码modem.c中的MCC和MNC
  2. 修改模拟器源码sim_card.c中控制mMncLength的值
  3. 修改Android镜像中的/system/etc/apns-conf.xml,添加国内运营商的配置信息
  4. 存量设备更新APN
分享