news 2026/9/23 9:35:59

Android 调用系统相机选择图片:TaoToken 统一 Key 接入与 settings.json 配置骨架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 调用系统相机选择图片:TaoToken 统一 Key 接入与 settings.json 配置骨架

1. 从一次拍照上传的崩溃说起

Android 调用系统相机选择图片,看起来只是IntentonActivityResult两段代码的事,但真正落到「拍完照要传给 AI 做识别」这条链路上,坑会一个接一个冒出来。我见过太多项目卡在同一个地方:图片路径拿到了,Bitmap也显示出来了,结果一上传就报FileNotFoundException,或者上传成功但服务端返回 401,因为 Key 写死在代码里、换环境就失效。

这篇要解决的就是这条完整链路:Android 通过 Intent 调用系统相机拍照或从相册选图,拿到图片后,用 TaoToken 统一 Key 把图片送到多模态模型做识别。适合正在做智能硬件配套 App、电商识图、拍照问答类功能的 Android 开发者。核心检索词就三个:android、系统相机、选择图片,但我会把「选完图之后怎么办」讲透,因为那才是真正决定项目能不能跑通的部分。

先明确一个边界:Android 调系统相机有两种主流写法,一种是ACTION_PICK从相册选,一种是ACTION_IMAGE_CAPTURE直接调相机拍。原文用的是前者,我会两种都给,并且补上 Android 7.0 之后必须处理的FileProvider,否则ACTION_IMAGE_CAPTURE在真机上直接崩。图片拿到之后,统一走 TaoToken 的 Key 接入,把「模型调用」这件事从 App 里解耦出去,配置一次,后面换模型、换环境都不用改客户端代码。

整篇的节奏是:先给可复制的settings.json配置骨架,再给 TaoToken 统一 Key 的接入步骤,然后是拍照回调到图片上传的验证动作,最后把几个高频报错逐个拆掉。你可以边看边改,配置一次跑通。

2. TaoToken 统一 Key 前置准备

在写 Android 代码之前,先把「Key 从哪来、放哪、怎么用」这件事定下来。很多团队的做法是把 API Key 硬编码在BuildConfig里,结果测试包和正式包混用、Key 泄露、换模型要重新打包,非常痛苦。TaoToken 的思路是提供一个统一的接入层,你只需要一个 Key,就能在多个模型之间切换,客户端不用关心背后是哪个厂商。

2.1 获取统一 Key

打开 TaoToken 官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台创建 API Key。这个 Key 就是后面所有请求的凭证,格式通常是一串以sk-开头的字符串。创建入口在控制台的 API Keys 页面,建议给不同环境(开发、测试、生产)建不同的 Key,方便单独吊销。

拿到 Key 之后,不要直接写进 Java 代码。推荐放在local.properties或者 CI 的环境变量里,通过buildConfigField注入。这样本地调试和线上构建用的是同一套代码,只是注入的值不同。

2.2 确认接入地址

TaoToken 的 API 基础地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,是纯粹的接口前缀。所有模型调用都走这个前缀,具体路径在文档里查。比如对话补全通常是/v1/chat/completions,多模态识图也是同一个入口,只是消息体里带图片。

这里有个容易踩的坑:有人把官网地址当成 API 地址用,结果请求打到网页上返回 HTML,解析 JSON 直接崩。记住官网是给人看的,API 是给程序调的,两者域名相同但路径不同。

2.3 配置骨架 settings.json

虽然 Android 原生项目不直接用settings.json,但很多跨端框架(比如 React Native、Flutter 的某些插件、或者你自建的配置中心)会用 JSON 来管理运行时配置。下面这份骨架可以直接复制,把apiKeybaseUrl换成你自己的即可:

{ "taotoken": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的统一Key", "defaultModel": "gpt-4o", "timeoutMs": 30000, "vision": { "enabled": true, "maxImageSizeMb": 5, "supportedFormats": ["jpg", "jpeg", "png", "webp"] } }, "android": { "fileProviderAuthority": "com.yourpackage.fileprovider", "cameraRequestCode": 1001, "galleryRequestCode": 1002 } }

这份配置里,baseUrl固定指向 TaoToken 的 API 前缀,apiKey是统一 Key,defaultModel指定默认走哪个多模态模型。vision段控制图片上传的限制,maxImageSizeMb建议设 5,因为大多数多模态接口对单图有大小限制,超过会被拒。android段里的fileProviderAuthority必须和AndroidManifest.xml里声明的一致,否则调相机时会抛IllegalArgumentException

注意:settings.json里的 Key 只适合本地开发或服务端下发场景。如果这个文件会打进 APK,务必确保它不被反编译泄露,生产环境更推荐由后端签发临时凭证。

3. 可复制的 Android 配置与代码

这一节是重头戏,把从调相机、选图片到上传的完整代码给全。你可以新建一个CameraImageHelper类,把逻辑收拢进去,Activity 里只留回调。

3.1 AndroidManifest 与 FileProvider

Android 6.0 之后相机权限要动态申请,7.0 之后file://URI 不能直接暴露给其他应用,必须用FileProvider。先在AndroidManifest.xml里声明:

<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application> <provider android:name="androidx.core.content.FileProvider" android:authorities="com.yourpackage.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> </application>

然后在res/xml/file_paths.xml里定义可共享的目录:

<?xml version="1.0" encoding="utf-8"?> <paths> <external-files-path name="images" path="Pictures/" /> <cache-path name="cache_images" path="images/" /> </paths>

authorities的值必须和settings.json里的fileProviderAuthority完全一致,大小写都不能差。

3.2 调用系统相机拍照

拍照的关键是先生成一个目标文件的 URI,再把这个 URI 通过EXTRA_OUTPUT传给相机应用。代码如下:

private Uri createImageUri() { File imageDir = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "images"); if (!imageDir.exists()) { imageDir.mkdirs(); } File imageFile = new File(imageDir, "capture_" + System.currentTimeMillis() + ".jpg"); return FileProvider.getUriForFile( this, "com.yourpackage.fileprovider", imageFile ); } private void openCamera() { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (intent.resolveActivity(getPackageManager()) != null) { currentImageUri = createImageUri(); intent.putExtra(MediaStore.EXTRA_OUTPUT, currentImageUri); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); startActivityForResult(intent, REQUEST_CAMERA); } }

currentImageUri是成员变量,拍照成功后从它读取图片,而不是从data.getData()取,因为ACTION_IMAGE_CAPTURE在指定了EXTRA_OUTPUT之后,data里是空的。

3.3 从相册选择图片

相册选择用ACTION_PICK,原文的写法基本可用,但要注意 Android 10 之后MediaStore.Images.Media.DATA这个列被标记为废弃,直接查路径可能拿到 null。更稳的做法是用ContentResolver打开输入流,或者用MediaStore.Images.Media._ID拼 URI。下面给一个兼容写法:

private void openGallery() { Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); intent.setType("image/*"); startActivityForResult(intent, REQUEST_GALLERY); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode != RESULT_OK) return; if (requestCode == REQUEST_CAMERA) { handleImage(currentImageUri); } else if (requestCode == REQUEST_GALLERY && data != null) { handleImage(data.getData()); } }

handleImage接收一个Uri,内部统一转成InputStream或临时文件,这样相机和相册两条路径就合并了,后面上传逻辑只写一份。

3.4 图片压缩与 Base64 编码

多模态接口通常接受 Base64 编码的图片,或者接受 multipart 上传。Base64 的好处是请求体简单,坏处是体积膨胀约 33%。所以上传前一定要压缩。下面这段把Uri转成压缩后的 Base64:

private String uriToBase64(Uri uri, int maxSizeKb) throws IOException { InputStream input = getContentResolver().openInputStream(uri); Bitmap bitmap = BitmapFactory.decodeStream(input); if (input != null) input.close(); int quality = 80; ByteArrayOutputStream output = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, quality, output); while (output.size() / 1024 > maxSizeKb && quality > 20) { quality -= 10; output.reset(); bitmap.compress(Bitmap.CompressFormat.JPEG, quality, output); } byte[] bytes = output.toByteArray(); return Base64.encodeToString(bytes, Base64.NO_WRAP); }

maxSizeKb建议设 800 到 1000,对应大约 1MB 以内的原图,压缩后通常能压到 200KB 左右,足够模型识别。

3.5 用统一 Key 发起请求

请求部分用OkHttpHttpURLConnection都行,这里给OkHttp的写法,因为拦截器加 Header 更方便:

private void callVisionModel(String base64Image) throws JSONException { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); JSONObject payload = new JSONObject(); payload.put("model", "gpt-4o"); JSONArray messages = new JSONArray(); JSONObject userMsg = new JSONObject(); userMsg.put("role", "user"); JSONArray content = new JSONArray(); content.put(new JSONObject().put("type", "text").put("text", "描述这张图片")); content.put(new JSONObject() .put("type", "image_url") .put("image_url", new JSONObject() .put("url", "data:image/jpeg;base64," + base64Image))); userMsg.put("content", content); messages.put(userMsg); payload.put("messages", messages); Request request = new Request.Builder() .url("https://taotoken.net/api/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(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { runOnUiThread(() -> tvResult.setText("请求失败: " + e.getMessage())); } @Override public void onResponse(Call call, Response response) throws IOException { String body = response.body().string(); runOnUiThread(() -> tvResult.setText(body)); } }); }

注意Authorization头是Bearer加空格再加 Key,少一个空格就是 401。BuildConfig.TAOTOKEN_API_KEY来自build.gradle里的buildConfigField,这样 Key 不会出现在源码里。

4. 验证请求与成功结果

配置写完,怎么确认真的跑通了?分三步验证,每一步都有明确的成功标志。

4.1 验证 Key 是否有效

先不传图片,发一个最简单的文本请求,确认 Key 和地址都对:

curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "ping"}] }'

如果返回的 JSON 里有choices字段,说明 Key 和地址都没问题。如果返回 401,检查 Key 有没有复制完整、有没有多余空格。如果返回 404,检查路径是不是/v1/chat/completions,别漏了v1

4.2 验证图片上传链路

在 App 里点拍照,拍一张清晰的文字图片,观察 Logcat。成功的话会看到类似这样的响应片段:

{ "choices": [ { "message": { "role": "assistant", "content": "图片中显示了一段文字,内容是..." } } ] }

如果返回content是空或者报image too large,说明压缩没生效,回去检查uriToBase64里的maxSizeKb是不是设太大。

4.3 验证不同模型的切换

统一 Key 的好处这时候体现出来:把settings.json里的defaultModelgpt-4o改成别的多模态模型,重新发一次请求,如果也能正常返回,说明接入层是通的。这一步能帮你确认「换模型不用改客户端代码」这个目标达成了。

5. 本篇常见错排查

下面这几个报错,基本覆盖了 90% 的翻车场景,逐个对照。

5.1 FileUriExposedException

报错信息:android.os.FileUriExposedException: file:///... exposed beyond app through Intent.getData()

原因:Android 7.0 之后直接传file://URI 给其他应用会抛这个异常。解决:所有跨应用传递的 URI 必须用FileProvider.getUriForFile()生成content://URI,并且加FLAG_GRANT_WRITE_URI_PERMISSION。检查file_paths.xml里的路径有没有覆盖你实际存图的目录。

5.2 拍照返回 data 为 null

现象:onActivityResultdata是 null,拿不到图片。

原因:调ACTION_IMAGE_CAPTURE时传了EXTRA_OUTPUT,相机把图写到你指定的 URI 了,data里自然没有东西。解决:用成员变量保存currentImageUri,回调时直接读它,不要读data.getData()

5.3 401 Unauthorized

现象:请求返回 401,提示 invalid api key。

排查顺序:第一,Key 有没有复制完整,前后有没有空格;第二,Authorization头格式是不是Bearer sk-xxxBearer和 Key 之间必须有一个空格;第三,Key 是不是被吊销了,去控制台确认状态;第四,请求地址是不是https://taotoken.net/api开头,别写成官网地址。

5.4 图片过大被拒

现象:返回 413 或提示 image size exceeds limit。

原因:Base64 编码后体积膨胀,原图 3MB 编码后接近 4MB,超过接口限制。解决:上传前压缩,uriToBase64里的循环压缩逻辑要确保生效。另外可以在settings.json里把maxImageSizeMb调小,作为双重保险。

5.5 相册选图拿到 null 路径

现象:MediaStore.Images.Media.DATA查询返回 null。

原因:Android 10 之后分区存储生效,DATA列不再可靠。解决:不要查路径,直接用ContentResolver.openInputStream(uri)读流,或者用MediaStore.Images.Media._IDcontent://media/external/images/media/{id}。上面给的handleImage统一走Uri就是为了避开这个问题。

6. 接入文档与后续动作

配置跑通之后,下一步是把这套逻辑固化到项目里。几个建议:把CameraImageHelper抽成独立模块,settings.json的读取封装成ConfigManager,Key 通过buildConfigField注入,这样换环境只改构建脚本。多模态模型的参数、超时、重试策略都放在配置里,别散落在代码各处。

如果你在接入过程中遇到 Key 相关的问题,比如权限、额度、模型列表,可以直接去 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 查看和管理。完整的接口参数、请求示例、错误码说明在接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 里,建议对照着把每个字段过一遍。想先快速验证模型效果,不写代码的话,模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 可以直接传图试。如果后面要做长期的编码辅助或者 Agent 类功能,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 里有更完整的方案。

最后留一个实操技巧:调试图片上传时,先把 Base64 字符串写到本地文件,用adb pull拉出来,手动解码看图片有没有损坏。这一步能帮你区分是「图片本身有问题」还是「请求构造有问题」,比盲猜快得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 9:35:59

Java SSM电影订票系统实战:从环境搭建到并发锁座全流程

简介&#xff1a;本资源是一套基于SSM框架&#xff08;SpringSpringMVCMyBatis&#xff09;开发的完整Java电影购票系统&#xff0c;面向Java Web初学者与课程设计实践者&#xff0c;解决影院线上订票场景下的前后端协同开发、数据库建模与权限分层管理等典型问题。压缩包共84个…

作者头像 李华
网站建设 2026/9/23 9:35:52

3步搞懂芯片烧录器底层逻辑保姆级教程

3步搞懂芯片烧录器底层逻辑保姆级教程 官方文档动辄几百页,翻开全是寄存器定义和时序图,根本抓不住重点。别慌,这篇保姆级教程不整虚的,直接带你拆解芯片烧录器的核心逻辑。我们跳过那些晦涩难懂的协议细节,用大白话把“怎么把代码写进芯片”这件事讲透。 一句话原理:它是数据的“搬运工”与“翻译官”…

作者头像 李华
网站建设 2026/9/23 9:35:47

3天搞定谢家宝树环境配置,一文搞懂从零搭建实战

3天搞定谢家宝树环境配置,一文搞懂从零搭建实战 配置环境就卡半天,是不是你的常态? 明明照着CSDN上的教程敲代码,结果依赖冲突、版本报错,折腾一下午还没跑通。别急,今天咱们不整虚的,直接上手。 谢家宝树这套技术栈,核心在于 环境隔离 与 依赖锁定…

作者头像 李华
网站建设 2026/9/23 9:35:43

后端面试长图制作避坑指南:5个高频考点与代码实战

后端面试长图制作避坑指南:5个高频考点与代码实战 官方文档翻了三遍还是觉得云里雾里?长图制作看似简单,实则坑多。这份避坑指南直击痛点,帮你理清思路。 考点梳理 面试官问“长图制作”,通常不是考你会不会用Pillow,而是考察你对 流式处理 、 内存管理 和 高并发稳定性 的理解。…

作者头像 李华
网站建设 2026/9/23 9:35:36

Atlas 300V 24G AI推理加速卡上部署YOLO模型全攻略

“atlas 300v 24g 是运算加速卡吗&#xff1f;”这个问题我在好几个技术群里都见过&#xff0c;问的人大概率是刚拿到一张Atlas卡&#xff0c;发现装不了CUDA、跑不了熟悉的PyTorch&#xff0c;第一反应就是怀疑自己买错了东西。我实际在一张Atlas 300V 24G上把YOLO模型从PyTor…

作者头像 李华
网站建设 2026/9/23 9:35:37

3步讲透一加3t怎么root原理,一文搞懂底层逻辑

3步讲透一加3t怎么root原理,一文搞懂底层逻辑 面试被问到一加3t怎么root的底层机制时,很多候选人只能停留在“刷个包”的层面,根本答不上来Bootloader解锁后的内存映射变化。别慌,今天我们把手机当成一个受限的计算机体系, 一文搞懂…

作者头像 李华