news 2026/10/7 19:33:24

安卓获取手机联系人和号码:TaoToken 统一 Key 通道下的权限申请与数据读取实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
安卓获取手机联系人和号码:TaoToken 统一 Key 通道下的权限申请与数据读取实战

1. 安卓读取联系人和号码,为什么一跑就崩

很多人第一次写「安卓获取手机联系人和号码」,代码看着没问题,真机一跑要么直接闪退,要么返回一堆 null。核心检索词先摆出来:安卓读取系统联系人、ContentResolver 查询号码、运行时权限申请,这三件事是整条链路的关键。它适合谁?适合正在做通讯录备份、来电名片、企业通讯录同步、客服工单自动带出客户信息的安卓开发者,尤其是刚接触 ContentProvider 的同学。

我先把最容易踩的坑说清楚。安卓 6.0(API 23)之后,READ_CONTACTS属于危险权限,光在 AndroidManifest.xml 里声明是不够的,必须在运行时动态申请。很多人只写了清单声明,结果ContentResolver.query()直接抛SecurityException,App 当场挂掉。第二个坑是号码字段:ContactsContract.CommonDataKinds.Phone.NUMBER拿到的号码可能带空格、横线、括号,甚至带国家码,直接拿去匹配会失败。第三个坑是查询方式,早期用ACTION_GET_CONTENT拉起系统选人界面,返回的 Uri 结构和你直接查Phone.CONTENT_URI完全不一样,混着用就会getColumnIndex返回 -1,再getString(-1)又崩。

这篇我会按真实项目链路走一遍:清单声明、运行时权限、ContentResolver 查询、号码字段兼容处理,最后把接口 endpoint 改到 TaoToken 统一 Key/API 通道做鉴权联调。你照着敲就能跑通,代码都是可复制的。整条链路里,本地读取是安卓系统能力,上传/联调走的是网络请求,这两段我会分开讲,避免你把权限问题和网络问题搅在一起排查。

先明确一个边界:读取联系人是设备本地行为,不需要联网;只有当你把读到的联系人同步到后端、或者调用大模型做号码归属/去重时,才涉及 API 通道。所以本文的网络部分只讲「怎么把请求 endpoint 指向统一通道并带上 Key」,不涉及任何绕过系统限制的操作。

2. TaoToken 统一 Key 通道前置准备

2.1 为什么联系人同步场景需要统一 Key

假设你做完本地读取,接下来要把联系人批量同步到服务端,或者调用模型做「号码格式归一化」「重复联系人合并建议」。传统做法是每个服务商一套 Key、一套 Base URL,散落在代码里,换环境就改一堆常量。TaoToken 的思路是给你一个统一的 API 通道:Base URL 固定,Key 统一管理,模型用 Model ID 区分。这样你在安卓端只需要维护一份配置,联调、测试、上线切换成本低很多。

官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API 根地址是 https://taotoken.net/api ,注意这个地址后面不加任何查询参数,保持干净。

2.2 拿到 Key 和确认 Model ID

进入控制台创建 API Key,路径是 console:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。创建完记得复制保存,页面关闭后一般不再完整显示。Key 的管理页在 api-keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,后续轮换、禁用都在这。

Model ID 不要凭记忆写,去文档页确认当前可用的模型标识:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你只是想先在网页里验证模型通不通,可以用模型对话页:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。

2.3 三件套:Base URL + Key + Model ID

不管你是用 OkHttp 手写请求,还是用 Cline、Codex 这类工具,配置永远是这三件套:

配置项值说明
Base URLhttps://taotoken.net/api固定,不带斜杠结尾参数
API Key控制台生成放请求头 Authorization
Model ID文档页确认请求体里的 model 字段

注意:Key 属于敏感凭证,不要硬编码进 APK 后直接发版。安卓端建议放local.properties或服务端下发,至少别提交到 Git。

如果你用的是 Claude Code 这类编码工具做联调辅助,接入文档在:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL 和鉴权头的写法。长期做编码和 Agent 任务的话,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。

3. 可复制配置:权限声明与查询代码

3.1 AndroidManifest.xml 权限声明

先声明权限,这是基础,但记住它不等于授权:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.contactsdemo"> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.INTERNET" /> <application android:allowBackup="true" android:label="ContactsDemo" android:theme="@style/Theme.AppCompat.Light"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

READ_CONTACTS是读联系人,INTERNET是后面联调 API 用的。如果你还要写回联系人,再加WRITE_CONTACTS,本文不涉及写入。

3.2 运行时权限申请

在 Activity 里动态申请,用registerForActivityResult这套新 API,别再用废弃的onRequestPermissionsResult:

class MainActivity : AppCompatActivity() { private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> if (granted) { loadContacts() } else { Toast.makeText(this, "未授予联系人权限,无法读取", Toast.LENGTH_SHORT).show() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btnLoad).setOnClickListener { if (ContextCompat.checkSelfPermission( this, Manifest.permission.READ_CONTACTS ) == PackageManager.PERMISSION_GRANTED ) { loadContacts() } else { requestPermission.launch(Manifest.permission.READ_CONTACTS) } } } }

这段逻辑很直白:已授权就直接读,没授权就申请,用户拒绝就提示。真机上第一次点按钮会弹系统对话框。

3.3 ContentResolver 查询联系人和号码

核心查询用Phone.CONTENT_URI,一次拿到名字和号码:

private fun loadContacts() { val result = mutableListOf<String>() val resolver = contentResolver val projection = arrayOf( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.CONTACT_ID ) val cursor = resolver.query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, projection, null, null, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC" ) cursor?.use { c -> val nameIdx = c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME) val numIdx = c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) while (c.moveToNext()) { val name = c.getString(nameIdx) ?: "未知" val rawNumber = c.getString(numIdx) ?: "" val cleanNumber = normalizeNumber(rawNumber) result.add("$name -> $cleanNumber") } } Log.d("Contacts", "共读取 ${result.size} 条") }

注意cursor?.use {}会自动关闭游标,避免内存泄漏。getColumnIndex拿到的索引如果查询失败会是 -1,所以 projection 必须和读取字段严格对应。

3.4 号码字段兼容处理

号码清洗是重点,不同厂商、不同 SIM 卡格式差异很大:

private fun normalizeNumber(raw: String): String { // 去掉空格、横线、括号 var n = raw.replace(Regex("[\\s\\-()]"), "") // 处理 +86 / 0086 前缀 n = n.removePrefix("+86").removePrefix("0086").removePrefix("86") // 只保留数字 n = n.filter { it.isDigit() } return n }

实测下来,+86 138-0000-0000会被清洗成13800000000,方便后续去重和匹配。如果你的业务需要保留原始格式,就把 raw 和 clean 都存下来。

3.5 把 endpoint 指向 TaoToken 统一通道

本地读完之后,如果要调用模型做号码归一化建议,用 OkHttp 发请求,Base URL 指向统一通道:

val client = OkHttpClient() val json = JSONObject().apply { put("model", "你的ModelID") put("messages", JSONArray().put(JSONObject().apply { put("role", "user") put("content", "帮我把这些号码去重并给出合并建议:$contactList") })) } val body = json.toString().toRequestBody("application/json".toMediaType()) val request = Request.Builder() .url("https://taotoken.net/api/v1/chat/completions") .addHeader("Authorization", "Bearer ${BuildConfig.TAOTOKEN_KEY}") .addHeader("Content-Type", "application/json") .post(body) .build()

Key 从BuildConfig读,别写死。Model ID 用文档页确认的那个。

4. 真机验证请求与成功结果

4.1 本地读取验证

装到真机,点按钮,授权后看 Logcat 过滤Contacts标签。正常输出类似:

D/Contacts: 共读取 128 条 D/Contacts: 张三 -> 13800000000 D/Contacts: 李四 -> 13911112222

如果条数是 0,先确认模拟器/真机通讯录里确实有联系人,很多模拟器默认是空的。

4.2 接口联调验证

用 curl 先验证通道通不通,再上安卓代码:

curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer 你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "你的ModelID", "messages": [{"role": "user", "content": "返回 ok"}] }'

成功会返回 JSON,choices[0].message.content里有内容。安卓端拿到响应后解析choices数组即可。如果 curl 通、安卓不通,八成是 Key 没打进请求头或者网络权限没加。

4.3 完整链路结果

理想状态下,你点一次按钮:申请权限 → 读取 128 条联系人 → 清洗号码 → 打包发给统一通道 → 返回去重建议。整个过程本地读取是毫秒级,网络请求取决于模型响应速度。真机验证时建议先断网测本地读取,确认权限和查询没问题,再联网测接口,这样出问题好定位。

5. 本篇常见错误排查

5.1 SecurityException: Permission Denial

报错原文类似:

java.lang.SecurityException: Permission Denial: reading com.android.providers.contacts.ContactsProvider2 uri content://com.android.contacts/data/phones from pid=... requires android.permission.READ_CONTACTS

原因:只声明了清单权限,没做运行时申请,或者用户点了拒绝。排查:确认checkSelfPermission返回PERMISSION_GRANTED再查询;被拒绝后引导用户去设置页手动开。

5.2 getColumnIndex 返回 -1 导致崩溃

报错:

java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow

原因:projection 里的字段和getColumnIndex取的字段不一致,或者用ACTION_GET_CONTENT返回的 Uri 去查Phone.NUMBER。排查:统一用Phone.CONTENT_URI查询,projection 和读取字段一一对应。

5.3 401 Unauthorized

接口返回:

{"error":{"message":"Invalid API key","type":"invalid_request_error"}}

原因:Key 没带、带错、或者Bearer后面多了空格。排查:确认请求头是Authorization: Bearer sk-xxx,Key 从控制台重新复制。Key 管理页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。

5.4 local proxy failed / connection refused

报错:

java.net.ConnectException: Failed to connect to taotoken.net/...

原因:设备网络不通,或者你本地配了什么代理把请求拦了。排查:先用浏览器或 curl 在设备同网络下测通,再排查 App 网络权限。注意不要使用任何非正规网络工具,保持直连即可。

5.5 reading choices 解析失败

报错:

org.json.JSONException: No value for choices

原因:响应结构和你解析的字段不匹配,可能是请求体格式错了(比如 messages 不是数组),服务端返回了错误对象而不是正常响应。排查:先把原始响应Log出来看,确认choices存在再解析。

5.6 OAuth / 鉴权头格式问题

如果你用某些工具接入,报 OAuth 相关错误,通常是鉴权方式选错了。统一通道用的是 Bearer Token,不是 OAuth 流程。接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。用 Cline MCP 或 Codex 的 auth.json 时,务必写全三件套:Base URL 填https://taotoken.net/api,Key 填你的凭证,Model ID 填文档确认的值,缺一个都会鉴权失败。

6. 继续联调与工具选择

本地读取跑通后,下一步通常是批量同步和智能处理。如果你只是偶尔验证模型输出,用模型对话页最快:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。如果你要把联系人同步做成长期任务、或者接 Agent 自动处理,Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。Key 的创建和轮换都在 api-keys 页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,接入细节查文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

最后给个实用建议:联系人读取这块,真机测试永远比模拟器靠谱,因为模拟器通讯录数据、权限弹窗行为都可能和真机不同。号码清洗函数建议单独写单元测试,把+86、0086、带横线、带括号这几种格式都覆盖一遍,上线前能省很多事。接口联调时,先把 curl 跑通再写安卓代码,能快速区分是通道问题还是客户端问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 19:33:01

Muse大更新:交互重构、Charm常驻与项目模式实战指南

刚刚&#xff0c;Muse迎来了一次大更新。作为一个从旧版本一路用过来的老用户&#xff0c;我几乎是更新推送的第一时间就升级了&#xff0c;连续高强度用了两天之后&#xff0c;才觉得自己有资格来聊聊这版到底改了什么。Muse这个名字&#xff0c;经常泡在AI工具圈的人应该不陌…

作者头像 李华
网站建设 2026/10/7 19:32:52

代码复现-FastVLM: Efficient Vision Encoding for Vision Language Models

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

作者头像 李华
网站建设 2026/10/7 19:32:33

毕业生必藏!2026年10款降AI软件实测红黑榜,热门AIGC检测避坑指南

现在写论文不容易&#xff0c;调整到符合检测要求的状态更是费精力。交稿前查一遍AI率&#xff0c;标红范围一大片&#xff0c;既没法顺利给导师过目&#xff0c;也通不过系统的正式检测。为了找到适配需求的降AI工具&#xff0c;我前前后后花了近一周时间&#xff0c;把市面上…

作者头像 李华
网站建设 2026/10/7 19:32:22

OpenMontage:面向视频工业化的可编排智能体工作流引擎

1. OpenMontage 是什么&#xff1a;一个被严重低估的开源视频智能体协作平台OpenMontage 这个名字乍一听像某个老派影视剪辑软件的开源分支&#xff0c;但实际完全不是。它既不是 Premiere 的平替&#xff0c;也不是 DaVinci Resolve 的简化版。我第一次在 GitHub 上看到它时&a…

作者头像 李华
网站建设 2026/10/7 19:32:15

Agent之skill:SkillsMP的简介、安装和使用方法、案例应用之详细攻略

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

作者头像 李华