You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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以上电话、通话记录相关的危险权限必须用户手动授权后才能正常使用。
正确实现步骤
  1. 先在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" />
  1. 在注册通话监听、接收来电广播前,先动态申请上述三个危险权限,用户授权通过后再执行后续逻辑。
  2. 优先从来电广播的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 02:15:39