☰
Android MediaStore 实战:从查询到写入的完整示例与 TaoToken 配置
2026/10/7 15:02:38 网站建设 项目流程

1. 为什么 MediaStore 查询总在真机上翻车

Android 的 MediaStore 是系统给所有 App 提供的一个「公共媒体数据库」,相册里的图片、视频、录音、下载文件,本质上都是通过它来索引的。它能做什么?一句话:让你不用自己遍历/sdcard,就能拿到系统已经扫描好的媒体条目,并且通过ContentResolver做增删改查。适合谁?做相册、文件管理、图片编辑、短视频剪辑、上传头像这类需要读写用户媒体文件的 Android 开发者。

但很多人第一次写 MediaStore 都会遇到同一个场景:在模拟器上跑得好好的,一上真机就返回空 Cursor,或者插入图片后相册里死活不显示。我试过最典型的一次,query返回的cursor.getCount()是 0,日志干干净净,没有任何异常。问题不在代码语法,而在三个地方:权限没申请全、DATA字段在 Android 10 之后被限制、插入时漏了IS_PENDING标记。

这篇就按「查询 → 写入 → 验证 → 排错」的完整链路走一遍,代码可以直接复制。同时我会把 AI 辅助生成 MediaStore 代码的环节接进来,用 TaoToken 的统一 Key 调模型,让它在你不确定某个字段名或某个版本行为时,快速给出可用的片段,而不是去翻半天文档。整个流程我会给出可复制的配置和验证命令,确保你能跑通。

需要先明确一个版本分界线,这决定了你后面所有代码的写法:

Android 版本关键行为影响
Android 9 及以下DATA字段可用,直接拿绝对路径老代码能跑
Android 10(API 29)引入分区存储,DATA被标记废弃查询仍可读,写入受限
Android 11+(API 30)强制分区存储,DATA基本不可依赖必须用Uri+ 流

所以下面所有查询代码,我都会用_ID拼ContentUris的方式拿 Uri,而不是依赖DATA。这是能跨版本稳定跑的关键。

2. TaoToken 前置:拿到统一 Key 并配好调用入口

在写代码之前,先把 AI 辅助这一环配好。TaoToken 的作用是给你一个统一的 API Key 和 Base URL,让你用同一套凭证去调不同的模型,省得每个模型单独申请。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。

第一步,进控制台创建 Key。打开 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,登录后在 API Keys 页面新建一个,复制出来。这个 Key 后面既用于命令行验证,也用于你在 IDE 插件里配置。

第二步,如果你只是想先验证模型能不能通,用模型对话页面最快: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。在这里选一个模型,直接发一句「用 Kotlin 写一个 MediaStore 查询图片的示例」,看返回是否正常。这一步能排除 Key 本身的问题。

第三步,如果你打算长期用 AI 辅助写 Android 代码,建议直接上 Coding Plan: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它适合把模型接进编辑器做补全和对话,MediaStore 这种字段多、版本差异大的 API,有补全会省很多查文档的时间。

第四步,如果你用的是 Claude Code 这类命令行 Agent,接入文档在这里: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL 和鉴权头的写法。API Key 管理页统一在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。

这里要提醒一句:TaoToken 是给你提供模型调用入口的,不是替代 Android Studio 的编辑器,也不是让你把生产数据库直连上去。它的定位是「统一 Key + 统一入口」,你该写的 MediaStore 代码还是得自己写、自己测。

配好之后,你可以用一条 curl 验证 Key 是否可用,把$TAOTOKEN_KEY换成你自己的:

curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-20250514", "messages": [ {"role": "user", "content": "用一句话说明 Android MediaStore 的 IS_PENDING 字段作用"} ] }'

返回里能看到choices[0].message.content就说明通了。这一步过了,后面让 AI 帮你生成 MediaStore 片段才有意义。

3. 可复制配置:权限清单与查询写入代码

这一节是核心,全部代码可直接粘贴。先看权限配置,AndroidManifest.xml里按版本声明:

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

注意READ_MEDIA_*是 Android 13(API 33)引入的,13 以下用READ_EXTERNAL_STORAGE。WRITE_EXTERNAL_STORAGE在 Android 10 之后对媒体库写入其实不再必需,但为了兼容 9 及以下保留,并用maxSdkVersion限制。

运行时申请用ActivityResultContracts,别再用旧的onRequestPermissionsResult:

private val permissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { result -> val allGranted = result.values.all { it } if (allGranted) { queryImages() } else { Log.w("MediaStore", "权限未全部授予: $result") } } private fun requestMediaPermission() { val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { arrayOf( Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO ) } else { arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) } permissionLauncher.launch(permissions) }

查询图片,用_ID拼 Uri,不依赖DATA:

private fun queryImages() { val projection = arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.DATE_ADDED ) val selection = "${MediaStore.Images.Media.MIME_TYPE} LIKE ?" val selectionArgs = arrayOf("image/%") val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, sortOrder )?.use { cursor -> val idCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val nameCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME) val sizeCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE) while (cursor.moveToNext()) { val id = cursor.getLong(idCol) val uri = ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id ) Log.d("MediaStore", "name=${cursor.getString(nameCol)} uri=$uri size=${cursor.getLong(sizeCol)}") } } }

写入一张图片到媒体库,关键是IS_PENDING:先插入占位,写完数据再置 0,否则相册可能不刷新:

private fun insertImage(bitmap: Bitmap, displayName: String) { val values = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, displayName) put(MediaStore.Images.Media.MIME_TYPE, "image/png") put(MediaStore.Images.Media.IS_PENDING, 1) } val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI val uri = contentResolver.insert(collection, values) ?: return contentResolver.openOutputStream(uri)?.use { out -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) } values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) contentResolver.update(uri, values, null, null) Log.d("MediaStore", "插入完成: $uri") }

如果你用 AI 辅助生成,可以把上面这段贴给模型,让它帮你改成查询视频或音频。用 TaoToken 的 Coding Plan 时,配置里三件套要写全:Base URL 填https://taotoken.net/api,Key 填你创建的,Model ID 填你选的模型名。缺任何一个都会报鉴权或模型不存在。

4. 验证请求:跑通查询与写入并确认结果

代码写完,怎么确认真的跑通了?分两步验证。

第一步,查询验证。在queryImages里打日志,运行后看 Logcat 过滤MediaStore标签。正常情况下你会看到类似:

D/MediaStore: name=IMG_20240101_120000.jpg uri=content://media/external/images/media/100001 size=2456789

如果cursor.getCount()是 0,先别怀疑代码,去系统相册确认设备里确实有图片,再检查权限是否真的授予了。可以在申请回调里打印result,看是不是被拒了。

第二步,写入验证。调用insertImage后,打开系统相册,看新图片是否出现。如果没出现,检查IS_PENDING是否被置回 0。这一步是新手最容易漏的,插入了但没更新IS_PENDING,系统会认为这个条目还在写入中,不对外展示。

如果你想用 AI 帮你验证字段名对不对,可以在模型对话页面发一句:「Android MediaStore.Images.Media 里表示文件显示名的字段是哪个?」正常会返回DISPLAY_NAME。如果返回的是TITLE或DATA,说明模型版本较旧,换一个模型再问。这一步能帮你快速确认 API 细节,不用去翻 AOSP 源码。

再补一个视频查询的验证,逻辑和图片一致,只是换MediaStore.Video.Media:

val videoUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI val projection = arrayOf( MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.DURATION ) contentResolver.query(videoUri, projection, null, null, null)?.use { c -> while (c.moveToNext()) { val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.Video.Media._ID)) val uri = ContentUris.withAppendedId(videoUri, id) Log.d("MediaStore", "video uri=$uri duration=${c.getLong(c.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION))}") } }

跑通这两个,说明你的查询和写入链路是通的。接下来就是排错环节。

5. 本篇常见错排查:401、Cursor 为空、OAuth 报错

这一节对照真实报错来。先说 AI 调用侧的,再说 MediaStore 侧的。

AI 调用侧最常见的三个:

401 Unauthorized,一般是 Key 没带对或过期。检查Authorization: Bearer $TAOTOKEN_KEY里的 Key 是否完整,有没有多余空格。如果是在 IDE 插件里配的,确认 Base URL 填的是https://taotoken.net/api,不是首页地址。

local proxy failed,通常是本地网络或代理配置问题。先确认你的请求能直连到taotoken.net,用 curl 单独测一次。如果 curl 通、插件不通,那就是插件里的 Base URL 或端口写错了。

reading choices报错,一般是返回体不是预期的 JSON 结构,可能是模型名写错导致返回了错误信息。检查 Model ID 是否和你在模型对话页面选的一致。

如果你用 Claude Code 或 Codex 这类工具,配置里三件套必须齐全:Base URL、Key、Model ID。以 Codex 的auth.json为例,结构大致是:

{ "base_url": "https://taotoken.net/api", "api_key": "你的Key", "model": "claude-sonnet-4-20250514" }

少任何一项都会报鉴权失败或模型不存在。OAuth 相关报错通常出现在用账号登录而非 Key 的场景,如果你走的是 Key 模式,一般不会碰到;碰到了就回到 API Keys 页面重新生成一个 Key 再试。

MediaStore 侧最常见的两个:

Cursor 返回空。按顺序排查:权限是否授予(打印result)、设备里是否真有媒体文件、selection条件是否写得太严(比如 MIME 过滤写错)。Android 13 上如果只申请了READ_MEDIA_IMAGES却去查视频,也会返回空。

插入后相册不显示。九成是IS_PENDING没置回 0,或者MIME_TYPE和实际写入的数据不匹配。比如你声明image/png却写入了 JPEG 数据,系统可能拒绝索引。

还有一个隐蔽的坑:在 Android 10 上用DATA字段拿路径,然后直接File读写,会抛FileNotFoundException或权限异常。解决办法就是全程用Uri+openInputStream/openOutputStream,不要碰绝对路径。

6. 把 AI 辅助接进你的日常 MediaStore 开发流

跑通上面的示例后,你可以把 AI 辅助固定成两个动作。第一个动作是「字段确认」:当你记不清某个 Media 字段名时,直接问模型,比翻文档快。第二个动作是「版本适配」:把一段 Android 9 的老代码贴给模型,让它改成兼容 Android 13 的写法,重点检查DATA和权限声明。

长期做 Android 媒体相关开发的话,建议用 Coding Plan 把模型接进编辑器,这样写ContentValues、projection数组时能直接补全,减少字段拼写错误。入口还是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。如果你只是想临时验证某个 API 行为,用模型对话页面就够: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。

最后留一个我踩过的坑:MediaStore 的DATE_ADDED单位是秒,不是毫秒,排序时别拿它和System.currentTimeMillis()直接比。这个细节模型有时候也会答错,所以生成完代码,关键字段还是自己扫一眼。

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

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

立即咨询