Android SMS(一)—— 用 TaoToken 统一 Key 读取短信
1. 从一次真机调试说起Android 读取短信到底卡在哪如果你正在做 Android 本地调试想验证「读取收件箱并打印短信列表」这条链路大概率会遇到三个卡点权限申请没生效、ContentResolver 查询返回空游标、字段解析时person拿到 null 直接崩。这篇就围绕 Android SMS 读取短信的完整流程把权限声明、查询代码、游标解析、Logcat 验证动作一次讲清楚顺带说明怎么把模型调用 endpoint 统一改到 TaoToken 做 Key 管理。先说清楚这套东西是什么、能做什么、适合谁。Android 系统把短信存在mmssms.db里对外通过content://sms/这组 ContentProvider URI 暴露。你不需要直接碰数据库文件用ContentResolver.query()就能拿到游标再按字段解析出号码、正文、时间、类型。适合的人群很明确做本地调试的 Android 开发者、需要验证短信读取链路的测试同学、以及想把短信数据接进自己工具链但不想碰灰色手段的工程同学。我试过在真机上直接跑老代码结果moveToFirst()返回 false一开始以为是权限没给后来发现是查询 URI 写成了content://sms/inbox但设备上收件箱为空。这类问题不复杂但排查起来很费时间所以下面会把每一步的验证动作都写出来你可以照着做。需要提前说明的是读取短信属于敏感权限仅用于你自己的设备本地调试和真机验证不要用于任何未经授权的数据采集。本文所有代码都在本地调试场景下验证。2. TaoToken 前置准备统一 Key 管理与 endpoint 配置在进入代码之前先把模型调用的 endpoint 统一到 TaoToken。这一步的意义在于当你的调试工具、脚本、IDE 插件都要调模型时不用每个地方各配一套 Key改一处就行。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。注意 API 地址后面不加 UTM 参数保持干净。你需要准备三件套Base URL、API Key、Model ID。这三者在任何接入场景里都是必须的缺一个都跑不通。Base URL 填https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面生成Model ID 按你实际要用的模型填。生成 Key 的入口在这里https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。如果你用的是 Claude Code 这类编码工具接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有对应的配置说明。想先验证模型能不能通可以直接用模型对话页面试一条https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。长期做编码或 Agent 任务的话Coding Plan 会更省心https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。这里要强调一点TaoToken 是模型调用的统一入口不是用来替代 Android Studio 或编辑器的。你的短信读取代码还是在 Android 工程里写TaoToken 只负责模型请求这一层。配置的时候把 Base URL、Key、Model ID 三件套记牢。下面给一个通用的配置片段路径按你实际工具调整{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: 你的ModelID }如果是 TOML 风格的配置[model] base_url https://taotoken.net/api api_key sk-你的Key model 你的ModelID把这段配好之后后面所有模型调用都走同一个 Key换工具时只改这一处。这一步做完再回到 Android 短信读取本身。3. 可复制配置权限声明与 ContentResolver 查询代码这一节是核心直接给可复制的代码。先看 AndroidManifest.xml 的权限声明。uses-permission android:nameandroid.permission.READ_SMS /注意从 Android 6.0API 23开始READ_SMS 属于危险权限光在 Manifest 里声明不够运行时还要动态申请。很多人卡在这里Manifest 写了代码里没申请查询直接抛 SecurityException。运行时申请可以这样写if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_SMS}, 1001); }申请结果在onRequestPermissionsResult里判断grantResults[0] PackageManager.PERMISSION_GRANTED才算通过。权限过了之后进入查询。短信的 URI 有好几种按需选URI含义content://sms/所有短信content://sms/inbox收件箱content://sms/sent已发送content://sms/draft草稿content://sms/outbox发件箱content://sms/failed发送失败content://sms/queued待发送列表查询代码片段Uri uri Uri.parse(content://sms/inbox); String[] projection new String[]{_id, address, person, body, date, type}; Cursor cur getContentResolver().query(uri, projection, null, null, date desc);projection是你想拿的字段selection和selectionArgs是过滤条件最后一个参数是排序。这里按date desc让最新的排前面。字段结构对照一下方便你解析字段含义_id短消息序号thread_id对话序号同一发信人相同address发件人手机号person联系人列表序号陌生人可能为 nulldate日期long 型type1 接收2 发送body短信内容read0 未读1 已读status-1 接收0 complete64 pending128 failed解析游标的时候先moveToFirst()判断有没有数据再循环moveToNext()。取列索引用getColumnIndex取字符串用getString取 long 用getLong。这里有个坑person字段对陌生短信是 null如果你直接getInt会拿到 0但更稳妥的做法是先判断isNull。int idxAddress cur.getColumnIndex(address); int idxBody cur.getColumnIndex(body); int idxDate cur.getColumnIndex(date); int idxType cur.getColumnIndex(type); if (cur.moveToFirst()) { do { String address cur.getString(idxAddress); String body cur.getString(idxBody); long date cur.getLong(idxDate); int type cur.getInt(idxType); Log.d(SMS_DEBUG, addr address body body date date type type); } while (cur.moveToNext()); } cur.close();游标用完一定要close()否则容易泄漏。查询放在子线程里做别在主线程跑不然会 ANR。4. 验证请求与成功结果Logcat 打印短信列表代码写完之后怎么确认真的跑通了最直接的办法是看 Logcat。在 Android Studio 底部打开 Logcat过滤SMS_DEBUG这个 tag。如果你在真机上跑先确认设备已授权 READ_SMS 权限然后触发查询逻辑。正常情况下你会看到类似这样的输出D/SMS_DEBUG: addr8613811810000 body你的验证码是123456 date1256539465022 type1 D/SMS_DEBUG: addr10086 body您本月流量已使用80% date1256539500000 type1每条记录包含号码、正文、时间戳、类型。时间戳是 long 型格式化一下更直观SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss, Locale.getDefault()); String dateStr sdf.format(new Date(date));注意这里用HH是 24 小时制老代码里用hh是 12 小时制容易看错时间。如果你想把结果直接显示在界面上可以用 TextView 拼接或者用 RecyclerView 列表。调试阶段用 Logcat 最快不用管 UI。验证成功的标志有三个一是 Logcat 有输出且字段完整二是没有 SecurityException三是游标正常关闭没有泄漏警告。三个都满足说明读取收件箱这条链路通了。如果查询返回空先确认设备收件箱里确实有短信再检查 URI 是不是写成了content://sms/inbox但设备上收件箱为空。可以换成content://sms/查所有短信试试。5. 本篇常见错排查401、local proxy failed、游标异常调试过程中最常见的几类报错这里逐个对照。第一类是权限相关。如果你看到java.lang.SecurityException: Permission Denial: reading com.android.providers.telephony.SmsProvider说明 READ_SMS 没授权。检查 Manifest 声明和运行时申请两步是否都做了。真机上还要去设置里确认应用权限没被手动关掉。第二类是模型调用侧的 401。如果你在调试工具里调模型时看到401 Unauthorized多半是 API Key 没配对或者 Base URL 写错了。回到 TaoToken 控制台确认 Key 有效Base URL 填https://taotoken.net/api注意不要多加斜杠或路径。三件套 Base URL、Key、Model ID 缺一不可。第三类是local proxy failed。这个通常出现在本地代理配置场景检查你的工具代理设置是否指向了正确的地址。如果你用的是 Claude Code 或类似工具参考接入文档里的配置说明确认 endpoint 和 Key 都填对。第四类是游标解析异常比如android.database.CursorIndexOutOfBoundsException。这通常是因为在moveToFirst()返回 false 的情况下还去取列索引。正确做法是先判断moveToFirst()的返回值再进入循环。第五类是person字段为 null 导致的空指针。前面说过陌生短信的person是 null解析时先cur.isNull(idxPerson)判断一下。第六类是 OAuth 相关报错。如果你在接入某些工具时看到 OAuth 失败检查授权流程是否走完Key 是否过期。TaoToken 的 Key 在控制台可以重新生成。排查顺序建议先确认权限再确认 URI 和查询参数最后确认模型调用配置。大部分问题出在前两步。6. 把模型调用接进你的调试链路短信读取跑通之后如果你想把短信内容接进模型做进一步处理比如自动分类或摘要这时候统一 Key 管理的价值就体现出来了。你只需要在调用侧配置一次 Base URL、Key、Model ID所有请求都走 TaoToken。想快速验证模型能不能通用模型对话页面发一条测试消息https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。确认返回正常后再把它接进你的代码。如果你在做长期编码或 Agent 任务Coding Plan 会更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。Key 管理入口在 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。最后给一个实用技巧调试短信读取时把查询结果先打到 Logcat确认字段完整再往模型送。不要一上来就把原始数据直接丢给模型先本地验证数据质量能省很多排查时间。游标记得关权限记得动态申请三件套记得配齐这三条做到基本不会卡住。