尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Android 短信接收实战:用 ContentObserver 监听短信并接入 TaoToken 配置骨架

发布时间:2026/9/25 13:48:14

资讯中心
01
ARTICLE

Android 短信接收实战:用 ContentObserver 监听短信并接入 TaoToken 配置骨架

Android 短信接收实战:用 ContentObserver 监听短信并接入 TaoToken 配置骨架
1. 短信监听为什么总在真机上翻车Android 短信接收这件事看起来只是注册一个ContentObserver就完事但真机跑起来经常出现「能拦截、拿不到内容」「onChange 只回调一次」「Android 13 之后直接静默失败」这些情况。核心检索词先摆出来ContentObserver 是 Android 里监听ContentProvider数据变化的观察者短信场景下它监听的是content://sms/这个数据库 URI它能做到的是在短信写入收件箱后收到通知适合做验证码回填、支付二次确认、自动化测试这类需要感知新短信的场景适合已经会写 Activity/Service、但对 ContentProvider 和运行时权限不熟的中级开发者。我试过在 Android 10 到 Android 14 的机器上跑同一套监听代码差异非常大。Android 9 及以前READ_SMS一个静态权限就能读收件箱Android 10 开始收紧了content://sms/的访问Android 13 把POST_NOTIFICATIONS拆出来Android 14 又对前台服务类型做了限制。所以本文不会只给你一段onChange而是把权限声明、注册/注销时机、查询字段、以及如何用 TaoToken 统一 Key/API 通道做验证串成一条能跑通的链路。目标很明确你照着配完能在 Logcat 里看到新短信的address和body并且确认 TaoToken 的配置骨架生效。先明确一个边界ContentObserver 监听的是「数据库变化」不是「短信到达广播」。SMS_RECEIVED_ACTION广播在 Android 4.4 之后只有默认短信应用能收到普通 App 拿不到。所以如果你的 App 不是默认短信应用ContentObserver 是更现实的路子但它依赖READ_SMS权限且查询时机要处理好——onChange触发时数据可能还没完全落库直接查可能拿到空 Cursor。2. TaoToken 前置把 Key 和 API 通道先理清在写监听代码之前先把验证通道搭好。TaoToken 在这里的角色是统一 Key 管理和 API 通道你不需要在 App 里散落多个厂商的 Key而是通过一个配置骨架统一走。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 这个不加 UTM。你需要先拿到 API Key入口在控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。拿到之后不要硬编码进 Java 代码而是放进settings.json或local.properties通过 BuildConfig 注入。这样做的原因是短信监听这类代码经常要跑在测试机上Key 泄露风险高集中管理比散落安全。配置骨架的核心字段就三个baseUrl、apiKey、model。baseUrl固定指向https://taotoken.net/apiapiKey从控制台复制model按你实际要验证的模型填。如果你只是想确认通道通不通用模型对话页面手动发一条最快https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。如果你后面要做长期编码或 Agent 类任务可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面写了请求头和返回结构。Claude Code 相关的接入说明在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 。这些链接先存着第 4 节验证请求会用到。3. 可复制配置权限、Observer、settings.json3.1 AndroidManifest 权限声明先解决权限。注意WRITE_SMS在 Android 4.4 之后对非默认短信应用已经无效声明了也不会授予但保留声明不会报错。真正需要的是READ_SMS和RECEIVE_SMS。uses-permission android:nameandroid.permission.READ_SMS / uses-permission android:nameandroid.permission.RECEIVE_SMS / uses-permission android:nameandroid.permission.READ_PHONE_STATE / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / application receiver android:name.SmsReceiver android:exportedtrue android:permissionandroid.permission.BROADCAST_SMS intent-filter android:priority999 action android:nameandroid.provider.Telephony.SMS_RECEIVED / /intent-filter /receiver /applicationPOST_NOTIFICATIONS是 Android 13 起必须的否则你的前台服务通知不显示监听容易被系统杀掉。READ_PHONE_STATE在部分机型上查询短信时会用到保留。3.2 运行时权限申请静态声明不够Android 6.0 起READ_SMS是危险权限必须运行时申请。private static final int REQ_SMS 1001; private void requestSmsPermission() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { if (checkSelfPermission(Manifest.permission.READ_SMS) ! PackageManager.PERMISSION_GRANTED) { requestPermissions( new String[]{Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_SMS}, REQ_SMS); } else { registerSmsObserver(); } } else { registerSmsObserver(); } } Override public void onRequestPermissionsResult(int code, String[] perms, int[] results) { super.onRequestPermissionsResult(code, perms, results); if (code REQ_SMS results.length 0 results[0] PackageManager.PERMISSION_GRANTED) { registerSmsObserver(); } else { Log.e(TAG, READ_SMS denied, observer not registered); } }3.3 ContentObserver 完整实现这是核心。相比原始片段我做了三处修正onChange里用getContentResolver().query而不是废弃的managedQuery查询条件用date lastTime避免重复处理while (cursor.moveToNext())而不是错误的while (cursor.isLast())。public class SmsObserver extends ContentObserver { private static final String TAG SmsObserver; private static final Uri SMS_URI Uri.parse(content://sms/inbox); private final Context ctx; private final Handler handler; private long lastQueryTime 0L; public SmsObserver(Handler handler, Context ctx) { super(handler); this.handler handler; this.ctx ctx; } Override public void onChange(boolean selfChange, Uri uri) { super.onChange(selfChange, uri); Log.i(TAG, onChange uri uri selfChange selfChange); queryLatestSms(); } private void queryLatestSms() { Cursor cursor null; try { cursor ctx.getContentResolver().query( SMS_URI, new String[]{_id, address, body, date, read}, date ?, new String[]{String.valueOf(lastQueryTime)}, date desc ); if (cursor null) { Log.w(TAG, cursor is null, maybe no permission); return; } while (cursor.moveToNext()) { String address cursor.getString(cursor.getColumnIndexOrThrow(address)); String body cursor.getString(cursor.getColumnIndexOrThrow(body)); long date cursor.getLong(cursor.getColumnIndexOrThrow(date)); lastQueryTime Math.max(lastQueryTime, date); Log.i(TAG, sms from address body body date date); Message msg handler.obtainMessage(MSG_SMS_BACK_1); Bundle b new Bundle(); b.putString(address, address); b.putString(body, body); msg.setData(b); handler.sendMessage(msg); } } catch (SecurityException e) { Log.e(TAG, query sms SecurityException, e); } finally { if (cursor ! null) cursor.close(); } } }3.4 注册与注销注册用registerContentObserver注销必须成对否则 Activity 销毁后泄漏。private SmsObserver smsObserver; private void registerSmsObserver() { if (smsObserver ! null) return; smsObserver new SmsObserver(new Handler(Looper.getMainLooper()), this); getContentResolver().registerContentObserver( Uri.parse(content://sms/), true, smsObserver); Log.i(TAG, SmsObserver registered); } Override protected void onDestroy() { super.onDestroy(); if (smsObserver ! null) { getContentResolver().unregisterContentObserver(smsObserver); smsObserver null; Log.i(TAG, SmsObserver unregistered); } }3.5 settings.json 配置骨架把 TaoToken 的通道配置抽出来放在app/src/main/assets/settings.json{ taotoken: { baseUrl: https://taotoken.net/api, apiKey: ${TAOTOKEN_API_KEY}, model: your-model-name, timeoutMs: 30000, headers: { Content-Type: application/json, Authorization: Bearer ${TAOTOKEN_API_KEY} } }, sms: { observerUri: content://sms/inbox, queryLimit: 20, filterAddress: } }${TAOTOKEN_API_KEY}在构建时通过build.gradle的buildConfigField替换不要提交真实 Key 到仓库。android { defaultConfig { buildConfigField String, TAOTOKEN_API_KEY, \${project.findProperty(TAOTOKEN_API_KEY) ?: }\ } }本地gradle.properties里写TAOTOKEN_API_KEY你的Key.gitignore排除它。4. 验证请求确认监听和配置都生效4.1 验证短信监听装到真机后用另一台手机给测试机发一条短信。Logcat 过滤SmsObserver应该看到I/SmsObserver: onChange uricontent://sms/ selfChangefalse I/SmsObserver: sms from106xxxxxxxx body【验证码】123456 date1710000000000如果只看到onChange没有sms from说明查询被拦了检查READ_SMS是否真的授予。用adb shell dumpsys package your.package | grep READ_SMS确认。4.2 验证 TaoToken 配置配置生效的验证分两步。第一步用 curl 直接打 API确认 Key 和 baseUrl 对curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: your-model-name, messages: [{role: user, content: ping}] }返回里如果有choices字段说明通道通。第二步在 App 里读settings.json并打印InputStream is getAssets().open(settings.json); BufferedReader br new BufferedReader(new InputStreamReader(is)); StringBuilder sb new StringBuilder(); String line; while ((line br.readLine()) ! null) sb.append(line); br.close(); JSONObject root new JSONObject(sb.toString()); JSONObject tt root.getJSONObject(taotoken); Log.i(TAG, baseUrl tt.getString(baseUrl) model tt.getString(model) keyLen BuildConfig.TAOTOKEN_API_KEY.length());keyLen大于 0 且baseUrl是https://taotoken.net/api就说明配置骨架生效。想手动确认模型可用性去模型对话页面发一条https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。4.3 把短信内容接到验证链路监听拿到body后可以把它作为 prompt 的一部分发给 TaoToken 做解析比如提取验证码。这一步不是必须的但能验证「监听 通道」整条链路private void verifySmsWithTaoToken(String smsBody) { new Thread(() - { try { JSONObject payload new JSONObject(); payload.put(model, BuildConfig.TAOTOKEN_MODEL); JSONArray messages new JSONArray(); JSONObject userMsg new JSONObject(); userMsg.put(role, user); userMsg.put(content, 从这条短信里提取6位数字验证码只返回数字 smsBody); messages.put(userMsg); payload.put(messages, messages); OkHttpClient client new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .build(); Request req new Request.Builder() .url(BuildConfig.TAOTOKEN_BASE_URL /v1/chat/completions) .addHeader(Authorization, Bearer BuildConfig.TAOTOKEN_API_KEY) .addHeader(Content-Type, application/json) .post(RequestBody.create(payload.toString(), MediaType.parse(application/json))) .build(); Response resp client.newCall(req).execute(); Log.i(TAG, taotoken resp code resp.code() body resp.body().string()); } catch (Exception e) { Log.e(TAG, verifySmsWithTaoToken failed, e); } }).start(); }返回 200 且 body 里有数字整条链路就通了。5. 本篇常见错排查5.1 onChange 不回调最常见原因是权限没给。Android 6.0 以上READ_SMS必须运行时申请只在 Manifest 声明无效。另一个原因是注册的 URI 写错必须是content://sms/或content://sms/inbox写成content://sms/inbox/带尾斜杠部分机型不触发。5.2 能回调但 Cursor 为空onChange触发时数据可能还没落库直接查会拿到空 Cursor。解决办法是在onChange里延迟 500ms 再查或者用date lastQueryTime条件配合重试。原始片段里while (cursor.isLast())是逻辑错误isLast()返回布尔值循环体根本不会按预期执行应该用while (cursor.moveToNext())。5.3 Android 13/14 上监听被系统杀Android 13 起后台进程限制更严ContentObserver 在 App 进入后台后可能不再收到回调。需要配合前台服务并在 Manifest 里声明foregroundServiceType。Android 14 要求前台服务类型明确短信相关可以用dataSync或specialUse具体看你的场景。5.4 TaoToken 请求 401先确认Authorization头是Bearer加 Key中间有空格。再确认 Key 没有多余换行从控制台复制时容易带上。如果还是 401去 API Keys 页面重新生成一个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 。5.5 查询报 SecurityExceptioncontent://sms/inbox在部分厂商 ROM 上需要额外权限比如小米的READ_SMS之外还要开「短信权限」开关。捕获SecurityException并打日志能快速定位是权限问题还是 URI 问题。6. 接入与排障的下一步如果你卡在权限或 Observer 注册上优先去 API Keys 页面确认 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 。如果只是想快速验证模型通道用模型对话页面手动发一条最省事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 。最后留一个实操建议把lastQueryTime持久化到 SharedPreferencesApp 重启后不会重复处理旧短信。这个细节在验证码场景里很关键否则用户每次打开 App 都会看到历史验证码被重新解析。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。