DocumentFile 与 DocumentsProvider 到底怎样用?

前言

Android 的文件操作真的是太难了,跟着版其他迭代,权限的收紧,给了开发者过渡时期,然后再次收紧权限。搞得开发者都不知道怎样操作文件了。

本文的内容并不涉及到多媒体文件挑选,都是以文件的概念来讲的,多媒体文件的操作经过 MediaStore 操作即可,并不杂乱,就不在本文的讨论范围了。

关于文件操作权限变化的大节点是 Android10,参与了分区存储的特性

什么是分区存储?

为了让用户更好地办理自己的文件并削减紊乱,以 Android 10(API 等级 29)及更高版别为方针渠道的运用在默许状况下被赋予了对外部存储空间的分区拜访权限(即分区存储)。 此类运用只能拜访外部存储空间上的运用专属目录,以及本运用所创立的特定类型的媒体文件,不能拜访其他运用的外部存储空间。

为了开发者过渡,能够挑选让运用不适配分区存储:

以 Android 9(API 等级 28)或更低版别为方针渠道。 假如您以 Android 10(API 等级 29)或更高版别为方针渠道,请在运用的清单文件中将 requestLegacyExternalStorage 的值设置为 true 当您将运用更新为以 Android 11(API 等级 30)为方针渠道后,假如运用在搭载 Android 11 的设备上运转,体系会疏忽 requestLegacyExternalStorage 特点。

除了分区存储之外关于操作文件概念还有SAF,额,不是startActivityForResult,是(Storage Access Framework)存储拜访结构:

什么是存储拜访结构 (SAF)?

Android 4.4(API 等级 19)引入了存储拜访结构 (SAF)。

借助 SAF,用户可轻松阅读和翻开各种文档、图片及其他文件,而不必管这些文件来自其首选文档存储供给程序中的哪一个。

用户可经过易用的规范界面,跨一切运用和供给程序以一致的办法阅读文件并拜访最近用过的文件。

云存储服务或本地存储服务可结束用于封装其服务的 DocumentsProvider,从而参与此生态体系。客户端运用如需拜访供给程序中的文档,只需几行代码即可与 SAF 集成。

关于以上几点知识点,我总结了几个疑问,咱们能够带着疑问往下看:

  1. 经过 File 能不能把文件存入到SD卡?能存到哪些文件夹?每一种文件都能存吗?有没有版别约束?
  2. DocumentFiler 怎样运用?有没有版别约束?
  3. 怎样经过 DocumentFile 存入文件呢?和 File 存文件有什么差异?
  4. 清晰的知道一个文件的途径,能不能直接经过 File 或 DocumentFile 读取到?
  5. 相对罕见的 DocumentsProvider 是怎样运用的?
  6. 有了 DocumentsProvider 为什么还要 FileProfider?他们之间有什么异同?

话不多说,Let’s go

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

一、File 与 DocumentFile

File 与 DocumentFile 的查找解说如下:

File 是 Java 中的一个类,用于表明操作体系中的文件或目录。DocumentFile 是 Android 中的一个类,用于表明存储拜访结构(SAF)中的文件或目录。 DocumentFile 可所以根据 File 的,也可所以根据另一种笼统称为 DocumentProvider 的。DocumentFile 的长处是能够拜访更多的存储位置,例如云端、SD 卡等。

File 能够获取文件的绝对途径和称号,而 DocumentFile 只能获取 Uri 和显现称号。File 的性能比 DocumentFile 高,因为 DocumentFile 需求经过ContentResolver 查询数据库来获取文件信息。

1.1 一、File操作文件

到底是什么意思咱们运用一个比方来解说,读取 assets 目录的文件,直接经过 IO 流的办法写入到 SD 卡。然后测验是否需求权限?能写入到哪些文件夹?有没有版别约束?(本项目根据Tartget31,而且不运用 requestLegacyExternalStorage 适配)

代码如下:

        val downLoadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
        val isDirectory = parent.isDirectory
        val canRead = parent.canRead()
        val canWrite = parent.canWrite()
        YYLogUtils.w("${android.os.Build.VERSION.RELEASE} isDirectory:$isDirectory canRead:$canRead canWrite:$canWrite path:$downLoadPath")
        //获取文件流写入文件到File
        val newFile = File(parent.absolutePath + "/资料清单PDF.pdf")
        if (!newFile.exists()) {
             newFile.createNewFile()
        }
        val inputStream = assets.open("资料清单PDF.pdf")
        val inBuffer = inputStream.source().buffer()
        newFile.sink(true).buffer().use {
            it.writeAll(inBuffer)
            inBuffer.close()
        }

很简单的代码,至于怎样运用 IO 流写入文件到 SD 卡?额…这不是本文的重点,办法很多,这儿不翻开介绍。

接下来这儿我以三台设备为比方,分别为Androd7、Android9、Android 12。咱们分别检查 Log 状况试试,

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

额,如同三个手机都不行额,什么问题?SD卡权限未请求的啦,不管是写入到SD卡哪个目录,仍是要外置卡权限的!

 extRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
     val downLoadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
     ...
    newFile.sink(true).buffer().use {
        it.writeAll(inBuffer)
        inBuffer.close()
    }
    ...
 }

好了,加上权限请求代码之后咱们再看看三台设备的Log:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

的确都已经履行完毕了!也都的确写入成功了:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

这没什么问题啊,说明只需有SD卡权限,体系的 DownLoad 目录都是能够写入的。那是不是所以的 SD 卡目录都能写入呢?

咱们创立一个自定义的文件夹试试呢?

代码如下:

    extRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
        val downLoadPath = Environment.getExternalStoragePublicDirectory("DownloadMyFiles").absolutePath
        val parent = File(downLoadPath)
        if (!parent.exists()){
            parent.mkdir()
        }
        val isDirectory = parent.isDirectory
        val canRead = parent.canRead()
        val canWrite = parent.canWrite()
        YYLogUtils.w("${android.os.Build.VERSION.RELEASE} isDirectory:$isDirectory canRead:$canRead canWrite:$canWrite path:$downLoadPath")
        //获取文件流写入文件到File
        val newFile = File(parent.absolutePath + "/资料清单PDF.pdf")
        if (!newFile.exists()) {
             newFile.createNewFile()
        }
        val inputStream = assets.open("资料清单PDF.pdf")
        val inBuffer = inputStream.source().buffer()
        newFile.sink(true).buffer().use {
            it.writeAll(inBuffer)
            inBuffer.close()
        }
    }

接下来咱们创立一个自定义的目录 DownloadMyFiles 。请求权限之后再度尝试写入:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

这… Android10 一下的是能够下入的,高版别就无法写入了,没权限操作!

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

低版其他确是能够写入的!

我不必自定义目录行了吧!那我写到体系的其他目录行了吧!

    extRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
        val downLoadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).absolutePath
       ...
        newFile.sink(true).buffer().use {
            it.writeAll(inBuffer)
            inBuffer.close()
        }
    }

比方咱们写入多媒体文件夹 DCIM 。会怎样样?其实是和自定义文件夹相同的作用:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

而 Android10 一下的版别都是能够正常写入的:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

所以咱们才说,Android10 以上的体系是无法运用 File 来写入的,仅仅谷歌给咱们放开了一个口儿,DownLoad文件夹是特别处理的都能写入。

那 Android10 以上的设备想写入自定义文件夹中的文件,该怎样操作呢?此时就轮到 DocumentFile 登场!

1.2 DocumentFile 操作文件

File 能够直接运用 java.io.File 接口操作外置 SD 卡文件,可是在 Android 10 以上版别需求请求特别权限或者运用 Storage Access Framework(SAF)。DocumentFile 则能够经过 SAF 在任何版别上拜访外置SD卡文件。

SAF的外置 SD 卡的拜访由 DocumentsUI (com.android.documentsui) 供给支撑。运用 ACTION_OPEN_DOCUMENT_TREE 跳转到 DocumentsUI 的存储挑选界面,之后用户手动翻开外置存储并挑选。

例如,咱们能够经过规范的 SAF 翻开办法 Intent 的 办法运用:


    fun wirteFile(){
       val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
       startActivityForResult(intent, 1)
    }
   override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        super.onActivityResult(requestCode, resultCode, resultData)
        if (resultCode == Activity.RESULT_OK && requestCode == 1) {
            resultData?.data?.let { uri ->
                YYLogUtils.w("翻开文件夹:$uri")
                DocumentFile.fromTreeUri(this, uri)
                    // 在文件夹内创立新文件夹
                    ?.createDirectory("DownloadMyFiles")
                    ?.apply {
                        // 在新文件夹内创立文件
                        YYLogUtils.w("在新文件夹内创立文件")
                        createFile("text/plain", "test.txt")
                        // 经过文件名找到文件
                        findFile("test.txt")?.also {
                            try {
                                // 在文件中写入内容
                                contentResolver.openOutputStream(uri)?.write("hello world".toByteArray())
                                YYLogUtils.w("在文件中写入内容结束")
                            }catch (e:Exception){
                                e.printStackTrace()
                            }
                        }
                        // 删去文件
                        // ?.delete()
                    }
                // 删去文件夹
                // ?.delete()
            }
        }
    }

经过用户指定文件夹之后咱们就能拿到 DocumentFile 对象,就能够创立文件夹,创立文件,删去文件等等操作:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

运用 Intent 的办法还要用户手动的挑选,这也太那啥了,莫非我下载一个插件或更新包还需求用户去选存放在哪?笑话!

咱们当然也能直接经过 DocumentFile 去操作,咱们只需求从 File 转换为 DocumentFile 就能够操作它的创立文件夹,创立文件,或写入文件等操作了。

    extRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
         val downLoadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
        val parent = File(downLoadPath)
        val documentFile: DocumentFile? = DocumentFile.fromFile(parent)
        YYLogUtils.w("documentFile:"+documentFile +" uri:"+documentFile?.uri)
        documentFile?.createDirectory("DownloadMyFiles")?.apply {
            createFile("text/plain", "test123")
            val findFile = findFile("test123.txt")
            YYLogUtils.w("findFile:"+findFile +" uri:"+findFile?.uri)
            findFile?.uri?.let {
                contentResolver.openOutputStream(it)?.write("hello world".toByteArray())
            }
        }
    }

这样不就能创立一个文本文件了吗?

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

依次类推,咱们现在和 File 的逻辑相同,咱们先创立文件夹,再创立文件,再翻开 outputstream 流,仍是相同的能把 asset 中的文件经过 IO 流写入到文件中。只不过之前是经过 File 拿到 IO 流,现在是经过 contentResolver.openOutputStream 翻开一个 uri 的输出流罢了!

    extRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) {
         val downLoadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
        val parent = File(downLoadPath)
        val documentFile: DocumentFile? = DocumentFile.fromFile(parent)
        YYLogUtils.w("documentFile:"+documentFile +" uri:"+documentFile?.uri)
         documentFile?.createDirectory("DownloadMyFiles")?.apply {
            val findFile = createFile("application/pdf", "资料清单PDF")
            YYLogUtils.w("findFile:"+findFile +" uri:"+findFile?.uri)
            findFile?.uri?.let {
                val outs = contentResolver.openOutputStream(it)
                val inBuffer = assets.open("资料清单PDF.pdf").source().buffer()
                outs?.sink()?.buffer()?.use {
                    it.writeAll(inBuffer)
                    inBuffer.close()
                }
            }
        }
    }

写入作用:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

我看代码你这仍是写在 Download 文件夹下面的啊? 那这样的话,我要 DocumentFile 写文件有什么优势? 我还不如运用 File 呢?

别慌,运用 DocumentFile 也是能够在 Android10 上写入自定义的文件夹内的,可是呢,比较麻烦,比方先运用Intent的办法,手动挑选文件夹,而且授权赞同权限!

大致的代码如下:

    val uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3ADownloadMyFiles%2Fabcd")
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
    intent.addFlags(
        Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
    )
    startActivityForResult(intent, 2)

选定指定文件夹之后,会呈现授权的弹窗,每个体系结束的UI不同,大致如下:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

只有请求了权限之后把授权永久保存起来,下次再运用这个文件夹就能够无需授权直接操作了

而且此时的操作办法换成了 DocumentFile.fromTreeUri 的办法,留意此时假如是用 DocumentFile.fromFile 是不行的,DocumentFile.fromFile 只能静默写入 DownLoad 文件夹。

可是还有一点,假如运用 DocumentFile.fromTreeUri 的话有一个大问题,我不知道 Uri 啊,依照常规是需求用 ACTION_OPEN_DOCUMENT 的办法获取到 treeUri 才干获取到 DocumentFile对象从而写入文件。

此时这就需求咱们依照规矩拼接 uri ,例如:

SD 卡目录下 DownloadMyFile 的自定义文件夹下面的文件夹abcd:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

成心搞一个长一点的途径便利咱们检查 URI 的拼接规矩,那么这么长的途径下拼接规矩则是 %3A %2F :

content://com.android.externalstorage.documents/tree/primary%3ADownloadMyFiles%2Fabcd

在回调授权结果中处理:

 override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        super.onActivityResult(requestCode, resultCode, resultData)
        if (requestCode == 2) {
            //单独请求指定文件夹权限
            resultData?.data?.let {
                contentResolver.takePersistableUriPermission(
                    it,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                )
                //进行文件写入的操作
                val documentFile: DocumentFile? = DocumentFile.fromTreeUri(mActivity, it)
                YYLogUtils.w("documentFile:" + documentFile + " uri:" + documentFile?.uri)
                documentFile?.run {
                    val findFile = createFile("application/pdf", "资料清单PDF")
                    YYLogUtils.w("findFile:" + findFile + " uri:" + findFile?.uri)
                    findFile?.uri?.let {
                        val outs = contentResolver.openOutputStream(it)
                        val inBuffer = assets.open("资料清单PDF.pdf").source().buffer()
                        outs?.sink()?.buffer()?.use {
                            it.writeAll(inBuffer)
                            inBuffer.close()
                        }
                    }
                }
            }
        }
    }

假如咱们判别用户已经授权过权限,那下面咱们还能运用无感知的办法运用 DocumentFile 的办法,鬼鬼祟祟的下载一个文件到这个外置SD卡自定义文件途径下面:


        val uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3ADownloadMyFiles%2Fabcd")
        contentResolver.takePersistableUriPermission(uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
        //假如是Download文件夹之外的文件夹需求运用 fromTreeUri 的办法
        val documentFile: DocumentFile? =  DocumentFile.fromTreeUri(mActivity, uri)
        YYLogUtils.w("documentFile:"+documentFile +" uri:"+documentFile?.uri)
        documentFile?.run {
            val findFile = createFile("application/pdf", "资料清单PDF")
            YYLogUtils.w("findFile:"+findFile +" uri:"+findFile?.uri)
            findFile?.uri?.let {
                val outs = contentResolver.openOutputStream(it)
                val inBuffer = assets.open("资料清单PDF.pdf").source().buffer()
                outs?.sink()?.buffer()?.use {
                    it.writeAll(inBuffer)
                    inBuffer.close()
                }
            }
        }

作用如图:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

其实只需有用户手动授权的 android.permission.MANAGE_DOCUMENTS 权限之后,咱们就能够在 SD 卡任意的目录存放文件了,不管是办理比较松的 Download 文件夹仍是办理较为严厉的 DCIM 目录都能够随意存入文件:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

尽管和 Android10 之前的 File 的办法仍是不能比,但也能说是勉强够用。是能够存了,也是较为麻烦,需求用户授权!假如有其他的办法当然是不引荐这种办法了。

so ,当咱们有文件存储的需求的时候,首要仍是存沙盒中,其次存 SD 卡的Download 目录,终究考虑的才是存放在 SD 卡的自定义目录。

二、DocumentsProvider 与 FileProvider

其实对咱们一般的运用来说存取还好说,大不了存沙盒,存 SD 卡的 Download 目录嘛,可是取文件怎样办?

作为一个一般运用也是有挑选文件的功用的,不比挑选图库,运用 MediaStore 能够轻松获取到多媒体内容,文件挑选相对而言便是十分的坑爹了。

2.1 文件的获取

市面上大多数的开源的文件挑选器都是运用的 File 的 Api 获取文件,也只能运用这种办法,在 Android10 以上的设备一些作者就会引荐运用 SAF去获取指定的文件。

假如 target api >= 30,那么 requestLegacyExternalStorage 会被疏忽,READ_EXTERNAL_STORAGE 权限仅答应读取媒体文件(比方图片),而无法读取其他类型的文件(比方PDF等)。 假如您处于此种状况,建议自己运用 SAF 结构自行结束文档拜访,而不要运用这个库。 另一个解决办法是请求 MANAGE_EXTERNAL_STORAGE,不过请您慎重考虑运用此权限。 假如 target api == 29,有必要运用 requestLegacyExternalStorage 符号

requestLegacyExternalStorage 是不行能用的,那我改怎样获取 SD卡的文件,管他真的假的,我先试试!

这儿咱们仍是请出咱们的三台设备 Android7、Android9、Android12。咱们一再台设备的 Download 目录下面分别导入相同的文件,分别为png格局,txt格局,doc格局与pdf格局。

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

咱们运用相同的代码,请求权限之后咱们能获取到这些文件吗?

代码很简单,便是一般的 File 的 API :

        fun readFile() {
            val downLoadPath = Environment.getExternalStoragePublicDirectory("Download").absolutePath
            val parentFile = File(downLoadPath)
            if (parentFile.exists() && parentFile.isDirectory) {
                val listFiles = parentFile.listFiles()
                if (listFiles != null && listFiles.isNotEmpty()) {
                    val nameList = arrayListOf<String>()
                    listFiles.forEach { file ->
                        if (file.exists()) {
                            if (file.isDirectory) {
                                val fileName = file.name
                                nameList.add(fileName)
                            } else {
                                val fileName = file.name
                                nameList.add(fileName)
                            }
                        }
                    }
                    YYLogUtils.w("${android.os.Build.VERSION.RELEASE} 找到的文件和文件夹为:$nameList")
                }
            }
        }

那么不同的版别履行的 Log 如下图所示:

能够看到 Android12 版其他确是无法读取到文档文件!而 Android10 以下的设备则能够正常的读取到。

已然 File 读取不到那咱们经过 DocumentFile 行不行?行是行,可是假如用 SAF 的办法去授权也太不高雅了吧!

那怎样办?最干流的办法当然是大佬们都引荐的直接启动 SAF 选取文件了:

比方如下代码:

    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    //可选:指定挑选文本类型的文件, 指定多类型查询
    intent.setType("*/*");
    intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{PDF, DOC, DOCX, PPT, PPTX, XLS, XLSX, TXT});
    activity.startActivityForResult(intent, 10402);

咱们能够挑选任何文件,也能够挑选指定文件格局的文件。

相似的作用如下:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

每一个体系的具体 UI 是不同的,而且能筛选出指定的文件,比方下图的图片是不行选的:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

结果在 onActivityResult 的回到中,经过 data 字段拿到 uri 数据,之后就能够拿到流进行一些 IO 操作了。

所以咱们都这么玩了!

   if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        //指定挑选文本类型的文件
        intent.type = "*/*"
        //指定多类型查询
        intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(PDF , DOC, DOCX, PPT, PPTX, XLS, XLSX, TXT))
        startActivityForResult(intent, 10402)
 }else{
        ...
        val listFiles = parentFile.listFiles()
        ...
 }

这样总算是能结束需求了,搞完收工!

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

没多大会问题就来了,老板不乐意了!为什么 Android10 以下的设备和 Android 10 以上的设备展现的 UI 作用不一致,你赶忙改成设计图相同的作用啊…你看看iOS就…巴拉巴拉。

“老板啊你听我讲,不是我不改,是谷歌就让这么干的!连官方都引荐这么用,不同的版别是会有不同的 UI 展现的,都是体系的 UI 页面的,这东西不能彻底按设计图来的。”

“别跟我巴巴的,就问是不是你不行?为什么 iOS 能做 Android 不能做?”

我当时的心情是这样的:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

我尼玛,额,冷静一下,收!

话说回来,那 Android 到底能不能结束一致的 UI 这个需求呢?

或者咱们换一个问法, Android10 以上的设备能否在 SD 卡的文件夹里自由读取文件呢?

大许上也是能够做的。其实本质上仍是 File 的操作,不过咱们能够运用 DocumentsProvider 和 FileProvider 进行一些包装罢了。

2.2 DocumentsProvider vs FileProvider

很多同学或许都只用过 FileProvider ,并没有用过 DocumentsProvider ,对哦它也不是很了解。

上一年的文章中 【Android中FileProvider的各种场景运用】 我大致讲解了 FileProvider 的用法,在文章的后面我尝试了一种办法,把自己的文件分享给其他 App 运用。

是不是和咱们现在的场景很相像呢?其实 FileProvider 并不引荐这么用,看 FileProvider 的源码注释就知道:

FileProvider 的 exported 和 grantUriPermissions 都是指定的写法,不能改动,且不答应暴露,不答应给其他 App 自动拜访!

此时再回看 DocumentsProvider 的运用,相对而言就没那么的严厉,不便是运用在此时此景吗?

他们两者的功用大致上是差不多的,都是经过 ContentProvider 的办法供给文件给对方运用的,可是他们又有不同:

FileProvider 它能够将文件的 Uri 转换为 content:// 的方法,从而在不同的运用间同享文件 DocumentsProvider 它能够供给一个文档树的结构,从而让用户在不同的运用间拜访和办理文档。

FileProvider 首要用于临时同享文件,而 DocumentsProvider 首要用于长期存储和办理文件。

DocumentsProvider 常见用于一些云盘存储的运用,能够自定义回来一些 URL 或其他的自定义字段,而 FileProvider 的作用则比较受限,只能用于本机上的一些文件。

在咱们的这个场景中,咱们只需求用到一般的本地文档办理即可。咱们乃至还能经过 DocumentsProvider 和 FileProvider 的相互合作才干结束功用。

加下来便是看看 DocumentsProvider 怎样结束一个自定义文件挑选器:

和 FileProvider 的定义办法相似,咱们需求先创立一个 Provider 文件:

public class SelectFileProvider extends DocumentsProvider {
    private final static String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_SUMMARY,
            Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_ICON,
            Root.COLUMN_AVAILABLE_BYTES};
    private final static String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID,
            Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS, Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE,
            Document.COLUMN_LAST_MODIFIED};
    public static final String AUTOHORITY = "com.guadou.kt_demo.selectfileprovider.authorities";
    //是否有权限
    private static boolean hasPermission(@Nullable Context context) {
        if (context == null) {
            return false;
        }
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
                == PackageManager.PERMISSION_GRANTED) {
            return true;
        }
        return false;
    }
    @Override
    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
    return null;
    }
    @Override
    public boolean isChildDocument(String parentDocumentId, String documentId) {
        return documentId.startsWith(parentDocumentId);
    }
    @Override
    public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
        // 判别是否缺少权限
        if (!hasPermission(getContext())) {
            return null;
        }
        // 创立一个查询cursor, 来设置需求查询的项, 假如"projection"为空, 那么运用默许项
        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
        includeFile(result, new File(documentId));
        return result;
    }
    @Override
    public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
        // 判别是否缺少权限
        if (!hasPermission(getContext())) {
            return null;
        }
        // 创立一个查询cursor, 来设置需求查询的项, 假如"projection"为空, 那么运用默许项
        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
        final File parent = new File(parentDocumentId);
        String absolutePath = parent.getAbsolutePath();
        boolean isDirectory = parent.isDirectory();
        boolean canRead = parent.canRead();
        boolean canWrite = parent.canWrite();
        File[] files = parent.listFiles();
        YYLogUtils.w("parent:" + parent + " absolutePath:" + absolutePath + " isDirectory:" +
                isDirectory + " canRead:" + canRead + " canWrite:" + canWrite + " files:" + files);
        if (isDirectory && canRead && files != null && files.length > 0) {
            for (File file : parent.listFiles()) {
                // 不显现隐藏的文件或文件夹
                if (!file.getName().startsWith(".")) {
                    // 添加文件的姓名, 类型, 巨细等特点
                    includeFile(result, file);
                }
            }
        }
        return result;
    }
    private void includeFile(final MatrixCursor result, final File file) throws FileNotFoundException {
        final MatrixCursor.RowBuilder row = result.newRow();
        row.add(Document.COLUMN_DOCUMENT_ID, file.getAbsolutePath());
        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
        String mimeType = getDocumentType(file.getAbsolutePath());
        row.add(Document.COLUMN_MIME_TYPE, mimeType);
        int flags = file.canWrite()
                ? Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_RENAME
                | (mimeType.equals(Document.MIME_TYPE_DIR) ? Document.FLAG_DIR_SUPPORTS_CREATE : 0) : 0;
        if (mimeType.startsWith("image/"))
            flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
        row.add(Document.COLUMN_FLAGS, flags);
        row.add(Document.COLUMN_SIZE, file.length());
        row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
    }
    @Override
    public String getDocumentType(String documentId) throws FileNotFoundException {
        if (!hasPermission(getContext())) {
            return null;
        }
        File file = new File(documentId);
        if (file.isDirectory()) {
            //假如是文件夹-先回来再说
            return Document.MIME_TYPE_DIR;
        }
        final int lastDot = file.getName().lastIndexOf('.');
        if (lastDot >= 0) {
            //假如文件有后缀-直接回来后缀名的类型
            final String extension = file.getName().substring(lastDot + 1);
            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
            if (mime != null) {
                return mime;
            }
        }
        return "application/octet-stream";
    }
    @Override
    public ParcelFileDescriptor openDocument(String documentId, String mode, @Nullable CancellationSignal signal) throws FileNotFoundException {
        return null;
    }
    @Override
    public boolean onCreate() {
        return true;
    }
}

首要是需求重写 onCreate 办法和几个 query 办法,看各自的需求结束,我这儿只查文件夹下面的内容,所以重点重视的是 queryChildDocuments 办法。

而且能够看到为什么说它比 FileProvider 更加的灵活呢?一是因为能够自定义Provider类,二是能够自定义查询各种目录,三是能够自定义回来字段。

比方假如咱们要做的是云盘运用,那么就能够回来图片或文件的 URL 链接,乃至还能自定义回来字段高分辩的图片与低画质的图片等。

当然咱们这儿运用的没那么杂乱,仅仅用于查询本地的文件夹罢了,当咱们定义结束之后需求在清单文件注册:

        <provider
            android:name=".demo.demo6_imageselect_premision_rvgird.files.SelectFileProvider"
            android:authorities="com.guadou.kt_demo.selectfileprovider.authorities"
            android:exported="true"
            android:grantUriPermissions="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER"/>
            </intent-filter>
        </provider>

需求留意的是 Android4.4 以上才可用,可是咱们也只用于 Android10 以上的设备,所以直接声明即可。

接下来咱们就能根据本地途径的 path 途径来拜访此文件夹,例如我直接拜访咱们之前创立好的 DownloadMyFiles 文件夹:

        val downLoadPath1 = Environment.getExternalStoragePublicDirectory("DownloadMyFiles").absolutePath
        val uri = DocumentsContract.buildChildDocumentsUri(
            "com.guadou.kt_demo.selectfileprovider.authorities",
            downLoadPath1
        )
        val cursor = contentResolver.query(uri, null, null, null, null)
        YYLogUtils.w("cursor $cursor")
        cursor?.run {
            while (moveToNext()) {
                val documentId = getString(getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID))
                val displayName = getString(getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME))
                val type = getString(getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE))
                val flag = getInt(getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS))
                val size = getLong(getColumnIndex(DocumentsContract.Document.COLUMN_SIZE))
                val updateAt = getLong(getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED))
                YYLogUtils.w("${android.os.Build.VERSION.RELEASE} documentId:$documentId displayName:$displayName type:$type flag:$flag size:$size updateAt:$updateAt")
            }
            close()
        }
    }

打印日志如下:

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

到此整个流程就结束了,咱们能够把文件夹下面的悉数文件打印出来,当然了或许咱们获取到的是原始 path 途径,或许无法直接拜访的,咱们最好是合作 FileProvider 把本地途径转换为 URI 去拜访。这也是需求完善的点。

总归到底,最底层的结束仍是没脱离 File 的范畴,仅仅终究或许用 DocumentsProvider 和 FileProvider 两者结合再包装一层罢了。

后记

File vs DocumentFile的差异 以及 DocumentsProvider vs FileProvider的异同,咱们看完应该有一些了解。

总的来说,当咱们有文件存储的需求的时候,首要考虑的仍是存沙盒,这是最保险的!

其次咱们能够存 SD 卡的 Download 目录,可是就算把文件放在 SD 卡内,也需求留意高版其他兼容问题,也最好运用 FileProvider 去获取 URI的办法获取文件资源,避免直接 File 或 DocumentFile 无法直接读取的状况

终究考虑真实没办法的才是存放在 SD 卡的自定义目录,需求用户手动挑选文件夹授权了之后才干直接存取文件。

而获取文件咱们首要是运用 File 的 API 去获取,对 Android10 以上的版别运用 SAF 结构拜访,这是最好的跟着Android 版其他迭代也是最为引荐的办法。

当然假如硬是有 UI 方面的约束,一定要运用设计师的作用,咱们也能用 DocumentsProvider 合作 FileProvider 结束自定义的文件数据获取,仅仅相对麻烦一点。

说到这儿挖一个坑,后期或许出一个兼容 Android10 以上文件挑选结构。。。不过因为时刻联系和一些其他的原因,需求等等,先待我仔细思量思量。

到结尾了,先说一声抱愧,文章篇幅太长太乱,时刻太赶了,还请各位见谅,特别是公司最近的项目也蛮多,需求改动的东西都是蛮赶的。真实抱愧!

常规了,我如有讲解不到位或错漏的地方,希望同学们能够指出。假如有更好的办法或其他办法,或者你有遇到的坑也都能够在谈论区沟通一下,咱们互相学习进步。

一起我也很好奇,咱们都是怎样存储文件,用什么办法挑选文件的呢?欢迎咱们到谈论区沟通!

假如感觉本文对你有一点点的帮助,还望你能点赞支撑一下,纯用爱发电,你的支撑是我最大的动力。

本文的部分代码能够在我的 Kotlin 测验项目中看到,【传送门】。你也能够重视我的这个Kotlin项目,我有时刻都会持续更新。

Ok,这一期就此结束。

Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs  FileProvider 的异同

本文正在参与「金石方案」