Android 11 ACTION_OPEN_DOCUMENT_TREE:将初始 URI 设置为 Documents 文件夹

问题描述

在 Android 11 中使用 Scoped Storage 模型我想让用户能够选择一个文件夹,从文档文件夹开始:

          val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
          intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,???     );
          startActivityForResult(intent,OPEN_DIRECTORY_REQUEST_CODE,null);

问题是,如何生成手机文档文件夹的正确 URI? (它就在 root / 中)在官方文档中,没有给出示例。我真的希望所有标准位置都有一些整洁的常量?

解决方法

我们将处理从 INITIAL_URI 获得的 StorageManager..getPrimaryStorageVolume().createOpenDocumentTreeIntent()

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q)
        {
            StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);

            Intent intent = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
            //String startDir = "Android";
            //String startDir = "Download"; // Not choosable on an Android 11 device
            //String startDir = "DCIM";
            //String startDir = "DCIM/Camera";  // replace "/","%2F"
            //String startDir = "DCIM%2FCamera";
            String startDir = "Documents";

            Uri uri = intent.getParcelableExtra("android.provider.extra.INITIAL_URI");

            String scheme = uri.toString();

            Log.d(TAG,"INITIAL_URI scheme: " + scheme);

            scheme = scheme.replace("/root/","/document/");

            scheme += "%3A" + startDir;

            uri = Uri.parse(scheme);

            intent.putExtra("android.provider.extra.INITIAL_URI",uri);

            Log.d(TAG,"uri: " + uri.toString());

            ((Activity) context).startActivityForResult(intent,REQUEST_ACTION_OPEN_DOCUMENT_TREE);

            return;
        }
,

所有功劳都归功于 blackapps 的回答! https://stackoverflow.com/a/67554693/2036264

这是 Kotlin 语言中的相同代码:

          if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
            val sm =  getSystemService(Context.STORAGE_SERVICE) as StorageManager
            intent = sm.primaryStorageVolume.createOpenDocumentTreeIntent()
            //String startDir = "Android";
            //String startDir = "Download"; // Not choosable on an Android 11 device
            //String startDir = "DCIM";
            //String startDir = "DCIM/Camera";  // replace "/","%2F"
            //String startDir = "DCIM%2FCamera";
            val startDir = "Documents"
            var uriroot = intent.getParcelableExtra<Uri>("android.provider.extra.INITIAL_URI")    // get system root uri
            var scheme = uriroot.toString()
            Log.d("Debug","INITIAL_URI scheme: $scheme")
            scheme = scheme.replace("/root/","/document/")
            scheme += "%3A$startDir"                        //change uri to Documents folder
            uriroot = Uri.parse(scheme)
            intent.putExtra("android.provider.extra.INITIAL_URI",uriroot)                        // give changed uri to Intent
            Log.d("Debug","uri: $uriroot")
          
            startActivityForResult(intent,OPEN_DIRECTORY_REQUEST_CODE);
          }

正如一些评论者所提到的,这段代码可能会在未来被破坏而无法运行,这是事实。但是,考虑到 Android 的过去,他们每隔一年都会更改存储 API。

,

如何生成手机文档文件夹的正确 URI?

测试:

  1. 小米 M2102J20SI
  2. 模拟器 Pixel 4 XL API 30

函数 askPermission() 打开目标目录。

@RequiresApi(Build.VERSION_CODES.Q)
private fun askPermission() {
    val storageManager = application.getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val intent =  storageManager.primaryStorageVolume.createOpenDocumentTreeIntent()

    val targetDirectory = "WhatsApp%2FMedia%2F.Statuses" // add your directory to be selected by the user
    var uri = intent.getParcelableExtra<Uri>("android.provider.extra.INITIAL_URI") as Uri
    var scheme = uri.toString()
    scheme = scheme.replace("/root/","/document/")
    scheme += "%3A$targetDirectory"
    uri = Uri.parse(scheme)
    intent.putExtra("android.provider.extra.INITIAL_URI",uri)
    startActivityForResult(intent,REQUEST_CODE)
}

文件的 Uri 将在 onActivityResult() 中返回

 override fun onActivityResult(requestCode: Int,resultCode: Int,data: Intent?) {
        super.onActivityResult(requestCode,resultCode,data)
        if (resultCode == RESULT_OK && requestCode == REQUEST_CODE) {
            if (data != null) {
                data.data?.let { treeUri ->

                    // treeUri is the Uri of the file
                    
                   // if life long access is required the takePersistableUriPermission() is used

                    contentResolver.takePersistableUriPermission(
                            treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION or
                                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    )

                  readSDK30(treeUri)
                }
            }
        }
    }

函数readSDK30()用于从Uri读取文件和文件夹

  private fun readSDK30(treeUri: Uri) {
        val tree = DocumentFile.fromTreeUri(this,treeUri)!!

        thread {
            val uriList  = arrayListOf<Uri>()
            listFiles(tree).forEach { uri ->
                 
                // Collect all the Uri from here
            }
            
        }
    }

函数 listFiles() 返回给定 Uri 中的所有文件和文件夹

fun listFiles(folder: DocumentFile): List<Uri> {
            return if (folder.isDirectory) {
                folder.listFiles().mapNotNull { file ->
                    if (file.name != null) file.uri else null
                }
            } else {
                emptyList()
            }
        }