☰
Android 使用 ContentProvider 获取内容:TaoToken 统一 Key 接入与 settings.json 配置骨架
2026/9/26 10:32:19 网站建设 项目流程

1. 从一次真机调试说起:ContentProvider 到底能拿到什么

Android 里的 ContentProvider 是四大组件之一,它把数据包装成一张"表",让别的应用通过content://开头的 Uri 去查询。你可以把它理解成手机内部的一个"只读数据库接口":系统把音频、视频、联系人、短信、日历这些数据都注册成了 Provider,你的 App 只要拿到ContentResolver,就能像查 SQLite 一样query出结果。

这个能力在两类场景里特别有用。第一类是媒体类 App,需要扫描本机所有音频、视频并展示列表;第二类是工具类 App,需要读取系统数据做二次加工。但真正做起来,麻烦往往不在query本身,而在后面:你想给这些数据接一个 AI 能力,比如自动给音频生成标题、给联系人做智能分组,就得处理模型调用的鉴权、Key 管理、多工具配置。每个工具一套 Key、一套环境变量,改起来很烦。

这篇就按"ContentProvider 读取 + TaoToken 统一 Key 接入"这条线走一遍。前半段交付可复制的查询代码和权限配置,后半段交付settings.json配置骨架,最后用 adb 和日志验证读取结果与鉴权是否生效。适合已经会写基础 Android 页面、想把手上的数据接上 AI 工具链的开发者。

2. TaoToken 前置:统一 Key 与 API 通道准备

TaoToken 在这里扮演的角色是"统一入口":你不需要为每个 AI 工具单独申请一套凭证,而是用同一个 Key 走同一个 API 通道。对 Android 项目来说,好处是配置集中——settings.json里写一次,本地调试、CI、真机都能复用。

先做两件事。

第一,拿到 Key。打开控制台创建 API Key,地址是https://taotoken.net/api-keys,创建后复制保存,它只显示一次。控制台入口在https://taotoken.net/console。

第二,确认 API 基地址。所有请求走https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 base URL 使用。

注意:Key 不要硬编码进MainActivity或提交到 Git。Android 项目里推荐放进local.properties或settings.json这类不纳入版本控制的文件,构建时再注入。

如果你后续要做长期编码或 Agent 类任务,可以了解 Coding Plan,入口是https://taotoken.net/coding-plan;只是想先验证模型通不通,用模型对话页面https://taotoken.net/models更快。官网总入口是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。

3. 可复制配置:settings.json 骨架与 ContentProvider 查询代码

3.1 settings.json 配置骨架

这个文件放在项目根目录,用来集中管理 API 通道和工具参数。字段名按你的工具链习惯调整,结构保持分层即可。

{ "taotoken": { "baseUrl": "https://taotoken.net/api", "apiKeyEnv": "TAOTOKEN_API_KEY", "timeoutMs": 30000, "defaultModel": "claude-sonnet" }, "android": { "minSdk": 23, "targetSdk": 34, "permissions": [ "android.permission.READ_EXTERNAL_STORAGE", "android.permission.READ_MEDIA_AUDIO" ] }, "contentProvider": { "audioUri": "content://media/external/audio/media", "projection": ["_id", "display_name", "artist", "album", "duration", "data"] } }

apiKeyEnv指向环境变量名,而不是明文 Key。运行时从环境变量读取,这样同一份配置可以在不同机器上跑。

3.2 权限声明

Android 13(API 33)之后,读音频要用READ_MEDIA_AUDIO,旧版本仍用READ_EXTERNAL_STORAGE。两个都写上,运行时按版本判断。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

3.3 ContentProvider 查询代码

下面这段是核心:拿到ContentResolver,用query查出音频列表,再逐条读取字段。相比原示例里用两个 Cursor 的做法,这里用一个 Cursor 同时取展示字段和路径,避免位置错位。

public class AudioQueryHelper { public static final Uri AUDIO_URI = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; public static final String[] PROJECTION = { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.DATA }; public static List<AudioItem> queryAll(Context context) { List<AudioItem> result = new ArrayList<>(); ContentResolver cr = context.getContentResolver(); try (Cursor cursor = cr.query( AUDIO_URI, PROJECTION, null, null, MediaStore.Audio.Media.DISPLAY_NAME + " ASC")) { if (cursor == null) { Log.e("AudioQuery", "cursor is null, provider not ready"); return result; } int idIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); int nameIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME); int artistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST); int albumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM); int durIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION); int dataIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA); while (cursor.moveToNext()) { AudioItem item = new AudioItem(); item.id = cursor.getLong(idIdx); item.name = cursor.getString(nameIdx); item.artist = cursor.getString(artistIdx); item.album = cursor.getString(albumIdx); item.duration = cursor.getLong(durIdx); item.path = cursor.getString(dataIdx); result.add(item); } } catch (SecurityException e) { Log.e("AudioQuery", "permission denied: " + e.getMessage()); } return result; } }

几个关键点:getColumnIndexOrThrow比getColumnIndex更安全,字段不存在会直接抛异常而不是返回 -1;try-with-resources保证 Cursor 关闭;排序字段用DISPLAY_NAME,避免默认顺序在不同机型上不一致。

3.4 运行时权限请求

private void ensurePermission() { String perm = Build.VERSION.SDK_INT >= 33 ? Manifest.permission.READ_MEDIA_AUDIO : Manifest.permission.READ_EXTERNAL_STORAGE; if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{perm}, 1001); } else { loadAudio(); } }

4. 验证请求:adb 与日志确认读取和鉴权都生效

4.1 用 adb 验证 ContentProvider 读取

不用装 App 也能先验证 Provider 通不通。连上真机后执行:

adb shell content query --uri content://media/external/audio/media \ --projection _id:display_name:artist:duration

如果返回若干行Row: 0 _id=..., display_name=...,说明系统 Provider 正常,你的 Uri 和字段名没写错。返回No result found通常是设备里确实没有音频,或者权限没给。

再验证权限是否真的生效:

adb shell dumpsys package com.example.audiotest | grep -i "READ_MEDIA_AUDIO"

看到granted=true才算通过。

4.2 用日志确认查询结果

在loadAudio()里加一行统计日志:

List<AudioItem> list = AudioQueryHelper.queryAll(this); Log.i("AudioQuery", "loaded " + list.size() + " audio items"); if (!list.isEmpty()) { Log.i("AudioQuery", "first: " + list.get(0).name + " | " + list.get(0).path); }

过滤日志:

adb logcat -s AudioQuery

预期输出类似:

I/AudioQuery: loaded 37 audio items I/AudioQuery: first: demo_track.mp3 | /storage/emulated/0/Music/demo_track.mp3

4.3 验证 TaoToken 鉴权是否生效

Key 配好后,先用一条最小请求确认通道可用。把 Key 放进环境变量:

export TAOTOKEN_API_KEY="你的Key"

然后发一条测试请求:

curl -s https://taotoken.net/api/v1/messages \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"claude-sonnet","max_tokens":64,"messages":[{"role":"user","content":"ping"}]}'

返回里带content字段说明鉴权通过。如果返回 401,检查 Key 是否复制完整、环境变量是否在当前 shell 生效(echo $TAOTOKEN_API_KEY确认非空)。返回 404 一般是路径拼错,确认 base URL 是https://taotoken.net/api,不要多加斜杠或后缀。

在 Android 端,把同样的请求封装进网络层,日志里打印状态码即可:

Log.i("TaoToken", "auth status=" + response.code());

状态码 200 表示读取链路和鉴权链路都通了。

5. 本篇常见错排查

查询返回空 Cursor。先跑adb shell content query确认系统层有数据,再检查 App 权限是否granted=true。Android 13 上只声明READ_EXTERNAL_STORAGE是不够的,必须加READ_MEDIA_AUDIO。

getColumnIndexOrThrow抛异常。说明PROJECTION里有字段在当前系统版本不存在。DATA字段在新版本上逐渐被弃用,可以改用MediaStore.Audio.Media._ID配合ContentUris.withAppendedId构造 Uri,兼容性更好。

Cursor 没关闭导致内存泄漏。用try-with-resources或finally里cursor.close()。查询量大时泄漏会很明显。

鉴权 401。三种可能:Key 复制时带了空格;环境变量没导出到运行进程;请求头写成Authorization: $KEY少了Bearer前缀。逐个排查。

鉴权 404。base URL 写成了https://taotoken.net/api/或https://taotoken.net/api/v1/,多加了路径。统一用https://taotoken.net/api,具体路径由工具自己拼。

真机连不上 adb。先adb devices确认设备在线,离线就adb kill-server && adb start-server重来。

6. 把两条链路接起来

到这里,ContentProvider 的读取链路和 TaoToken 的鉴权链路各自都验证过了。接下来要做的,是把queryAll拿到的音频列表喂给模型做二次处理,比如批量生成摘要或分类标签。这一步的接入细节,可以对照接入文档https://taotoken.net/doc里的请求格式来写,Key 管理仍在 API Keys 页面https://taotoken.net/api-keys。

如果你打算把这个能力做成长期跑的编码或 Agent 任务,Coding Plan 的入口在https://taotoken.net/coding-plan,配置方式和上面settings.json骨架一致,把baseUrl和apiKeyEnv指过去就行。Claude Code 相关的接入说明在https://taotoken.net/claudecode。

实测下来,最容易踩的坑不是代码本身,而是权限版本差异和 base URL 多写一个斜杠。把这两处固定住,剩下的就是按字段名取数据、按状态码判断鉴权,链路很直。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询