首页 / 资讯中心 / 文章详情

Android SMS(一)—— 读取短信:从权限到 TaoToken 通道的完整链路

Android SMS(一)—— 读取短信:从权限到 TaoToken 通道的完整链路 ★ FEATURED ARTICLE
1. 从一次真机崩溃说起READ_SMS 权限与 ContentResolver 查询短信如果你正在做 Android 短信读取大概率会遇到两个经典问题一是SecurityException: Permission Denial: reading com.android.providers.telephony.SmsProvider二是CursorIndexOutOfBoundsException或者NullPointerException。前者是权限没给到位后者是字段映射写错了。这篇就围绕 Android SMS 读取短信的完整链路从READ_SMS权限申请、ContentResolver查询 SMS 表、Cursor 字段映射一直讲到怎么把读到的短信内容通过 TaoToken 统一 Key/API 通道做后续处理。先说清楚这个能力是什么Android 系统把短信存在mmssms.db里通过content://sms/这个 ContentProvider 暴露出来你用ContentResolver.query()就能拿到 Cursor。适合谁做短信备份、验证码自动填充、消息归档、客服工单聚合的 Android 开发者。能做什么读取收件箱、已发送、草稿、发件箱、失败、待发送等不同 URI 下的短信记录拿到发件人、正文、时间、类型等字段。我试过在 Android 13 真机上直接跑老代码结果READ_SMS被系统直接拒绝因为从 Android 6.0 开始危险权限必须运行时申请Android 10 之后还对后台读取做了额外限制。所以这篇不是简单贴一段查询代码而是把权限声明、运行时申请、Cursor 映射、空指针防护、日志排查全部串起来最后给出一个可复制的 TaoToken 接入配置让你读到的短信能走统一通道做后续处理。核心检索词先摆出来Android 读取短信、READ_SMS 权限申请、ContentResolver 查询 SMS 表、Cursor 字段映射、TaoToken API 通道。下面按工程落地顺序展开。2. TaoToken 前置准备统一 Key 与 API 通道接入配置在写查询代码之前先把后续处理通道准备好。TaoToken 提供统一的 API 入口官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。你需要先拿到一个 Key然后就可以在 Android 端或者服务端调用模型能力对读到的短信做分类、摘要、验证码提取等处理。为什么要在短信读取教程里提前讲这个因为很多同学读完短信后不知道下一步干什么要么把数据堆在本地要么自己搭一套转发逻辑。用统一通道的好处是Base URL、Key、Model ID 三件套固定换模型只改 Model ID不用动网络层代码。先拿 Key。打开 https://taotoken.net/api-keys 登录后创建一个 API Key复制保存。注意这个 Key 只显示一次丢了就重新建。然后确认你要用的模型 ID比如claude-sonnet-4-20250514或者gpt-4o-mini这类具体以控制台模型列表为准。控制台地址是 https://taotoken.net/console 。如果你用的是 Claude Code 这类编码工具可以走 Coding Plan地址是 https://taotoken.net/coding-plan 。如果是想在 Android 里直接发 HTTP 请求那就用标准 API 通道。接入文档在 https://taotoken.net/doc 模型对话调试在 https://taotoken.net/chat 。这里给一个 Android 端可用的配置片段放在local.properties或者BuildConfig里都行。注意不要硬编码到版本库# local.properties TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_MODEL_IDclaude-sonnet-4-20250514然后在build.gradle里读取android { buildTypes { debug { buildConfigField String, TAOTOKEN_BASE_URL, \${project.findProperty(TAOTOKEN_BASE_URL) ?: }\ buildConfigField String, TAOTOKEN_API_KEY, \${project.findProperty(TAOTOKEN_API_KEY) ?: }\ buildConfigField String, TAOTOKEN_MODEL_ID, \${project.findProperty(TAOTOKEN_MODEL_ID) ?: }\ } } }如果你更习惯用 JSON 配置文件可以放一个taotoken.json在 assets 里{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: claude-sonnet-4-20250514, timeout_seconds: 30 }三件套必须齐全Base URL 指向https://taotoken.net/apiKey 用你创建的那串Model ID 用控制台里确认过的。少任何一个请求都会失败。实测下来最常见的错误就是 Base URL 写成了首页地址或者 Key 前后带了空格。注意Key 属于敏感信息不要提交到 Git也不要在日志里打印完整 Key。建议用BuildConfig注入发布包做混淆。3. 可复制配置权限声明、运行时申请与 SMS 查询代码这一节是核心直接给可复制的代码。先看AndroidManifest.xml权限声明manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.smsread uses-permission android:nameandroid.permission.READ_SMS / uses-permission android:nameandroid.permission.RECEIVE_SMS / uses-permission android:nameandroid.permission.INTERNET / application android:allowBackuptrue android:labelSmsRead android:themestyle/Theme.AppCompat activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifestREAD_SMS是危险权限Android 6.0 以上必须运行时申请。下面是一个完整的 Activity 示例包含权限申请、查询、Cursor 映射和空指针防护package com.example.smsread; import android.Manifest; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; public class MainActivity extends AppCompatActivity { private static final String TAG SmsRead; private static final int REQ_READ_SMS 1001; private static final String SMS_URI_ALL content://sms/; private static final String SMS_URI_INBOX content://sms/inbox; private TextView tvResult; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); tvResult new TextView(this); setContentView(tvResult); if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_SMS}, REQ_READ_SMS); } else { tvResult.setText(getSmsInPhone()); } } Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQ_READ_SMS) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { tvResult.setText(getSmsInPhone()); } else { tvResult.setText(READ_SMS 权限被拒绝无法读取短信); Log.w(TAG, READ_SMS denied by user); } } } private String getSmsInPhone() { StringBuilder sb new StringBuilder(); Uri uri Uri.parse(SMS_URI_INBOX); String[] projection new String[]{ _id, address, person, body, date, type }; Cursor cursor null; try { cursor getContentResolver().query( uri, projection, null, null, date desc); if (cursor null) { Log.e(TAG, query returned null cursor); return 查询返回 null; } int idxAddress cursor.getColumnIndex(address); int idxPerson cursor.getColumnIndex(person); int idxBody cursor.getColumnIndex(body); int idxDate cursor.getColumnIndex(date); int idxType cursor.getColumnIndex(type); if (idxAddress 0 || idxBody 0 || idxDate 0) { Log.e(TAG, column index invalid: address idxAddress body idxBody date idxDate); return 字段映射失败; } SimpleDateFormat sdf new SimpleDateFormat( yyyy-MM-dd HH:mm:ss, Locale.getDefault()); while (cursor.moveToNext()) { String address cursor.getString(idxAddress); int person idxPerson 0 ? cursor.getInt(idxPerson) : -1; String body cursor.getString(idxBody); long date cursor.getLong(idxDate); int type cursor.getInt(idxType); String typeStr; if (type 1) { typeStr 接收; } else if (type 2) { typeStr 发送; } else { typeStr 其他( type ); } sb.append([ ).append(address null ? 未知 : address) .append(, person).append(person) .append(, ).append(body null ? : body) .append(, ).append(sdf.format(new Date(date))) .append(, ).append(typeStr) .append( ]\n\n); } } catch (SecurityException e) { Log.e(TAG, SecurityException: e.getMessage()); sb.append(权限异常: ).append(e.getMessage()); } catch (Exception e) { Log.e(TAG, query failed: e.getMessage(), e); sb.append(查询异常: ).append(e.getMessage()); } finally { if (cursor ! null !cursor.isClosed()) { cursor.close(); } } return sb.toString(); } }几个关键点。第一projection里列出的字段必须和后面getColumnIndex用的一致否则索引返回 -1getString(-1)直接抛异常。第二person字段对陌生短信可能是 null所以用getInt之前先判断索引是否有效取出来是 0 还是 -1 要看具体数据不要直接当联系人 ID 用。第三date是 long 型毫秒时间戳格式化时用new Date(longDate)。第四Cursor 一定要在 finally 里 close否则容易泄漏。短信 URI 对照表URI含义content://sms/所有短信content://sms/inbox收件箱content://sms/sent已发送content://sms/draft草稿content://sms/outbox发件箱content://sms/failed发送失败content://sms/queued待发送列表sms 表主要字段字段含义_id短消息序号thread_id对话序号同一发信人相同address发件人手机号person联系人列表序号陌生人可能为 nulldate日期long 型毫秒protocol0 SMS_RPOTO1 MMS_PROTOread0 未读1 已读status-1 接收0 complete64 pending128 failedtype1 接收2 发送body短信内容service_center短信服务中心号码如果你想把读到的短信通过 TaoToken 做后续处理可以在拿到 body 之后发一个 HTTP 请求。这里给一个用HttpURLConnection的最小示例避免引入额外依赖private String callTaoToken(String smsBody) throws Exception { URL url new URL(BuildConfig.TAOTOKEN_BASE_URL /v1/messages); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setRequestProperty(Content-Type, application/json); conn.setRequestProperty(x-api-key, BuildConfig.TAOTOKEN_API_KEY); conn.setRequestProperty(anthropic-version, 2023-06-01); conn.setDoOutput(true); conn.setConnectTimeout(15000); conn.setReadTimeout(30000); String payload {\model\:\ BuildConfig.TAOTOKEN_MODEL_ID \, \max_tokens\:256, \messages\:[{\role\:\user\,\content\:\提取验证码 smsBody.replace(\, \\\) \}]}; try (OutputStream os conn.getOutputStream()) { os.write(payload.getBytes(StandardCharsets.UTF_8)); } int code conn.getResponseCode(); InputStream is code 200 code 300 ? conn.getInputStream() : conn.getErrorStream(); BufferedReader reader new BufferedReader( new InputStreamReader(is, StandardCharsets.UTF_8)); StringBuilder resp new StringBuilder(); String line; while ((line reader.readLine()) ! null) { resp.append(line); } reader.close(); conn.disconnect(); Log.d(TAG, TaoToken response code code body resp); return resp.toString(); }注意x-api-key和anthropic-version这两个 header具体以接入文档 https://taotoken.net/doc 为准。如果你用的是 OpenAI 兼容格式header 换成Authorization: Bearer Key即可。三件套里的 Base URL 就是https://taotoken.net/api不要多加斜杠。4. 验证请求与成功结果真机跑通与日志确认代码写完后真机验证步骤要按顺序来。第一步安装 APK打开应用系统会弹出权限对话框点允许。第二步看 Logcat过滤 TAGSmsRead确认没有SecurityException。第三步看 TextView 是否显示了短信列表格式类似[ 106xxxxxxxx, person0, 您的验证码是 1234565分钟内有效。, 2025-01-15 10:23:41, 接收 ] [ 106xxxxxxxx, person0, 【某平台】您正在登录验证码 654321。, 2025-01-15 10:20:12, 接收 ]如果列表为空先确认设备里确实有短信再确认 URI 用的是content://sms/inbox而不是content://sms/。有些定制 ROM 对content://sms/全表查询限制更严用 inbox 更稳。第四步验证 TaoToken 通道。在拿到一条短信 body 后调用callTaoTokenLogcat 里应该看到类似D/SmsRead: TaoToken response code200 body{id:msg_xxx,content:[{type:text,text:123456}]}如果返回 200 并且 content 里有文本说明通道通了。如果返回 401检查 Key 是否正确、有没有多余空格。如果返回 404检查 Base URL 是不是写成了https://taotoken.net/api/带尾斜杠或者路径拼错了。真机验证时建议开飞行模式再关掉触发一次短信接收确认新短信能被读到。Android 10 之后后台读取短信有限制前台 Activity 里查询一般没问题。如果你在 Service 里查需要确认权限和前台服务声明。日志排查清单现象可能原因处理SecurityException Permission Denial没申请 READ_SMS 或用户拒绝运行时申请检查 grantResultsCursor nullURI 错误或 provider 未就绪换 inbox URI检查设备短信column index -1projection 与 getColumnIndex 不一致统一字段名NullPointerException on bodybody 字段为 null取值前判空401 UnauthorizedKey 错误或缺失检查 x-api-key header404 Not FoundBase URL 或路径错误确认 https://taotoken.net/apireading choices 相关解析错误响应 JSON 结构不符按文档解析 content 数组提示调试阶段把完整响应打到 Logcat但发布前记得关掉避免泄露 Key 和短信内容。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节把真实会撞到的报错逐个拆开。第一个401 Unauthorized。TaoToken 返回 401 基本就是 Key 问题。检查三处Key 是否复制完整、header 名是否正确x-api-key还是Authorization、Key 是否被换行符污染。可以在 https://taotoken.net/api-keys 重新生成一个再试。第二个local proxy failed或连接超时。这类错误通常是网络层配置问题不是 Key 问题。检查AndroidManifest.xml里有没有INTERNET权限检查设备网络是否正常检查connectTimeout和readTimeout是否太短。如果你在模拟器里跑确认模拟器能访问外网。注意不要在代码里写任何非官方代理地址统一走https://taotoken.net/api。第三个reading choices或解析响应时报JSONException。这通常是因为你按 OpenAI 格式解析但实际返回的是 Anthropic 格式或者反过来。Anthropic 格式的文本在content[0].textOpenAI 格式在choices[0].message.content。先打印原始响应确认结构再写解析。如果你用的是 Claude Code 相关工具注意 OAuth 流程和 API Key 流程是两套别混用。第四个OAuth相关报错。如果你在 Claude Code 或类似工具里配置OAuth 登录和 API Key 是两种模式。用 TaoToken 统一 Key 时选 API Key 模式Base URL 填https://taotoken.net/apiModel ID 填控制台确认的模型。如果工具提示 OAuth 失败检查是不是选错了认证方式。第五个CursorIndexOutOfBoundsException。这个和网络无关纯粹是字段映射问题。getColumnIndex返回 -1 时不要直接取值先判断。另外moveToFirst和moveToNext的配合要注意用while (cursor.moveToNext())更安全不用先moveToFirst。第六个person字段为 null 导致联系人信息取不到。这是数据本身的问题如果先收到陌生短信再添加联系人person就是 null。解决办法是通过address去ContactsContract里反查联系人不要依赖person字段。如果你用 Cline MCP 或 CC Switch 这类工具配置时同样要写全三件套Base URL、Key、Model ID。缺一个都会报错。Codex 的auth.json里也是类似结构确认字段名和值对应。6. 语义一致 CTA把读到的短信接进统一通道短信读出来只是第一步真正有价值的是后续处理。你可以把验证码提取、短信分类、工单聚合这些逻辑接到 TaoToken 统一通道上。需要 Key 就去 https://taotoken.net/api-keys 创建接入细节看 https://taotoken.net/doc 想先试模型效果可以去 https://taotoken.net/chat 对话调试。如果你长期做编码和 Agent 相关开发Coding Plan 在 https://taotoken.net/coding-plan 控制台在 https://taotoken.net/console 。配置记住三件套Base URL 用https://taotoken.net/apiKey 用你创建的那串Model ID 用控制台确认的。Android 端把这三个值通过BuildConfig注入查询短信的代码按第 3 节的写法Cursor 记得 close字段索引记得判 -1权限记得运行时申请。真机跑通后Logcat 里看到 200 和 content 文本链路就完整了。
阅读完成 · 觉得有帮助?
咨询建站