☰
Android registerForActivityResult 获取联系人失败:PickContact 契约的配置与验证
2026/9/28 19:06:57 网站建设 项目流程

1. 为什么 PickContact 拿不到联系人:先看一个真实报错

registerForActivityResult(ActivityResultContracts.PickContact())是 Android 官方在 Activity Result API 里提供的一个联系人选择契约,它能做什么?简单说就是拉起系统联系人界面,让用户点一个联系人,然后把选中联系人的Uri回传给你。适合谁?适合所有需要在 App 里让用户"选一个联系人"的场景,比如分享、邀请、填表单。

但很多人第一次用就会踩坑:回调里拿到Uri之后,用ContentResolver.query()去查姓名和号码,结果要么抛java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow,要么cursor.moveToFirst()直接返回 false,查询为空。更迷惑的是,这段查询代码以前明明好使,今天突然不行了。

这个报错的本质是:PickContact()返回的Uri指向的是ContactsContract.Contacts.CONTENT_TYPE(联系人聚合表),而你的查询投影用的是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME / NUMBER(电话明细表)。两张表的列名对不上,getColumnIndex()返回 -1,cursor.getString(-1)自然就崩了。下面从权限声明、契约注册时机、回调解析三个方向,把这个问题彻底拆开。

2. 前置准备:权限、依赖与 TaoToken 通道

在动手改代码之前,先把环境确认清楚。联系人读取涉及运行时权限,Android 6.0 之后必须在AndroidManifest.xml里声明,并在运行时动态申请。

<!-- AndroidManifest.xml --> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 读取联系人,PickContact 本身不需要,但回调里查询联系人详情需要 --> <uses-permission android:name="android.permission.READ_CONTACTS" /> <application android:allowBackup="true" android:label="@string/app_name"> <!-- ... --> </application> </manifest>

依赖方面,Activity Result API 已经包含在androidx.activity:activity和androidx.fragment:fragment里,只要你的项目用了 AndroidX,通常不需要额外加库。确认一下build.gradle:

dependencies { implementation "androidx.activity:activity-ktx:1.8.2" implementation "androidx.fragment:fragment-ktx:1.6.2" }

如果你在排查过程中需要对照官方文档、或者用大模型帮忙分析ActivityResultContract的源码逻辑,可以走 TaoToken 的统一 Key/API 通道,一个 Key 就能切换不同模型,省去到处找入口的麻烦。模型对话入口在 https://taotoken.net/api,接入文档在 https://taotoken.net/api-keys,长期做 Android 编码或 Agent 的话可以看 Coding Plan 页面。这些只是辅助排查的工具,核心问题还是得回到代码本身。

3. 可复制配置:契约注册时机与代码骨架

3.1 注册时机必须在 STARTED 之前

registerForActivityResult()有一个硬性要求:必须在Activity或Fragment的onCreate()阶段(更准确说是STARTED状态之前)调用。如果你把它写在按钮点击回调里,会直接抛IllegalStateException: LifecycleOwner is attempting to register while current state is RESUMED。

正确做法是把它声明为成员变量,在onCreate里或直接用属性初始化:

class ContactPickerActivity : AppCompatActivity() { // 关键:作为成员变量注册,生命周期安全 private val pickContactLauncher = registerForActivityResult( ActivityResultContracts.PickContact() ) { uri: Uri? -> if (uri == null) { Log.w(TAG, "用户取消了选择,uri 为空") return@registerForActivityResult } Log.d(TAG, "选中联系人 uri = $uri") readContactFromUri(uri) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_contact_picker) findViewById<Button>(R.id.btn_pick).setOnClickListener { // 先检查权限,再拉起 if (hasReadContactsPermission()) { pickContactLauncher.launch(null) } else { requestContactsPermission.launch(Manifest.permission.READ_CONTACTS) } } } }

注意PickContact的输入类型是Void,Kotlin 里传null即可,Java 里传null。

3.2 运行时权限申请也要用 Activity Result API

不要再用onRequestPermissionsResult那套老写法,统一用RequestPermission契约:

private val requestContactsPermission = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted: Boolean -> if (granted) { Log.d(TAG, "READ_CONTACTS 已授权,拉起联系人") pickContactLauncher.launch(null) } else { Log.e(TAG, "READ_CONTACTS 被拒绝,无法读取联系人详情") Toast.makeText(this, "需要联系人权限才能读取号码", Toast.LENGTH_SHORT).show() } } private fun hasReadContactsPermission(): Boolean { return ContextCompat.checkSelfPermission( this, Manifest.permission.READ_CONTACTS ) == PackageManager.PERMISSION_GRANTED }

3.3 回调解析:区分 Contacts 表和 Phone 表

这是最容易出错的地方。PickContact()返回的Uri形如content://com.android.contacts/contacts/lookup/xxx/123,它指向的是Contacts表。而你要拿电话号码,得去Phone表查,用Contacts._ID做关联。

private fun readContactFromUri(uri: Uri) { val contentResolver = contentResolver // 第一步:从 Contacts 表拿到 contactId val contactId = uri.lastPathSegment?.toLongOrNull() Log.d(TAG, "contactId = $contactId") if (contactId == null) { Log.e(TAG, "无法从 uri 解析 contactId: $uri") return } // 第二步:用 contactId 去 Phone 表查姓名和号码 val projection = arrayOf( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER ) val selection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?" val selectionArgs = arrayOf(contactId.toString()) contentResolver.query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, selection, selectionArgs, null )?.use { cursor -> Log.d(TAG, "cursor count = ${cursor.count}") if (cursor.moveToFirst()) { val nameIndex = cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME ) val numberIndex = cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.NUMBER ) Log.d(TAG, "nameIndex=$nameIndex, numberIndex=$numberIndex") if (nameIndex >= 0 && numberIndex >= 0) { val name = cursor.getString(nameIndex) val number = cursor.getString(numberIndex) Log.d(TAG, "联系人: $name, 号码: $number") } } else { Log.w(TAG, "Phone 表查询为空,可能该联系人没有电话号码") } } }

关键点:查询的Uri必须是Phone.CONTENT_URI,投影必须是Phone.DISPLAY_NAME / NUMBER,两者要匹配。如果你直接用PickContact返回的uri去查Phone的列,就会触发col -1那个异常。

4. 验证请求:日志确认回调数据是否为空

改完代码后,怎么确认问题真的解决了?靠日志。下面是一套完整的验证动作。

第一步,在拉起联系人前打日志,确认权限状态:

Log.d(TAG, "权限状态: ${hasReadContactsPermission()}")

第二步,在回调入口打日志,确认uri是否为空:

Log.d(TAG, "回调触发, uri=$uri, resultCode 由契约内部处理")

如果uri为 null,说明用户点了返回或取消,不是 bug。

第三步,在查询前后打日志,确认cursor的 count 和列索引:

Log.d(TAG, "查询 uri=$queryUri") Log.d(TAG, "cursor count=${cursor.count}") Log.d(TAG, "nameIndex=$nameIndex, numberIndex=$numberIndex")

正常成功的日志长这样:

D/ContactPicker: 权限状态: true D/ContactPicker: 回调触发, uri=content://com.android.contacts/contacts/lookup/0r1-2A3B4C/123 D/ContactPicker: contactId = 123 D/ContactPicker: 查询 uri=content://com.android.contacts/data/phones D/ContactPicker: cursor count=1 D/ContactPicker: nameIndex=0, numberIndex=1 D/ContactPicker: 联系人: 张三, 号码: 13800138000

如果cursor count=0,说明这个联系人没有存电话号码,或者你查的表不对。如果nameIndex=-1,说明投影和 Uri 不匹配,回到 3.3 检查。

5. 本篇常见错排查

5.1 IllegalStateException: Couldn't read row 0, col -1

这是最典型的报错。原因就是投影列在返回的 Cursor 里不存在。PickContact返回的是Contacts表的 Uri,你却用Phone表的列去getColumnIndex,返回 -1,getString(-1)直接崩。解决方式就是 3.3 里的两步查询:先从 Uri 拿 contactId,再去Phone.CONTENT_URI查。

5.2 查询结果为空但没报错

cursor.moveToFirst()返回 false,通常是三种情况:一是联系人本身没存号码;二是selection条件写错,比如用了Contacts._ID而不是Phone.CONTACT_ID;三是权限没给,query返回空 Cursor 而不抛异常。先打日志确认权限,再确认 selection。

5.3 自定义 PickContact 契约

如果你不想做两步查询,也可以自定义一个契约,直接让系统返回Phone表的 Uri:

class PickPhoneContact : ActivityResultContract<Void?, Uri?>() { override fun createIntent(context: Context, input: Void?): Intent { return Intent(Intent.ACTION_PICK).apply { type = ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE } } override fun parseResult(resultCode: Int, intent: Intent?): Uri? { if (intent == null || resultCode != Activity.RESULT_OK) return null return intent.data } }

这样返回的Uri直接指向Phone表,投影用Phone.DISPLAY_NAME / NUMBER就能一次查到。代价是用户看到的选择界面可能略有不同,取决于系统联系人 App 的实现。

5.4 注册时机导致的崩溃

registerForActivityResult写在onClick里会抛LifecycleOwner is attempting to register while current state is RESUMED。记住:它必须在onCreate阶段完成注册,作为成员变量是最稳的写法。

5.5 权限被永久拒绝

用户勾选"不再询问"后,RequestPermission回调返回 false,此时应该引导用户去设置页手动开启,而不是反复弹窗。可以用shouldShowRequestPermissionRationale判断。

6. 接入与排障:用 TaoToken 统一通道辅助定位

Android 的ActivityResultContract源码、ContactsContract的列定义、以及各种CursorWindow异常,排查起来经常需要在多个文档之间跳。如果你想让大模型帮你逐行分析PickContact的parseResult逻辑,或者对比自定义契约和官方契约的差异,可以走 TaoToken 的统一 Key/API 通道,一个 Key 切换模型,不用反复配置。

具体入口:模型对话在 https://taotoken.net/api,接入文档和 API Keys 在 https://taotoken.net/api-keys,长期做 Android 编码或 Agent 开发的可以看 Coding Plan 页面。把报错日志和代码片段贴进去,让它帮你确认投影列和 Uri 是否匹配,比人肉翻文档快得多。

回到问题本身:PickContact拿不到联系人,九成是 Uri 类型和查询投影不匹配。记住那个核心结论——PickContact返回Contacts表 Uri,查号码要去Phone表,用CONTACT_ID关联。把 3.3 的两步查询抄进项目,日志一打,问题基本就定位了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询