news 2026/9/11 8:46:13

Android文件管理器源码:适配Scoped Storage的SAF工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android文件管理器源码:适配Scoped Storage的SAF工程实践

简介:本资源是一份面向Android开发初学者与进阶者的文件管理器完整源码工程,聚焦文件系统操作、UI交互与权限适配等核心实践能力培养。压缩包共66个文件,含33个编译后class文件、15张界面截图PNG、5个XML布局与配置文件、5个Java业务逻辑文件,以及APK安装包、Dex字节码、项目配置(.project/.classpath)和说明文档(txt),整体仅296KB,轻量易读,结构清晰体现典型Android Studio工程组织方式。已有333人学习下载,适合通过真实项目理解Activity生命周期、RecyclerView动态列表、运行时存储权限申请、SQLite本地元数据管理及Intent文件打开/分享等关键知识点。源码中包含目录树遍历、文件类型筛选排序、异步加载优化等实用设计,辅以截图与说明文档,便于快速上手分析与二次开发。

1. 这不是“下载即用”的 APK,而是一份可调试、可定制、可嵌入的 Android 文件管理器工程源码

当你在搜索引擎输入“Android 文件管理器源码.zip”,真正想找的往往不是某个现成 App 的安装包,而是能在 Android Studio 中打开、编译、断点调试、修改权限逻辑、替换 UI 组件、甚至集成进自有系统应用的完整工程。这类源码通常基于androidx.documentfileStorageManagerAPI 构建,适配 Android 10(Scoped Storage)及更高版本,而非停留在READ_EXTERNAL_STORAGE粗粒度授权的老模式。它面向的是需要深度控制文件访问路径的场景:比如企业级设备管控系统中限制用户仅能浏览指定目录;车载信息娱乐系统中隔离媒体与配置文件;或教育类 App 中实现沙箱化作业提交与反馈。如果你正卡在FileProviderURI 解析失败、DocumentFile.fromSingleUri()返回 null、或ACTION_OPEN_DOCUMENT_TREE后无法持久化访问权限——这份源码的价值,恰恰在于它已把getUriForFile()takePersistableUriPermission()createDocument()等关键链路封装为可复现、可打断点的 Java/Kotlin 模块。新手能照着跑通基础浏览,5 年以上开发者则会重点关注其StorageAccessFrameworkHelper类对不同 Android 版本权限降级的兼容策略。

2. 从 ZIP 解压到 Android Studio 可编译:环境配置与模块依赖解析

2.1 解压后目录结构识别与核心模块定位

解压Android 文件管理器源码.zip后,典型目录包含app/(主模块)、lib-storage/(抽象存储层)、ui-common/(自定义 RecyclerView Adapter 与 ItemDecoration)和gradle.properties不要直接双击build.gradle打开——这会导致 Android Studio 以单文件模式加载,缺失 Gradle Wrapper 配置。正确做法是:在 Android Studio 中选择Open → 选中解压后的根目录 → 勾选 “Use gradle wrapper”。首次同步时,Gradle 会自动下载对应版本(常见为7.48.0),若提示Could not resolve com.android.tools.build:gradle,需检查gradle/wrapper/gradle-wrapper.propertiesdistributionUrl是否指向国内镜像,例如:

distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.0-bin.zip

提示:若项目使用kotlin编写,build.gradle(Module: app)中必须存在apply plugin: 'kotlin-android'dependencies块含implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version",否则编译报Unresolved reference: kotlin

2.2 关键依赖项配置与 Android 版本适配声明

源码中app/build.gradlecompileSdktargetSdk决定其行为边界。常见配置如下:

配置项典型值作用说明
compileSdk34决定可调用的 API 最高版本,如StorageManager.getStorageVolume()在 33+ 才可用
targetSdk33 或 34强制启用 Scoped Storage 行为,若设为 29 以下,getExternalFilesDir()返回路径将暴露真实/sdcard/路径
minSdk21支持 Android 5.0,但StorageManagerAPI 需 29+,旧设备需回退逻辑

targetSdk≥ 30,必须在AndroidManifest.xml中声明android:requestLegacyExternalStorage="false"(显式关闭旧模式),并确保所有文件操作走 SAF(Storage Access Framework)。例如读取外部存储根目录的代码不能写:

// ❌ 错误:targetSdk≥30 时此路径不可访问 File root = Environment.getExternalStorageDirectory();

而应改为:

// ✅ 正确:通过 SAF 获取持久化访问权限 val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY)

2.3 Gradle 同步失败的三大高频原因与修复命令

当 Android Studio 显示 “Failed to resolve” 或 “Plugin with id ‘kotlin-android’ not found”,按顺序执行以下终端命令(在项目根目录下):

# 1. 清理 Gradle 缓存(解决依赖冲突) ./gradlew clean --refresh-dependencies # 2. 强制重新下载依赖(跳过本地缓存) ./gradlew build --no-daemon --refresh-dependencies # 3. 若仍失败,重置 Gradle 用户家目录(Windows 替换 %USERPROFILE%\.gradle) rm -rf ~/.gradle/caches/

注意:./gradlew是 Linux/macOS 命令,Windows 用户用gradlew.bat。执行前确认gradle/wrapper/gradle-wrapper.jar存在且未损坏——若 ZIP 解压时出现乱码或文件截断,需重新下载源码包。

3. 核心功能落地:实现可持久化访问的目录浏览与文件操作链路

3.1 使用 ACTION_OPEN_DOCUMENT_TREE 获取根目录 URI 并持久化权限

文件管理器启动时需让用户手动选择一个可访问的目录(如Android/data/com.your.app/或 SD 卡根目录)。关键步骤如下:

3.1.1 发起目录选择 Intent 并处理返回结果

在 Activity 中:

private fun openDocumentTree() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY) } override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == Activity.RESULT_OK) { resultData?.data?.let { treeUri -> // 持久化权限:即使 App 重启也能访问该目录 contentResolver.takePersistableUriPermission( treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) // 保存 URI 到 SharedPreferences(供后续使用) saveRootUri(treeUri) } } }
3.1.2 通过 DocumentFile 构建可操作的目录树

获取treeUri后,用DocumentFile.fromTreeUri()创建根节点:

val rootDocumentFile = DocumentFile.fromTreeUri(this, treeUri) // 列出子目录与文件(注意:此操作需在后台线程,避免阻塞 UI) Thread { val children = rootDocumentFile.listFiles() runOnUiThread { updateRecyclerView(children) } }.start()

提示:listFiles()返回的DocumentFile[]对象已封装了canRead()canWrite()isDirectory()等方法,无需再解析原始 URI 字符串。若listFiles()返回空数组,大概率是treeUri权限未持久化或用户未授予写入权限(需检查Intent.FLAG_GRANT_WRITE_URI_PERMISSION是否传入)。

3.2 文件复制与重命名:绕过 Scoped Storage 限制的 SAF 操作

在 Scoped Storage 下,直接File.copy()会失败。必须通过ContentResolver.openInputStream()openOutputStream()流式操作:

3.2.1 复制文件到目标 DocumentFile 目录
fun copyFileToDirectory(sourceFile: File, targetDir: DocumentFile): Boolean { return try { val inputStream = FileInputStream(sourceFile) // 在目标目录创建新文件 val newFile = targetDir.createFile("text/plain", sourceFile.name) val outputStream = contentResolver.openOutputStream(newFile.uri) inputStream.copyTo(outputStream!!) inputStream.close() outputStream.close() true } catch (e: Exception) { Log.e("FileCopy", "Failed to copy ${sourceFile.name}", e) false } }
3.2.2 重命名 DocumentFile 的标准流程

DocumentFile.renameTo()仅在 Android 11+ 支持,且需目标目录有写权限:

// ✅ 安全重命名(兼容 Android 10+) if (targetFile.renameTo("new_name.txt")) { Log.d("Rename", "Success") } else { // ❌ fallback:复制新文件 + 删除旧文件(需两次权限校验) val newFile = targetDir.createFile(targetFile.type, "new_name.txt") copyFileToDocumentFile(targetFile, newFile) targetFile.delete() }

3.3 权限状态实时校验:避免 “Permission Denied” 运行时异常

用户可能手动撤销权限,需在每次操作前校验:

fun hasUriPermission(uri: Uri, modeFlags: Int): Boolean { val permissionFlags = contentResolver?.takePersistableUriPermission(uri, modeFlags) return permissionFlags != null && (permissionFlags and modeFlags) == modeFlags } // 使用示例:操作前检查 if (!hasUriPermission(rootUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)) { Toast.makeText(this, "权限已失效,请重新选择目录", Toast.LENGTH_LONG).show() openDocumentTree() return }

4. UI 层深度定制:替换默认列表样式与实现多选批量操作

4.1 自定义 RecyclerView Adapter 以支持 DocumentFile 数据绑定

原生ArrayAdapter无法直接绑定DocumentFile,需创建专用 Adapter:

4.1.1 ViewHolder 绑定逻辑与图标映射
class DocumentFileAdapter( private val onItemClicked: (DocumentFile) -> Unit, private val onItemLongClicked: (DocumentFile) -> Unit ) : RecyclerView.Adapter<DocumentFileAdapter.ViewHolder>() { private var items = mutableListOf<DocumentFile>() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_document_file, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val file = items[position] holder.bind(file) holder.itemView.setOnClickListener { onItemClicked(file) } holder.itemView.setOnLongClickListener { onItemLongClicked(file) true } } override fun getItemCount() = items.size fun updateItems(newItems: List<DocumentFile>) { items.clear() items.addAll(newItems) notifyDataSetChanged() } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val fileName: TextView = itemView.findViewById(R.id.tv_file_name) private val fileType: TextView = itemView.findViewById(R.id.tv_file_type) private val fileIcon: ImageView = itemView.findViewById(R.id.iv_file_icon) fun bind(file: DocumentFile) { fileName.text = file.name fileType.text = if (file.isDirectory) "文件夹" else "文件" fileIcon.setImageResource( if (file.isDirectory) R.drawable.ic_folder else R.drawable.ic_file ) } } }
4.1.2 在 Fragment 中初始化并设置数据源
class FileListFragment : Fragment() { private lateinit var adapter: DocumentFileAdapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val view = inflater.inflate(R.layout.fragment_file_list, container, false) val recyclerView = view.findViewById<RecyclerView>(R.id.rv_file_list) adapter = DocumentFileAdapter( onItemClicked = { file -> navigateToFile(file) }, onItemLongClicked = { file -> startMultiSelect(file) } ) recyclerView.adapter = adapter recyclerView.layoutManager = LinearLayoutManager(context) return view } private fun loadDirectoryContents(uri: Uri) { val root = DocumentFile.fromTreeUri(requireContext(), uri) adapter.updateItems(root.listFiles().toList()) } }

4.2 实现多选模式与批量删除/移动操作

4.2.1 状态管理与 ActionMode 集成
private var selectedFiles = mutableSetOf<DocumentFile>() private var actionMode: ActionMode? = null private fun startMultiSelect(firstFile: DocumentFile) { selectedFiles.add(firstFile) if (actionMode == null) { actionMode = activity?.startActionMode(actionModeCallback) } updateActionModeTitle() } private val actionModeCallback = object : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { mode?.menuInflater?.inflate(R.menu.menu_multi_select, menu) return true } override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { return when (item?.itemId) { R.id.action_delete -> { deleteSelectedFiles() mode?.finish() true } else -> false } } override fun onDestroyActionMode(mode: ActionMode?) { selectedFiles.clear() actionMode = null } } private fun updateActionModeTitle() { actionMode?.title = "${selectedFiles.size} 项已选" }
4.2.2 批量删除的安全实现(需逐个调用 delete())
private fun deleteSelectedFiles() { Thread { val failedList = mutableListOf<String>() for (file in selectedFiles) { try { if (!file.delete()) { failedList.add(file.name ?: "未知文件") } } catch (e: SecurityException) { failedList.add("${file.name}:权限不足") } } requireActivity().runOnUiThread { if (failedList.isEmpty()) { Toast.makeText(context, "已删除 ${selectedFiles.size} 项", Toast.LENGTH_SHORT).show() } else { Toast.makeText(context, "部分文件删除失败:${failedList.joinToString(", ")}", Toast.LENGTH_LONG).show() } selectedFiles.clear() } }.start() }

5. 进阶技巧:适配 Android 14 权限变更与构建轻量级 APK

5.1 应对 Android 14(API 34)新增的 MANAGE_MEDIA_PERMISSION

从 Android 14 开始,访问媒体文件(照片、视频、音频)需单独申请MANAGE_MEDIA_PERMISSION,即使已获得READ_MEDIA_IMAGES。源码中若涉及媒体文件浏览,必须补充:

5.1.1 动态申请媒体管理权限
private fun requestMediaManagementPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (ContextCompat.checkSelfPermission( this, Manifest.permission.MANAGE_MEDIA_PERMISSION ) != PackageManager.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.MANAGE_MEDIA_PERMISSION), REQUEST_CODE_MANAGE_MEDIA ) } } }
5.1.2 在 AndroidManifest.xml 中声明(Android 14 必须)
<uses-permission android:name="android.permission.MANAGE_MEDIA_PERMISSION" /> <!-- 同时保留旧权限以兼容低版本 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

注意:MANAGE_MEDIA_PERMISSION是普通权限(normal permission),无需运行时弹窗,但必须在 manifest 中声明,否则MediaStore查询会返回空结果。

5.2 构建最小化 APK:移除无用资源与启用 R8 全面压缩

为降低 APK 体积(尤其当集成进系统固件时),在app/build.gradle中配置:

android { buildTypes { release { // 启用 R8 代码压缩与资源压缩 minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' // 移除未使用的语言资源(保留中文) resConfigs "zh-rCN" // 移除未使用的屏幕密度资源(仅保留通用 mdpi) ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' } } } }
5.2.1 ProGuard 规则防止 DocumentFile 类被混淆

proguard-rules.pro中添加:

# DocumentFile 及相关 SAF 类禁止混淆 -keep class androidx.documentfile.provider.** { *; } -keep class android.provider.DocumentFile { *; } -keep class android.system.Os { *; }
5.2.2 验证构建产物体积与功能完整性

执行构建后检查:

# 查看 APK 内部结构(确认无冗余 assets/drawable-xxhdpi) unzip -l app/build/outputs/apk/release/app-release.apk | grep -E "(res/|assets/)" # 静态扫描是否含敏感日志(避免 release 版本输出 Log.d) grep -r "Log\.d\|Log\.i" app/src/main/ --include="*.java" --include="*.kt"

app-release.apk体积超过 8MB,需检查libs/目录是否误打入第三方 SDK;若DocumentFile.fromTreeUri()在 release 包中返回 null,大概率是 ProGuard 规则缺失导致DocumentFile类被移除。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 8:44:20

Hoops SDK 2026.1:3D工程应用开发工具包的技术解析与应用

1. Hoops SDK 2026.1的核心定位与技术架构 Hoops SDK作为3D工程应用领域的专业开发工具包&#xff0c;其2026.1版本延续了Tech Soft 3D公司一贯的技术优势。这套SDK最显著的特点是其专为工程领域优化的可视化管线&#xff0c;能够高效处理大型装配体和复杂曲面模型。与通用3D引…

作者头像 李华
网站建设 2026/9/11 8:41:53

STM32F103 AB分区OTA实战:UART固件升级与安全回滚

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 8:39:31

Umi + Mako 下 Unocss 构建后样式丢失?3 个检查点快速定位

Umi Mako 下 Unocss 构建后样式丢失&#xff1f;3 个检查点快速定位 【免费下载链接】umi A framework in react community ✨ 项目地址: https://gitcode.com/GitHub_Trending/um/umi 本地 umi dev 里原子类全部生效&#xff0c;umi build 之后页面直接裸奔——Umi M…

作者头像 李华
网站建设 2026/9/11 8:37:52

Spring Boot端口占用问题全面解决方案

1. 项目概述 作为一名Java开发者&#xff0c;Spring Boot项目启动时遇到端口占用报错几乎是每个人都会踩的坑。最常见的就是那个让人头疼的提示&#xff1a;"Port 8080 was already in use"或者"Port 8081 was already in use"。这个问题看似简单&#xf…

作者头像 李华
网站建设 2026/9/11 8:36:58

阿里开源Agent项目AgentScope实战:从单智能体到多Agent协作

最近阿里开源了一个Agent项目&#xff0c;朋友圈里直接刷屏了。作为一个常年折腾大模型应用的人&#xff0c;我第一反应是&#xff1a;这又是啥新轮子&#xff1f;结果花了一个周末&#xff0c;从读文档到上手跑通多Agent协作&#xff0c;再回头看这个项目的设计&#xff0c;确…

作者头像 李华