【问题标题】:Android 11 (R) file path accessAndroid 11 (R) 文件路径访问
【发布时间】:2020-06-07 03:58:05
【问题描述】:

根据 docs 文件路径访问权限是在 Android R 中授予的:

从 Android 11 开始,拥有 READ_EXTERNAL_STORAGE 权限的应用可以使用直接文件路径和原生库读取设备的媒体文件。这项新功能使您的应用可以更顺畅地与第三方媒体库配合使用。

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


另外,the docs say the following:

所有文件访问权限

某些应用的核心用例需要广泛的文件访问权限,例如文件管理或备份和恢复操作。他们可以通过执行以下操作获得所有文件访问权限:

  1. 声明 MANAGE_EXTERNAL_STORAGE 权限。
  2. 将用户引导至系统设置页面,他们可以在其中为您的应用启用“允许访问以管理所有文件”选项。

此权限授予以下内容:

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

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


请注意:

  • 我知道Uri 是从MediaStore 返回的,并不指向文件。
  • 我知道我可以将文件复制到我的应用程序目录并将其传递给FFmpeg,但我可以在 Android R 之前这样做。
  • 我无法将FileDescriptor 传递给FFmpeg,也无法使用/proc/self/fd/(我在从SD 卡中选择文件时得到/proc/7828/fd/70: Permission denied),看看this issue

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

【问题讨论】:

    标签: android android-11


    【解决方案1】:

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

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

      android:requestLegacyExternalStorage="true"
      

      这是为了确保文件路径适用于 Android 10(Q)。在 Android R 上,此属性将被忽略。

    • 不要使用 DATA 列插入或更新媒体存储,使用 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/ 和所有子目录中选择单个文件。

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

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

    【讨论】:

    • 你在 Github 上是否也有一个用于自动查找媒体文件的示例,以便列出它们?
    • @androiddeveloper“自动查找媒体文件”我不确定我明白你在问什么?
    • 画廊应用程序和音乐应用程序不是使用框架中的 API 自动查找文件吗,并且每个文件夹中的特殊文件(“.nomedia”)会告诉框架避免扫描它吗?
    • @HB 我使用您的库并针对 Android 11 对其进行测试。pickiT.getPath 返回 null。当路径解析为媒体并且 getDataColumn() 返回 null 时,出现问题。光标适用于 Android 11 吗?可能我什么都不懂?
    • @AntonStukov 看“read me”,它解释了如何使用库(你不应该直接使用pickiT.getPath,路径会在PickiTonCompleteListener中返回)。如果您仍然无法使其正常工作,请在库上打开一个问题并填写问题模板(包括您的日志以及您如何实现该库) - github.com/HBiSoft/PickiT#implementation
    【解决方案2】:

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

    共享存储再次进一步分为媒体和下载集合。媒体集合存储图像、音频和视频文件。下载集合会处理非媒体文件。

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

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

    注意:如果您使用文件或Uri util类(如RealPathUtil、FilePathUtils等)获取文件路径,此处可以获取所需的文件路径但无法读取文件,因为它会在 Android 11 中引发 Read Access 异常(如权限被拒绝),因为您无法读取由其他应用程序创建的文件。

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

    在我的场景中,我使用了这两种 API 来获取 Android 11 中的文件访问权限。为了获取媒体文件(即图像、视频、音频)的文件路径,我使用了 Media Store API(请参阅此链接:Media Store API Example - Access media files from shared storage),为了获取非媒体文件(即Documents和其他文件)的文件路径,我使用了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。

    【讨论】:

      【解决方案3】:

      为了获取路径,我将带有 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)
          }
      
      }
      

      【讨论】:

      • 您的回答中需要注意的几件事。 1) 复制文件不适用于处理较大文件的应用程序。想象一下必须复制一个 2GB 的文件。如果您想使用该文件,您首先必须等待它完成。 2) 您仍在使用已弃用的 _data 列,因此您必须在清单中添加 requestLegacyExternalStorage。 3)您正在早早返回路径。在返回路径之前,您首先必须等待文件被复制。 4)文件的创建/复制应该在后台线程上完成。
      • @HB。你是对的,对于大文件来说,这不是一个好的解决方案。我将 _data 列用于较旧的 android 版本。如果可用,_data 列仍然是更好的解决方案。我只是复制了主要解决方案。它应该在后台解决方案中完成(可以使用协程)。谢谢我的朋友:)
      猜你喜欢
      • 1970-01-01
      • 2020-10-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-05
      • 1970-01-01
      相关资源
      最近更新 更多