Android Content Provider 基础:把 settings 改到 TaoToken 的跨应用数据共享实践

发布时间:2026/10/18 18:08:36

Android Content Provider 基础:把 settings 改到 TaoToken 的跨应用数据共享实践 1. 从 settings 里那个写死的 endpoint 说起如果你正在做 Android 端的多模型接入大概率遇到过这个场景App 里有个 settings 页面endpoint、apiKey、model 三个字段写死在 SharedPreferences 里改一次要重新打包测试同事想换个通道得找你重新出包。更麻烦的是如果同时有多个 App比如一个主 App 加一个工具 App都要读同一份配置每个 App 各存一份改一处漏一处。Content Provider 正好能解决这个问题。它是 Android 四大组件之一核心职责是在不同应用之间安全地共享数据。你可以把它理解成一个“数据仓库管理员”底层是 SQLite 还是文件、是网络还是 XML调用方都不关心只通过一套content://开头的 URI 和ContentResolver的增删改查接口来访问。系统里的通讯录、媒体库、短信全都是通过 Content Provider 暴露出来的。这篇要做的是从零搭一个可被其他应用查询的 Provider把 settings 里的 endpoint 统一改到 TaoToken 的 API 通道https://taotoken.net/api再用adb命令验证跨应用读取是否成功。适合谁看写过一点 Android、知道 Activity 和 SharedPreferences 是什么、但没自己写过 Provider 的同学。全程可以跟着敲代码都是能直接跑的骨架。先明确一下我们要共享的数据结构。settings 里其实就三样东西endpoint、api_key、model_id。我把它设计成一张单表settings用key做唯一索引value存具体值。这样以后加字段不用改表结构Provider 的 query 逻辑也不用动。TaoToken 的 endpoint 填https://taotoken.net/apikey 在控制台生成model 填你实际要用的模型 ID。这三件套后面在 Provider 里就是三条记录。为什么不用 SharedPreferences 直接跨应用读因为 Android 的沙盒机制给每个应用分配独立的用户 IDA 应用默认读不到 B 应用的私有目录。Content Provider 通过 Binder 机制做跨进程访问是官方认可的正规通道还能配合权限控制决定谁能读、谁只能写。这就是我们要自己写一个 Provider 的根本原因。2. TaoToken 前置把统一 Key 和 API 通道准备好在写 Provider 之前得先把要共享的数据准备好。这一步不涉及 Android 代码但决定了后面 settings 里填什么。TaoToken 在这里扮演的角色是统一 Key 和 API 通道。以前你可能每个模型供应商存一个 key、一个 base urlsettings 里字段越加越多。现在把 endpoint 统一成https://taotoken.net/apikey 用同一个model 字段区分具体模型。这样 Provider 里存的就永远是三条记录调用方拿到的也是同一套结构。具体操作打开官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册登录后进控制台。在 API Keys 页面创建一个新 key复制出来先存好这个 key 只会完整显示一次。模型 ID 在文档页能查到填你实际要调用的那个。endpoint 就是https://taotoken.net/api注意不要在后面多加/v1之类的路径具体拼接方式以文档为准。这里有个容易踩的坑key 不要硬编码在代码里提交到 Git。我们后面会把它写进 Provider 的数据库但初始化数据这一步建议放在首次启动时从 BuildConfig 或本地配置文件读入或者干脆让用户在 settings 页面手动填一次。Provider 只负责“存和共享”不负责“从哪来”。如果你还没决定用哪个模型可以先去模型对话页面试一下确认通道能通、返回正常再把 key 和 model 填进 settings。这一步花两分钟能省掉后面排查 401 的时间。准备好这三样之后我们开始写 Provider。整个工程结构大概是一个SettingsContract放常量和 URI一个SettingsDatabaseHelper建表一个SettingsProvider继承 ContentProvider 实现增删改查最后在 Manifest 里注册。下面逐个来。3. 可复制配置Contract、Helper、Provider 与 Manifest这一节是全文的核心代码都可以直接复制。我按文件拆开讲每个文件的作用和关键点都会标出来。先建 Contract 类把 authority、路径、列名都集中管理。authority 建议用包名加.provider后缀保证全局唯一object SettingsContract { const val AUTHORITY com.example.settingsprovider.provider const val PATH_SETTINGS settings val CONTENT_URI: Uri Uri.parse(content://$AUTHORITY/$PATH_SETTINGS) object Entry { const val TABLE_NAME settings const val _ID _id const val COLUMN_KEY key const val COLUMN_VALUE value } }接着是数据库帮助类。表结构很简单key加唯一约束value存字符串。onCreate里顺便把 TaoToken 的三条初始数据插进去这样 Provider 一启动就有数据可查class SettingsDatabaseHelper(context: Context) : SQLiteOpenHelper(context, settings.db, null, 1) { override fun onCreate(db: SQLiteDatabase) { db.execSQL( CREATE TABLE ${SettingsContract.Entry.TABLE_NAME} ( ${SettingsContract.Entry._ID} INTEGER PRIMARY KEY AUTOINCREMENT, ${SettingsContract.Entry.COLUMN_KEY} TEXT UNIQUE, ${SettingsContract.Entry.COLUMN_VALUE} TEXT ) ) db.execSQL( INSERT INTO ${SettingsContract.Entry.TABLE_NAME} (key, value) VALUES (?, ?), arrayOf(endpoint, https://taotoken.net/api) ) db.execSQL( INSERT INTO ${SettingsContract.Entry.TABLE_NAME} (key, value) VALUES (?, ?), arrayOf(api_key, sk-替换成你在控制台生成的Key) ) db.execSQL( INSERT INTO ${SettingsContract.Entry.TABLE_NAME} (key, value) VALUES (?, ?), arrayOf(model_id, 替换成你要用的模型ID) ) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL(DROP TABLE IF EXISTS ${SettingsContract.Entry.TABLE_NAME}) onCreate(db) } }然后是 Provider 本体。用UriMatcher区分“整表”和“单条”两种 URIquery 支持按 key 过滤insert 用CONFLICT_REPLACE实现 upsert这样改 endpoint 不用先删再插class SettingsProvider : ContentProvider() { private lateinit var dbHelper: SettingsDatabaseHelper private val uriMatcher UriMatcher(UriMatcher.NO_MATCH).apply { addURI(SettingsContract.AUTHORITY, SettingsContract.PATH_SETTINGS, CODE_SETTINGS) addURI(SettingsContract.AUTHORITY, ${SettingsContract.PATH_SETTINGS}/*, CODE_SETTING_KEY) } companion object { private const val CODE_SETTINGS 100 private const val CODE_SETTING_KEY 101 } override fun onCreate(): Boolean { dbHelper SettingsDatabaseHelper(context!!) return true } override fun query( uri: Uri, projection: Arrayout String?, selection: String?, selectionArgs: Arrayout String?, sortOrder: String? ): Cursor? { val db dbHelper.readableDatabase return when (uriMatcher.match(uri)) { CODE_SETTINGS - db.query( SettingsContract.Entry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder ) CODE_SETTING_KEY - { val key uri.lastPathSegment db.query( SettingsContract.Entry.TABLE_NAME, projection, ${SettingsContract.Entry.COLUMN_KEY}?, arrayOf(key), null, null, sortOrder ) } else - throw IllegalArgumentException(Unknown URI: $uri) } } override fun insert(uri: Uri, values: ContentValues?): Uri? { val db dbHelper.writableDatabase val id db.insertWithOnConflict( SettingsContract.Entry.TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE ) if (id 0) { context?.contentResolver?.notifyChange(uri, null) return ContentUris.withAppendedId(SettingsContract.CONTENT_URI, id) } throw SQLException(Insert failed for URI: $uri) } override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Arrayout String? ): Int { val db dbHelper.writableDatabase val rows db.update( SettingsContract.Entry.TABLE_NAME, values, selection, selectionArgs ) if (rows 0) context?.contentResolver?.notifyChange(uri, null) return rows } override fun delete( uri: Uri, selection: String?, selectionArgs: Arrayout String? ): Int { val db dbHelper.writableDatabase val rows db.delete( SettingsContract.Entry.TABLE_NAME, selection, selectionArgs ) if (rows 0) context?.contentResolver?.notifyChange(uri, null) return rows } override fun getType(uri: Uri): String? when (uriMatcher.match(uri)) { CODE_SETTINGS - vnd.android.cursor.dir/vnd.${SettingsContract.AUTHORITY}.settings CODE_SETTING_KEY - vnd.android.cursor.item/vnd.${SettingsContract.AUTHORITY}.settings else - throw IllegalArgumentException(Unknown URI: $uri) } }最后是 Manifest 注册。exportedtrue允许外部应用访问同时用自定义权限保护读写。注意android:authorities必须和 Contract 里的 AUTHORITY 完全一致差一个字符都会导致Failed to find providerpermission android:namecom.example.permission.READ_SETTINGS android:protectionLevelnormal / permission android:namecom.example.permission.WRITE_SETTINGS android:protectionLevelnormal / provider android:name.SettingsProvider android:authoritiescom.example.settingsprovider.provider android:exportedtrue android:readPermissioncom.example.permission.READ_SETTINGS android:writePermissioncom.example.permission.WRITE_SETTINGS /到这里Provider 侧就齐了。调用方应用需要在 Manifest 里uses-permission android:namecom.example.permission.READ_SETTINGS /然后就能用contentResolver.query读数据。下一节我们用 adb 直接验证不用再写一个 App。4. 验证请求adb 命令与查询结果校验写完代码别急着写调用方 App先用adb shell content命令验证 Provider 是否真的暴露出来了。这是最快、最省事的验证方式能直接看到 Cursor 返回的内容。先装好你的 App然后执行查询整表的命令adb shell content query --uri content://com.example.settingsprovider.provider/settings如果一切正常你会看到类似这样的输出三行记录每行带_id、key、valueRow: 0 _id1, keyendpoint, valuehttps://taotoken.net/api Row: 1 _id2, keyapi_key, valuesk-xxxx Row: 2 _id3, keymodel_id, valueyour-model-id再验证按 key 查询单条确认UriMatcher的CODE_SETTING_KEY分支生效adb shell content query --uri content://com.example.settingsprovider.provider/settings/endpoint预期只返回 endpoint 那一行。如果返回空先检查 URI 末尾的endpoint有没有拼错再检查addURI里用的是settings/*还是settings/#——我们这里 key 是字符串所以必须用*用#只能匹配数字。接着验证写入用content insert改一下 endpoint模拟 settings 页面保存的动作adb shell content insert --uri content://com.example.settingsprovider.provider/settings \ --bind key:s:endpoint \ --bind value:s:https://taotoken.net/api注意--bind的格式是列名:类型:值s表示字符串。因为用了CONFLICT_REPLACE重复插入同一个 key 会覆盖而不是报错。插完再 query 一次确认 value 已经更新。最后验证跨应用读取。如果你有第二个 App在它的代码里这样读val uri Uri.parse(content://com.example.settingsprovider.provider/settings/endpoint) contentResolver.query(uri, null, null, null, null)?.use { cursor - if (cursor.moveToNext()) { val endpoint cursor.getString(cursor.getColumnIndexOrThrow(value)) Log.d(Settings, endpoint $endpoint) } }Logcat 里能看到endpoint https://taotoken.net/api就说明跨应用共享成功了。这里用use {}自动关闭 Cursor避免忘记 close 导致内存泄漏。校验结果时重点看三件事一是 URI 能不能匹配上匹配不上会抛IllegalArgumentException二是权限有没有放行没放行会抛SecurityException三是 Cursor 的列名对不对用getColumnIndexOrThrow比getColumnIndex更安全列名写错会直接抛异常而不是返回 -1。5. 本篇常见错排查401、local proxy failed 与 reading choices这一节把实际会撞到的报错列出来对照着改。很多问题不是 Provider 本身写错而是配置或权限没对齐。第一个高频错误是Failed to find provider info for com.example.settingsprovider.provider。原因通常是 Manifest 里的android:authorities和代码里SettingsContract.AUTHORITY不一致或者 Provider 类名写错、没加.前缀。检查方法在 Android Studio 的 Merged Manifest 里搜 authorities确认最终合并结果和你 query 用的 URI 完全一致。另一个可能是 App 没装成功adb shell pm list packages | grep 你的包名确认一下。第二个是java.lang.SecurityException: Permission Denial: opening provider ... requires com.example.permission.READ_SETTINGS。这说明调用方没声明权限或者权限的protectionLevel设成了signature而两个 App 签名不同。排查顺序先在调用方 Manifest 加uses-permission再把protectionLevel改成normal测试。如果还不行临时把 Provider 的readPermission去掉、只留exportedtrue确认是权限问题还是 URI 问题。第三个是调用 TaoToken 接口时返回 401。这跟 Provider 无关是 key 或 endpoint 的问题。先确认 Provider 里存的 endpoint 是https://taotoken.net/api没有多余斜杠或路径再确认 key 是完整的、没有前后空格。用adb shell content query把 api_key 读出来和你在控制台复制的对比。如果 key 正确还 401去控制台看这个 key 是否被禁用或额度用尽。第四个是local proxy failed或连接超时。这类报错通常出现在网络层先确认设备本身能正常访问外网再确认 endpoint 拼写。注意不要在 endpoint 后面手动加/v1/chat/completions之类的路径具体路径拼接以文档为准加错了会返回 404 而不是 401两者要区分开。第五个是解析响应时报reading choices或 JSON 解析异常。这多半是 model_id 填错了或者请求体格式和模型不匹配。先用模型对话页面确认这个 model 能正常返回再把同样的 model_id 写进 Provider。如果 Provider 里存的是旧值记得用content insert覆盖更新别只改代码不更新数据。第六个是OAuth相关报错。如果你用的是需要 OAuth 的通道key 的形态和普通 API Key 不一样别混用。TaoToken 的 API Key 直接放在 Authorization 头里即可具体头格式看文档。Provider 只负责存这个字符串怎么用是调用方的事但存错了后面一定报错。排查时养成一个习惯先用adb shell content query把 Provider 里的三条数据读出来确认 endpoint、api_key、model_id 都是你期望的值。数据对了再查网络和权限能省一半时间。6. 把 settings 统一到 TaoToken 之后走到这里你已经有了一个能被其他应用查询的 Providersettings 里的 endpoint 指向https://taotoken.net/apikey 和 model 也统一存好了。后面再改配置只需要content insert覆盖对应 key所有依赖这个 Provider 的 App 下次 query 就能拿到新值不用重新打包。如果后面要长期做编码类或 Agent 类项目可以考虑 Coding Plan把 key 和额度集中管理省得每个项目单独申请。验证模型是否通的时候模型对话页面是最快的入口。接入文档里有完整的请求示例和参数说明遇到路径拼接、请求头格式的问题先翻文档。几个实用建议Provider 的onCreate里不要做耗时操作它执行在Application.onCreate之前且在主线程初始化重了会拖慢冷启动Cursor 用完一定 close用use {}最省心数据变更后调notifyChange配合ContentObserver能让 UI 自动刷新。这些坑我在不同项目里都踩过提前注意能少调半天。最后留一个可以自己扩展的点现在 settings 是单表 key-value如果以后要存多用户配置可以加一个user_id列把 URI 改成settings/{userId}UriMatcher多加一个分支就行。Provider 的骨架不用大改这也是当初选 key-value 结构的原因。
延伸阅读

更多相关文章

2026/10/18 18:08:36

抖音私信自动回复系统:协议解析与轻量规则引擎实践

简介:这是一套面向抖音企业号运营者的私信自动化响应解决方案,专为解决高频私信处理效率低、人工响应不及时等实际痛点而设计,适用于具备基础前端与配置能力的开发者或运营人员。资源包含2个核心文件:1个HTML主程序页面&#xff0…

2026/10/18 19:13:41

安全审计工程化:从扫描报告到可验证代码资产

1. 这不是“安全扫描”,而是用代码写审计报告的硬功夫“security-audit-skill”——光看这个词,很多人第一反应是:又一个带“-”的英文组合词,大概率是某个工具命令、CI/CD里的一个stage名,或者某份技术文档里被加粗的…

2026/10/18 19:13:41

BrowserSkill:基于CDP的浏览器CLI控制工具

1. 项目概述:BrowserSkill 是什么,它解决的是哪类真实痛点?BrowserSkill 这个名字乍看像某个新出的浏览器插件或 Chrome 扩展,但结合热词中反复出现的CLI、Chrome、Edge、codex cli、zcode cli、trae cli、deveco cli等关键词&…

2026/10/18 19:13:41

ASP.NET供应商管理系统毕业设计:分层架构与权限状态机实战

简介:ASP.NET供应商管理系统是一套面向计算机专业毕业设计与企业供应链管理学习者的Web应用源码,基于C#与ASP.NET框架开发,采用MVC架构,配合SQL Server存储数据,帮助理解供应商分类、信息维护、评价、留言与用户权限等…

2026/10/18 19:13:41

BrowserSkill:面向开发者的 Chromium 浏览器 CLI 自动化工具

1. BrowserSkill 是什么:一个被误读但极具潜力的浏览器自动化 CLI 工具BrowserSkill(常缩写为 bsk)不是插件、不是浏览器壳、更不是某种“破解工具”或“下载神器”——它是一个面向开发者与高级用户的命令行界面(CLI)…

2026/10/18 19:08:41

Python旅游推荐数据分析可视化:从CSV到交互看板实战

简介:这是一套面向计算机相关专业毕设学生与Python项目实战学习者的旅游推荐数据分析可视化系统,采用pythonDjangomysql协同过滤算法构建,可直接作为毕业设计、课程设计或期末大作业使用。系统分为用户端与管理员端:用户可完成登录…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/18 0:01:50

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑