Android 11(R)文件路径访问

28

根据文档,在安卓R中授予文件路径访问权限:

从Android 11开始,拥有READ_EXTERNAL_STORAGE权限的应用可以使用直接文件路径和本地库读取设备的媒体文件。这种新能力允许您的应用与第三方媒体库更加流畅地工作。

问题在于我无法从MediaStore获取文件路径,那么我们该如何读取无法访问/检索的文件路径呢? 我们不知道的方式是什么,可以从MediaStore获取文件路径吗?


此外,文档中说:

完全文件访问权限

某些应用有一个核心用例,需要广泛的文件访问权限,例如文件管理或备份和还原操作。它们可以通过执行以下操作来获得完全文件访问权限:

  1. 声明MANAGE_EXTERNAL_STORAGE权限。
  2. 引导用户到系统设置页面,在那里他们可以为您的应用启用访问所有文件的选项。

此权限授予以下内容:

  • 读取和写入共享存储中的所有文件。
  • 访问MediaStore.Files表的内容。

但我不需要所有文件的访问权限,我只想让用户从MediaStore中选择一个视频并将文件路径传递给FFmpeg(它需要文件路径)。 我知道我不能再使用_data列来检索文件路径。


请注意:

  • 我知道MediaStore返回的是Uri,而不是指向文件的指针。
  • 我知道我可以将文件复制到我的应用程序目录并将其传递给FFmpeg,但在Android R之前我也可以这样做。
  • 我不能将FileDescriptor传递给FFmpeg,也不能使用/proc/self/fd/(当从SD卡中选择文件时,我会得到/proc/7828/fd/70:权限被拒绝),请参见此问题

那么我该怎么办? 我错过了什么吗? “可以使用直接文件路径和本机库读取设备的媒体文件”是什么意思?

3个回答

18

在向issuetracker提出问题后,我得出了以下结论:

  • 在Android R上,Android Q中添加的File限制已被删除。因此,我们可以再次访问File对象。
  • 如果您的目标是Android 10及以上版本,并且您想要访问/使用文件路径,则必须在清单文件中添加/保留以下内容:

  • android:requestLegacyExternalStorage="true"
    

    这是为了确保文件路径在安卓10(Q)上能够正常工作。但是在安卓R上,此属性将被忽略。

  • 不要使用数据列(DATA column)来插入或更新媒体存储(Media Store),请使用DISPLAY_NAMERELATIVE_PATH,以下是一个示例:

  • ContentValues valuesvideos;
    valuesvideos = new ContentValues();
    valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "YourFolder");
    valuesvideos.put(MediaStore.Video.Media.TITLE, "SomeName");
    valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, "SomeName");
    valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
    valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
    valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
    valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 1);
    ContentResolver resolver = getContentResolver();
    Uri collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
    Uri uriSavedVideo = resolver.insert(collection, valuesvideos);
    
  • 您无法再使用ACTION_OPEN_DOCUMENT_TREEACTION_OPEN_DOCUMENT意图操作来请求用户从Android/data/, Android/obb/和所有子目录选择单个文件。

  • 建议仅在需要执行“寻求”(例如使用FFmpeg时)时使用File对象。
  • 您只能使用数据列访问磁盘上的文件。 您应相应地处理I / O异常。

如果您想要访问File或者想要从MediaStore返回的Uri获取文件路径,我创建了一个库,可以处理您可能遇到的所有异常 可以访问所有磁盘上的文件,包括内部和可移动磁盘。 例如选择来自Dropbox的File时,将会将该File复制到您的应用程序目录中,您可以完全访问该复制后的文件路径。


@androiddeveloper “自动查找媒体文件”,我不确定我理解你的意思? - HB.
@HB 我使用了你的库并在Android 11上进行了测试。pickiT.getPath返回null。当路径解析为媒体并且getDataColumn()返回null时,问题就出现了。游标在Android 11上是否可用?也许我什么都没理解? - Anton Stukov
@AntonStukov 请查看“read me”,它会解释如何使用库(您不应直接使用pickiT.getPath,路径将在PickiTonCompleteListener中返回)。如果您仍然无法使其正常工作,请在库上打开一个问题并填写问题模板(包括日志和您实现库的方式)- https://github.com/HBiSoft/PickiT#implementation - HB.
你能为Ionic实现类似的东西吗? - Shinichi Kudo
非常感谢。做得很好。除了这个库,没有其他解决方案适用于我。 - Prantik Mondal
显示剩余2条评论

10

如果你的目标是针对Android 11 API,你不能直接访问文件路径,因为在API 30(Android R)中有许多限制。由于作用域存储API在Android 10(API 29)中引入,因此存储现在被分为作用域存储(私有存储)和共享存储(公共存储)。作用域存储是一种只能访问在你的scoped storage目录中创建的文件的存储方式(即/Android/data/或/Android/media/ your-package-name)。你无法从共享存储(即内部存储/外部SD卡存储等)访问文件。

共享存储再次分为Media和Download集合。Media集合存储图像、音频和视频文件。Download集合将处理非媒体文件。

要了解关于作用域存储和共享存储的更多细节,请参阅Android 10与11中的Scoped Storage链接。

如果你正在处理媒体文件(即图像、视频、音频),你可以使用支持API 30(Android 11)的Media Store API获取文件路径。如果你正在处理非媒体文件(即文档和其他文件),则可以使用file Uri获取文件路径。

注意:如果你正在使用文件或Uri util类(如RealPathUtil、FilePathUtils等)来获取文件路径,则可以获取所需的文件路径,但你无法读取该文件。因为在Android 11中,它会抛出读访问异常(如权限被拒绝),因为你无法读取由另一个应用程序创建的文件。

因此,在Android 11(API 30)中实现获取文件路径的情况,建议使用File Uri将文件复制到应用程序的缓存目录中,并从缓存目录中获取文件访问路径。

在我的场景中,我同时使用了这两个API来获取Android 11中的文件访问。为了获取媒体文件(即图像、视频、音频)的文件路径,我使用了Media Store API(请参阅Media Store API示例 - 从共享存储访问媒体文件链接),而对于非媒体文件(即文档和其他文件)的文件路径,则使用了fileDescriptor。

文件描述符示例: 我已经创建了系统对话框文件选择器来选择文件。

private fun openDocumentAction() {
    val mimetypes = arrayOf(
        "application/*",  //"audio/*",
        "font/*",  //"image/*",
        "message/*",
        "model/*",
        "multipart/*",
        "text/*"
    )
    // you can customize the mime types as per your choice.
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        //type = "application/pdf"    //only pdf files
        type = "*/*"
        putExtra(Intent.EXTRA_MIME_TYPES, mimetypes)
        addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker when it loads.
        //putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, RC_SAF_NON_MEDIA)
}

在活动的onActivityResult方法中处理文件选择器的结果。在此处获取文件URI。

 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    when (requestCode) {
        
        RC_SAF_NON_MEDIA -> {
            //document selection by SAF(Storage Access Framework) for Android 11
            if (resultCode == RESULT_OK) {
                // The result data contains a URI for the document or directory that
                // the user selected.
                data?.data?.also { uri ->

                    //Permission needed if you want to retain access even after reboot
                    contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    // Perform operations on the document using its URI.
                   
                    val path = makeFileCopyInCacheDir(uri)
                    Log.e(localClassName, "onActivityResult: path ${path.toString()} ")
                   
                }
            }
        }
    }
}

将文件URI传递给以下方法以获取文件路径。此方法将在应用程序的缓存目录中创建一个文件对象,并从该位置您可以轻松地获取对该文件的读取访问权限。

private fun makeFileCopyInCacheDir(contentUri :Uri) : String? {
    try {
        val filePathColumn = arrayOf(
            //Base File
            MediaStore.Files.FileColumns._ID,
            MediaStore.Files.FileColumns.TITLE,
            MediaStore.Files.FileColumns.DATA,
            MediaStore.Files.FileColumns.SIZE,
            MediaStore.Files.FileColumns.DATE_ADDED,
            MediaStore.Files.FileColumns.DISPLAY_NAME,
            //Normal File
            MediaStore.MediaColumns.DATA,
            MediaStore.MediaColumns.MIME_TYPE,
            MediaStore.MediaColumns.DISPLAY_NAME
        )
        //val contentUri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(mediaUrl))
        val returnCursor = contentUri.let { contentResolver.query(it, filePathColumn, null, null, null) }
        if (returnCursor!=null) {
            returnCursor.moveToFirst()
            val nameIndex = returnCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
            val name = returnCursor.getString(nameIndex)
            val file = File(cacheDir, name)
            val inputStream = contentResolver.openInputStream(contentUri)
            val outputStream = FileOutputStream(file)
            var read = 0
            val maxBufferSize = 1 * 1024 * 1024
            val bytesAvailable = inputStream!!.available()

            //int bufferSize = 1024;
            val bufferSize = Math.min(bytesAvailable, maxBufferSize)
            val buffers = ByteArray(bufferSize)
            while (inputStream.read(buffers).also { read = it } != -1) {
                outputStream.write(buffers, 0, read)
            }
            inputStream.close()
            outputStream.close()
            Log.e("File Path", "Path " + file.path)
            Log.e("File Size", "Size " + file.length())
            return file.absolutePath
        }
    } catch (ex: Exception) {
        Log.e("Exception", ex.message!!)
    }
    return contentUri.let { UriPathUtils().getRealPathFromURI(this, it).toString() }
}

注意:您可以使用此方法获取媒体文件(图像、视频、音频)和非媒体文件(文档和其他文件)的文件路径。只需要传递一个文件Uri即可。

Note: 您可以使用此方法获取媒体文件(图像、视频、音频)和非媒体文件(文档和其他文件)的文件路径。只需要传递一个文件Uri即可。


你的方法包含几个错误。首先在这一行 val returnCursor = contentUri.let { contentResolver.query(it, filePathColumn, null, null, null) } 中,contentResolver 变量在哪里?我得到了 Unresolved reference: contentResolver 的错误。其次,在这一行 val file = File(cacheDir, name) 中也没有名为 cacheDir 的变量。接下来一行也会报错,因为它也使用了 contentResolver。在 outputStream 中,我得到了这个消息 Overload resolution ambiguity. All these functions match. 最后,return contentUri.let { UriPathUtils().getRealPathFromURI(this, it).toString() } - Dr Mido
未解决的引用:UriPathUtils。我认为这是一个库,对吧?你应该编辑这个方法并修复提到的错误! - Dr Mido

9

为获取路径,我使用文件描述符将文件复制到新路径并使用该路径。

查找文件名:

private static String copyFileAndGetPath(Context context, Uri realUri, String id) {
    final String selection = "_id=?";
    final String[] selectionArgs = new String[]{id};
    String path = null;
    Cursor cursor = null;
    try {
        final String[] projection = {"_display_name"};
        cursor = context.getContentResolver().query(realUri, projection, selection, selectionArgs,
                null);
        cursor.moveToFirst();
        final String fileName = cursor.getString(cursor.getColumnIndexOrThrow("_display_name"));
        File file = new File(context.getCacheDir(), fileName);

        FileUtils.saveAnswerFileFromUri(realUri, file, context);
        path = file.getAbsolutePath();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null)
            cursor.close();
    }
    return path;
}

使用文件描述符进行复制:

fun saveAnswerFileFromUri(uri: Uri, destFile: File?, context: Context) {
    try {
        val pfd: ParcelFileDescriptor =
            context.contentResolver.openFileDescriptor(uri, "r")!!
        if (pfd != null) {
            val fd: FileDescriptor = pfd.getFileDescriptor()
            val fileInputStream: InputStream = FileInputStream(fd)
            val fileOutputStream: OutputStream = FileOutputStream(destFile)
            val buffer = ByteArray(1024)
            var length: Int
            while (fileInputStream.read(buffer).also { length = it } > 0) {
                fileOutputStream.write(buffer, 0, length)
            }

            fileOutputStream.flush()
            fileInputStream.close()
            fileOutputStream.close()
            pfd.close()
        }
    } catch (e: IOException) {
        Timber.w(e)
    }

}

5
请注意以下几点:1) 复制文件不适用于处理较大文件的应用程序。想象一下复制2GB的文件,如果你想要使用该文件,必须等待复制完成。2) 你仍在使用_data列,这已经过时了,所以你需要在清单文件中添加requestLegacyExternalStorage 。3) 你在返回路径之前太急了。在返回路径之前,你必须先等待文件被复制。4) 文件的创建/复制应在后台线程上执行。 - HB.
@HB。你是对的,对于大文件来说这不是一个好的解决方案。我在旧版Android上使用_data列。如果_data列可用,则_data列仍然是更好的解决方案。我只是复制了主要解决方案。它应该在后台解决方案中完成(可以使用协程)。谢谢我的朋友 :) - Hosein Haqiqian

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