百度、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。