百度、Google、ChatGPT、Github、CSDN,许多能问答案的地方我都去找了,就是找不到答案,什么用AT命令的,什么用反射的,根本就不行,在我文章的最后有很多CSDN推荐的相关获取带字母iccid的文章,有些文章甚至需要开会员才能看,然而,这些文章的答案根本就不行,太难了,就这么简单的一个功能,为什么Google不提供,真是恶心!最终还是在Github上找到了答案,Github上也是经历了千翻万找,找了好多开源项目才发现一个可以使用的,结果发现答案竟然如此简单。
mTelecomManager.getCallCapablePhoneAccounts()函数返回一个PhoneAccountHandle列表,如果插了双卡,就会有两个PhoneAccountHandle对象,通过这个对象的getId()方法即可拿到sim卡的iccid(sim卡序列号),不论是否带字母都可以获取到,非常的简单,且只需要READ_PHONE_STATE权限即可,普通App也能获取。但是它不是万能的,只能在Android6.0 ~ Android 12版本有用,因为该API是在Android6.0的时候出的,为什么说在Android12以后就不行了,因为我试了Android13不行了,更高版本就没试过了。在公司的以下设备试过是OK的:
Android 11的手机Android 12的手机Android 7.1.1的手机Android 11的球机
但是在公司的一台Android 7.1.1的球机设备则无法获取到。所以该方式并不是百分百可靠。另外,在Github上找相关实现的时候了解到一个情况,国外的sim卡的iccid是19位,而我们国内的是20位的。
Google非常的鸡贼,它的文档声明没有明确指出getId()返回的就是iccid,但是到了后面的版本,它确实也不再使用iccid,在我的小米11 pro (Android 14版本)中,双卡分别返回id为1和2。
示例代码如下:
权限:
布局:
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textSize="16sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
代码:
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
log("读电话状态权限:$it")
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
getIccid()
}
}.launch(Manifest.permission.READ_PHONE_STATE)
log("当前的Android版本:API ${Build.VERSION.SDK_INT}")
}
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private fun getIccid() {
val tm = getSystemService(TELECOM_SERVICE) as TelecomManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// API 23,Android 6.0
val phoneAccounts = tm.callCapablePhoneAccounts
printIccid("Android 6.0", phoneAccounts)
if (phoneAccounts.size == 1) {
binding.textView.text = "iccid = ${phoneAccounts[0].id}"
} else if (phoneAccounts.size == 2) {
binding.textView.text = "卡1 iccid = ${phoneAccounts[0].id}\n" +
"卡2 iccid = ${phoneAccounts[1].id}"
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// API 26,Android 8.0
printIccid("Android 8.0", tm.selfManagedPhoneAccounts)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// API 33,Android 13.0
printIccid("Android 13.0", tm.ownSelfManagedPhoneAccounts)
}
}
private fun printIccid(sdk: String, phoneAccounts: List
log("$sdk, size = ${phoneAccounts.size}")
phoneAccounts.forEach { log("$sdk, iccid = ${it.id}") }
}
private fun log(msg: String) {
Log.i("MainActivity", msg)
}
}
在TelecomManager中,能返回PhoneAccountHandle列表的函数有3个:
callCapablePhoneAccountsselfManagedPhoneAccountsownSelfManagedPhoneAccounts
实际测试发现只有第一个是管用的。
在我的小米11 pro(Android 14)上运行结果如下:
当前的Android版本:API 34
读电话状态权限:true
Android 6.0, size = 2
Android 6.0, iccid = 1
Android 6.0, iccid = 2
Android 8.0, size = 0
Android 13.0, size = 0
在公司的一台Android12的设备上运行结果如下:
当前的Android版本:API 31
读电话状态权限:true
Android 6.0, size = 2
Android 6.0, iccid = 89860440191890114929
Android 6.0, iccid = 898604401918C0574463
Android 8.0, size = 0
可以看到,这里其中一张卡的iccid是有字母C的,一样可以正确获取到。当我把卡拨掉再运行时,效果如下:
当前的Android版本:API 31
读电话状态权限:true
Android 6.0, size = 1
Android 6.0, iccid = E
Android 8.0, size = 0
可以看到,在没卡的情况下,它的列表依旧会返回一个数据,获取的ID为E,可能是表示Error吧,所以,当获取到iccid后,要判断它的长度是否大于等20位,如果不是则是无效的。
在公司的一台Android11的设备上运行结果如下:
当前的Android版本:API 30
读电话状态权限:true
Android 6.0, size = 2
Android 6.0, iccid = 89861121347756070742
Android 6.0, iccid = 89860117859015321920
Android 8.0, size = 0
这里我用的是我自己的手机卡,在SIM卡背后显示是这样的:
中国电信:8986112134775607074X
中国联通:8986011785901532192V
可以看到,在实体卡片上,它把最后一位数字用字母给隐藏起来,实际上它是一个数字。
在公司的一台Android 7.1.1的球机上运行结果如下:
当前的Android版本:API 25
读电话状态权限:true
Android 6.0, size = 2
Android 6.0, iccid = 10
Android 6.0, iccid = 11
当我拨掉一张卡后:
当前的Android版本:API 25
读电话状态权限:true
Android 6.0, size = 1
Android 6.0, iccid = 4
另外,在公司的一台Android12版本的执法记录仪设备上,发现有系统签名时,使用老的方式也能获取到iccid,如下:
val telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
val iccid = telephonyManager.simSerialNumber
所以,在整合一个工具类方法时,不论是什么版本,都用老的方式先获取一下,获取不到再用新方式获取,如下:
fun getSimSerialNumber(context: Context): String? {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
return null
}
val telephonyManager = context.getSystemService(TELEPHONY_SERVICE) as TelephonyManager
var simSerialNumber: String? = null
try {
simSerialNumber = telephonyManager.simSerialNumber
} catch (e: Exception) {
println("传统方式获取sim卡序列号时出现异常:${e.message}")
}
if (simSerialNumber == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 下面的获取方式的API是在Android M(6.0) 才出来的。
try {
val telecomManager = context.getSystemService(TELECOM_SERVICE) as TelecomManager
val phoneAccounts = telecomManager.callCapablePhoneAccounts
if (phoneAccounts != null && phoneAccounts.isNotEmpty()) {
val id = phoneAccounts[0].id
if (id != null && id.length >= 20) {
simSerialNumber = id
}
}
} catch (e: Exception) {
println("新方式获取sim卡序列号时出现异常:${e.message}")
}
}
return simSerialNumber
}
2025-01-15:在公司的一台Android 11的球机设备上,使用phoneAccounts的方式可以获取到sim卡序列号的 ,但是另一款球机不行,它也Andorid11系统,这就很神奇了,在有系统签名的情况下,telephonyManager.simSerialNumber可以获取到sim卡序列号,但是有字母会被截断,用下面的方法可以获取到带字母的:
fun getDefaultDataSimCardSerialNumber(context: Context): String? {
return try {
val sm = context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager
val activeSubscriptionInfoList = sm.activeSubscriptionInfoList
if (activeSubscriptionInfoList != null) {
val defaultDataSubscriptionId = getDefaultDataSubscriptionId()
defaultDataSubscriptionId?.let { subscriptionId ->
activeSubscriptionInfoList.find { it.subscriptionId == subscriptionId }?.iccId
}
} else {
null
}
} catch (e: Exception) {
null
}
}
private fun getDefaultDataSubscriptionId(): Int? {
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
null // Android 5.0 或以下(即Android L,即Api21) 时还没有SubscriptionManager类,所以无法获取默认数据Sim卡的位置
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
SubscriptionManager.getDefaultDataSubscriptionId() // 此函数是在Android N版本才出来的
} catch (e: Exception) {
null
}
} else {
try {
SubscriptionManager::class.java.getMethod("getDefaultDataSubId").invoke(null) as? Int?
} catch (e: Exception) {
null
}
}
}
这个方式,在一款Android 7.1.1版本的球机上获取带字母的iccid时也是会被截断的,但是在一款Android11的球机上获取又是完整的,所以每个设备,不同方法都要试一下,看哪个方法可以,然后就写死用那个方法。
完整代码:https://gitee.com/daizhufei/SIM_ICCID
2025-01-22:今天又有新发现,在telephonyManager.uiccCardsInfo中也保存有iccid,代码如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Q 为 Android 10
App.telephonyManager.uiccCardsInfo.forEach { uiccCardInfo: UiccCardInfo ->
Timber.i("$uiccCardInfo")
}
}
打印结果如下:
UiccCardInfo (mIsEuicc=false, mCardId=2, mEid=null, mIccId=898608351823D0009543, mSlotIndex=0, mIsRemovable=true)
可以看到,这里有iccid,且包含有字母。这个方法也是需要有系统签名才可以的。另外,这个对象中获取iccid的方法在API 33(Android 13)已经过时,推荐使用UiccPortInfo.getIccId()来获取iccid。另外,在UiccCardInfo中有getSlotIndex()获取这个卡所在的卡槽位置,此函数也是在API33过时 ,推荐使用该对象的另一个函数:getPhysicalSlotIndex()。还有,在这个对象中的getPorts()返回的对象中也保存有iccid。