Android11(R)文件路径访问

Android 11 (R) file path access

根据文档文件路径,在 Android R:

中授予访问权限

Starting in Android 11, apps that have the READ_EXTERNAL_STORAGE permission can read a device's media files using direct file paths and native libraries. This new capability allows your app to work more smoothly with third-party media libraries.

问题是我无法从 MediaStore 获取文件路径,那么我们应该如何读取我们无法 access/retrieve 的文件路径?有没有办法,我不知道,我们可以从 MediaStore?

获取文件路径

此外,the docs say the following

All Files Access

Some apps have a core use case that requires broad file access, such as file management or backup & restore operations. They can get All Files Access by doing the following:

  1. Declare the MANAGE_EXTERNAL_STORAGE permission.
  2. Direct users to a system settings page where they can enable the Allow access to manage all files option for your app.

This permission grants the following:

  • Read access and write access to all files within shared storage.
  • Access to the contents of the MediaStore.Files table.

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


请注意:


那我该怎么办,我是不是错过了什么? can read a device's media files using direct file paths and native libraries 是什么意思?

issuetracker 上提问后,我得出以下结论:

  • 在 Android R 上,删除了 Android Q 中添加的 File 限制。所以我们可以再次访问 File 个对象。
  • 如果您的目标是 Android 10 > 并且您想要 access/use 文件路径,则必须 add/keep 清单中的以下内容:

    android:requestLegacyExternalStorage="true"
    

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

  • 不要使用 DATA 列插入或更新到 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 意图操作来请求用户 select 来自 Android/data/Android/obb/和所有子目录。

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

如果您想要访问 File 或想要从 MediaStore 返回的 Uri 的文件路径,I've created a library 可以处理您可能遇到的所有异常得到。这包括磁盘、内部和可移动磁盘上的所有文件。例如,当从 Dropbox selecting File 时,File 将被复制到您具有完全访问权限的应用程序目录,然后将返回复制的文件路径。

为了获取路径,我正在将带有 fileDescriptor 的文件复制到新路径并使用该路径。

查找文件名:

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

}

如果您的目标是 Android 11 API,您无法直接访问文件路径,因为 API 30(Android 中有很多限制右)。由于范围存储API是在Android10(API29)中引入的,现在存储分为范围存储(私有存储)和共享存储(public存储)。分区存储是一种您只能访问在 scoped storage directory(i.e. /Android/data/ or /Android/media/<your-package-name>) 中创建的文件的类型。您无法从共享存储(即内部 storage/external SD 卡存储等)

访问文件

共享存储再次进一步分为媒体和下载collection。 Media collection 存储图像、音频和视频文件。下载 collection 将处理 non-media 个文件。

要了解有关分区存储和共享存储的更多详细信息,请参阅此 link:Scoped Storage in Android 10 & Android 11

如果您正在处理媒体文件(即图像、视频、音频),您可以使用支持 API 30(Android 11).如果您正在处理 non-media 个文件(即文档和其他文件),您可以使用文件 Uri 获取文件路径。

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

所以要实现这种获取文件路径在Android11(API30)的场景,建议使用File Uri将文件复制到应用程序的缓存目录中并获取从缓存目录访问文件的路径。

在我的场景中,我使用了两个 API 来获取 Android 中的文件访问权限 11. 要获取媒体文件(即图像、视频、音频)的文件路径,我使用了 Media Store API(参考这个 link: Media Store API Example - Access media files from shared storage),并获取 non-media 文件(即文档和其他文件)的文件路径,我用过 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)
}

并在 activity 的 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传递给下面的方法来获取文件路径。此方法将在您的应用程序的缓存目录中创建一个文件 object,您可以从该位置轻松获得对该文件的读取权限。

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() }
}

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