安卓开发新姿势:文件Picker全攻略,无痛适配不再难

15 阅读16分钟

安卓开发新姿势:文件Picker全攻略,无痛适配不再难

传统文件读取痛点大揭秘

在过去的安卓开发中,当应用需要读取文件信息时,常规的做法是申请存储权限或所有文件访问权限 ,只有在用户主动授权后,应用才能够读取手机存储里的文件。这种传统方案虽然在一定程度上满足了应用对文件访问的需求,但也逐渐暴露出了诸多痛点,对应用的使用体验和用户数据安全带来了挑战。

权限申请范围过大

大多数应用其实并不需要读取手机上的所有文件数据 。以一个简单的文本编辑应用为例,它可能仅仅需要读取用户指定的文本文件,对图片、音频、视频等其他类型的文件并没有需求。但按照传统的权限申请方式,一旦应用申请并获得了存储权限或所有文件访问权限,它实际上就具备了读取存储空间上所有文件数据的能力。这无疑导致了权限的过度授予,增加了用户数据泄露的风险,就好比给一个只需要打开特定房间的人一把能打开整栋大楼所有房间的钥匙。

用户拒绝授权导致功能不可用

如今,用户的隐私意识日益增强,当应用弹出权限申请弹窗时,出于对个人隐私的保护,用户可能会拒绝授权。一旦用户拒绝,应用中依赖文件读取的相关功能就无法正常使用 。比如一个文件分享应用,如果用户拒绝授予存储权限,那么就无法选择本地文件进行分享,这直接影响了应用的核心功能,使得用户体验大打折扣,甚至可能导致用户放弃使用该应用。

引导手动授权影响体验

更糟糕的是,如果用户多次拒绝授权,应用为了保证功能的正常运行,就需要引导用户跳转到设置中手动授权。这个过程对于普通用户来说并不简单,很多用户可能会觉得繁琐和困惑。而且,频繁地引导用户进行手动授权操作,不仅打断了用户使用应用的流畅性,还容易让用户产生厌烦情绪,进一步降低了应用的用户粘性 。

文件 Picker 闪亮登场

(一)它是什么

为了解决传统文件读取方式的痛点,文件 Picker 应运而生。文件 Picker 是一种创新的文件选择解决方案,它允许应用通过接入系统级的文件选择界面 ,当用户需要在应用中进行文件相关操作时,比如上传文件、选择图片进行编辑等,应用就会弹出这个文件 Picker 选择界面。这个界面是系统级别的,具有统一的风格和交互方式,用户可以在其中手动浏览手机存储中的文件 ,然后选择他们需要授权给三方应用读取的文件。

(二)有何神奇之处

  1. 告别权限烦恼:文件 Picker 最大的优势之一就是应用不再需要申请完整的存储权限 。这意味着应用无需再为了获取部分文件的访问权而申请对整个存储空间的广泛权限,大大降低了权限申请的风险和用户对隐私泄露的担忧。即使用户之前拒绝过应用的存储权限申请,也不会影响应用通过文件 Picker 获取用户指定文件的能力,有效解决了因用户拒绝授权导致应用功能不可用的问题 。

  2. 最小化授权保障隐私:应用通过文件 Picker 只能读取用户选择的文件数据 ,这完美地满足了用户最小化授权的要求。用户可以精确控制哪些文件被应用访问,而不用担心应用会越权访问其他无关文件,极大地保护了用户的隐私安全。 例如,在一个文档编辑应用中,用户使用文件 Picker 选择了一个特定的文档进行编辑,应用就只能读取这个被选中的文档内容,对手机中其他的文档、图片、音频等文件毫无访问权限。

适用范围早知道

不是所有的安卓系统都能享受文件 Picker 带来的便捷体验哦。目前,支持最新视觉效果文件 Picker 的操作系统版本主要有以下这些:

  • ColorOS 16(基于 Android 16)及以上:OPPO 手机搭载的 ColorOS 系统在其 16 版本及后续版本中,基于 Android 16 的底层支持,为用户带来了文件 Picker 功能 。这意味着 OPPO 手机用户如果将系统升级到 ColorOS 16 及以上,在使用支持文件 Picker 的应用时,就能体验到更安全、便捷的文件选择方式。比如在 OPPO Find X7 手机上,升级到 ColorOS 16 后,使用办公应用选择文档时,就可以通过文件 Picker 精准选择所需文件,而不用担心应用越权访问其他文件。

  • HyperOS 3(基于 Android 16)及以上:小米手机的 HyperOS 系统从 3 版本开始,基于 Android 16,也适配了文件 Picker 。这使得小米手机用户在使用相关应用时能够更加灵活地选择文件,保障隐私安全。以小米 13 系列手机为例,当系统更新到 HyperOS 3 后,在文件分享应用中,通过文件 Picker 可以轻松选择要分享的文件,无需授予应用过多的存储权限。

  • OriginOS 6(基于 Android 16)及以上:vivo 手机的 OriginOS 系统在 6 版本及以上,基于 Android 16 实现了对文件 Picker 的支持 。用户在使用搭载 OriginOS 6 及以上系统的 vivo 手机时,在各类应用中涉及文件选择的操作都能通过文件 Picker 来完成。例如 vivo X90 系列手机,运行 OriginOS 6 后,在图片编辑应用中选择图片素材时,文件 Picker 可以快速定位并选择用户需要的图片,操作更加高效。

  • MagicOS 10(基于 Android 16)及以上:荣耀手机的 MagicOS 系统从 10 版本起,基于 Android 16 支持了文件 Picker 。这为荣耀手机用户在应用使用过程中的文件选择提供了更优的解决方案。在荣耀 Magic 5 手机上,当系统升级到 MagicOS 10 后,在备份应用中选择要备份的文件时,文件 Picker 的便捷性就得以体现,用户可以精准选择,避免不必要的文件被访问。

接口使用说明书

(一)打开文件 Intent 参数构造

在使用文件 Picker 时,首先需要构造 Intent 来拉起文件 Picker 界面 ,这其中涉及到多个关键参数。

  1. action 参数:action 参数是构造 Intent 时必不可少的 。它就像是一把钥匙,用于告诉系统我们要执行的操作是打开文件选择界面。其取值固定为 Intent.ACTION_OPEN_DOCUMENT ,即 “android.intent.action.OPEN_DOCUMENT” 。在代码中,我们可以这样使用:

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

通过这行代码,我们创建了一个 Intent 对象,并为其设置了 action 参数,明确了要打开文件选择器的意图。 2. type 参数:type 参数用于指定文件的数据类型 ,这也是必须指定的参数。它决定了文件 Picker 界面中展示的文件类型范围。取值范围是各种文件类型字符串,比如 “text/plain” 代表纯文本文件 ,“application/pdf” 代表 PDF 文件 ,“/” 则代表所有文件类型。例如,当我们只想让用户选择纯文本文件时,可以这样设置:


intent.setType("text/plain");

这样,文件 Picker 界面就只会展示纯文本文件供用户选择。 3. EXTRA_MIME_TYPES 参数:这是一个可选参数 ,它的作用是配合 type 参数,进一步限制用户只能选择特定类型的文件。通过传入一个 MIME 类型数组,我们可以精确控制用户可选择的文件类型。name 参数固定为 Intent.EXTRA_MIME_TYPES ,表示限制用户选择的文件类型;filterFileType 参数是一个字符串数组,里面包含具体的文件类型。比如,我们希望用户只能选择图片和 PDF 文件,可以这样写:


intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{
        "image/*",
        "application/pdf"
});

这里先将 type 设置为 “/”,表示所有类型,然后通过 EXTRA_MIME_TYPES 参数限制为图片和 PDF 文件,这样在文件 Picker 界面中,用户就只能看到这两种类型的文件。 4. EXTRA_ALLOW_MULTIPLE 参数:这个参数用于决定文件选择是否支持多选 。如果不传入该参数,系统会默认为单选模式。当 name 参数为 Intent.EXTRA_ALLOW_MULTIPLE 时,value 参数取值为 true 表示支持多选,取值为 false 表示单选。文件 Picker 多选模式下最大支持选择 100 个文件。假设我们要开启多选功能,可以这样实现:


intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);

通过这行代码,用户在文件 Picker 界面中就可以选择多个文件了。

(二)Intent 返回值解读

当用户在文件 Picker 界面完成文件选择操作后,系统会通过 Intent 返回相关结果 ,我们需要对这些返回值进行正确解读。

  1. requestCode:在 onActivityResult 回调中,requestCode 的值与我们传入 startActivityForResult 的 requestCode 是一致的 。它就像是一个身份标识,用于关联回调的结果与对应的请求。比如,我们在启动文件 Picker 时传入了 requestCode 为 100:

startActivityForResult(intent, 100);

那么在 onActivityResult 回调中,通过判断 requestCode 是否为 100,就可以确定这个回调结果是对应哪个文件选择请求的,方便我们进行后续的处理逻辑。 2. resultCode:resultCode 用于表示操作的结果状态 。它的取值范围主要有两种:Activity.RESULT_OK 表示用户成功完成了文件选择操作 ,比如用户在文件 Picker 中选择了文件并点击确定;Activity.RESULT_CANCELED 表示用户取消了操作 ,比如用户点击了文件 Picker 界面的取消按钮。在代码中,我们可以通过以下方式判断:


if (resultCode == Activity.RESULT_OK) {
    // 处理用户选择文件后的逻辑
} else if (resultCode == Activity.RESULT_CANCELED) {
    // 处理用户取消操作后的逻辑
}
  1. data:data 参数是一个 Intent 对象 ,通过它的 getData () 方法可以拿到文件的 Uri。这个 Uri 就像是文件在系统中的地址,拿到它之后,我们就可以通过 ContentResolver.openInputStream 方法来读取文件数据了。例如:

Uri uri = data.getData();
try (InputStream inputStream = getContentResolver().openInputStream(uri)) {
    if (inputStream != null) {
        byte[] bytes = inputStream.readAllBytes();
        // 处理读取到的文件数据
    }
} catch (IOException e) {
    // 处理读取文件失败的情况
}

这样,我们就能够顺利获取并处理用户选择的文件内容。

(三)持久化授权

在某些场景下,我们希望应用在设备重启和应用重新启动后,仍然能够保留对用户所选文档的访问权限 ,这时候就需要用到持久化授权。takePersistableUriPermission 是 Android ContentResolver 类中的一个方法 ,专门用于获取针对给定 URI 的长期持久权限授予。比如,当我们通过 ACTION_OPEN_DOCUMENT 选择了特定文件后,就可以使用这个方法来实现持久化授权。在 onActivityResult 方法中,我们可以这样使用:


// 在onActivityResult中
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);  
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);

通过这几行代码,我们从 intent 的 flags 中获取读写权限标志,并调用 takePersistableUriPermission 方法,将 uri 和权限标志传入,从而实现对所选文件的持久化授权,确保应用在后续操作中能够持续访问该文件。

(四)androidX 库接口

为了让开发者更方便地使用文件 Picker 功能,androidX 库提供了封装好的接口 。

  • 打开单个文件:可以使用developer.android.com/reference/k… 这个接口。它对打开单个文件的操作进行了封装,简化了开发流程,开发者无需再手动处理复杂的 Intent 构造和结果回调逻辑。

  • 打开多个文件developer.android.com/reference/k… 这个接口则专门用于打开多个文件 。通过它,开发者可以轻松实现文件 Picker 的多选功能,并且能够方便地处理用户选择的多个文件的相关操作。 如果开发者想要深入了解这些接口的详细实现代码,可以访问cs.android.com/androidx/pl… ,这里面包含了丰富的代码细节,有助于开发者更好地理解和使用这些接口,提升开发效率和代码质量。

实战演练:使用示例详解

(一)兼容性适配技巧

  1. 移除权限配置:在支持文件 Picker 的系统版本中,应用在文件读取方面迎来了重大变革。以前,我们需要在 AndroidManifest.xml 文件中申请读取存储相关权限,像这样:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

但现在,随着文件 Picker 的出现,应用不再需要这些权限来实现文件选择功能。我们可以直接将这些权限申请代码移除,就像下面这样注释掉:


<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />-->
<!--<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />-->

当然,如果你不想完全移除,也可以选择在 AndroidManifest.xml 中配置读取存储相关权限最高可用的 Android 版本 。比如,如果你希望应用最高支持在 Android 15(SDK 版本号 35)申请存储相关权限 ,可以这样配置:


<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="35" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:maxSdkVersion="35" />

这样,在高于 Android 15 的系统版本中,应用就不会再申请这些存储权限,而是通过文件 Picker 来实现文件选择,有效提升了应用的安全性和隐私合规性。 2. 判断系统支持:在实际开发中,我们需要判断当前系统是否支持文件 Picker ,以确保应用的兼容性。以下是判断系统级文件 Picker 是否可用的参考代码:


public boolean isFilePickerAvailable(Intent intent) {
    return getPackageManager().resolveActivity(intent, 0) != null;
}

这段代码通过 PackageManager 的 resolveActivity 方法来判断是否存在能够处理该 Intent 的 Activity ,如果存在,就说明系统支持文件 Picker 。基于 Android 16 及以上的国内机型,通常都具备厂商优化后的新 UI 风格,文件 Picker 的体验会更好 。

在不支持文件 Picker 的操作系统中,我们需要继续申请读取文件相关权限 。以下是兼容代码示例:


Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
if (isFilePickerAvailable(intent)) {    
    // 支持,拉起文件Picker
    startActivityForResult(intent, REQUEST_CODE);
} else {
    // 不支持,申请权限等方式
    // 这里可以添加申请权限的逻辑,比如使用ActivityCompat.requestPermissions方法申请权限
}

通过这段代码,我们可以根据系统是否支持文件 Picker 来灵活选择文件读取方式,保证应用在不同系统环境下都能正常运行。

(二)打开文件代码实操

  1. Java 用法示例

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
import java.io.InputStream;

public class MainActivity extends AppCompatActivity {

    private static final String LOG_TAG = "PickerDemo";

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

        Button pickFileButton = findViewById(R.id.pick_file_button);
        pickFileButton.setOnClickListener(v -> pickOpenFile());
    }

    public void pickOpenFile() {
        // 构造intent,设置action
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("text/plain");
        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"text/plain"});
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); // 是否支持多选,true支持,false不支持

        openActivityResultLauncher.launch(intent);
    }

    ActivityResultLauncher<Intent> openActivityResultLauncher = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            new ActivityResultCallback<ActivityResult>() {
                @Override
                public void onActivityResult(ActivityResult result) {
                    if (result.getResultCode() == RESULT_OK) {
                        Intent data = result.getData();
                        Uri uri = data.getData();
                        if (uri != null) { // 单选
                            try (InputStream inputStream = getContentResolver().openInputStream(uri)) {
                                if (inputStream != null) {
                                    byte[] bytes = inputStream.readAllBytes();
                                    Log.d(LOG_TAG, "Selected URI: " + uri + ", read success:" + new String(bytes));
                                }
                            } catch (IOException e) {
                                Log.d(LOG_TAG, "Selected URI: " + uri + ", read failed");
                            }
                        } else { // 多选
                            android.content.ClipData clipData = data.getClipData();
                            if (clipData != null) {
                                for (int i = 0; i < clipData.getItemCount(); i++) {
                                    Log.d(LOG_TAG, "Selected URI is " + clipData.getItemAt(i).getUri());
                                }
                            }
                        }
                    }
                }
            });
}

在这段 Java 代码中,首先在 onCreate 方法中为按钮设置点击监听器,点击按钮时调用 pickOpenFile 方法 。在 pickOpenFile 方法中,构造了一个 Intent 对象,设置了 action、category、type、EXTRA_MIME_TYPES 和 EXTRA_ALLOW_MULTIPLE 等参数 ,然后通过 openActivityResultLauncher.launch (intent) 启动文件 Picker 。在 openActivityResultLauncher 的回调中,根据 resultCode 判断操作结果是否成功 ,如果成功,再根据是否是多选模式来处理返回的文件 Uri,读取文件内容或获取多个文件的 Uri。 2. Kotlin 用法示例


import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import java.io.IOException
import java.io.InputStream

class MainActivity : AppCompatActivity() {

    private val LOG_TAG = "PickerDemo"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val pickFileButton: Button = findViewById(R.id.pick_file_button)
        pickFileButton.setOnClickListener { pickOpenFile() }
    }

    fun pickOpenFile() {
        // 构造intent,设置action
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        intent.setType("text/plain")
        intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/plain"))
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) // 是否支持多选,true支持,false不支持

        openActivityResultLauncher.launch(intent)
    }

    private val openActivityResultLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()) { result ->
        if (result.resultCode == RESULT_OK) {
            val data: Intent? = result.data
            if (data != null) {
                val uri = data.data
                try {
                    if (uri != null) { // 单选
                        contentResolver.openInputStream(uri).use { inputStream ->
                            if (inputStream != null) {
                                val bytes: ByteArray = inputStream.readAllBytes()
                                Log.d(LOG_TAG, "Selected URI: " + uri + ", read success:" + String(bytes))
                            }
                        }
                    } else { // 多选
                        val clipData = data.clipData
                        if (clipData != null) {
                            for (i in 0 until clipData.itemCount) {
                                Log.d(LOG_TAG, "Selected URI is " + clipData.getItemAt(i).uri)
                            }
                        }
                    }
                } catch (e: IOException) {
                    Log.e(LOG_TAG, "Selected URI: $uri, read failed")
                }
            }
        }
    }
}

Kotlin 版本的代码逻辑与 Java 类似 。在 onCreate 方法中为按钮设置点击事件,点击时调用 pickOpenFile 方法 。在 pickOpenFile 方法中构建 Intent 并设置相关参数 ,通过 openActivityResultLauncher 启动文件 Picker 。在 openActivityResultLauncher 的回调中,处理文件 Picker 返回的结果,根据单选或多选模式进行不同的文件处理操作,读取文件内容或获取多个文件的 Uri,并在日志中记录相关信息 。

参考资料助你一臂之力

在安卓开发的文件 Picker 之旅中,为了让你更深入地学习和掌握相关知识,这里为你准备了丰富的参考资料:

  • 使用存储访问框架打开文件developer.android.com/guide/topic… ,这是官方提供的关于存储访问框架的详细指南,里面包含了文件 Picker 相关的底层原理、操作规范以及各种使用场景的示例代码,无论是初学者还是有经验的开发者,都能从中获取到有价值的信息,进一步理解文件 Picker 在安卓系统中的工作机制和应用方式。

  • 常用筛选的文件类型mime.wcode.net/zh-hans/ ,这个链接汇总了各种常用的文件类型及其对应的 MIME 类型 。在使用文件 Picker 时,我们经常需要根据不同的业务需求来筛选文件类型,这个网站就像是一个文件类型的百科全书,为我们提供了全面且准确的文件类型参考,方便我们在代码中正确设置 type 和 EXTRA_MIME_TYPES 参数,实现精准的文件筛选功能。

  • 金标联盟官网文档地址www.itgsa.com/doc/6631953… ,金标联盟官网中公示的适配指南与本文的适配指南一致 。在这里,你可以获取到更权威、更全面的文件 Picker 适配信息,包括不同厂商对文件 Picker 的优化细节、最新的技术动态以及行业标准等。它就像是一个行业知识库,能帮助你紧跟技术发展趋势,确保你的应用在各种安卓系统版本和机型上都能完美适配文件 Picker 功能 。