☰
史上最全Android文件管理器技术方案细节:TaoToken统一Key接入与settings.json配置骨架
2026/9/26 11:08:06 网站建设 项目流程

1. Android 文件管理器从零搭建:先把存储访问框架跑通

Android 文件管理器看起来只是「列目录、点文件、复制粘贴」,但真正动手写就会发现,坑集中在三块:存储访问框架(SAF / MediaStore / File API 的边界)、分区存储下的权限适配、以及文件操作后系统媒体库不刷新导致列表「看不见刚操作的文件」。这篇按可跟做的顺序,把聚合分类列表、文件浏览列表、排序、文件操作、收藏夹、搜索、缩略图缓存、zip 解压、分享落盘、权限申请串成一条链路,并在关键节点接入 TaoToken 统一 Key,让「文件管理 + 智能能力」一次跑通。

适合谁:正在做 Android 文件管理器、需要一套能直接抄的配置骨架和验证动作的开发者;也适合想把 AI 能力(比如按内容给文件打标签、自然语言找文件)接进文件管理器的同学。下面所有配置都以可复制为前提,settings.json 骨架、权限声明、验证请求都给出完整片段。

2. 前置准备:TaoToken 统一 Key 与 settings.json 骨架

2.1 为什么文件管理器要接统一 Key

文件管理器本身是本地能力,但一旦你想加「按语义搜索文件」「自动归类截图/文档」「对图片生成描述」这类功能,就需要调用模型。如果每个功能各写一套鉴权、各存一份 Key,维护成本会迅速失控。TaoToken 提供统一 Key 和兼容常见接口协议的入口,把模型调用收敛到一个配置里,文件管理器只关心「传什么、拿什么」。

先到控制台创建 Key:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite

创建后到 API Keys 页面管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite

接口基地址统一用:https://taotoken.net/api(不加 UTM)。

2.2 settings.json 配置骨架

把下面这份骨架放到app/src/main/assets/settings.json,字段含义写在注释里(实际 JSON 不支持注释,落地时删掉注释行即可)。这份骨架同时覆盖「本地文件管理开关」和「模型通道」两部分,避免配置散落。

{ "fileManager": { "showHiddenFiles": false, "defaultSort": "name", "sortAscending": true, "gridColumnsAuto": true, "recycleBinEnabled": true, "recycleBinPath": "/Android/data/<your.package>/files/.recycle" }, "aiChannel": { "baseUrl": "https://taotoken.net/api", "apiKey": "sk-替换成你在控制台创建的Key", "model": "claude-sonnet-4-5", "timeoutMs": 30000, "maxRetries": 2 }, "features": { "semanticSearch": true, "autoTagging": false, "imageCaption": false } }

注意:apiKey 不要硬编码进 Git 仓库。生产环境建议从local.properties或服务端下发,assets 里的这份只用于本地联调。

2.3 读取配置的 Kotlin 封装

object SettingsLoader { private const val TAG = "SettingsLoader" fun load(context: Context): JSONObject { val text = context.assets.open("settings.json") .bufferedReader().use { it.readText() } return JSONObject(text) } fun aiConfig(context: Context): AiConfig { val root = load(context) val ai = root.getJSONObject("aiChannel") return AiConfig( baseUrl = ai.getString("baseUrl"), apiKey = ai.getString("apiKey"), model = ai.getString("model"), timeoutMs = ai.optInt("timeoutMs", 30000) ) } } data class AiConfig( val baseUrl: String, val apiKey: String, val model: String, val timeoutMs: Int )

3. 可复制配置:存储访问框架与权限适配

3.1 聚合分类列表:MediaStore 查询骨架

聚合分类(音乐、视频、图片、文档、压缩包、APK)走 MediaStore,核心是「按类别组装 URI + selection + sortOrder」。下面这份是可直接用的骨架,枚举和查询方法都保留。

enum class FileCategory { Music, Video, Picture, Theme, Doc, Zip, Apk } enum class SortMethod { name, size, date, type } private fun contentUriByCategory(cat: FileCategory): Uri? { val volume = "external" return when (cat) { FileCategory.Theme, FileCategory.Doc, FileCategory.Zip, FileCategory.Apk -> MediaStore.Files.getContentUri(volume) FileCategory.Music -> MediaStore.Audio.Media.getContentUri(volume) FileCategory.Video -> MediaStore.Video.Media.getContentUri(volume) FileCategory.Picture -> MediaStore.Images.Media.getContentUri(volume) } } private fun selectionByCategory(cat: FileCategory): String? = when (cat) { FileCategory.Theme -> "${MediaStore.Files.FileColumns.DATA} LIKE '%.mtz'" FileCategory.Doc -> buildDocSelection() FileCategory.Zip -> "(${MediaStore.Files.FileColumns.MIME_TYPE} == 'application/zip')" FileCategory.Apk -> "${MediaStore.Files.FileColumns.DATA} LIKE '%.apk'" else -> null } private fun sortOrder(sort: SortMethod): String = when (sort) { SortMethod.name -> "${MediaStore.Files.FileColumns.TITLE} asc" SortMethod.size -> "${MediaStore.Files.FileColumns.SIZE} asc" SortMethod.date -> "${MediaStore.Files.FileColumns.DATE_MODIFIED} desc" SortMethod.type -> "${MediaStore.Files.FileColumns.MIME_TYPE} asc, " + "${MediaStore.Files.FileColumns.TITLE} asc" } fun query(context: Context, cat: FileCategory, sort: SortMethod): Cursor? { val uri = contentUriByCategory(cat) ?: return null val columns = arrayOf( MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.SIZE, MediaStore.Files.FileColumns.DATE_MODIFIED ) return context.contentResolver.query( uri, columns, selectionByCategory(cat), null, sortOrder(sort) ) }

拿到 Cursor 后交给自定义 CursorAdapter,调用changeCursor刷新即可。切换排序时重新走一次query,因为排序规则是拼进 SQL 的,不能只对内存数据排序。

3.2 文件浏览列表:File API 遍历

浏览某个目录时用 File API,把File转成自己封装的FileInfo,再交给 ArrayAdapter。

fun listDir(path: String, showHidden: Boolean): List<FileInfo> { val result = ArrayList<FileInfo>() val dir = File(path) val children = dir.listFiles() ?: return result for (child in children) { if (!showHidden && child.isHidden) continue if (!isNormalFile(child.absolutePath)) continue result.add(FileInfo.from(child)) } return result }

3.3 权限声明与动态申请

AndroidManifest 里声明读写权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Android 6.0 之后,读写权限必须在运行时申请,只写 Manifest 会在读写 SD 卡时直接 crash。读写属于同一权限组,申请 READ 即可覆盖 WRITE。

private fun requestReadPermission() { if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { requestPermissions( arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQ_READ ) } }

Android 11 及以上还要考虑分区存储:访问自己的目录用getExternalFilesDir,访问公共媒体用 MediaStore,访问任意目录需要MANAGE_EXTERNAL_STORAGE并引导用户到系统设置页授权。文件管理器类应用通常需要后者,但上架审核会重点看用途说明,务必在隐私政策里写清楚。

4. 验证请求:启动后检查文件列表与 API 通道

4.1 文件列表加载验证

启动应用后,先确认聚合分类能出数据。在onCreate里加一段日志:

val cursor = query(this, FileCategory.Picture, SortMethod.date) Log.d("FileMgr", "picture count = ${cursor?.count ?: -1}") cursor?.close()

如果 count 为 0,先检查权限是否授予、MediaStore 是否已扫描到文件(新下载的文件可能还没入库)。可以手动触发一次媒体扫描:

MediaScannerConnection.scanFile( this, arrayOf(filePath), null, null )

4.2 API 通道连通性验证

用 curl 先验证 Key 和通道是否通,再写进 App:

curl -X POST "https://taotoken.net/api/v1/messages" \ -H "Content-Type: application/json" \ -H "x-api-key: sk-你的Key" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 64, "messages": [{"role": "user", "content": "只回复两个字:连通"}] }'

返回里能看到content字段就说明通道正常。想直接在网页里试模型,可以用模型对话页:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite

4.3 App 内发起一次请求

suspend fun pingAi(config: AiConfig): String = withContext(Dispatchers.IO) { val conn = URL("${config.baseUrl}/v1/messages").openConnection() as HttpURLConnection conn.requestMethod = "POST" conn.setRequestProperty("Content-Type", "application/json") conn.setRequestProperty("x-api-key", config.apiKey) conn.setRequestProperty("anthropic-version", "2023-06-01") conn.connectTimeout = config.timeoutMs conn.doOutput = true val body = JSONObject().apply { put("model", config.model) put("max_tokens", 64) put("messages", JSONArray().put(JSONObject().apply { put("role", "user"); put("content", "只回复两个字:连通") })) } conn.outputStream.use { it.write(body.toString().toByteArray()) } conn.inputStream.bufferedReader().use { it.readText() } }

把返回结果打到 Logcat,看到内容即代表「文件列表 + API 通道」两条链路都通了。

5. 本篇常见错排查

5.1 文件操作后聚合列表不显示

这是最高频的坑。用 File API 重命名、删除、复制后,MediaStore 并不知道变化,聚合分类里就看不到。解决办法是操作完成后主动通知扫描:

MediaScannerConnection.scanFile(context, arrayOf(newPath), null, null)

删除时也要对旧路径通知一次,否则旧记录会残留。

5.2 权限已声明仍 crash

检查是否只在 Manifest 声明、没做运行时申请。另外 Android 11+ 的MANAGE_EXTERNAL_STORAGE不是普通运行时权限,要用Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION跳转授权页。

5.3 缩略图导致 OOM

不要用软引用做缓存,Android 2.3 之后软引用回收时机不可控。用 LruCache,容量取应用最大内存的 1/8:

val maxMemory = Runtime.getRuntime().maxMemory() val cacheSize = (maxMemory / 8).toInt() val lru = object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, value: Bitmap) = value.byteCount }

图片和视频缩略图通过 MediaStore 的_ID去Images.Thumbnails/Video.Thumbnails取,比自己解码快得多。

5.4 zip 解压卡主线程

解压必须放异步任务,doInBackground里遍历ZipFile.entries(),onProgressUpdate更新进度。注意ZipEntry是目录时先mkdirs再继续,否则会写文件失败。

5.5 分享落盘拿不到路径

从Intent.EXTRA_STREAM拿到的 Uri 在分区存储下可能不是真实路径,别直接getPath()当文件路径用。用ContentResolver.openInputStream(uri)读流再写到你自己的目录,这样最稳。

6. 长期编码与 Agent 场景:把配置沉淀下来

文件管理器这类项目迭代周期长,配置项会越来越多。建议把 settings.json 的 schema 固定下来,新增字段走默认值兜底,避免老版本读新配置崩溃。如果你打算长期用模型能力做文件语义搜索、自动归类,甚至让 Agent 帮你改文件管理逻辑,可以了解 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite

接入细节和参数说明统一看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

如果你用的是 Claude Code 这类命令行工具,Anthropic 兼容入口的配置方式在:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite

最后留一个我踩过的坑:settings.json 里的recycleBinPath一定要放在应用私有目录下,放公共目录在分区存储下会因权限失败,而且用户清理数据时容易误删。把回收站做进私有目录,清空回收站时再真正删除,这个顺序能省掉很多「文件莫名消失」的客诉。

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

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

立即咨询