Android Studio Kotlin获取来电联系人姓名与号码异常求助
问题说明
开发需获取来电联系人姓名与手机号码的Android应用时,公开参考方案多为Java实现,Kotlin相关参考较少。
参考的Java实现代码如下:
public class ServiceReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { TelephonyManager telephony = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE); telephony.listen(new PhoneStateListener(){ @Override public void onCallStateChanged(int state, String incomingNumber) { super.onCallStateChanged(state, incomingNumber); System.out.println("incomingNumber : "+incomingNumber); } },PhoneStateListener.LISTEN_CALL_STATE); } }
转换得到的初始Kotlin实现代码如下:
@Suppress("DEPRECATION", "DEPRECATION") class ServiceReceiver : BroadcastReceiver() { @SuppressLint("UnsafeProtectedBroadcastReceiver", "InlinedApi") override fun onReceive(context : Context?, intent : Intent?) { val tm = context?.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { tm.registerTelephonyCallback( context.mainExecutor, object : TelephonyCallback(), TelephonyCallback.CallStateListener { override fun onCallStateChanged(state: Int) { } }) } else { tm.listen(object : PhoneStateListener() { @Deprecated("Deprecated in Java") override fun onCallStateChanged(state: Int, incomingNumber: String) { super.onCallStateChanged(state, incomingNumber) println("incomingNumber : $incomingNumber") } }, PhoneStateListener.LISTEN_CALL_STATE) } } }
运行时println方法正常触发,但打印的来电号码为空,运行日志如下:
D/CompatibilityChangeReporter: Compat change id reported: 147600208; UID 10122; state: ENABLED I/System.out: incomingNumber :
问题根因
- Android 9(API 28)及以上版本新增权限限制,未授予
READ_CALL_LOG权限时,系统会将来电号码字段置空,旧版PhoneStateListener回调、广播Intent中的号码字段都不会返回有效值,日志中提示的compat 147600208就是该权限限制的系统提示。 - Android 12(API 31)及以上版本使用的
TelephonyCallback.CallStateListener的onCallStateChanged方法仅返回通话状态,不直接携带来电号码,初始实现中没有补充号码获取逻辑。 - 直接在
BroadcastReceiver的onReceive方法中注册通话状态监听器存在生命周期问题,onReceive执行完成后上下文会被系统回收,注册的监听器随时可能被销毁,导致状态回调异常。 - 初始实现未做运行时动态权限申请,Android 6.0以上电话、通话记录相关的危险权限必须用户手动授权后才能正常使用。
正确实现步骤
- 先在
AndroidManifest.xml中声明所需权限:
<!-- 读取电话状态基础权限 --> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <!-- Android 9+ 获取来电号码必需权限 --> <uses-permission android:name="android.permission.READ_CALL_LOG" /> <!-- 读取联系人信息权限,用于根据号码查询联系人姓名 --> <uses-permission android:name="android.permission.READ_CONTACTS" />
- 在注册通话监听、接收来电广播前,先动态申请上述三个危险权限,用户授权通过后再执行后续逻辑。
- 优先从来电广播的Intent中直接取号码,取不到时再通过通话状态监听、查询最新通话记录的方式补获号码,拿到结果后及时注销监听器避免内存泄漏。
修正后的可运行Kotlin代码如下:
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") class ServiceReceiver : BroadcastReceiver() { @SuppressLint("MissingPermission", "UnsafeProtectedBroadcastReceiver") override fun onReceive(context: Context?, intent: Intent?) { context ?: return intent ?: return // 第一步:优先从广播Extra直接取来电号码,兼容全版本 val extraNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) if (!extraNumber.isNullOrBlank()) { handleIncomingCall(context, extraNumber) return } val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager // 分版本处理监听逻辑 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val callCallback = object : TelephonyCallback(), TelephonyCallback.CallStateListener { override fun onCallStateChanged(state: Int) { if (state == TelephonyManager.CALL_STATE_RINGING) { // 响铃状态下查询最新一条来电记录拿号码 val incomingNumber = queryLatestIncomingNumber(context) if (incomingNumber.isNotBlank()) handleIncomingCall(context, incomingNumber) } // 状态变化后及时注销回调 telephonyManager.unregisterTelephonyCallback(this) } } telephonyManager.registerTelephonyCallback(context.mainExecutor, callCallback) } else { telephonyManager.listen(object : PhoneStateListener() { override fun onCallStateChanged(state: Int, incomingNumber: String?) { super.onCallStateChanged(state, incomingNumber) if (state == TelephonyManager.CALL_STATE_RINGING) { val validNumber = if (!incomingNumber.isNullOrBlank()) { incomingNumber } else queryLatestIncomingNumber(context) if (validNumber.isNotBlank()) handleIncomingCall(context, validNumber) } // 注销监听 telephonyManager.listen(this, PhoneStateListener.LISTEN_NONE) } }, PhoneStateListener.LISTEN_CALL_STATE) } } /** * 处理拿到号码后的逻辑:打印号码、查询联系人姓名 */ private fun handleIncomingCall(context: Context, phoneNumber: String) { println("来电号码:$phoneNumber") val contactName = queryContactNameByNumber(context, phoneNumber) println("来电联系人姓名:${contactName ?: "未知联系人"}") } /** * 根据号码查询本地联系人姓名 */ private fun queryContactNameByNumber(context: Context, phoneNumber: String): String? { val lookupUri = Uri.withAppendedPath( ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber) ) var displayName: String? = null context.contentResolver.query( lookupUri, arrayOf(ContactsContract.PhoneLookup.DISPLAY_NAME), null, null, null )?.use { cursor -> if (cursor.moveToFirst()) { displayName = cursor.getString( cursor.getColumnIndexOrThrow(ContactsContract.PhoneLookup.DISPLAY_NAME) ) } } return displayName } /** * 查询通话记录中最新的一条来电号码,解决回调返回空值的问题 */ @SuppressLint("MissingPermission") private fun queryLatestIncomingNumber(context: Context): String { var number = "" val projection = arrayOf(CallLog.Calls.NUMBER, CallLog.Calls.TYPE, CallLog.Calls.DATE) val selection = "${CallLog.Calls.TYPE} = ${CallLog.Calls.INCOMING_TYPE}" val sortRule = "${CallLog.Calls.DATE} DESC LIMIT 1" context.contentResolver.query( CallLog.Calls.CONTENT_URI, projection, selection, null, sortRule )?.use { cursor -> if (cursor.moveToFirst()) { number = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER)) } } return number } }
注意事项
- 不要依赖
TelephonyManager.getLine1Number()获取来电号码,该字段在多数SIM卡上为空或存储错误值,没有实际使用价值。 - 部分国产定制ROM对通话、通话记录权限有额外限制,需要引导用户在系统设置中手动开启对应权限,否则依然会出现号码为空的问题。
- 不要在
BroadcastReceiver中执行耗时操作,逻辑处理完成后及时注销注册的监听器,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Akshay Saambram
相关产品推荐
相关产品推荐

