Storage Access Framework

Android 4.4引入的Storage Access Framework (SAF)允许应用通过DocumentProvider与云或本地存储服务交互。SAF包括Document Provider、Client App和Picker,支持ACTION_OPEN_DOCUMENT和ACTION_CREATE_DOCUMENT操作,提供统一的文件访问接口。客户端不直接与Provider交互,而是通过系统Picker筛选并返回文件URI,从而实现文件的读取、创建和删除。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android API Guide

Android 4.4 引入了存储访问框架Storage Access Framework(SAF),云或本地存储服务可以实现DocumentProvider来分装他们的服务以便加入到该生态系统中。

SAF包含了
Document Provider, Android 包含几个内置的DocumentsProvider,注入下载Downloads, Images, Videos.
Client App: 一个自定义的App,可发送ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENT,接收返回的文件。
Picker: 一个系统UI, 允许用户访问所有document providers 满足client app的搜索条件的。

Overview

架构图
这里写图片描述

  • 以Root开始,指向单一节点,然后改单一节点再扇形展开。每个Root都有一个独一无二的COLUMN_ROOT_ID, 并且指向一个document或者directory, 代表了该Root下的内容。
  • 在每个root下是一个single document, 该document指向1 to N个documents, 这下面的每个再指向1 to N个documents。
  • 每个文件或者目录 都有COLUMN_DOCUMENT_ID, 该ID必须是独一无二的,且在issue后不可修改的。
  • documents 可以是可打开的文件,也可是目录。
  • 每个document 可以具有不同的能力,由COLUMN_FLAGS指定。例如:FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE, and FLAG_SUPPORTS_THUMBNAIL. 此外,COLUMN_DOCUMENT_ID可以出现在多个目录下。

Control Flow

document provider 的数据模型是基于传统的文件架构。但是,物理上,你可以随意存储,只要可以被DocumentProvider API访问。

下图显示了一个Photo App如何使用SAF访问图片
一个Photo App如何使用SAF访问图片

注意到:
1. Client and Provider 不直接交互。
2. 当客户端发起一个Intent(ACTION_OPEN_DOCUMENT or ACTION_CREATE_DOCUMENT), 可能包括一些filters
3. 一旦Intent发出, the system picker 就会到每个注册的provider,并显示匹配结果。
4. Picker提供一个标准的接口访问文件。

Writing a Client App

在Android 4.3之前,使用ACTION_PICK或者ACTION_GET_CONTENT 获得文件,对于后者是仅copy一份文件。
在Android 4.4及更高版本,将可使用ACTION_OPEN_DOCUMENT来获取文件,是直接使用源文件。

Search for documents

发起搜索图片请求

private static final int READ_REQUEST_CODE = 42;
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
public void performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones)
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only images, using the image MIME data type.
    // If one wanted to search for ogg vorbis files, the type would be 
<think>我们使用Storage Access Framework (SAF)来让用户选择文件。但是,SAF通常不允许直接打开一个特定的目录(因为不同的DocumentProvider可能对目录结构有不同的表示)。不过,我们可以通过指定一个URI(代表一个目录)来打开该目录。这个URI可以通过之前的用户选择(例如,通过Intent.ACTION_OPEN_DOCUMENT_TREE选择一个目录)获得并保存下来,然后再次使用它来打开同一个目录。 但是,用户的问题是想要打开一个特定的目录(如`MIUI/sound_recorder/call_rec`)来选择文件。由于SAF的设计是抽象的,我们无法直接通过路径字符串来打开。不过,我们可以尝试以下两种方法: 方法1:使用`Intent.ACTION_OPEN_DOCUMENT_TREE`让用户先选择整个目录(比如`MIUI/sound_recorder/call_rec`),然后保存返回的URI。之后,我们可以使用这个URI来打开该目录进行文件选择。 方法2:如果我们已经知道一个目录的URI(例如,通过之前的选择保存的),我们可以使用`Intent.ACTION_OPEN_DOCUMENT`并设置`EXTRA_INITIAL_URI`来指向这个目录,这样文件选择器就会从这个目录开始。 然而,注意:`EXTRA_INITIAL_URI`是在Android 5.0(API 21)中添加的,所以我们需要确保设备版本>=21。 步骤: 1. 首先,我们需要用户选择目录(如果之前没有保存过该目录的URI): ```java Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY); ``` 2. 在`onActivityResult`中,我们获取到目录的URI,并保存(例如使用SharedPreferences): ```java if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == RESULT_OK) { Uri treeUri = data.getData(); // 保存这个URI,例如使用SharedPreferences getSharedPreferences("SAF", MODE_PRIVATE).edit() .putString("call_rec_uri", treeUri.toString()).apply(); } ``` 3. 当我们需要让用户从这个目录中选择文件时,我们使用`Intent.ACTION_OPEN_DOCUMENT`,并设置初始URI为之前保存的目录URI: ```java String uriString = getSharedPreferences("SAF", MODE_PRIVATE).getString("call_rec_uri", null); if (uriString != null) { Uri treeUri = Uri.parse(uriString); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); // 可以根据需要设置MIME类型,比如音频"audio/*" // 设置初始目录(仅适用于API 21+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, treeUri); } startActivityForResult(intent, REQUEST_CODE_OPEN_FILE); } ``` 4. 在`onActivityResult`中处理选择的文件(REQUEST_CODE_OPEN_FILE): ```java if (requestCode == REQUEST_CODE_OPEN_FILE && resultCode == RESULT_OK) { Uri uri = data.getData(); // 使用这个URI来读取文件 } ``` 注意:由于SAF的抽象性,我们无法保证所有DocumentProvider都支持`EXTRA_INITIAL_URI`,但系统自带的DocumentsUI是支持的。 另外,我们也可以尝试直接使用路径来构造一个URI,但是这是不推荐的,因为不同的设备可能存储方案不同。不过,对于外部存储的根目录下的路径,我们可以尝试使用`DocumentsContract.buildDocumentUriUsingTree`和`DocumentsContract.buildChildDocumentsUriUsingTree`等方法来构建,但这通常需要知道该目录的document id,而document id是DocumentProvider内部使用的,我们无法直接通过路径获取。 因此,最可靠的方式是让用户先选择一次目录,然后保存该URI。 但是,如果我们的应用有MANAGE_EXTERNAL_STORAGE权限(在Android 11+),我们可以直接访问文件路径,而不需要使用SAF。不过,这个权限需要用户手动在设置中开启,且Google Play对它的使用有严格限制。 所以,总结:使用SAF时,我们无法直接通过路径字符串打开特定目录,但可以通过保存用户之前选择的目录URI,然后在下次打开文件选择器时设置初始目录。 参考文档:https://developer.android.com/reference/android/content/Intent#ACTION_OPEN_DOCUMENT 根据引用[1]和[2]的内容,Storage Access Framework 提供了访问文档的机制,通过Intent和内置的UI来选择文件。而引用[3]提到,存储访问框架最终操作的是数据库,所以我们不能直接使用文件路径。 因此,我们采用上述方法。 代码示例: 1. 请求用户选择目录(只需要一次): ```java private void requestDirectoryAccess() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY); } ``` 2. 处理返回的目录URI并保存。 3. 当需要选择文件时,使用保存的URI创建文件选择Intent。 完整示例: ```java public class MainActivity extends AppCompatActivity { private static final int REQUEST_CODE_OPEN_DIRECTORY = 1; private static final int REQUEST_CODE_OPEN_FILE = 2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 检查是否已经保存了目录URI String uriString = getSharedPreferences("SAF", MODE_PRIVATE).getString("call_rec_uri", null); if (uriString == null) { // 没有保存,请求用户选择目录 requestDirectoryAccess(); } else { // 已经保存,可以开始选择文件 openFileInDirectory(Uri.parse(uriString)); } } private void requestDirectoryAccess() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY); } private void openFileInDirectory(Uri directoryUri) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, directoryUri); } startActivityForResult(intent, REQUEST_CODE_OPEN_FILE); } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == RESULT_OK) { if (data != null) { Uri treeUri = data.getData(); // 保存URI getSharedPreferences("SAF", MODE_PRIVATE).edit() .putString("call_rec_uri", treeUri.toString()).apply(); // 现在可以打开这个目录下的文件选择 openFileInDirectory(treeUri); } } else if (requestCode == REQUEST_CODE_OPEN_FILE && resultCode == RESULT_OK) { if (data != null) { Uri uri = data.getData(); // 处理选中的文件 handleSelectedFile(uri); } } } private void handleSelectedFile(Uri uri) { // 读取文件内容或获取文件信息 // 注意:需要使用ContentResolver来打开输入流 try { InputStream in = getContentResolver().openInputStream(uri); // 处理输入流... } catch (FileNotFoundException e) { e.printStackTrace(); } } } ``` 注意:保存的URI在设备重启后可能仍然有效,但无法保证所有DocumentProvider都支持持久化URI。因此,我们可能需要持久化权限: 在获取到目录的URI后,我们可以调用`getContentResolver().takePersistableUriPermission`来获取持久化权限,这样即使重启应用,我们也可以使用这个URI。 修改`onActivityResult`中保存目录URI的部分: ```java if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == RESULT_OK) { if (data != null) { Uri treeUri = data.getData(); // 获取持久化权限 final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().takePersistableUriPermission(treeUri, takeFlags); // 保存URI getSharedPreferences("SAF", MODE_PRIVATE).edit() .putString("call_rec_uri", treeUri.toString()).apply(); openFileInDirectory(treeUri); } } ``` 这样,即使应用重启,我们也可以使用这个URI。 总结:使用SAF打开指定目录选择文件,需要先让用户选择该目录(一次),保存返回的URI,然后利用这个URI作为初始URI来打开文件选择器。 相关问题: 1. 如何让用户选择目录并保存访问权限? 2. 如何使用SAF读取文件内容? 3. SAF的持久化URI权限在应用升级后是否仍然有效?</think>### 使用 Storage Access Framework (SAF) 打开指定目录选择文件 在 Android 中,Storage Access Framework (SAF) 提供了一种标准化的方式来访问文件系统。虽然 SAF 不允许直接通过路径字符串打开目录,但可以通过以下两种方法实现打开指定目录的功能: --- #### 方法一:使用初始 URI 引导到指定目录(推荐) ```java // 步骤1:定义目录URI(需要先获取该目录的URI) Uri targetDirUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AMIUI%2Fsound_recorder%2Fcall_rec"); // 步骤2:创建文件选择Intent Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); // 选择所有文件类型 // 步骤3:设置初始目录(Android 5.0+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, targetDirUri); } // 步骤4:启动文件选择器 startActivityForResult(intent, REQUEST_CODE_FILE_PICK); ``` --- #### 方法二:通过目录选择器获取持久权限 ```java // 步骤1:启动目录选择器 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_DIR_PICK); // 步骤2:在onActivityResult中处理结果 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_DIR_PICK && resultCode == RESULT_OK) { Uri treeUri = data.getData(); // 获取持久访问权限 final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION); getContentResolver().takePersistableUriPermission(treeUri, takeFlags); // 保存URI供后续使用 PreferenceManager.getDefaultSharedPreferences(this) .edit() .putString("SAF_URI", treeUri.toString()) .apply(); } } // 步骤3:使用保存的URI打开目录 void openSavedDirectory() { String uriString = PreferenceManager.getDefaultSharedPreferences(this) .getString("SAF_URI", null); if (uriString != null) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(uriString)); startActivityForResult(intent, REQUEST_CODE_FILE_PICK); } } ``` --- ### 关键注意事项 1. **URI 格式说明**: - 目录 URI 格式:`content://com.android.externalstorage.documents/tree/primary%3A[路径]` - 示例:`MIUI/sound_recorder/call_rec` 需要转换为 `primary%3AMIUI%2Fsound_recorder%2Fcall_rec` - 路径中的特殊字符需使用 URL 编码(如 `:` → `%3A`, `/` → `%2F`) 2. **权限处理**: ```xml <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> ``` - Android 11+ 需要添加以下权限声明: ```xml <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> ``` 3. **文件操作**: ```java // 通过URI读取文件内容 try (InputStream in = getContentResolver().openInputStream(fileUri)) { // 处理文件流 } catch (IOException e) { e.printStackTrace(); } ``` 4. **MIUI 特殊适配**: - 在 MIUI 上需要额外引导用户授权: ```java if (!Environment.isExternalStorageManager()) { Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); startActivity(intent); } ``` --- ### 工作原理示意图 ```mermaid graph TD A[启动SAF Intent] --> B{系统文件选择器} B --> C[用户选择目录] C --> D[返回URI] D --> E[获取持久权限] E --> F[保存URI] F --> G[后续通过URI直接访问] ``` > **提示**:首次使用时需要用户交互选择目录,后续即可直接使用保存的 URI 访问指定目录[^1][^2]。实际路径可能因设备厂商定制存在差异,建议在代码中添加备用路径处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值