【问题标题】:Android: FileProvider.getUriForFile reading MediaStore-generated file?Android:FileProvider.getUriForFile 读取 MediaStore 生成的文件?
【发布时间】:2022-07-04 17:14:18
【问题描述】:

TL;DR

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

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

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

我在做什么

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

我做了什么:

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

就我而言,我想通过ShareSheet 函数共享文件,这需要

  • File 的 Uri

但通常的 file.toUri() 会在 SDK 29 以上抛出异常。因此我按照 Google 官方的建议将其更改为 FileProvider.getUriForFile()

直接问题代码

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")

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

我的 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

我最初想要实现的是sending binary data to other app。正如官方记录的那样,它似乎只接受文件Uri

如果有任何其他方法可以实现这一点,我将不胜感激,例如直接分享 File 等。

但我现在想知道的很简单——我如何使FileProvider 可用于获取/读取/生成文件Uri 就像/external/file/9359 等等? 这可能会出现不仅在这种情况下有所帮助,而且对我来说似乎是更一般/基本的知识。

【问题讨论】:

    标签: android uri mediastore android-fileprovider


    【解决方案1】:

    内容 uris 没有文件路径或方案。您应该创建一个输入流并使用此输入流创建一个临时文件。您可以从此文件中提取文件 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()
            }
        }
       }
    

    --编辑--

    内容 Uri 有路径但没有文件路径。例如;

    content 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 添加方案可能行不通

    【讨论】:

    • "上下文 uris 没有路径或方案。"那可能是我想念的部分!你能详细说明吗?像参考一样,或者我们是否有机会将路径/方案添加回该 Uri? (例如问题中所述的那个)
    • @SamuelT.Chou 您可以查看 [this] (developer.android.com/reference/android/content/ContentUris) 文档。内容 Uri 有路径但没有文件路径。例如;内容URI路径:content://media/external/audio/media/710,文件URI路径:file:///sdcard/media/audio/ringtones/nevergonnagiveyouup.mp3我>
    • @SamuelT.Chou “我们是否有机会将路径/方案添加回该 Uri?” -> 通过使用上面的函数,你可以创建一个临时文件,你可以像这样提取 uri 和路径:val tempFile: File = createFileFromContentUri(contentUri) - val filePath = tempFile.pathval fileUri = tempFile.getUri()。因此,您不必编辑内容 uri。向内容 uri 添加方案可能行不通
    • 好吧,IMO,暂时重新创建一个文件并使用它似乎有点矫枉过正(因为我们已经有了文件)。但是您提供的信息对于理解整个事情非常有帮助。我建议您将 cmets 编辑到您的答案中,然后我会考虑接受它。
    • “暂时重新创建一个文件并使用它似乎有点矫枉过正”->同样的想法。同样的问题问here 。这里有一个有用的link 很好地描述了文件-uri 关系
    【解决方案2】:

    兄弟试试这个可能对你有用,把你的 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://stackoverflow.com/a/42516202/7905787

    【讨论】:

    • 是的,我以前看过这个,试过了,还是不行。答案集中在Context -provided 目录创建的文件上;但在我的情况下,我使用的是MediaStore
    • 如果您能帮我将保存样式从MediaStore 更改为Context-provided 目录,虽然可能无法回答问题,但仍然可以帮助我。我阅读并尝试了很多,似乎Context 无法真正在Download 文件夹中创建文件...?
    【解决方案3】:
    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 不允许的。
    【解决方案4】:

    内容:/media/external/file/9370

    您可以通过为它打开一个流来读取该媒体存储 uri:

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

    然后从流中读取。

    如果您想共享文件,请按原样使用此 uri。

    如果您已经有一个不错的 uri,则无需使用 FileProvider。

    【讨论】:

    • 我正在按照官方文档编写的“共享文件”操作,似乎只接受uri。 developer.android.com/training/sharing/send#send-binary-content 那我应该只找Uri。我怎么能“只从流中读取 uri”? (是从uri打开的...?)
    • 您可以编辑您的帖子并告诉您要共享文件吗?现在我们只能读取你想读取的文件。如您所见,令人困惑。
    • 我想知道如何从FileProvider 中读取UriFile -- 我在标题和第一部分中已经说明了这一点。如果您回答我如何不通过Uri 共享文件的部分,我将不胜感激,但不要关闭问题(或标记为已接受的答案)。路径阅读是我现在想解决的难题。
    • 哦,我知道现在哪个部分让您感到困惑。我说清楚了,希望现在描述足够清楚。
    • ???我看到没有任何改变。您仍然不会以告诉您要共享文件来开始您的帖子。你帖子的标题也没有这样说。
    【解决方案5】:

    首先,在最近的 Android 中,您通常不会从 Uri 获取文件路径,因为您可能只能临时访问它,您通常使用 context.contentResolver.openInputStream(externalUri) 将内容从它的流中复制到一个文件中,然后您可以之后使用。

    但您不需要在将 Uri 插入 Media Store 时从创建的 Uri 中获取文件路径,因为您已经拥有真正的完整路径。

    put(MediaStore.MediaColumns.DISPLAY_NAME, "test.txt" )       
    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
    

    所以您的文件将位于File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),"test.txt")

    通过这样做,您将文件放在不属于您的应用程序的目录中,因此 可能不需要 FileProvider,您可以只传递从 MediaStore 插入获得的 uri。

    正确的做法是将文件放入您的 FileProvider 路径之一 所以假设你有

    <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" />
    

    您将在该路径中使用 MediaStore 插入

    val sharedFolder=context.getExternalFilesDir("shared")
    val fileName="test.txt"
    val fileUri: Uri? = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
        put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // create file in fileprovider directory.
    put(MediaStore.MediaColumns.RELATIVE_PATH,sharedFolder.replace("/storage/emulated/0/", ""))
        }
    }.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 uri = 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(uri)
                            .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    )
    
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-10
      • 1970-01-01
      相关资源
      最近更新 更多