分区存储获取文件

从 Android Jetpack 的 Activity Result API 开始,推荐使用 ActivityResultLauncher 来处理活动结果(如启动文件选择器或拍照等操作)。相比于传统的 startActivityForResult() 方法,ActivityResultLauncher 提供了更简洁、现代化的 API。

以下是使用 ActivityResultLauncherStorage Access Framework (SAF) 获取文件的完整示例代码:

1. 使用 ActivityResultLauncher 启动 SAF 文件选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

private ActivityResultLauncher<Intent> filePickerLauncher;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 初始化 ActivityResultLauncher
filePickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null) {
handleSelectedFile(uri);
}
}
});

// 打开文件选择器
openFilePicker();
}

private void openFilePicker() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); // 支持所有文件类型
filePickerLauncher.launch(intent); // 使用 ActivityResultLauncher 启动文件选择器
}

private void handleSelectedFile(Uri fileUri) {
// 在这里处理选中的文件
System.out.println("Selected file URI: " + fileUri.toString());

try {
getContentResolver().openInputStream(fileUri).use(inputStream -> {
if (inputStream != null) {
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);

// 将字节数组转换为字符串(仅用于调试)
String content = new String(buffer);
System.out.println("File content: " + content);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}

2. 关键点解析

2.1 注册 ActivityResultLauncher

  • 使用 registerForActivityResult() 方法注册一个 ActivityResultLauncher
  • 第一个参数是 ActivityResultContracts.StartActivityForResult(),表示我们希望接收一个活动结果。
  • 第二个参数是回调函数,当用户完成文件选择后会触发该回调。
1
2
3
4
5
filePickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
// 处理返回的文件 URI
});

2.2 启动文件选择器

  • 使用 filePickerLauncher.launch(intent) 启动文件选择器。
  • Intent 配置与传统的 SAF 文件选择器相同,例如设置 MIME 类型和可打开类别。
1
2
3
4
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
filePickerLauncher.launch(intent);

2.3 处理文件内容

  • 通过 ContentResolver.openInputStream() 打开文件输入流并读取内容。
  • 如果需要解压 .tar 或其他格式的文件,可以结合第三方库(如 Apache Commons Compress)进行处理。

3. 运行效果

  1. 用户点击按钮或触发逻辑,启动文件选择器。
  2. 用户从设备中选择一个文件。
  3. 应用通过 ActivityResultLauncher 获取到文件的 Uri,并通过输入流读取其内容。

4. 优势

  • 现代 APIActivityResultLauncher 是 Jetpack 提供的现代化 API,避免了传统 startActivityForResult() 的复杂性。
  • 生命周期安全ActivityResultLauncher 自动绑定到 Activity 或 Fragment 的生命周期,减少了内存泄漏的风险。
  • 简洁易读:代码结构更加清晰,便于维护。

5. 注意事项

  1. MIME 类型:根据需求设置合适的 MIME 类型。如果不确定文件类型,可以使用通配符 */*
  2. 持久化权限:如果需要长期访问用户选择的文件,可以通过 takePersistableUriPermission() 请求持久化权限。
1
2
3
4
if (fileUri != null) {
final int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(fileUri, takeFlags);
}
  1. 大文件处理:对于较大的文件,建议分块读取或直接写入本地存储以避免内存溢出。

6. 总结

使用 ActivityResultLauncher 和 SAF 的组合是现代 Android 开发中推荐的方式,既符合分区存储的要求,又简化了代码逻辑。通过这种方式,你可以轻松实现对用户选择的文件的访问和处理,而无需动态申请传统存储权限(如 READ_EXTERNAL_STORAGE)。

是的,Storage Access Framework (SAF) 的工作原理确实依赖于 Content Provider。SAF 提供了一种标准化的方式来访问设备上的文件和其他数据,而这些数据通常是通过 ContentProvider 来提供的。

SAF 和 Content Provider

  1. Content Provider 概述

    • 在 Android 中,ContentProvider 是一种用于在不同应用之间共享数据的标准接口。它封装了数据存储、检索和更新的操作,并提供了统一的接口来访问这些数据。
    • 例如,MediaStore API 就是基于 ContentProvider 实现的,允许应用查询、插入、更新或删除媒体文件(如图片、音频、视频)。
  2. SAF 如何使用 Content Provider

    • 当你使用 SAF 启动文件选择器(如 Intent.ACTION_OPEN_DOCUMENTIntent.ACTION_CREATE_DOCUMENT),实际上是在请求访问由其他应用(如文件管理器或其他存储服务)提供的内容。
    • 这些内容通常是由特定的 ContentProvider 提供的。一旦用户选择了某个文件,Android 系统会返回一个 Uri,该 Uri 可以用来通过 ContentResolver 访问所选文件的内容。
    • ContentResolver 是 Android 提供的一个类,允许你与任何实现了 ContentProvider 接口的数据源进行交互。你可以使用它来执行各种操作,比如打开输入/输出流、查询元数据等。

具体流程

当你使用 SAF 获取文件时,整个过程大致如下:

  1. 启动文件选择器

    • 使用 Intent.ACTION_OPEN_DOCUMENT 或类似的意图启动文件选择器。
      1
      2
      3
      4
      Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
      intent.addCategory(Intent.CATEGORY_OPENABLE);
      intent.setType("application/pdf"); // 示例:只显示 PDF 文件
      startActivityForResult(intent, REQUEST_CODE); // 使用 ActivityResultLauncher 更推荐
  2. 获取 Uri

    • 用户从文件选择器中选择一个文件后,系统会返回一个 Uri,这个 Uri 指向所选文件的位置。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Override
      public void onActivityResult(int requestCode, int resultCode, Intent data) {
      super.onActivityResult(requestCode, resultCode, data);
      if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
      if (data != null) {
      Uri uri = data.getData();
      handleDocument(uri);
      }
      }
      }
  3. 通过 ContentResolver 访问文件

    • 使用 ContentResolver 打开文件输入流并读取文件内容。
      1
      2
      3
      4
      5
      6
      7
      8
      private void handleDocument(Uri uri) {
      try {
      InputStream inputStream = getContentResolver().openInputStream(uri);
      // 处理输入流...
      } catch (IOException e) {
      e.printStackTrace();
      }
      }

在这个过程中,uri 实际上是一个指向特定 ContentProvider 的标识符,该 ContentProvider 负责提供对所选文件的实际访问。这使得 SAF 成为一种非常灵活且安全的方式,因为它允许应用仅访问用户明确选择的文件,而不是直接访问整个文件系统。

总结

  • SAF 利用了 ContentProvider 来实现文件的选择和访问。
  • 它通过返回一个 Uri 给调用方,允许调用方通过 ContentResolver 与提供该 UriContentProvider 进行交互,从而实现对选定文件的读取、写入等操作。
  • 这种机制不仅提高了安全性,还简化了跨应用的数据共享,同时符合 Android 分区存储的要求。