ARTICLE DETAIL

资讯详情

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

Android学习之Provider:TaoToken 统一 Key 接入与内容提供器调试实战

Android学习之Provider:TaoToken 统一 Key 接入与内容提供器调试实战 1. 从一次 adb 查询失败说起ContentProvider 到底难在哪如果你正在学 Android 四大组件ContentProvider 大概率是那个“看得懂概念、写不出能跑的代码”的组件。Activity 你能看到界面Service 你能看到日志BroadcastReceiver 你能看到回调但 ContentProvider 一旦query返回 null你连错在哪都不容易判断。更麻烦的是很多教程只给你一段ContentProvider子类代码却不告诉你authorities写错会怎样、exported没开会出现什么报错、Android 10 之后为什么 adb 能查但应用查不到。这篇就按“能跟做”的思路来先搭一个最小可用的 Provider把注册、权限、URI 匹配写清楚再用 TaoToken 的统一 Key 和 API 通道把 Provider 查询出来的数据发到模型侧做一次结构化处理顺便把网络请求配置也跑通。这样你练的不只是组件本身还有真实项目里“本地数据 远端能力”的链路。核心检索词先明确ContentProvider 是 Android 中用于跨应用共享结构化数据的组件适合联系人、相册、自定义数据库这类场景适合正在学四大组件、想跑通数据共享链路的初学者。它和普通网络请求的区别在于Provider 工作在本地靠 URI 定位资源靠 ContentResolver 访问不需要网络也能查而远端 API 需要 Base URL、Key、Model ID 三件套。把这两条链路接起来你对“数据从哪来、到哪去”的理解会清晰很多。我试过在没配queries的情况下直接查目标应用adb 能出数据应用里却抛SecurityException这个坑后面会单独讲。先把环境准备好。2. TaoToken 前置准备统一 Key 与 API 通道怎么配TaoToken 在这里的角色是“统一 Key API 通道”。你不需要为每个模型单独维护一套鉴权逻辑拿一个 Key配一个 Base URL选一个 Model ID就能在 Android 侧发起请求。对学习者来说这能省掉大量和业务无关的配置时间把精力放在 Provider 的 CRUD 和 URI 匹配上。先做三件事。第一拿到 API Key。进入控制台创建密钥路径是console创建后复制保存。注意 Key 只显示一次丢了就重新建。控制台地址https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite第二确认 Base URL。TaoToken 的 API 入口是https://taotoken.net/api这个地址不加 UTM直接作为 Retrofit 或 OkHttp 的 baseUrl 使用。注意结尾不要多加/v1之类的路径具体路径在接口调用时拼。第三选 Model ID。模型对话页可以看到当前可用的模型列表选一个你熟悉的即可。模型对话入口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接入文档在这里遇到路径或参数问题优先查它https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite把这三样记成一个配置对象后面代码里直接用object TaoTokenConfig { const val BASE_URL https://taotoken.net/api const val API_KEY sk-你的Key const val MODEL_ID 你的ModelID }这里有个容易忽略的点Android 模拟器访问外网需要INTERNET权限而且从 Android 9 开始默认禁止明文 HTTP。TaoToken 走 HTTPS所以不用配usesCleartextTraffic但权限必须声明uses-permission android:nameandroid.permission.INTERNET /如果你在真机上调试确认网络可用即可。整个前置准备不超过五分钟重点是别把 Key 硬编码进要提交的仓库练习阶段可以放local.properties或 BuildConfig后面会提。3. 可复制配置Provider 注册、权限声明与网络请求三件套这一节是全文的核心所有片段都可以直接复制。先写 Provider 本体再写清单注册最后写网络请求配置。3.1 Provider 类与 URI 匹配先定义一个数据库帮助类建一张最简单的表class MyDatabaseHelper(context: Context) : SQLiteOpenHelper(context, demo.db, null, 1) { override fun onCreate(db: SQLiteDatabase) { db.execSQL( CREATE TABLE my_table ( _id INTEGER PRIMARY KEY AUTOINCREMENT, column1 TEXT, column2 TEXT) ) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL(DROP TABLE IF EXISTS my_table) onCreate(db) } }然后是 Provider 本体注意UriMatcher的用法和notifyChange的位置class MyContentProvider : ContentProvider() { private lateinit var dbHelper: MyDatabaseHelper private val uriMatcher UriMatcher(UriMatcher.NO_MATCH) companion object { const val AUTHORITY com.example.myprovider const val TABLE_NAME my_table const val CODE_DIR 1 const val CODE_ITEM 2 val CONTENT_URI: Uri Uri.parse(content://$AUTHORITY/$TABLE_NAME) } init { uriMatcher.addURI(AUTHORITY, TABLE_NAME, CODE_DIR) uriMatcher.addURI(AUTHORITY, $TABLE_NAME/#, CODE_ITEM) } override fun onCreate(): Boolean { dbHelper MyDatabaseHelper(context!!) return true } override fun query( uri: Uri, projection: ArrayString?, selection: String?, selectionArgs: ArrayString?, sortOrder: String? ): Cursor? { val db dbHelper.readableDatabase val cursor when (uriMatcher.match(uri)) { CODE_DIR - db.query(TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder) CODE_ITEM - { val id uri.lastPathSegment db.query(TABLE_NAME, projection, _id ?, arrayOf(id), null, null, sortOrder) } else - throw IllegalArgumentException(Unknown URI: $uri) } cursor.setNotificationUri(context!!.contentResolver, uri) return cursor } override fun insert(uri: Uri, values: ContentValues?): Uri? { val db dbHelper.writableDatabase val id db.insert(TABLE_NAME, null, values) if (id 0) { context!!.contentResolver.notifyChange(uri, null) return Uri.parse($CONTENT_URI/$id) } return null } override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: ArrayString? ): Int { val db dbHelper.writableDatabase val count db.update(TABLE_NAME, values, selection, selectionArgs) if (count 0) context!!.contentResolver.notifyChange(uri, null) return count } override fun delete(uri: Uri, selection: String?, selectionArgs: ArrayString?): Int { val db dbHelper.writableDatabase val count db.delete(TABLE_NAME, selection, selectionArgs) if (count 0) context!!.contentResolver.notifyChange(uri, null) return count } override fun getType(uri: Uri): String? when (uriMatcher.match(uri)) { CODE_DIR - vnd.android.cursor.dir/vnd.$AUTHORITY.$TABLE_NAME CODE_ITEM - vnd.android.cursor.item/vnd.$AUTHORITY.$TABLE_NAME else - null } }3.2 清单注册与权限声明在AndroidManifest.xml的application内注册provider android:name.MyContentProvider android:authoritiescom.example.myprovider android:exportedtrue android:grantUriPermissionstrue /authorities必须和代码里的AUTHORITY完全一致大小写敏感。exportedtrue表示允许其他应用访问练习阶段可以开生产环境要配合权限控制。如果你要访问其他应用的 Provider比如系统相册需要在清单里声明权限uses-permission android:nameandroid.permission.READ_CONTACTS / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE /Android 10 之后如果目标应用不在默认可见范围内还要加queriesqueries package android:namecom.example.targetapp / /queries3.3 网络请求三件套配置用 OkHttp 发请求把 Base URL、Key、Model ID 三件套写全val client OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .build() val json JSONObject().apply { put(model, TaoTokenConfig.MODEL_ID) put(messages, JSONArray().put(JSONObject().apply { put(role, user) put(content, 把这条数据整理成一句话$rawData) })) } val request Request.Builder() .url(${TaoTokenConfig.BASE_URL}/v1/chat/completions) .addHeader(Authorization, Bearer ${TaoTokenConfig.API_KEY}) .addHeader(Content-Type, application/json) .post(json.toString().toRequestBody(application/json.toMediaType())) .build()注意Authorization是Bearer加 Key中间有空格。路径/v1/chat/completions是常见对话接口路径具体以接入文档为准。如果你用 Retrofit把baseUrl设为https://taotoken.net/api/接口注解写POST(v1/chat/completions)。4. 验证请求从 adb 查询到模型返回的完整链路配置写完必须验证。分两步先验证 Provider 本地能查再验证网络请求能通。4.1 adb 验证 Provider先安装应用然后用 adb 直接查 Provideradb shell content query --uri content://com.example.myprovider/my_table如果返回No result found说明表是空的先插一条adb shell content insert --uri content://com.example.myprovider/my_table \ --bind column1:s:hello --bind column2:s:world再查一次应该能看到类似输出Row: 0 _id1, column1hello, column2world这一步能过说明 Provider 注册、authorities、URI 匹配都没问题。如果报Unknown URI回去检查UriMatcher.addURI的路径和content://后面的部分是否一致。4.2 应用内查询并发送在 Activity 里查询并调用网络val resolver contentResolver val cursor resolver.query(MyContentProvider.CONTENT_URI, null, null, null, null) val sb StringBuilder() cursor?.use { while (it.moveToNext()) { val c1 it.getString(it.getColumnIndexOrThrow(column1)) val c2 it.getString(it.getColumnIndexOrThrow(column2)) sb.append($c1 $c2; ) } } val rawData sb.toString() // 用 3.3 的 request 发送 rawData发送后成功返回的 JSON 结构大致是{ choices: [ { message: { role: assistant, content: 整理后的一句话 } } ] }解析时取choices[0].message.content。如果返回里没有choices先看error字段。4.3 日志排查动作在 OkHttp 加拦截器打印请求和响应val logging HttpLoggingInterceptor().apply { level HttpLoggingInterceptor.Level.BODY }然后看 Logcat过滤你的 TAG。重点看三处请求 URL 是否正确、Authorization 头是否带上、响应体里是choices还是error。Provider 侧则看Cursor是否为空、getColumnIndexOrThrow是否抛异常。5. 本篇常见错排查401、SecurityException 与 choices 缺失这一节按真实报错来对照遇到问题直接查。401 Unauthorized最常见。原因通常是 Key 写错、Bearer后面少了空格、或者 Key 已失效。检查Authorization头的完整值确认是Bearer sk-xxx。如果用的是环境变量或 BuildConfig打印出来确认没有多余引号。local proxy failed / connection refused这类报错通常出现在网络层。先确认设备网络可用再确认 Base URL 是https://taotoken.net/api没有多余路径。如果你在模拟器里配了代理关掉再试。注意不要使用任何非官方的网络中转方式直接用官方 API 入口即可。reading choices 报错 / choices 为 null说明响应体里没有choices字段。先打印完整响应体看是不是返回了error。常见原因是 Model ID 写错或者请求体 JSON 格式不对比如messages不是数组。用JSONObject构造时确认put的层级正确。SecurityException: Permission Denial访问其他应用 Provider 时出现。检查是否声明了对应权限Android 10 以上是否加了queries。如果是自己写的 Provider 被别的应用访问检查exported是否为 true以及是否有权限限制。OAuth 相关报错如果你用的是需要 OAuth 的通道确认 token 是否过期。TaoToken 的 Key 方式不需要额外 OAuth 流程直接 Bearer 即可。如果混用了其他鉴权方式先统一成 Key 方式。Unknown URIProvider 侧报错。检查UriMatcher.addURI的 authority 和 path 是否与请求 URI 完全一致。content://com.example.myprovider/my_table对应addURI(com.example.myprovider, my_table, CODE_DIR)。Cursor 为空但 adb 能查到检查应用是否有读取权限以及查询的 URI 是否和插入时一致。Android 10 之后跨应用查询还要确认目标包可见性。把这几类报错对照一遍基本能覆盖初学阶段 90% 的问题。剩下的看接入文档和 Logcat 完整堆栈。6. 继续往下走把 Provider 和远端能力接起来Provider 跑通之后你可以做几件更有意思的事。第一把 Provider 的 CRUD 封装成一个 Repository让上层不直接碰 Cursor。第二用ContentObserver监听数据变化数据一更新就触发远端请求。第三把模型返回的结构化结果写回 Provider形成一个闭环。如果你要长期做这类编码和 Agent 任务Coding Plan 会更顺手https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite需要新建或管理 Key 时走这里https://taotoken.net/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最后留一个实用技巧把AUTHORITY、BASE_URL、MODEL_ID抽到一个Config对象里改一处全生效。Provider 的authorities和代码里的常量一定要同步改这是最容易漏的地方。调试时先 adb 查本地再发网络请求两段分开验证出问题能快速定位是哪一段。
返回列表