Android 11 ACTION_OPEN_DOCUMENT_TREE:将初始URI设置为“文档”文件夹

4

在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?(它位于根目录/)在官方文档中,没有给出任何示例。我真的希望有一些整洁的常量适用于所有标准位置?

1
问题是,我如何生成手机文档文件夹的正确URI?据我所知,目前没有这样的选项,抱歉。EXTRA_INITIAL_URI旨在成为您之前从ACTION_OPEN_DOCUMENT_TREE获取的某些Uri - CommonsWare
@CommonsWare请看一下我的回答。 - blackapps
3个回答

21

我们将操作从StorageManager..getPrimaryStorageVolume().createOpenDocumentTreeIntent()获取的INITIAL_URI

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

你正在对 Uri 格式进行假设;这些假设在 Android 版本之间可能不稳定。 - CommonsWare
非常感谢,我会尝试这个方法。只要它能在大多数手机上运行,而不会在其他手机上崩溃,那就足够好了。这是一种“总比没有”的方法。 - Kenobi
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) - blackapps
1
希望Android团队能在未来的Android版本中通过标准实现来修复上述解决方案出现的问题。 - user1608385
是的,新安装是可以正常工作的,但是当你使用更新应用程序进行测试时,它就无法正常工作了。 - undefined
显示剩余6条评论

8
如何生成手机文档文件夹的正确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)
}

onActivityResult() 方法中将返回文件的Uri。

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

谢谢,这基本上告诉了如何使用Scoped Storage来选择一个随机的起始目录。感谢您提供干净的代码。不幸的是,我的问题是如何将Scoped Storage用于一个固定的起始目录,而用户不能自己选择。 - Kenobi
@Kenobi,如何使用ACTION_OPEN_DOCUMENT_TREE……这与作用域存储无关。 - blackapps
我们如何从documentfile对象中获取属性,例如持续时间、分辨率(如果文件是视频)? - Himanshu
这在我的Android 12上不起作用。 - denver

4

所有功劳归于上面@blackapp的答案

以下是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);
}

正如一些评论者所提到的,这段代码可能会在未来出现问题而无法工作,这是真实的。不过,考虑到安卓的过去经历,他们每隔一年就会更改存储API。

请在顶部提到的“问题已有答案”帖子中再次发布您的Kotlin代码。此页面的访问者将不会再寻找,因此我认为您的代码最好放在那里。 - blackapps
当我将更新的SDK 33 APK应用于旧的SDK 29版本的APK时,"使用此文件夹"权限被授予,但是我无法访问该文件夹中的文件。我正在访问设备内“音乐”文件夹中下载的音乐文件,并使用我的应用程序名称。 - undefined

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