Android:FileProvider.getUriForFile读取MediaStore生成的文件?

4

简述

我想通过FileProvider获取(读取/生成)以下路径的文件Uri,但不知道如何操作:

val file = File("/external/file/9359") // how do I get its Uri then?

我知道的只有 FileProvider.getUriForFile 方法,但它会抛出异常。

我的操作

我试图模拟下载进度 -- 在 Download 文件夹中创建一个文件,然后传递给 ShareSheet 让用户做任何想做的事情。

我已经完成的工作:

  1. 使用 MediaStoreContentResolverDownload 中创建文件。
  2. 有一个 ShareSheet 实用函数。
  3. 注册了 FileProviderfilepath.xml

在我的情况下,我想通过 ShareSheet 函数共享该文件,需要:

  • File 的 Uri

但是通常的 file.toUri() 在 SDK 29 以上会抛出上面提到的异常。因此,我将其改为 FileProvider.getUriForFile(),这是 Google 官方推荐的方法。

直接导致问题的代码

val fileUri: Uri = FileProvider.getUriForFile(context, "my.provider", file)

会抛出异常:

java.lang.IllegalArgumentException: Failed to find configured root that contains /external/file/9359

我的文件创建代码

val fileUri: Uri? = ContentValues().apply {
    put(MediaStore.MediaColumns.DISPLAY_NAME, "test.txt")
    put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // create file in download directory.
        put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
    }
}.let { values ->
    context.contentResolver.insert(MediaStore.Files.getContentUri("external"), values)
}
if (fileUri == null) return

context.contentResolver.openOutputStream(fileUri)?.use { fileOut ->
    fileOut.write("Hello, World!".toByteArray())
}

val file = File(fileUri.path!!) // File("/external/file/9359")

我可以保证代码是正确的,因为我可以在下载文件夹中看到正确的文件。

我的FileProvider设置

我已在AndroidManifest.xml中注册提供程序:

<application>
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="my.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepath" />
    </provider>
</application>

以下是filepath.xml文件的内容:
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="external"
        path="." />
</paths>

我也尝试了很多路径变量,一个接一个地:

<external-files-path name="download" path="Download/" />
<external-files-path name="download" path="." />
<external-path name="download" path="Download/" />
<external-path name="download" path="." />
<external-path name="external" path="." />
<external-files-path name="external_files" path="." />

我甚至还注册了外部存储读取权限:

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

我哪部分做错了?

编辑1

发布后,我认为 File 的初始化可能是问题所在:

File(fileUri.path!!)

所以我把它改成了

File(fileUri.toString())

但仍然无法正常工作。

它们的内容差异如下:

fileUri.path       -> /external/file/9359
(file.path)        -> /external/file/9359
error              -> /external/file/9359

fileUri.toString() -> content://media/external/file/9359
(file.path)        -> content:/media/external/file/9359
error              -> /content:/media/external/file/9359

编辑2

我最初想要实现的是向其他应用程序发送二进制数据。根据官方文档,似乎只接受文件Uri

如果有其他方法可以实现此目的,例如直接共享文件File等,我会非常感激。

但我现在想知道的很简单——如何使FileProvider可用以获取/读取/生成文件Uri,例如/external/file/9359等?这不仅可以帮助解决当前问题,对我来说似乎是更一般/基本的知识点。

7个回答

4

Content Uri没有文件路径或方案。您应该创建一个输入流,并使用此输入流创建一个临时文件。您可以从此文件中提取文件 URI 或路径。以下是我编写的一个函数,用于从 Content Uri 创建临时文件:

 fun createFileFromContentUri(fileUri : Uri) : File{

    var fileName : String = ""

    fileUri.let { returnUri ->
        requireActivity().contentResolver.query(returnUri,null,null,null)
    }?.use { cursor ->
        val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
        cursor.moveToFirst()
        fileName = cursor.getString(nameIndex)
    }
    
    //  For extract file mimeType
    val fileType: String? = fileUri.let { returnUri ->
        requireActivity().contentResolver.getType(returnUri)
    }

    val iStream : InputStream = requireActivity().contentResolver.openInputStream(fileUri)!!
    val outputDir : File = context?.cacheDir!!
    val outputFile : File = File(outputDir,fileName)
    copyStreamToFile(iStream, outputFile)
    iStream.close()
    return  outputFile
}

fun copyStreamToFile(inputStream: InputStream, outputFile: File) {
    inputStream.use { input ->
        val outputStream = FileOutputStream(outputFile)
        outputStream.use { output ->
            val buffer = ByteArray(4 * 1024) // buffer size
            while (true) {
                val byteCount = input.read(buffer)
                if (byteCount < 0) break
                output.write(buffer, 0, byteCount)
            }
            output.flush()
        }
    }
   }

--编辑--

Content Uri 具有路径,但不具备文件路径。举个例子:

内容 Uri 路径:content://media/external/audio/media/710

文件 Uri 路径:file:///sdcard/media/audio/ringtones/nevergonnagiveyouup.mp3

通过使用上述函数,您可以创建一个临时文件,并像这样提取 Uri 和路径:

val tempFile: File = createFileFromContentUri(contentUri)  
val filePath = tempFile.path  
val fileUri = tempFile.getUri() 

使用tempFile后,请将其删除以防止内存问题(它将被删除,因为它是写入缓存中的):

tempFile.delete()

不需要编辑内容URI。向内容URI添加方案可能不起作用。


“Context uris does not have path or scheme.” 这可能是我错过的部分!你能详细说明一下吗?比如参考文献,或者我们有没有可能将路径/方案添加回该Uri中?(例如问题中所述的那个) - Samuel T. Chou
@SamuelT.Chou 您可以查看此文档。内容Uri具有路径但没有文件路径。例如:内容Uri路径:_content://media/external/audio/media/710_,文件Uri路径:_file:///sdcard/media/audio/ringtones/nevergonnagiveyouup.mp3_。 - alpertign
1
@SamuelT.Chou "有没有可能将路径/方案添加回该Uri?" -> 通过使用上面的函数,您可以创建一个临时文件,并像这样提取uri和路径:val tempFile: File = createFileFromContentUri(contentUri) - val filePath = tempFile.pathval fileUri = tempFile.getUri()。因此,您无需编辑内容uri。向内容uri添加方案可能不起作用。 - alpertign
1
在我看来,重新创建一个临时文件并使用它似乎有点过度杀伐(因为我们已经有了这个文件)。但是你提供的信息非常有帮助,可以帮助我们理解整个事情。我建议你将评论编辑到你的答案中,然后我会考虑接受它。 - Samuel T. Chou
1
“重新创建一个临时文件并使用它似乎有点过度设计。” -> 相同的想法。这里也有一个类似的问题链接。这里还有一个有用的链接,很好地描述了文件URI之间的关系。 - alpertign

1

时间过去了,我终于弄明白了——或者说,我猜我弄明白了?

我会在这里留下我目前所知道的东西。如果有任何错误,请纠正我。

简而言之

对于像这样生成的文件:

val file = File("/external/file/9359")

无论如何,即使通过FileProvider.getUriForFile,也没有办法获取其(完整)uri。

事实上,“/external/file/9359”不是标准文件uri。我将在下面进行更详细的解释。

让我们谈谈几件事:

  1. FileProvider到底是什么?
  2. MediaStore是做什么的?
  3. 如何将它们两个结合起来?
  4. 最终如何进行下载过程?

如果您想了解有关文件/uri/path概念的更快、更简洁的版本,请查看@Alperen Acikgoz的答案

FileProvider的概念

正如官方参考文档所述:

FileProviderContentProvider的一个特殊子类,它通过创建content://Uri代替file:///Uri方便地共享与应用程序相关联的文件

因此,FileProvider的作用非常清晰--它只是转换uri,使其更安全使用和共享。

原始的File URI可能如下所示:

file:///sdcard/media/audio/ringtones/nevergonnagiveyouup.mp3

那么FileProvider可能会将其更改为类似于以下内容:

content://my.app.fileprovider/sercet-bgm/nevergonnagiveyouup.mp3

这就是 FileProvider 的全部作用。

它实际上不会修复Uri,也不会生成/获取文件。它所做的只是将(合法的)文件Uri转换为更安全的Uri。

还要注意的是,FileProvider 来自 androidx 库。

使用 MediaStore

媒体文件的官方指南官方参考文档也非常清晰明了。

长话短说,MediaStore 系列(与 ContentResolver 结合使用)为应用程序提供了一种访问/更新/创建媒体文件(如图像、视频、音频或任何 File)的方式,可以在特定文件夹中进行操作,例如 DownloadDCIM/ 等。

MediaStore 系列就像是应用程序和 Android 系统之间的协议。创建/修改的文件可以被任何其他应用程序(或用户本身)共享/访问。

MediaStore 系列的结果通常带有一个 Uri:

val values = ContentValues() // set something inside

val fileUri = context.contentResolver.insert(MediaStore.Files.getContentUri("external"), values)

请注意,上面的fileUri将类似于这样:
// fileUri.toString()
"content://media/external/file/9359"

MediaStore系列(以及ContentResolver)是Android内置功能。

FileProvider和MediaStore

那么,FileProviderMediaStore系列之间有什么关系呢?

答案是没有。它们实际上没有任何关联。

  1. FileProvider解决了已弃用、不太安全的file:/// uri的问题。
  2. MediaStore提供了一种访问媒体文件的方式,提供类似于content://media/external/...的uri。

因此,如果您正在寻找如何将它们组合在一起,那就放弃吧。它们不能一起工作。

额外奖励:下载过程

那么,我该如何实现下载过程,也就是最初的目标呢?

我放弃了MediaStore系列。相反,我使用了@Alperen Acikgoz提到的方法——逐字节复制以创建文件:

// create file
val file: File? = context.getExternalFilesDir(null)?.let { File(it, fileName) }
// Write File: Simple Copy Process
val iStream: InputStream // any file from local, downloading, etc.
file?.outputStream().use { outputStream ->
    var bytesCopied = 0
    val bufferLengthBytes = 1024
    val buffer = ByteArray(bufferLengthBytes)
    try {
        var bytes = iStream.read(buffer)
        while (bytes >= 0) {
            outputStream.write(buffer, 0, bytes)
            bytesCopied += bytes
            bytes = iStream.read(buffer)
            val progress = bytesCopied * 1f / length // if needed
        }
    } catch (e: IOException) {
        // failed writing. Might be insufficient storage.
    }
}
iStream.close()

要分享它,请使用FileProvider传递给分享对话框:

val file: File
assertNotNull(file)
val fileUri: Uri = FileProvider.getUriForFile(context, "my.app.provider", file)

// from 
val shareIntent: Intent = Intent().apply {
    action = Intent.ACTION_SEND
    putExtra(Intent.EXTRA_STREAM, fileUri)
    type = "image/jpeg" // or any other type, depending on file.
}
startActivity(Intent.createChooser(shareIntent, null))

关于上述过程的一些注意事项:

  1. 是的,该文件在此时将有2个副本。但在内存释放后,它将被清除。
  2. 如果您不需要逐字创建,则可以使用MediaStore系列来创建文件。
  3. 该文件默认情况下不会存在于Download/文件夹中。要这样做,请使用MediaStore插入一个(通过复制)

这就是我目前所知道的全部内容。希望这能帮助未来的谷歌搜索者。


1
首先,在最近的Android系统中,通常不会从Uri获取文件路径,因为您可能只对其进行临时访问,您通常会使用context.contentResolver.openInputStream(externalUri)将其流内容复制到文件中,然后才能在之后使用。
通过这样做,您将文件放置在一个不属于您的应用程序的目录中,因此可能不需要使用FileProvider,并且可以直接传递从MediaStore插入中获取的uri。
正确使用FileProvider的方法是将文件放置在您的FileProvider路径之一中,而不使用MediaStore,假设您有:
<provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepath" />
</provider>

并且

<external-files-path name="external" path="shared" />

您将在该路径中创建文件。

val sharedFolder=context.getExternalFilesDir("shared")
val fileName="test.txt"

File(sharedFolder,fileName).outputStream()?.use { fileOut ->
    fileOut.write("Hello, World!".toByteArray())
}

val fileProviderUri = FileProvider.getUriForFile(
                    context,
                    BuildConfig.APPLICATION_ID + ".provider",
                    File(sharedFolder,fileName)
                )
//example intent for viewing file, to change if you need to pass to another application
                context.startActivity(
                    Intent(Intent.ACTION_VIEW).setData(fileProviderUri )
                        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                )


0

兄弟,试试这个,也许对你有用,将你的path.xml文件更改为以下内容;

<?xml version="1.0" encoding="utf-8"?>
<paths>
  <external-path
    name="external"
    path="." />
  <external-files-path
    name="external_files"
    path="." />
  <cache-path
    name="cache"
    path="." />
  <external-cache-path
    name="external_cache"
    path="." />
<files-path
    name="files"
    path="." />
</paths>

或者如果上面的解决方案不起作用,也可以检查这个。

https://dev59.com/DFgQ5IYBdhLWcg3wl1F3#42516202


是的,我之前看到过这个,尝试了一下,但还是不起作用。答案侧重于由“Context”提供的目录创建的文件;但在我的情况下,我正在使用“MediaStore”。 - Samuel T. Chou
如果您能帮我将保存样式从MediaStore更改为由Context提供的目录,尽管可能不是回答问题,但仍然有所帮助。我已经阅读并尝试了很多,似乎Context无法真正在Download文件夹中创建文件...? - Samuel T. Chou

0

内容:/media/external/file/9370

您可以通过打开流来读取该mediastore uri:

InputStream is = getContentResolver().openInpurStream(uri);

然后从流中读取。

如果您想共享文件,请直接使用此URI。

如果您已经有一个好的URI,则无需使用FileProvider。


我正在执行官方文档中所述的“共享文件”操作,该操作似乎只接受uri。https://developer.android.com/training/sharing/send#send-binary-content 那么我应该只查找Uri。我应该如何“仅从流中读取uri”?(这是从uri打开的流...?) - Samuel T. Chou
你能编辑你的帖子并说明你想要分享一个文件吗?因为现在我们只能看到你想要读取一个文件,这很令人困惑。 - blackapps
哦,我明白你现在困惑的部分了。我已经表述清楚了,希望现在的描述足够清晰了。 - Samuel T. Chou
文件共享不是我想要处理的重点(因为错误不在那里)。我更像是在寻找如何获取特定文件的Uri,或者正确使用MediaStoreFileProvider的组合的答案。 - Samuel T. Chou
我完全不知道你想要什么。你写的帖子无法阅读。请重新撰写。并记住:在使用MediaStore uri时,使用FileProvider毫无意义。你之前已经被告知过了。 - blackapps
显示剩余3条评论

0
public  void image_download(Context context, String uRl, String bookNmae) {

        File direct = new File(Environment.getExternalStorageDirectory()
                + "/Islam Video App/books");

        if (!direct.exists()) {
            direct.mkdirs();
        }

        DownloadManager mgr = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

        Uri downloadUri = Uri.parse(uRl);
        DownloadManager.Request request = new DownloadManager.Request(
                downloadUri);

        request.setAllowedNetworkTypes(
                DownloadManager.Request.NETWORK_WIFI
                        | DownloadManager.Request.NETWORK_MOBILE)
                .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
                .setAllowedOverRoaming(false).setTitle(bookNmae)
                .setDestinationInExternalPublicDir("/Islam Video App/books", bookNmae + ".pdf");

        mgr.enqueue(request);

    }

抱歉,但是DownloadManager不符合我的需求。我的API使用带有请求体的POST方法,而DownloadManager不允许这样做。 - Samuel T. Chou

0

不要在XML文件路径中使用相同的名称。Android似乎会选择与提供的名称匹配的第一个路径。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接