当前位置:首页 > 文章列表 > 文章 > java教程 > Android Scoped Storage 如何读取指定文件夹

Android Scoped Storage 如何读取指定文件夹

2025-09-09 11:11:28 0浏览 收藏

来到golang学习网的大家,相信都是编程学习爱好者,希望在这里学习文章相关编程知识。下面本篇文章就来带大家聊聊《Android Scoped Storage 如何读取指定文件夹》,介绍一下,希望对大家的知识积累有所帮助,助力实战开发!

如何在 Android Scoped Storage 中读取特定文件夹的文件

本文档旨在指导开发者如何在 Android 的 Scoped Storage 环境下,通过 Storage Access Framework (SAF) 读取外部存储特定文件夹中的文件。Scoped Storage 是 Android 10 (API level 29) 引入的存储机制,旨在提高用户隐私和数据安全。本文将提供详细的代码示例,帮助开发者理解 SAF 的使用方法,并解决在 Scoped Storage 中访问特定目录的问题。

Scoped Storage 简介

Scoped Storage 限制了应用对外部存储的直接访问,应用只能访问自身的特定目录以及用户明确授予访问权限的目录。这提高了用户数据的安全性,防止应用未经授权访问其他应用的数据。

Storage Access Framework (SAF)

SAF 是一种允许用户选择特定文件或目录并授予应用访问权限的机制。通过 SAF,应用可以安全地访问外部存储,而无需请求广泛的存储权限。

实现步骤

以下是通过 SAF 读取特定文件夹的步骤:

  1. 检查 Android 版本: 确保设备运行的是 Android 10 (API level 29) 或更高版本,因为 Scoped Storage 在此版本中强制执行。

  2. 创建 Check 类 (Java): 此类的作用是构建一个 Intent,请求用户选择一个目录。它利用 StorageManager 获取主存储卷,并设置初始 URI 为指定的子目录。

package com.axanor.saf_sample;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.storage.StorageManager;
import android.util.Log;

public class Check {
    Context context;
    private String TAG="SOMNATH";

    public Check(Context context) {
        this.context = context;
    }

    public boolean  ch(){
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
            StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);

            Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
            //String startDir = "Android";
            //String startDir = "Download"; // Not choosable on an Android 11 device
            //String startDir = "DCIM";
            //String startDir = "DCIM/Camera";  // replace "/", "%2F"
            //String startDir = "DCIM%2FCamera";
            String startDir = "Documents";

            Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");

            String scheme = uri.toString();

            Log.d(TAG, "INITIAL_URI scheme: " + scheme);

            scheme = scheme.replace("/root/", "/document/");

            scheme += "%3A" + startDir;

            uri = Uri.parse(scheme);

            intent.putExtra("android.provider.extra.INITIAL_URI", uri);

            Log.d(TAG, "uri: " + uri.toString());

            ((Activity) context).startActivityForResult(intent, 12123);

            return true;
        }
        else{
            return false;
        }
    }
}

代码解释:

  • StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);: 获取 StorageManager 实例。
  • sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();: 创建一个 Intent,用于启动目录选择器。
  • String startDir = "Documents";: 设置初始目录。可以修改为需要访问的特定文件夹名称。
  • uri = Uri.parse(scheme);: 构建包含目标目录的 URI。
  • intent.putExtra("android.provider.extra.INITIAL_URI", uri);: 将 URI 传递给 Intent。
  • ((Activity) context).startActivityForResult(intent, 12123);: 启动目录选择器,并使用 12123 作为请求代码。
  1. 创建 StorageAccess 类 (Kotlin): 此类封装了 SAF 的核心逻辑,包括请求权限、处理目录选择结果、创建文件和读写文件。
package com.axanor.saf_sample

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException

class StorageAccess {
    val activity:Activity;
    val LOGTAG = "SOMNATH"
    val REQUEST_CODE = 12123
    constructor(activity: Activity) {
        this.activity = activity
    }

    public fun openDocumentTree() {
        val check = Check(activity)
        val uriString = SpUtil.getString(SpUtil.FOLDER_URI, "")
        when {
            uriString == "" -> {
                Log.w(LOGTAG, "uri not stored")
                if (!check.ch()){
                    askPermission()
                }
            }
            arePermissionsGranted(uriString) -> {
                makeDoc(Uri.parse(uriString))
            }
            else -> {
                Log.w(LOGTAG, "uri permission not stored")
                if (!check.ch()){
                    askPermission()
                }
            }
        }
    }

    // this will present the user with folder browser to select a folder for our data
    public fun askPermission() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
        activity.startActivityForResult(intent, REQUEST_CODE)
    }

    public fun makeDoc(dirUri: Uri) {
        val dir = DocumentFile.fromTreeUri(activity, dirUri)
        if (dir == null || !dir.exists()) {
            //the folder was probably deleted
            Log.e(LOGTAG, "no Dir")
            //according to Commonsware blog, the number of persisted uri permissions is limited
            //so we should release those we cannot use anymore
            //https://commonsware.com/blog/2020/06/13/count-your-saf-uri-permission-grants.html
            releasePermissions(dirUri)
            //ask user to choose another folder
            Toast.makeText(activity,"Folder deleted, please choose another!", Toast.LENGTH_SHORT).show()
            openDocumentTree()
        } else {
            val file = dir.createFile("*/txt", "test.txt")
            if (file != null && file.canWrite()) {
                Log.d(LOGTAG, "file.uri = ${file.uri.toString()}")
                alterDocument(file.uri)
            } else {
                Log.d(LOGTAG, "no file or cannot write")
                //consider showing some more appropriate error message
                Toast.makeText(activity,"Write error!", Toast.LENGTH_SHORT).show()

            }
        }
    }

    public fun releasePermissions(uri: Uri) {
        val flags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        activity.contentResolver.releasePersistableUriPermission(uri,flags)
        //we should remove this uri from our shared prefs, so we can start over again next time
        SpUtil.storeString(SpUtil.FOLDER_URI, "")
    }

    //Just a test function to write something into a file, from https://developer.android.com
    //Please note, that all file IO MUST be done on a background thread. It is not so in this
    //sample - for the sake of brevity.
    public fun alterDocument(uri: Uri) {
        try {

            activity.contentResolver.openFileDescriptor(uri, "w")?.use { parcelFileDescriptor ->
                FileOutputStream(parcelFileDescriptor.fileDescriptor).use {
                    it.write(
                        ("String written at ${System.currentTimeMillis()}\n")
                            .toByteArray()
                    )
                    Toast.makeText(activity,"File Write OK!", Toast.LENGTH_SHORT).show()

                    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
                    intent.addCategory(Intent.CATEGORY_OPENABLE)
                    intent.type = "application/pdf"

                    // Optionally, specify a URI for the file that should appear in the
                    // system file picker when it loads.
                    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)

                    activity.startActivityForResult(intent, 2)
                }
            }
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    public fun arePermissionsGranted(uriString: String): Boolean {
        // list of all persisted permissions for our app
        val list = activity.contentResolver.persistedUriPermissions
        for (i in list.indices) {
            val persistedUriString = list[i].uri.toString()
            //Log.d(LOGTAG, "comparing $persistedUriString and $uriString")
            if (persistedUriString == uriString && list[i].isWritePermission && list[i].isReadPermission) {
                //Log.d(LOGTAG, "permission ok")
                return true
            }
        }
        return false
    }
}

代码解释:

  • openDocumentTree(): 此方法是入口点。它首先检查是否已经存储了 URI,以及是否已经授予了权限。如果没有,它会调用 askPermission() 或 check.ch()来请求用户选择目录。
  • askPermission(): 启动 ACTION_OPEN_DOCUMENT_TREE Intent,提示用户选择目录。
  • makeDoc(dirUri: Uri): 接收目录 URI,并使用 DocumentFile API 创建或访问目录中的文件。
  • releasePermissions(uri: Uri): 释放对 URI 的持久化权限。
  • alterDocument(uri: Uri): 一个示例函数,用于向文件中写入数据。请注意,实际的文件 I/O 操作应该在后台线程中完成。
  • arePermissionsGranted(uriString: String): 检查是否已经授予了对指定 URI 的读写权限。
  1. 在 Activity 中调用: 在你的 Activity 中,创建 StorageAccess 实例并调用 openDocumentTree() 方法。
StorageAccess access = new StorageAccess(this);
access.openDocumentTree();
  1. 处理 onActivityResult: 在你的 Activity 中重写 onActivityResult 方法,处理目录选择器的结果。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 12123 && resultCode == Activity.RESULT_OK) {
        val uri = data?.data
        if (uri != null) {
            val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            applicationContext.contentResolver.takePersistableUriPermission(uri, flags)
            SpUtil.storeString(SpUtil.FOLDER_URI, uri.toString())
            val storageAccess = StorageAccess(this)
            storageAccess.makeDoc(uri)
        }
    }
}

代码解释:

  • 检查 requestCode 和 resultCode 以确保操作成功。
  • 获取用户选择的目录的 URI。
  • 使用 contentResolver.takePersistableUriPermission() 持久化 URI 权限。这允许应用在重启后仍然可以访问该目录。
  • 将 URI 存储到 SharedPreferences 中,以便下次启动时使用。
  • 调用 storageAccess.makeDoc(uri) 来创建或访问目录中的文件。
  1. SpUtil 工具类: 为了持久化存储选择的目录 URI,需要一个简单的 SharedPreferences 工具类。
package com.axanor.saf_sample;

import android.content.Context;
import android.content.SharedPreferences;

public class SpUtil {

    private static final String PREF_NAME = "my_prefs";
    public static final String FOLDER_URI = "folder_uri";

    public static void storeString(String key, String value) {
        SharedPreferences prefs = App.getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString(key, value);
        editor.apply();
    }

    public static String getString(String key, String defaultValue) {
        SharedPreferences prefs = App.getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
        return prefs.getString(key, defaultValue);
    }

    // 假设你有一个 App 类,可以提供全局的 Context
    public static class App {
        private static Context context;

        public static void setContext(Context c) {
            context = c;
        }

        public static Context getContext() {
            return context;
        }
    }
}

代码解释:

  • storeString(String key, String value): 将字符串值存储到 SharedPreferences 中。
  • getString(String key, String defaultValue): 从 SharedPreferences 中检索字符串值。
  • App 类提供了一个全局的 Context 实例,这对于在工具类中访问资源很有用。 请确保在 Application 类的 onCreate() 方法中初始化 App.context。
  1. AndroidManifest.xml 配置: 确保在 AndroidManifest.xml 文件中声明了所需的权限。
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

注意: MANAGE_EXTERNAL_STORAGE 权限通常需要经过 Google Play 审核,因此请谨慎使用,并确保你的应用确实需要访问所有文件。 如果没有特殊需求,请尽量避免使用此权限,而选择 SAF。

注意事项

  • 用户体验: 在使用 SAF 时,请确保提供清晰的用户提示,告知用户为什么需要访问特定目录,并指导用户完成选择过程。
  • 异常处理: 在进行文件 I/O 操作时,务必进行适当的异常处理,以避免应用崩溃。
  • 后台线程: 所有文件 I/O 操作都应该在后台线程中执行,以避免阻塞主线程,导致应用无响应。
  • 权限管理: 及时释放不再需要的 URI 权限,避免占用过多的资源。
  • 目录选择: 用户可以选择任何目录,因此请确保你的应用能够处理各种情况,包括用户选择了根目录或其他不相关的目录。
  • 版本兼容性: 在 Android 10 之前,可以使用传统的存储权限模型。请根据设备的 Android 版本选择合适的存储访问方式。
  • 文件类型过滤: 在调用 ACTION_OPEN_DOCUMENT intent时,可以使用 intent.setType("application/pdf") 来过滤文件类型,方便用户选择。

总结

通过使用 Storage Access Framework (SAF),开发者可以在 Android Scoped Storage 环境下安全地访问外部存储的特定文件夹。本文档提供了详细的代码示例和步骤,帮助开发者理解 SAF 的使用方法,并解决在 Scoped Storage 中访问特定目录的问题。 请记住,提供良好的用户体验、进行适当的异常处理、并在后台线程中执行文件 I/O 操作是至关重要的。

文中关于的知识介绍,希望对你的学习有所帮助!若是受益匪浅,那就动动鼠标收藏这篇《Android Scoped Storage 如何读取指定文件夹》文章吧,也可关注golang学习网公众号了解相关技术文章。

Snakemake动态参数引用方法详解Snakemake动态参数引用方法详解
上一篇
Snakemake动态参数引用方法详解
事务处理如何使用?保障数据一致性技巧
下一篇
事务处理如何使用?保障数据一致性技巧
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    514次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    499次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • SEO  AI Mermaid 流程图:自然语言生成,文本驱动可视化创作
    AI Mermaid流程图
    SEO AI Mermaid 流程图工具:基于 Mermaid 语法,AI 辅助,自然语言生成流程图,提升可视化创作效率,适用于开发者、产品经理、教育工作者。
    24次使用
  • iTerms:一站式法律AI工作台,智能合同审查起草与法律问答专家
    iTerms
    iTerms是一款专业的一站式法律AI工作台,提供AI合同审查、AI合同起草及AI法律问答服务。通过智能问答、深度思考与联网检索,助您高效检索法律法规与司法判例,告别传统模板,实现合同一键起草与在线编辑,大幅提升法律事务处理效率。
    30次使用
  • 迅捷AIPPT:AI智能PPT生成器,高效制作专业演示文稿
    迅捷AIPPT
    迅捷AIPPT是一款高效AI智能PPT生成软件,一键智能生成精美演示文稿。内置海量专业模板、多样风格,支持自定义大纲,助您轻松制作高质量PPT,大幅节省时间。
    18次使用
  • 酷宣AI:智能文章生成器,高颜值图文排版与多平台发布神器
    酷宣AI
    酷宣AI是一款专注于高颜值文章快速生成的智能工具。它能根据主题或文字智能排版,实现图文高清整合,并支持一键同步至微信公众号、导出PDF,大幅提升内容创作效率与美观度。
    16次使用
  • 花瓣网:创意灵感与正版素材平台,助力设计师高效创作
    花瓣网
    花瓣网是中国领先的创意灵感与版权素材平台,提供海量正版素材、设计工具和灵感发现引擎,服务设计师、企业用户及创意从业者,助力高效创作。
    21次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码