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 解决方法总结
- 修改模拟器源码modem.c中的MCC和MNC
- 修改模拟器源码sim_card.c中控制mMncLength的值
- 修改Android镜像中的/system/etc/apns-conf.xml,添加国内运营商的配置信息
- 存量设备更新APN