ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

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.documentfile和StorageManagerAPI 构建适配 Android 10Scoped 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.4或8.0若提示Could not resolve com.android.tools.build:gradle需检查gradle/wrapper/gradle-wrapper.properties中distributionUrl是否指向国内镜像例如distributionUrlhttps\://mirrors.cloud.tencent.com/gradle/gradle-8.0-bin.zip提示若项目使用kotlin编写build.gradleModule: app中必须存在apply plugin: kotlin-android且dependencies块含implementation org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version否则编译报Unresolved reference: kotlin。2.2 关键依赖项配置与 Android 版本适配声明源码中app/build.gradle的compileSdk和targetSdk决定其行为边界。常见配置如下配置项典型值作用说明compileSdk34决定可调用的 API 最高版本如StorageManager.getStorageVolume()在 33 才可用targetSdk33 或 34强制启用 Scoped Storage 行为若设为 29 以下getExternalFilesDir()返回路径将暴露真实/sdcard/路径minSdk21支持 Android 5.0但StorageManagerAPI 需 29旧设备需回退逻辑若targetSdk≥ 30必须在AndroidManifest.xml中声明android:requestLegacyExternalStoragefalse显式关闭旧模式并确保所有文件操作走 SAFStorage 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需创建专用 Adapter4.1.1 ViewHolder 绑定逻辑与图标映射class DocumentFileAdapter( private val onItemClicked: (DocumentFile) - Unit, private val onItemLongClicked: (DocumentFile) - Unit ) : RecyclerView.AdapterDocumentFileAdapter.ViewHolder() { private var items mutableListOfDocumentFile() 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: ListDocumentFile) { 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.findViewByIdRecyclerView(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 mutableSetOfDocumentFile() 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 mutableListOfString() 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 权限变更与构建轻量级 APK5.1 应对 Android 14API 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:nameandroid.permission.MANAGE_MEDIA_PERMISSION / !-- 同时保留旧权限以兼容低版本 -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission android:nameandroid.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类被移除。本文还有配套的精品资源点击获取
返回列表