首页/新闻资讯/正文详情

Android 内容提供器读取手机联系人:TaoToken 统一 Key 配置与动态权限验证

发布时间:2026/9/26 4:00:11 来源:云帆数科 栏目:资讯中心
Android 内容提供器读取手机联系人:TaoToken 统一 Key 配置与动态权限验证
1. 从一次真机调试说起联系人读取为什么总在权限上翻车Android 内容提供器ContentProvider读取手机联系人是很多初学者接触跨应用数据共享的第一个实战场景。它能做什么简单说就是让你的 App 通过ContentResolver去查询系统联系人数据库把姓名和号码拉出来展示。适合谁适合正在学 Android 四大组件、准备做通讯录备份、来电秀、批量导入这类功能的朋友。但真正上手你会发现代码逻辑本身不复杂翻车点几乎全集中在两件事上一是AndroidManifest.xml里权限声明漏了或者写错位置二是动态申请READ_CONTACTS的回调没处理好导致点了「允许」还是读不到数据。更隐蔽的是很多教程只讲本地读取没讲当你要把联系人数据同步到远端、或者调用大模型做智能整理时Key 和 API 通道怎么统一配置。这篇就把这条完整链路拆开从清单声明、动态权限骨架到 TaoToken 统一 Key 的 config 配置再到真机验证和报错排查一步步给你可复制的代码。我试过在 Android 13 和 Android 14 真机上跑同一套代码行为差异还挺明显后面会具体说。2. TaoToken 前置统一 Key 与 API 通道准备在动手写联系人读取之前先把「数据出去」的通道准备好。因为联系人读出来之后你大概率要做点事情——比如同步到云端、做去重、或者丢给模型做智能分组。这时候如果每个功能都单独配一套 Key维护起来会很乱。TaoToken 的思路是给你一个统一的 Key 和 API 入口Android 端只需要维护一份配置。你需要先拿到两样东西一个 API Key以及确认接入地址。控制台里创建 Key 的入口在这里https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite创建完 Key 之后API 的基础地址是https://taotoken.net/api注意这个 API 地址后面不加任何 UTM 参数保持干净。Key 的管理页面在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite如果你后面要做的是长期编码、Agent 类任务而不是单次调用可以看下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite接入文档在这里配置字段有疑问直接查https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite注意Key 属于敏感凭证不要硬编码进 Git 仓库。Android 端建议放在local.properties或BuildConfig里通过 Gradle 注入。3. 可复制配置清单声明 动态权限 统一 Key 骨架3.1 AndroidManifest.xml 权限声明片段联系人读取属于危险权限必须在清单里声明否则动态申请时系统直接拒绝。把下面这段放到manifest标签内、application标签之前uses-permission android:nameandroid.permission.READ_CONTACTS / uses-permission android:nameandroid.permission.WRITE_CONTACTS /WRITE_CONTACTS按需加只读的话第一个就够了。这里有个容易踩的坑有人把uses-permission写进了application里面编译能过但运行时权限永远拿不到一定要放在application外面。3.2 动态权限申请代码骨架Android 6.0 以后危险权限必须运行时申请。下面这个骨架把「检查—申请—回调」三段串起来了public class ContactActivity extends AppCompatActivity { private static final int REQ_READ_CONTACTS 1001; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_contact); ensureContactPermission(); } private void ensureContactPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) PackageManager.PERMISSION_GRANTED) { readContacts(); } else { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, REQ_READ_CONTACTS); } } Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQ_READ_CONTACTS) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { readContacts(); } else { Toast.makeText(this, 未授予联系人权限, Toast.LENGTH_SHORT).show(); } } } }3.3 读取联系人的核心查询逻辑拿到权限后用ContentResolver.query()查询ContactsContract.CommonDataKinds.Phone.CONTENT_URI逐行读取private void readContacts() { Cursor cursor null; try { cursor getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null); if (cursor ! null) { while (cursor.moveToNext()) { String name cursor.getString(cursor.getColumnIndexOrThrow( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); String number cursor.getString(cursor.getColumnIndexOrThrow( ContactsContract.CommonDataKinds.Phone.NUMBER)); Log.d(Contact, name - number); } } } catch (Exception e) { Log.e(Contact, 读取失败, e); } finally { if (cursor ! null) cursor.close(); } }3.4 TaoToken 统一 Key 的 config 配置骨架联系人读出来之后如果要走远端通道把 Key 和地址统一收口到一个配置类里。下面用BuildConfig注入的方式避免硬编码public final class ApiConfig { // 在 build.gradle 中通过 buildConfigField 注入 public static final String BASE_URL BuildConfig.TAOTOKEN_BASE_URL; public static final String API_KEY BuildConfig.TAOTOKEN_API_KEY; public static final String CHAT_ENDPOINT BASE_URL /v1/chat/completions; private ApiConfig() {} }对应的build.gradle模块级里这样注入android { buildFeatures { buildConfig true } defaultConfig { buildConfigField String, TAOTOKEN_BASE_URL, \https://taotoken.net/api\ buildConfigField String, TAOTOKEN_API_KEY, \${project.findProperty(TAOTOKEN_API_KEY) ?: }\ } }Key 的值放在项目根目录gradle.properties或本地local.properties里不要提交到版本库。4. 验证请求与成功结果4.1 真机验证联系人读取装到真机后第一次进入页面会弹出权限对话框。点「允许」后看 Logcat 过滤Contact标签应该能看到类似输出D/Contact: 张三 - 13800000000 D/Contact: 李四 - 13900000001 D/Contact: 王五 - 13700000002如果列表为空但没报错先确认手机通讯录里确实有联系人再确认查询的 URI 没写错。Phone.CONTENT_URI和Contacts.CONTENT_URI返回的字段不一样前者带号码后者只有姓名。4.2 验证统一 Key 通道联系人数据拿到后用一次最小请求验证 Key 通道是否通。下面用HttpURLConnection发一个请求确认能正常返回public void verifyChannel() throws IOException { URL url new URL(ApiConfig.CHAT_ENDPOINT); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setRequestProperty(Authorization, Bearer ApiConfig.API_KEY); conn.setRequestProperty(Content-Type, application/json); conn.setDoOutput(true); String body {\model\:\gpt-4o-mini\,\messages\: [{\role\:\user\,\content\:\ping\}]}; try (OutputStream os conn.getOutputStream()) { os.write(body.getBytes(StandardCharsets.UTF_8)); } int code conn.getResponseCode(); Log.d(ApiConfig, HTTP code); }返回HTTP 200就说明 Key 和地址配置正确。想直接在网页上验证模型对话可以用这个入口https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite5. 本篇常见错排查5.1 权限已允许但 Cursor 为 null最常见的原因是清单里权限声明位置错了或者用了READ_CONTACTS却查了需要WRITE_CONTACTS的 URI。先检查uses-permission是否在application外面再确认查询的 URI 和权限匹配。5.2 回调里 grantResults 为空数组用户点了「拒绝」或者系统直接取消了对话框grantResults会是空数组。代码里判断grantResults.length 0是必须的否则会数组越界。另外 Android 11 以后用户拒绝两次后系统不再弹窗需要引导去设置页手动开Intent intent new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.fromParts(package, getPackageName(), null)); startActivity(intent);5.3 Android 13 读取部分联系人字段异常Android 13 对联系人读取做了更细的权限拆分但READ_CONTACTS仍然覆盖基本读取。如果遇到SecurityException先确认 targetSdk 和真机系统版本再检查是否在query时请求了未授权的列。用getColumnIndexOrThrow而不是getColumnIndex能在字段不存在时尽早暴露问题。5.4 统一 Key 请求返回 401401 基本是 Key 没带上或者带错了。检查Authorization头是不是Bearer加空格再加 Key空格漏了也会 401。另外确认BASE_URL结尾没有多余斜杠https://taotoken.net/api后面直接拼/v1/chat/completions不要出现双斜杠。5.5 主线程查询导致 ANR联系人数据量大时query放在主线程会卡顿甚至 ANR。把readContacts()丢到子线程用ExecutorService或者Coroutine处理回调再更新 UI。这个坑在联系人上千条时特别明显。6. 继续接入与长期使用建议联系人读取跑通之后下一步通常是把数据用起来。如果你只是偶尔验证模型效果直接用模型对话页面就够了https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite如果你要做的是长期编码、Agent 自动化这类持续调用场景建议走 Coding PlanKey 和配额管理会更省心https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteKey 的创建和轮换在控制台完成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite配置字段拿不准的时候接入文档里都有对照说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite最后给个实用建议把联系人读取和网络请求彻底解耦。读取只负责拿到ListContact上传或调用模型单独封装一层这样权限逻辑和业务逻辑互不干扰排查问题时也能快速定位是权限没给还是通道没通。真机上多试几个系统版本Android 13 和 14 的权限行为差异只有实际跑过才知道。

相关推荐

Elsevier 投稿 LaTeX 报错排查:TaoToken 统一 Key 通道下修复 expl3 支持文件不匹配
Elsevier 投稿 LaTeX 报错排查:TaoToken 统一 Key 通道下修复 expl3 支持文件不匹配

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

如何用codex-claude-academic-skills做MATLAB仿真与信号处理?从入门到论文配图
如何用codex-claude-academic-skills做MATLAB仿真与信号处理?从入门到论文配图

如何用codex-claude-academic-skills做MATLAB仿真与信号处理?从入门到论文配图 【免费下载链接】codex-claude-academic-skills 本仓库包含三个面向学术科研人员的Skills,覆盖从文献阅读、论文写作到科学计算的完整研究工作流。office-academic-skill 负… · 2026/9/26 4:00:04

LangGraph 比 LangChain 有哪些核心优势?
LangGraph 比 LangChain 有哪些核心优势?

一、 从“链(Chain)”到“图(Graph)”:认知范式的根本转向┌────────────────────────────────────────────────────────────────────────┐… · 2026/9/26 3:59:58

PHP名片系统源码实战:从环境部署到二维码生成与二次开发
PHP名片系统源码实战:从环境部署到二维码生成与二次开发

简介:这是一个基于PHP开发的名片管理系统完整源码包,内置前端展示、后端业务逻辑与数据库脚本,适合PHP初学者、Web开发者以及需要快速搭建名片管理功能的项目参考。源码包共146个文件,体积约1.79MB,以PHP、JavaScript、… · 2026/9/26 6:35:43

DC-DC三大拓扑选型本质:BUCK/BOOST/BUCK-BOOST的工程逻辑
DC-DC三大拓扑选型本质:BUCK/BOOST/BUCK-BOOST的工程逻辑

1. 为什么只讲BUCK、BOOST、BUCK-BOOST?——拓扑选择的本质逻辑DC-DC转换器的“三大拓扑”这个说法,在电源工程师圈子里几乎成了条件反射式的开场白。但你有没有想过,为什么是这三个,而不是四个、五个,或者干脆换成LLC… · 2026/9/26 6:35:37

TypeScript 7 原生工具链整合:tsgo 名称退场、代码库回归主仓库与 VS Code 扩展捆绑
TypeScript 7 原生工具链整合:tsgo 名称退场、代码库回归主仓库与 VS Code 扩展捆绑

文档教程 【免费下载链接】typescript-book The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source. 项目地址: https://gitcode.com/gh_mirrors/typ/typescript-book 点击查看 免费下载 TypeScript 7.0 稳… · 2026/9/26 6:35:37

SSM医院住院综合管理系统:从业务闭环到源码调试全解析
SSM医院住院综合管理系统:从业务闭环到源码调试全解析

1. 住院系统到底“全”在哪:模块边界与核心业务流每年到课程设计和毕业设计的节点,SSM医院住院综合管理系统都是后台私信里问得最勤的题目之一。原因很简单:这个选题业务场景足够真实,模块划分有得写,SSM三件套又能把J… · 2026/9/26 6:35:30

Java Docker镜像瘦身:JDK精简与多阶段构建实战
Java Docker镜像瘦身:JDK精简与多阶段构建实战

1. 为什么一个Java应用的Docker镜像动辄800MB?——从JDK膨胀说起你有没有在CI/CD流水线里盯着构建日志发过呆?“Sending build context to Docker daemon 2.5GB”——这行字一出来,心里就咯噔一下。更别提推送到私有仓库时,镜像层… · 2026/9/26 6:35:30

gplearn实战:用遗传规划自动挖掘量化因子
gplearn实战:用遗传规划自动挖掘量化因子

简介:基于gplearn模型的量化交易因子自动生成完整项目,利用遗传规划中的选择、交叉与变异操作,自动挖掘能预测价格变动的数学表达式,面向量化分析师、金融工程人员及Python开发者,弥补传统手工因子提取的局限。压缩包共… · 2026/9/26 6:35:30

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

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

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码