ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android 向系统通讯录添加联系人:TaoToken 统一 Key 配置与验证

Android 向系统通讯录添加联系人:TaoToken 统一 Key 配置与验证 1. Android 通讯录写入为什么会失败从权限到 RawContacts 的完整链路Android 向系统通讯录添加联系人看起来只是调一个insert()实际踩坑的人非常多。核心原因是通讯录的数据模型不是一张表而是RawContacts原始联系人和Data数据行两张表通过rawContactId关联你必须先插入一个空的 RawContacts 拿到 id再往 Data 表里分别写姓名、电话、邮箱。任何一步权限没给、URI 写错、MIMETYPE 对不上都会静默失败或者抛SecurityException。这篇面向三类人一是刚接触 ContentProvider 的 Android 新手二是需要在 App 内做「一键保存客服电话/邀请码联系人」的开发者三是想把联系人写入能力接到统一 API 通道做验证的工程同学。我会把 AndroidManifest 权限声明、ContentValues 插入骨架、adb 验证命令全部给全同时把 TaoToken 统一 Key 的配置流程串进来——因为很多团队现在把「模型调用 业务接口」收敛到一套 Key 上通讯录写入后的校验、日志上报、异常归因也走同一条通道配置一次就能复用。先说结论通讯录写入本身不需要联网但如果你要在写入后做数据校验、批量导入、或者让模型帮你解析一段名片文本再落库那就需要一个稳定的 API 通道。TaoToken 在这里扮演的是「统一 Key 统一入口」的角色官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 可以看整体能力API 入口是 https://taotoken.net/api这个地址不加 UTM。下面从权限开始一步步把可复制的代码和验证命令铺开。2. TaoToken 前置统一 Key 与 API 通道准备在写联系人代码之前先把 Key 拿到手后面验证环节会用到。整个流程不复杂但有几个细节容易卡住。2.1 获取统一 Key登录后进入控制台找到 API Keys 页面创建密钥。建议按项目维度建 Key比如android-contact-demo方便后面排查是哪个端在调用。创建后立刻复制保存页面刷新后就看不到完整值了。控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite2.2 确认接入方式TaoToken 的 API 地址统一为https://taotoken.net/api兼容常见的 OpenAI 风格调用格式。也就是说你原来用base_url的地方换成这个地址再把 Key 填进去即可。Android 端如果只是做联系人写入其实用不到网络但如果你要做「名片文本解析后写入通讯录」就需要在 App 里发一次请求。注意Key 不要硬编码在 APK 里。Android 反编译成本很低建议放在服务端中转或者至少用BuildConfig 混淆 本地加密存储别直接写死在 Java 常量里。2.3 文档与模型对话入口接入细节看文档验证模型是否通可以用模型对话页面接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite如果你长期做 Android 侧的编码和 Agent 任务可以关注 Coding Plan把日常开发里的模型调用额度固定下来Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite3. 可复制配置权限声明与 ContentValues 插入骨架这一章是全文的技术主体代码可以直接抄进项目跑。3.1 AndroidManifest 权限声明从 Android 6.0API 23开始读写通讯录属于危险权限必须运行时申请。先在 Manifest 里声明manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.contactdemo uses-permission android:nameandroid.permission.READ_CONTACTS / uses-permission android:nameandroid.permission.WRITE_CONTACTS / application android:allowBackuptrue android:labelContactDemo android:themestyle/AppTheme activity android:name.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifest只写WRITE_CONTACTS是不够的很多机型在查询去重时会读通讯录所以READ_CONTACTS一起声明更稳。3.2 运行时权限申请在 Activity 里申请别指望用户第一次就点允许private static final int REQ_CONTACTS 1001; private void requestContactPermission() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { if (checkSelfPermission(Manifest.permission.WRITE_CONTACTS) ! PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{ Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS }, REQ_CONTACTS); } else { addContact(this, 张三, 13800001111); } } else { addContact(this, 张三, 13800001111); } } Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQ_CONTACTS grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { addContact(this, 张三, 13800001111); } }3.3 核心插入代码骨架这是全文最关键的一段。注意RawContacts先插入空值拿 id再往Data表写具体字段import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.net.Uri; import android.provider.ContactsContract; import android.util.Log; public class ContactUtil { private static final String TAG ContactUtil; public static long addContact(Context context, String name, String phoneNumber) { ContentValues values new ContentValues(); // 第一步插入空的 RawContacts拿到 rawContactId Uri rawContactUri context.getContentResolver() .insert(ContactsContract.RawContacts.CONTENT_URI, values); if (rawContactUri null) { Log.e(TAG, insert RawContacts failed); return -1; } long rawContactId ContentUris.parseId(rawContactUri); Log.d(TAG, rawContactId rawContactId); // 第二步写入姓名 values.clear(); values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId); values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, name); context.getContentResolver() .insert(ContactsContract.Data.CONTENT_URI, values); // 第三步写入电话号码 values.clear(); values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId); values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE); values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber); values.put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE); context.getContentResolver() .insert(ContactsContract.Data.CONTENT_URI, values); return rawContactId; } }几个参数对照方便你改字段作用常见取值RAW_CONTACT_ID关联 RawContacts 与 Data上一步返回的 longMIMETYPE声明这行数据是什么类型StructuredName / Phone / EmailGIVEN_NAME名字任意字符串Phone.NUMBER电话号码字符串建议带区号Phone.TYPE号码类型TYPE_MOBILE / TYPE_WORK3.4 批量写入与去重如果要一次写多个联系人别在循环里反复insert主线程容易 ANR。用ContentProviderOperation批量提交ArrayListContentProviderOperation ops new ArrayList(); int rawContactInsertIndex ops.size(); ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) .build()); ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, 李四) .build()); try { context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); } catch (Exception e) { Log.e(TAG, applyBatch failed, e); }withValueBackReference是关键它让后面的 Data 行自动引用前面 RawContacts 插入后生成的 id不用手动 parse。4. 验证请求与成功结果adb 命令与日志确认代码写完不代表写进去了必须验证。三种方式从快到慢。4.1 adb 查询通讯录连上设备后直接用 content 命令查adb shell content query --uri content://com.android.contacts/raw_contacts --projection _id,display_name如果看到你刚插入的名字说明 RawContacts 写成功了。再查电话adb shell content query --uri content://com.android.contacts/data/phones --projection raw_contact_id,data1,data2data1是号码data2是类型。对照raw_contact_id能确认姓名和电话挂在同一条原始联系人上。4.2 用 Logcat 看插入结果在addContact里已经打了rawContactId过滤一下adb logcat -s ContactUtil:D正常输出类似D/ContactUtil: rawContactId 1024如果rawContactId是 -1 或者抛异常往下看第 5 章的排查。4.3 通过 TaoToken 通道做写入后校验如果你的 App 在写入后要把结果上报、或者让模型判断「这条联系人信息是否完整」可以发一次请求。用 curl 先验证 Key 是否可用curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: gpt-4o-mini, messages: [ {role: user, content: 判断这条联系人是否完整姓名张三电话13800001111} ] }返回里有choices[0].message.content就说明通道正常。Android 端用 OkHttp 发同样的 body 即可注意把 Key 从BuildConfig读别写死。5. 本篇常见错排查SecurityException、空 URI 与重复联系人这一章是我实际踩过的坑按报错现象归类。5.1 SecurityException: Permission Denial最常见。原因有三种Manifest 没声明、运行时没申请、或者用户在设置里手动关了权限。排查顺序是先看adb shell dumpsys package your.package | grep permission确认权限状态。如果是国产 ROM还要检查「权限管理」里通讯录是否被单独限制。5.2 insert 返回 nullContentResolver.insert()返回 null通常是 URI 写错或者账户类型问题。检查两点一是ContactsContract.RawContacts.CONTENT_URI别写成Contacts.CONTENT_URI二是部分机型要求 RawContacts 必须带ACCOUNT_TYPE和ACCOUNT_NAME哪怕是 null 也要显式 putvalues.put(ContactsContract.RawContacts.ACCOUNT_TYPE, null); values.put(ContactsContract.RawContacts.ACCOUNT_NAME, null);5.3 联系人重复每次调用都新建 RawContacts自然重复。去重逻辑是先用Phone.NUMBER查一遍Cursor cursor context.getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[]{ContactsContract.CommonDataKinds.Phone.CONTACT_ID}, ContactsContract.CommonDataKinds.Phone.NUMBER ?, new String[]{phoneNumber}, null); if (cursor ! null cursor.moveToFirst()) { // 已存在走更新逻辑 }5.4 写入成功但通讯录不显示有些 ROM 的通讯录默认只显示「有账户」的联系人。你插入的 RawContacts 如果ACCOUNT_TYPE为 null属于本地联系人需要在通讯录设置里打开「显示本地联系人」。这不是代码问题是显示过滤。5.5 主线程 ANR批量写入超过几十条时applyBatch也可能卡。放到子线程或者用 WorkManagernew Thread(() - ContactUtil.addContact(getApplicationContext(), 王五, 13900002222)).start();6. 语义一致收尾把 Key 配置和联系人写入串成一条线回到开头的问题Android 向系统通讯录添加联系人本质是 RawContacts Data 两张表的关联写入权限、URI、MIMETYPE 三者缺一不可。代码骨架和 adb 验证命令上面都给全了你可以直接复制到项目里跑通。TaoToken 在这里的价值是把「模型调用」和「业务接口」收敛到一套 Key 上。通讯录写入本身不依赖网络但写入后的校验、批量导入的名片解析、异常日志归因都可以走同一条 API 通道省去每个端单独配 Key 的麻烦。需要长期做 Android 编码和 Agent 任务的Coding Plan 能把额度固定下来只是临时验证模型通不通的用模型对话页面最快。最后留一个实用技巧调试阶段把rawContactId打到 Logcat配合adb shell content query交叉验证比在 UI 上翻通讯录快得多。写完一条就查一条别攒着一起测出问题时定位成本会低很多。
返回列表