最近伊朗的少将苏莱曼尼遇刺,被全网称为2020年第一只黑天鹅事件。人们为美国没有夺走其余无辜生命状况下,精准打击目标的能力感到震惊。毕竟上一个有记载能作到“在百万军中取上将首级如探囊取物”的人是我们的张飞大哥。java
我也闲来无事,打开万年不开的微博,看到一条微博内容:android
忽然感受虎躯一震,在现在的科技时代,掌握科技的制高点和核心研发能力是多么的重要。不过,若是对于这号人物真经过IMEI就能简单的定位的话,或许他们须要一台装了xposed的安卓手机。好像发现了商机?git
在震惊之余,我拿出恰好这周复习的安卓设备ID信息和Sim卡信息的笔记,认真复习了一番。github
下面是Demo的运行结果:api
IMEI和MEID都是移动设备的身份识别码。相似于设备的身份证。他们的区别以下:markdown
在安卓8.0如下手机,咱们经过APITelephonyManager.getDeviceId()
获取插有电信运营商SIM卡的卡槽时,默认返回MEID号;移动和联通运营商SIM卡的卡槽返回IMEI号。网络
在Android 8.0以上的系统,TelephonyManager废弃了getDeviceId()
方法,提供了两个独立的API来准确的获取IMEI和MEID:getImei()
和getMeid()
。两个API均可以支持传入slotIndex来获取对应位置的IMEI和MEID。jsp
在Android8.0如下的系统,TelephonyManager经过getDeviceId()
来获取IMEI或MEID;且在Android 6.0系统及以上系统,getDeviceId(slotIndex)
支持经过传入slotIndex来获取对应位置的IMEI和MEID,来适应一机双卡的状况。当存在电信SIM卡时,getDeviceId()
方法优先返回MEID,当移动和联通SIM卡时,返回IMEI。ide
卡1 | 卡2 | getDeviceId(0) | getDeviceId(1) |
---|---|---|---|
无/非电信卡 | 无/非电信卡 | IMEI | IMEI |
电信卡 | 无/非电信卡 | MEID | IMEI |
无/非电信卡 | 电信卡 | IMEI | MEID |
/** * 获取IMEI或MEID */ fun getIMEIORMEID(){ //检查是否有READ_PHONE_STATE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED){ //获取IMEI和MEID val tm = getSystemService<TelephonyManager>() //getDeviceId()从API1就已经存在,默认返回卡槽一的IMEI或MEID var imei: String? = tm?.getDeviceId() //getDeviceId(solotIndex)在API23加入能够经过指定卡槽位置获取IMEI或MEID if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //solotIndex:0 -> 卡槽1 IMEI或MEID tvIMEI1.text = "卡槽一 IMEI或MEID:${tm?.getDeviceId(0)}" //solotIndex:1 -> 卡槽2 IMEI或MEID tvIMEI2.text = "卡槽二 IMEI或MEID:${tm?.getDeviceId(1)}" } } } /** * 获取MEID */ fun getMEID(){ //检查是否有READ_PHONE_STATE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED){ //获取IMEI和MEID val tm = getSystemService<TelephonyManager>() //获取MEID在API26加入 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //返回默承认用卡槽的MEID var meid:String? = tm?.meid //能够经过指定卡槽获取MEID tvMEID.text = "MEID:${tm?.getMeid(0)}" } } } 复制代码
MAC地址(Media Access Control Address),它是一个用来确认网络设备位置的地址。MAC地址用于在网络中惟一标示一个网卡,一台设备若一个或多个网卡,则每一个网卡都存在一个惟一的MAC地址。工具
通常会将IMEI或MEID结合MAC地址做为设备的惟一标识。
MAC地址在Android中不一样版本的获取方式也不太同样。你们能够自行Google。这里就再也不赘述。后续的Demo中也会给出代码。
国际移动用户识别码(International Mobile Subscriber Identity),是用于区分蜂窝网络中不一样用户的、在全部蜂窝网络中不重复的识别码。
咱们能够经过TelephonyManager.getSubscriberId()
获取SIM卡的IMSI,该API在SDK API 1就已经存在,但若是在双卡双待的手机上有多个卡槽的状况下,咱们想获取对应卡槽的IMSI,则须要调用TelephonyManager.getSubscriberId(int subId)
方法,传入对应卡槽的subId,0表明卡槽1,1表明卡槽2,该方法在SDK API 21加入,但在SDK API 29及以上则没法再调用,但咱们目前能够经过反射的方式去调用。
在这里安利一个工具:对于一个API,在各个Android系统的源码,咱们能够在http://androidxref.com/ 这个网站进行查看。
咱们经过这个网站搜索查看,还会发如今SDK API 21加入时,TelephonyManager.getSubscriberId(long subId)
它的入参是Long类型,在以后的版本都是int类型。因此在版本兼容的时候,咱们若是是反射调用时,可能须要注意参数类型。
SDK API | getSubscriberId() |
---|---|
1~20 | getSubscriberId() |
21 | getSubscriberId()或getSubscriberId(long subId) |
22~28 | getSubscriberId()或getSubscriberId(int subId) |
29及以上 | 暴露getSubscriberId(),但getSubscriberId(int subId)方法再也不暴露 |
/** * 经过反射调用 */ fun getIMSI(){ //检查是否有READ_PHONE_STATE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED){ //获取IMSI val tm = getSystemService<TelephonyManager>() //在SDKAPI 1就存在getSubscriberId()方法,返回默认卡槽IMSI var imsi = tm?.subscriberId //在SDKAPI 21加入getSubscriberId(subId)来指定返回卡槽位置IMSI //在SDKAPI 29以上,指定卡槽位置的方法再也不暴露,但依旧能经过反射来获取 tvIMSI1.text = "卡槽一 IMSI:${getReflectMethod(this,"getSubscriberId",0) as CharSequence?}" tvIMSI2.text = "卡槽二 IMSI:${getReflectMethod(this,"getSubscriberId",1) as CharSequence?}" } } fun getReflectMethod(context: Context, method: String, param: Int): Any? { val telephony = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager try { val telephonyClass = Class.forName(telephony.javaClass.name) val parameter = arrayOfNulls<Class<*>>(1) parameter[0] = Int::class.javaPrimitiveType val getSimState = telephonyClass.getMethod(method, *parameter) val ob_phone = getSimState.invoke(telephony, param) if (ob_phone != null) { return ob_phone } } catch (e: Exception) { e.printStackTrace() } return null } 复制代码
上面的代码展现了如何经过反射获取IMSI。在获取到IMSI以后,咱们能够经过IMSI的开头五位数来区别移动用户所属的国家和运营商。
在中国IMSI的开头三位为460,以后的两位表明中国的运营商信息:
运营商 | MNC号码 |
---|---|
移动 | 00, 02,04, 07,08 |
联通 | 01, 06,09 |
电信 | 03, 05 |
电信4G | 11 |
铁通 | 20 |
SIM卡卡号,是卡的标识,不做接入网络的鉴权认证,可在SIM卡卡后查询到。格式:大多为19或20位0-9的数字,亦存在6位/12位的状况。
获取ICCID有两种方式:
getSimSerialNumber()
方法来获取ICCID,在SDK API 21加入了getSubscriberId(subId)
来指定获取对应卡槽位置的ICCID;可是和IMSI同样,在SDK API 29及以上,getSubscriberId(subId)
的方法再也不暴露外部调用,但咱们能够经过反射来调用。getActiveSubscriptionInfoList()
方法来获取SubscriptionInfo
列表,这个列表包含了有效的SIM卡的信息,若是有多个SIM卡,则会返回多个SubscriptionInfo
对象。每一个SubscriptionInfo
对象表明一个有效的SIM卡。经过循环遍历SubscriptionInfo
列表,调用SubscriptionInfo
的getIccId()
的方法,能够获取ICCID的值,可是SubscriptionManager在SDk API 22才被加到SDK中。/** * 经过反射调用 */ fun getReflectMethod(context: Context, method: String, param: Int): Any? { val telephony = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager try { val telephonyClass = Class.forName(telephony.javaClass.name) val parameter = arrayOfNulls<Class<*>>(1) parameter[0] = Int::class.javaPrimitiveType val getSimState = telephonyClass.getMethod(method, *parameter) val ob_phone = getSimState.invoke(telephony, param) if (ob_phone != null) { return ob_phone } } catch (e: Exception) { e.printStackTrace() } return null } /** * 经过TelephonyManager获取CCID */ fun getICCIDByTM(){ //检查是否有READ_PHONE_STATE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED){ //获取IMSI val tm = getSystemService<TelephonyManager>() //在SDKAPI 1就存在getSubscriberId()方法,返回默认卡槽IMSI var iccid = tm?.simSerialNumber //在SDKAPI 21加入getSubscriberId(subId)来指定返回卡槽位置IMSI //在SDKAPI 29以上,指定卡槽位置的方法再也不暴露,但依旧能经过反射来获取 tvICCID1.text = "卡槽一 ICCID:${getReflectMethod(this,"getSimSerialNumber",0) as CharSequence?}" tvICCID2.text = "卡槽二 ICCID:${getReflectMethod(this,"getSimSerialNumber",1) as CharSequence?}" } } /** * 经过SubscriptionManager获取CCID SDK API 22及以上使用 */ fun getICCIDBySM(){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { val mSubscriptionManager = getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager //检查是否有READ_PHONE_STATE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED){ //获取当前有效的SIM卡列表 var subInfos = mSubscriptionManager.activeSubscriptionInfoList for ((index,item) in subInfos.withIndex()){ when(index){ 0->tvICCID1.text = "卡槽一 ICCID:${item.iccId}" 1->tvICCID2.text = "卡槽二 ICCID:${item.iccId}" } } } } } 复制代码
SN码是Serial Number的缩写,是产品的序列号,主要为了验证"产品的合法性",主要用来保护用户的正版权益。通常是不可改变的。咱们能够经过adb devices
命令来查看。
咱们在SDK API 26如下,能够直接经过Build.SERIAL
进行获取,在SDK API 26以上则须要READ_PHONE_STATE
权限,调用Build.getSerial()
方法进行获取。
/** * 获取SN */ fun getSN(){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED){ // 须要READ_PHONE_STATE权限 tvSN.text = "SN:${Build.getSerial()}" } }else{ tvSN.text = "SN:${Build.SERIAL}" } } 复制代码
设备首次启动随机生成的ID,设备还原出厂设置后或系统升级后会从新生成 格式:长度为16位的字符串,但因为生产厂商定制系统的Bug,有的设备会生成相同的ID,或者返回null
经过Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID
便可获取ANDROID ID
/** * 获取AndroidId */ fun getAndroidId(){ tvAndroidId.text = "Android ID:${Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID)}" } 复制代码
在Android10已经没法获取设备硬件相关的Id,例如IMEI、MAC地址等。若是代码中获取这方面的信息,则会给你抛出一个大写的SecurityException
错误。
受影响的API为:
Build
getSerial()
TelephonyManager
getImei()
getDeviceId()
getMeid()
getSimSerialNumber()
getSubscriberId()
在Android10,除非系统应用或者特殊的应用商应用READ_PRIVILEGED_PHONE_STATE
才能访问以上的API。这就意味这Android ID的许多获取方法在Android10基本GG。
Google为咱们提出了Android10获取软件范畴的ID的方法。具体参考文章:惟一标识符最佳作法
但这方案不能解决咱们不少场景:须要获取设备的惟一不可更改的ID,例如:风控、推送和用户画像等领域。但不用担忧,咱们伟大的祖国厂商系统,为咱们提供了统一的OAID(匿名设备标识符)来替代IMEI的做用。具体参考文档:OAID文档
最后再补充几点与SIM卡相关,但和Android设备信息无关的内容。主要是整理了一下如何检测一台设备当前插入了几张SIM卡、当前手机最多支持几张Sim卡和获取当前使用蜂窝网络的SIM卡。
获取当前手机插入几张SIM卡也有两个途径,一个是经过TelephonyManager的getSimState()
方法获取,另外一个是经过SubscriptionManager
获取
方法获取默认的Sim卡状态;在SDK API 21加入了
getSimState(int slotId)方法,获取对应卡槽的Sim卡状态,可是该方法倒是经过注解
@hide`修饰的,因此没有对外暴露方法,外部没法调用,但能够经过反射进行调用;在SDK API 26才对外暴露。SDK API | getSubscriberId() |
---|---|
1~20 | getSimState() |
21~26 | getSimState()或反射调用getSimState(int slotId) |
26以上 | getSimState()或getSimState(int slotId) |
Sim卡状态表:
状态 | 状态码 | 解释 |
---|---|---|
SIM_STATE_UNKNOWN | 0 | SIM卡状态:未知 |
SIM_STATE_ABSENT | 1 | SIM卡状态:设备中没有SIM卡 |
SIM_STATE_PIN_REQUIRED | 2 | SIM卡状态:锁定:要求用户的SIM卡PIN解锁 |
SIM_STATE_PUK_REQUIRED | 3 | SIM卡状态:锁定:要求用户的SIM PUK解锁 |
SIM_STATE_NETWORK_LOCKED | 4 | SIM卡状态:锁定:须要网络PIN才能解锁 |
SIM_STATE_READY | 5 | SIM卡状态:就绪 |
SIM_STATE_NOT_READY | 6 | SIM卡状态:SIM卡还没有就绪 |
SIM_STATE_PERM_DISABLED | 7 | SIM卡状态:SIM卡错误,已永久禁用 |
SIM_STATE_CARD_IO_ERROR | 8 | SIM卡状态:SIM卡错误,存在但有故障 |
SIM_STATE_CARD_RESTRICTED | 9 | SIM卡状态:SIM卡受限,存在,但因为运营商的限制而没法使用 |
SubscriptionManager.getActiveSubscriptionInfoCount()
方法获取有效的Sim卡个数。但这个方法须要READ_PHONE_STATE
用户权限。所以结合TelephonyManager.getSimState()
和SubscriptionManager.getActiveSubscriptionInfoCount()
方法,我的整理了一个获取当前手机SIM卡的方法。
/** * 获取Sim卡个数 * @param context * @return */ fun checkSimCount(context: Context): Int { var simCount: Int try { //若是SDK Version >=22 && 拥有Manifest.permission.READ_PHONE_STATE 权限 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1 && ContextCompat.checkSelfPermission( context, Manifest.permission.READ_PHONE_STATE ) == PackageManager.PERMISSION_GRANTED ) { simCount = getSimCountBySubscriptionManager(context) if (simCount == -1) { //若是SubscriptionManager获取失败,则TelephonyManager尝试获取 simCount = getSimCountByTelephonyManager(context) } } else { simCount = getSimCountByTelephonyManager(context) } } catch (e: Exception) { simCount = getSimCountByTelephonyManager(context) } return simCount } /** * 经过SubscriptionManager获取Sim卡张数,若是获取失败返回-1 * @param context * @return */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) @RequiresPermission(Manifest.permission.READ_PHONE_STATE) private fun getSimCountBySubscriptionManager(context: Context): Int { try { val subscriptionManager = context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager return subscriptionManager?.activeSubscriptionInfoCount ?: -1 } catch (e: Throwable) { return -1 } } /** * 经过TelephonyManager反射获取Sim卡张数 * @param context * @return */ private fun getSimCountByTelephonyManager(context: Context): Int { val slotOne = getSimStateBySlotIdx(context, 0) val slotTwo = getSimStateBySlotIdx(context, 1) if (slotOne && slotTwo) { return 2 } else if (slotOne || slotTwo) { return 1 } else { val telephony = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val simState = telephony.simState return if (simState != TelephonyManager.SIM_STATE_ABSENT && simState != TelephonyManager.SIM_STATE_UNKNOWN) { 1 } else 0 } } /** * 经过反射去调用TelephonyManager.getSimState(int slotIdx)方法,获取sim卡状态 * SIM_STATE_UNKNOWN 0 SIM卡状态:未知 * SIM_STATE_ABSENT 1 SIM卡状态:设备中没有SIM卡 * SIM_STATE_PIN_REQUIRED 2 SIM卡状态:锁定:要求用户的SIM卡PIN解锁 * SIM_STATE_PUK_REQUIRED 3 SIM卡状态:锁定:要求用户的SIM PUK解锁 * SIM_STATE_NETWORK_LOCKED 4 SIM卡状态:锁定:须要网络PIN才能解锁 * SIM_STATE_READY 5 SIM卡状态:就绪 * SIM_STATE_NOT_READY 6 SIM卡状态:SIM卡还没有就绪 * SIM_STATE_PERM_DISABLED 7 SIM卡状态:SIM卡错误,已永久禁用 * SIM_STATE_CARD_IO_ERROR 8 SIM卡状态:SIM卡错误,存在但有故障 * SIM_STATE_CARD_RESTRICTED 9 SIM卡状态:SIM卡受限,存在,但因为运营商的限制而没法使用。 * @param context * @param slotIdx:0(sim1),1(sim2) * @return */ fun getSimStateBySlotIdx(context: Context, slotIdx: Int): Boolean { var isReady = false try { val getSimState = getSimByMethod(context, "getSimState", slotIdx) if (getSimState != null) { val simState = Integer.parseInt(getSimState.toString()) if (simState != TelephonyManager.SIM_STATE_ABSENT && simState != TelephonyManager.SIM_STATE_UNKNOWN) { isReady = true } } } catch (e: Throwable) { } return isReady } /** * 经过反射获取Sim卡状态 * @param context * @param method * @param param * @return */ private fun getSimByMethod(context: Context, method: String, param: Int): Any? { val telephony = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager try { val telephonyClass = Class.forName(telephony.javaClass.name) val parameter = arrayOfNulls<Class<*>>(1) parameter[0] = Int::class.javaPrimitiveType val getSimState = telephonyClass.getMethod(method, *parameter) val obParameter = arrayOfNulls<Any>(1) obParameter[0] = param val result = getSimState.invoke(telephony, *obParameter) if (result != null) { return result } } catch (e: Throwable) { e.printStackTrace() } return null } 复制代码
SubscriptionManager咱们能够经过getActiveSubscriptionInfoCountMax()
方法来获取当前手机可支持的Sim卡最大数量。
/** * 获取当前支持最多sim卡数量 */ fun getMaxSimCount(context: Context){ try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { val subscriptionManager = context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager tvSimMaxCount.text = "当前支持最多Sim卡数量:${subscriptionManager?.activeSubscriptionInfoCountMax}" } } catch (e: Throwable) { } } 复制代码
咱们能够经过TelephonyManager的getSimOperator()
方法获取当前网络的SIM卡的MCC+MNC(mobile country code + mobile network code)其实就是IMSI的前五位,这个API在SDK API 1就已经加入。而后根据返回的信息根据运营商的MNC去判断对应的运营商信息。
/** * 经过TelephonyManager当前蜂窝网络运营商信息 */ fun getNetType(){ //检查是否有READ_PHONE_STATE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED){ //获取TelephonyManager val tm = getSystemService<TelephonyManager>() //在SDK API 1 getSimOperator()方法 when(tm?.simOperator){ "46000","46002","46004","46007","46008"->tvSimNetType.text = "当前上网SIM卡运营商:移动" "46001","46006","46009"->tvSimNetType.text = "当前上网SIM卡运营商:联通" "46003","46005","46011"->tvSimNetType.text = "当前上网SIM卡运营商:电信" "46020"->tvSimNetType.text = "当前上网SIM卡运营商:铁通" } } } 复制代码