ARTICLE DETAIL

资讯详情

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

Android实例-发送信息:用TaoToken统一Key打通短信与推送通道

Android实例-发送信息:用TaoToken统一Key打通短信与推送通道 1. Android 发送信息完整链路从 SmsManager 到 FCM 推送的凭证统一管理Android 端发送信息这件事看起来只是调一个sendTextMessage真到项目里往往会拆成两条完全不同的通道一条走运营商短信SmsManager一条走云端推送FCM。前者依赖设备 SIM 卡和SEND_SMS权限后者依赖 Google 服务、服务端凭证和一套 HTTP 接口。两条链路的技术栈、调试方式、失败表现都不一样最麻烦的是凭证散落在三四个地方短信网关的 Key、FCM 服务账号 JSON、后端接口 Token改一次配置要翻半天。这篇就按「Android 实例-发送信息」这个场景把两条通道串起来跑一遍。目标很具体在真机上发出一条短信同时通过统一 Key 通道触发一条推送并且把多服务凭证收敛到一处管理。适合已经写过 Android 基础页面、想搞清楚发送信息完整链路的同学也适合被多套 Key 折腾过的后端联调同学。核心检索词先摆出来Android 发送信息、SmsManager 发短信、FCM 推送、统一 Key 管理多服务凭证。这几个词会贯穿全文。先说清楚两条通道的本质差异。SmsManager 是 Android 系统 API代码跑在设备上短信直接从 SIM 卡出去不经过任何第三方服务器所以它没有「Key」的概念只有权限和运营商限制。FCM 推送相反消息从你的服务端发出经过 FCM 服务投递到设备服务端需要凭证Service Account 或 OAuth Token设备端需要注册 Token。把这两者放进同一个 App凭证管理就成了绕不开的问题。我试过的做法是短信通道保持系统 API 直发推送通道和后续要扩展的短信网关、邮件、验证码服务统一走一个 API 网关来管理 Key。这样设备端只关心「发短信」和「请求推送」两个动作凭证的轮换、限额、审计都放在网关侧。下面按这个思路一步步落地。先看整体结构。App 里有两个入口一个「发送短信」按钮走 SmsManager一个「发送推送」按钮走 HTTP 请求到统一网关由网关去调 FCM。这样设备端不需要内置 FCM 服务账号那玩意儿放客户端是安全事故只需要一个网关的 API Key。这个 Key 就是我们要统一管理的东西。你可能会问为什么不直接在 App 里集成 FCM SDK 收推送收推送确实要集成但「发推送」这个动作必须由服务端发起客户端只能接收。所以本文的推送部分重点在「如何用统一 Key 从客户端触发服务端发送」而不是客户端自己发 FCM。这个区分很关键很多新手会混淆。接下来进入实操。整个流程分四块环境与依赖、权限声明、短信发送代码、推送触发代码最后是验证和排障。每一块都给可复制的片段你照着改包名就能跑。2. TaoToken 前置准备统一 Key 与 API 通道配置在写代码之前先把凭证这层理清楚。我们要用 TaoToken 作为统一 API 通道把推送触发、后续可能加的短信网关、模型调用等服务的 Key 收敛到一处。这样 App 里只需要配置一个 Base URL 和一个 API Key换服务不用改客户端代码。先拿 Key。打开 https://taotoken.net/api-keys 登录后创建一个 API Key复制保存。这个 Key 就是客户端请求网关时带的凭证。注意它只显示一次丢了就重新建。拿到 Key 之后你需要知道请求打到哪个地址。TaoToken 的 API 入口是 https://taotoken.net/api 所有兼容接口都挂在这个 Base URL 下。客户端配置里填这个地址不要带多余的路径后缀具体端点由代码里的相对路径决定。如果你后面要接 Claude Code 或者做长期编码 Agent可以了解下 Coding Planhttps://taotoken.net/coding-plan 。本文的推送触发用普通 API 调用就够了不需要额外套餐。模型和端点信息可以在文档里查https://taotoken.net/doc 。推送触发本质是一次 HTTP POST端点路径和参数格式文档里都有。建议先把文档里「对话/请求」那部分的请求体格式看一眼因为下面代码里的 JSON 结构要和它对齐。这里要强调一个安全点API Key 不要硬编码在 Java 源码里提交到 Git。正确做法是放在local.properties或BuildConfig里通过 Gradle 注入。下面配置章节会给具体写法。把 Key 写死在MainActivity.java里一旦仓库公开就是事故。统一 Key 的好处在这里体现短信通道不需要 Key推送通道用一个 Key将来加验证码服务还是这个 Key客户端只维护一份配置。轮换时改一处所有通道生效。这就是「统一 Key 管理多服务凭证」的实际价值不是概念是少改代码。配置完成后你手里应该有三样东西Base URLhttps://taotoken.net/api 、API Keysk- 开头那串、以及要调用的端点路径。三件套齐了再往下写代码否则调不通会浪费很多时间在猜问题上。顺便提一句如果你更想先在网页上验证 Key 是否可用可以打开模型对话页面 https://taotoken.net/chat 发一条消息试试。能正常返回说明 Key 和账户状态没问题再去写 Android 代码心里有底。3. 可复制配置Gradle 依赖、Manifest 权限与 Key 注入这一节全是可直接复制的配置。按顺序改别跳步。先改app/build.gradle。短信部分用系统 API 不需要额外依赖推送触发用 OkHttp 发 HTTP 请求JSON 解析用 org.jsonAndroid 自带。加上 OkHttpdependencies { implementation com.squareup.okhttp3:okhttp:4.12.0 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 }然后在同一个文件的android块里从local.properties读取 Key 并注入 BuildConfigandroid { compileSdk 34 defaultConfig { applicationId com.example.messagesender minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 // 从 local.properties 读取避免硬编码 def apiKey project.findProperty(TAOTOKEN_API_KEY) ?: buildConfigField String, TAOTOKEN_API_KEY, \${apiKey}\ buildConfigField String, TAOTOKEN_BASE_URL, \https://taotoken.net/api\ } buildFeatures { buildConfig true } }在项目根目录的local.properties里加一行这个文件默认在 .gitignore 里安全TAOTOKEN_API_KEYsk-你的实际Key接着改AndroidManifest.xml声明短信权限和网络权限manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.messagesender uses-permission android:nameandroid.permission.SEND_SMS / uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / application android:allowBackuptrue android:labelMessageSender 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 /manifest注意SEND_SMS是危险权限Android 6.0 以上必须运行时申请光在 Manifest 里声明不够。下面代码里会处理。布局文件activity_main.xml沿用经典结构两个输入框加两个按钮LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp EditText android:idid/editText1 android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入手机号 / EditText android:idid/editText2 android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入短信内容 android:minLines3 android:gravitytop / Button android:idid/button1 android:layout_widthmatch_parent android:layout_heightwrap_content android:text发送短信 / Button android:idid/button2 android:layout_widthmatch_parent android:layout_heightwrap_content android:text发送推送 / /LinearLayout到这里配置层就齐了。三件套对应关系Base URL 是BuildConfig.TAOTOKEN_BASE_URLKey 是BuildConfig.TAOTOKEN_API_KEYModel ID 在推送请求体里指定下面代码里用model字段。这三样在客户端配置里必须同时存在缺一个请求就会失败。注意local.properties不要提交到版本库。团队协作时每个人本地配自己的 Key或者用 CI 的环境变量注入。这是凭证管理的基本纪律。4. 发送信息验证SmsManager 发短信与统一 Key 触发推送现在写核心逻辑。MainActivity.java里做两件事短信直发、推送触发。先看完整代码再拆解关键点。package com.example.messagesender; import android.Manifest; import android.content.pm.PackageManager; import android.os.Bundle; import android.telephony.SmsManager; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import java.io.IOException; import java.util.ArrayList; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import org.json.JSONArray; import org.json.JSONObject; public class MainActivity extends AppCompatActivity { private static final int SMS_PERMISSION_CODE 100; private EditText numberText; private EditText contentText; private final OkHttpClient client new OkHttpClient(); Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); numberText findViewById(R.id.editText1); contentText findViewById(R.id.editText2); Button smsButton findViewById(R.id.button1); Button pushButton findViewById(R.id.button2); smsButton.setOnClickListener(v - { if (checkSmsPermission()) { sendSms(); } else { ActivityCompat.requestPermissions( MainActivity.this, new String[]{Manifest.permission.SEND_SMS}, SMS_PERMISSION_CODE); } }); pushButton.setOnClickListener(v - sendPush()); } private boolean checkSmsPermission() { return ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) PackageManager.PERMISSION_GRANTED; } Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode SMS_PERMISSION_CODE grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { sendSms(); } else { Toast.makeText(this, 未授予短信权限, Toast.LENGTH_SHORT).show(); } } private void sendSms() { String number numberText.getText().toString().trim(); String content contentText.getText().toString().trim(); if (number.isEmpty() || content.isEmpty()) { Toast.makeText(this, 手机号或内容为空, Toast.LENGTH_SHORT).show(); return; } try { SmsManager manager SmsManager.getDefault(); ArrayListString parts manager.divideMessage(content); for (String part : parts) { manager.sendTextMessage(number, null, part, null, null); } Toast.makeText(this, 短信已提交发送, Toast.LENGTH_SHORT).show(); } catch (Exception e) { Toast.makeText(this, 发送失败: e.getMessage(), Toast.LENGTH_LONG).show(); } } private void sendPush() { String content contentText.getText().toString().trim(); if (content.isEmpty()) { Toast.makeText(this, 推送内容为空, Toast.LENGTH_SHORT).show(); return; } try { JSONObject payload new JSONObject(); payload.put(model, gpt-4o-mini); JSONArray messages new JSONArray(); JSONObject msg new JSONObject(); msg.put(role, user); msg.put(content, 请把这条通知精简成一句话推送文案 content); messages.put(msg); payload.put(messages, messages); MediaType JSON MediaType.parse(application/json; charsetutf-8); RequestBody body RequestBody.create(payload.toString(), JSON); Request request new Request.Builder() .url(BuildConfig.TAOTOKEN_BASE_URL /v1/chat/completions) .addHeader(Authorization, Bearer BuildConfig.TAOTOKEN_API_KEY) .addHeader(Content-Type, application/json) .post(body) .build(); client.newCall(request).enqueue(new Callback() { Override public void onFailure(NonNull Call call, NonNull IOException e) { runOnUiThread(() - Toast.makeText(MainActivity.this, 推送请求失败: e.getMessage(), Toast.LENGTH_LONG).show()); } Override public void onResponse(NonNull Call call, NonNull Response response) throws IOException { String respBody response.body() ! null ? response.body().string() : ; runOnUiThread(() - { if (response.isSuccessful()) { Toast.makeText(MainActivity.this, 推送触发成功, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, 推送失败 code response.code() body respBody, Toast.LENGTH_LONG).show(); } }); } }); } catch (Exception e) { Toast.makeText(this, 构造请求异常: e.getMessage(), Toast.LENGTH_LONG).show(); } } }几个关键点拆开说。短信部分SmsManager.getDefault()拿到管理器divideMessage按 70 字符中文或 160 字符英文自动拆分长短信然后逐条sendTextMessage。第四个参数是发送状态 PendingIntent第五个是送达状态 PendingIntent这里传 null 表示不监听。生产环境建议传 PendingIntent 做状态回调否则你只知道「提交了」不知道「发出去了没」。推送部分请求打到BuildConfig.TAOTOKEN_BASE_URL /v1/chat/completionsHeader 里带Authorization: Bearer KeyBody 是标准 chat 格式。这里用模型把通知内容精简成推送文案实际项目里你可以把这一步换成直接调 FCM 的端点或者调你自己的后端。关键是凭证走同一套 Base URL Key。三件套在这里的体现Base URL 来自BuildConfig.TAOTOKEN_BASE_URLKey 来自BuildConfig.TAOTOKEN_API_KEYModel ID 是请求体里的model: gpt-4o-mini。三个都在请求才成立。跑之前确认真机插了 SIM 卡、有信号、短信余额够。模拟器发不了真实短信必须真机。推送部分需要网络Wi-Fi 或流量都行。5. 本篇常见错排查401、local proxy failed 与 reading choices 报错这一节按真实报错来。你跑上面代码大概率会遇到下面几个。401 Unauthorized。表现是推送按钮点了Toast 显示推送失败 code401。原因通常是 Key 没注入成功或格式不对。排查顺序先看BuildConfig.TAOTOKEN_API_KEY是不是空字符串如果是说明local.properties里的TAOTOKEN_API_KEY没被 Gradle 读到检查属性名拼写和文件位置必须在项目根目录不是 app 目录。再看 Key 有没有多余空格复制时容易带上换行。最后确认 Header 格式是Bearer sk-xxx中间一个空格别写成Bearer: sk-xxx。local proxy failed。这个报错一般出现在你本地配了抓包工具或代理OkHttp 走了系统代理但代理没起来。表现是onFailure里收到Failed to connect to /127.0.0.1:8888之类。解决要么关掉代理要么在 OkHttpClient 里显式设置proxy(Proxy.NO_PROXY)。代码里加一行private final OkHttpClient client new OkHttpClient.Builder() .proxy(java.net.Proxy.NO_PROXY) .build();注意这里说的代理是本地调试用的抓包代理不是网络访问层面的东西别混淆。reading choices 报错。如果你把请求体改成了别的格式或者端点路径写错返回的 JSON 结构对不上解析时可能报No value for choices或类似。表现是response.isSuccessful()为 true 但后续解析崩了。排查先打印respBody看实际返回结构确认choices数组存在。如果返回的是错误对象比如{error: {...}}说明请求参数有问题检查model字段是否是有效模型 ID。OAuth 相关报错。如果你用的是需要 OAuth 的端点而不是 API Key可能会看到invalid_grant或OAuth token expired。本文用的是 API Key 方式不涉及 OAuth 流程。如果你确实要接 OAuth 类服务凭证刷新逻辑要单独处理别和 API Key 混在一起。短信发不出去但没报错。sendTextMessage不抛异常不代表发出去了。常见原因手机号格式带空格或86前缀处理不当、短信中心号码配置错误、被运营商拦截。排查先用系统短信 App 手动发一条确认卡正常再回来测代码。长短信拆分后如果某条失败整条就断了建议加 PendingIntent 监听每条状态。权限申请后仍报 SecurityException。检查是不是在onCreate里直接调了sendSms而没走权限检查。Android 6.0 以上必须运行时申请Manifest 声明只是前提。上面代码里的checkSmsPermissionrequestPermissions流程要完整走一遍。对照这几个报错基本能覆盖 90% 的首次运行问题。剩下的多半是网络环境或账户状态去 https://taotoken.net/api-keys 确认 Key 有效、余额正常。6. 从实例到工程把统一 Key 通道用起来跑通上面这条链路之后你手里其实有了一个可复用的模式设备端只维护一份 Base URL Key 配置所有需要服务端能力的动作都通过这个通道走。短信是系统 API 直发推送、验证码、内容处理都走统一网关。加新服务时客户端代码几乎不用动改的是网关侧的路由。如果你后面要做更复杂的编码任务或者 Agent 类功能可以看下 Coding Planhttps://taotoken.net/coding-plan 它适合长期、高频的调用场景。日常调试和验证模型连通性用模型对话页面 https://taotoken.net/chat 就够了。接入文档在 https://taotoken.net/doc 端点、参数、错误码都在里面遇到不确定的请求格式先查文档再改代码。最后给一个实用技巧把TAOTOKEN_BASE_URL和TAOTOKEN_API_KEY的读取逻辑封装成一个Config类所有网络请求统一从这里取。这样将来换环境测试/生产只改一处也方便做 Key 轮换。凭证管理这件事散着放迟早出问题收拢到一处才是长久做法。代码跑起来后真机上应该能看到点「发送短信」对方收到短信点「发送推送」返回成功并触发服务端动作。两条通道一套 Key这就是本文要交付的完整链路。
返回列表