新闻详情

新闻详情

首页 / 资讯中心 / 详情

Android registerForActivityResult 获取联系人失败:PickContact 契约的配置与验证

发布时间:2026/9/28 19:06:59来源:尧图网络
Android registerForActivityResult 获取联系人失败:PickContact 契约的配置与验证
1. 为什么 PickContact 拿不到联系人先看一个真实报错registerForActivityResult(ActivityResultContracts.PickContact())是 Android 官方在 Activity Result API 里提供的一个联系人选择契约它能做什么简单说就是拉起系统联系人界面让用户点一个联系人然后把选中联系人的Uri回传给你。适合谁适合所有需要在 App 里让用户选一个联系人的场景比如分享、邀请、填表单。但很多人第一次用就会踩坑回调里拿到Uri之后用ContentResolver.query()去查姓名和号码结果要么抛java.lang.IllegalStateException: Couldnt read row 0, col -1 from CursorWindow要么cursor.moveToFirst()直接返回 false查询为空。更迷惑的是这段查询代码以前明明好使今天突然不行了。这个报错的本质是PickContact()返回的Uri指向的是ContactsContract.Contacts.CONTENT_TYPE联系人聚合表而你的查询投影用的是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME / NUMBER电话明细表。两张表的列名对不上getColumnIndex()返回 -1cursor.getString(-1)自然就崩了。下面从权限声明、契约注册时机、回调解析三个方向把这个问题彻底拆开。2. 前置准备权限、依赖与 TaoToken 通道在动手改代码之前先把环境确认清楚。联系人读取涉及运行时权限Android 6.0 之后必须在AndroidManifest.xml里声明并在运行时动态申请。!-- AndroidManifest.xml -- manifest xmlns:androidhttp://schemas.android.com/apk/res/android !-- 读取联系人PickContact 本身不需要但回调里查询联系人详情需要 -- uses-permission android:nameandroid.permission.READ_CONTACTS / application android:allowBackuptrue android:labelstring/app_name !-- ... -- /application /manifest依赖方面Activity Result API 已经包含在androidx.activity:activity和androidx.fragment:fragment里只要你的项目用了 AndroidX通常不需要额外加库。确认一下build.gradledependencies { 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 为空) returnregisterForActivityResult } Log.d(TAG, 选中联系人 uri $uri) readContactFromUri(uri) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_contact_picker) findViewByIdButton(R.id.btn_pick).setOnClickListener { // 先检查权限再拉起 if (hasReadContactsPermission()) { pickContactLauncher.launch(null) } else { requestContactsPermission.launch(Manifest.permission.READ_CONTACTS) } } } }注意PickContact的输入类型是VoidKotlin 里传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: 回调触发, uricontent://com.android.contacts/contacts/lookup/0r1-2A3B4C/123 D/ContactPicker: contactId 123 D/ContactPicker: 查询 uricontent://com.android.contacts/data/phones D/ContactPicker: cursor count1 D/ContactPicker: nameIndex0, numberIndex1 D/ContactPicker: 联系人: 张三, 号码: 13800138000如果cursor count0说明这个联系人没有存电话号码或者你查的表不对。如果nameIndex-1说明投影和 Uri 不匹配回到 3.3 检查。5. 本篇常见错排查5.1 IllegalStateException: Couldnt read row 0, col -1这是最典型的报错。原因就是投影列在返回的 Cursor 里不存在。PickContact返回的是Contacts表的 Uri你却用Phone表的列去getColumnIndex返回 -1getString(-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表的 Uriclass PickPhoneContact : ActivityResultContractVoid?, 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 的两步查询抄进项目日志一打问题基本就定位了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

YOLOv5专用火焰检测数据集:工业级标注与训练落地指南 2026/9/29 1:44:50

YOLOv5专用火焰检测数据集:工业级标注与训练落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
推杆电机H桥驱动设计:从选型到热设计的全栈指南 2026/9/29 1:44:50

推杆电机H桥驱动设计:从选型到热设计的全栈指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
YOLO驱动的自动标注工程实践指南 2026/9/29 1:44:50

YOLO驱动的自动标注工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
STM32CubeMX 6.14 从下载安装到生成工程全流程避坑指南 2026/9/29 1:44:50

STM32CubeMX 6.14 从下载安装到生成工程全流程避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
高德地图个人开发者Key与安全密钥Vue接入实战 2026/9/29 1:44:50

高德地图个人开发者Key与安全密钥Vue接入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
手写一个 Claude Code(1):从 Agent Loop 到工具、权限、Hooks 与任务规划 2026/9/29 1:44:42

手写一个 Claude Code(1):从 Agent Loop 到工具、权限、Hooks 与任务规划

最近在学 AI Agent 开发,我把 learn-claude-code 的前五章整理成了这篇学习笔记:从最小的 Agent Loop 出发,逐步加入工具分发、权限检查、Hooks 和 TodoWrite,理解一个编码 Agent 的运行骨架。 读完你会得到什么:知道…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉