最近伊朗的少將蘇萊曼尼遇刺,被全網稱爲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卡運營商:鐵通" } } } 複製代碼