某些应用程序如何在没有 root 的情况下访问 Android 11 上“.../Android/...”子文件夹的内容?

and*_*per 13 android storage-access-framework android-11

背景

Android 10 和 11各种存储限制,其中还包括访问所有文件的新权限 ( MANAGE_EXTERNAL_STORAGE )(但它不允许访问真正的所有文件),而之前的存储权限已减少以仅授予对媒体的访问权限文件:

  1. 应用程序可以自由访问“媒体”子文件夹。
  2. 应用程序永远无法访问“数据”子文件夹,尤其是内容。
  3. 对于“obb”文件夹,如果应用程序被允许安装应用程序,它可以访问它(将文件复制到那里)。否则不能。
  4. 使用 USB 或 root,您仍然可以访问它们,作为最终用户,您可以通过内置的文件管理器应用程序“文件”访问它们。

问题

我注意到一个名为“ X-plore ”的应用程序以某种方式克服了这个限制(此处):一旦您进入“Android/data”文件夹,它会要求您授予对它的访问权限(直接使用 SAF,不知何故),并且当您授予它,您可以访问“Android”文件夹的所有文件夹中的所有内容。

这意味着可能仍然有一种方法可以达到它,但问题是由于某种原因,我无法制作具有相同效果的样本。

我发现并尝试过的

该应用程序似乎针对 API 29 (Android 10),并且它尚未使用新权限,并且它具有标志requestLegacyExternalStorage。我不知道他们在针对 API 30 时使用的相同技巧是否有效,但我可以说,在我的情况下,在 Pixel 4 和 Android 11 上运行,它工作正常。

所以我尝试做同样的事情:

  1. 我制作了一个针对 Android API 29 的示例 POC,已授予(各种)存储权限,包括遗留标志。

  2. 我试图请求直接访问“Android”文件夹(基于此处),但遗憾的是由于某种原因无法正常工作(一直转到 DCIM 文件夹,不知道为什么):

val androidFolderDocumentFile = DocumentFile.fromFile(File(primaryVolume.directory!!, "Android"))
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) 
        .putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidFolderDocumentFile.uri)
startActivityForResult(intent, 1)
Run Code Online (Sandbox Code Playgroud)

我尝试了各种标志组合。

  1. 启动应用程序时,当我自己手动到达“Android”文件夹时,因为这不能正常工作,我授予了对该文件夹的访问权限,就像在其他应用程序上一样。

  2. 获取结果时,我尝试获取路径中的文件和文件夹,但无法获取它们:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    Log.d("AppLog", "resultCode:$resultCode")
    val uri = data?.data ?: return
    if (!DocumentFile.isDocumentUri(this, uri))
        return
    grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    val fullPathFromTreeUri = FileUtilEx.getFullPathFromTreeUri(this, uri) // code for this here: /sf/ask/3966034761/
    val documentFile = DocumentFile.fromTreeUri(this, uri)
    val listFiles: Array<DocumentFile> = documentFile!!.listFiles() // this returns just an array of a single folder ("media")
    val androidFolder = File(fullPathFromTreeUri)
    androidFolder.listFiles()?.forEach {
        Log.d("AppLog", "${it.absoluteFile} children:${it.listFiles()?.joinToString()}") //this does find the folders, but can't reach their contents
    }
    Log.d("AppLog", "granted uri:$uri $fullPathFromTreeUri")
}
Run Code Online (Sandbox Code Playgroud)

因此,使用 DocumentFile.fromTreeUri 我仍然可以获得无用的“media”文件夹,而使用 File 类我只能看到还有“data”和“obb”文件夹,但仍然无法访问它们的内容......

所以这根本不起作用。

后来我发现了另一个使用这个技巧的应用程序,叫做“ MiXplorer ”。在这个应用程序上,它无法直接请求“Android”文件夹(也许它甚至没有尝试过),但是一旦您允许,它就会授予您对其及其子文件夹的完全访问权限。而且,它以 API 30 为目标,所以这意味着它不能仅仅因为您以 API 29 为目标而工作。

我注意到(有人写给我)通过对代码进行一些更改,我可以分别请求访问每个子文件夹(意味着对“数据”的请求和对“obb”的新请求),但这是不是我在这里看到的,应用程序所做的。

意思是,为了进入“Android”文件夹,我使用这个 Uri 作为 Intent.EXTRA_INITIAL_URI 的参数:

val androidUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
                    .appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android").build()
Run Code Online (Sandbox Code Playgroud)

但是,一旦获得对它的访问权限,您将无法从中获取文件列表,不能通过 File 也不能通过 SAF。

但是,正如我所写的,奇怪的是,如果您尝试类似的操作,而不是访问“Android/data”,您将能够获取其内容:

val androidDataUri=Uri.Builder().scheme("content").authority("com.android.externalstorage.documents")
                 .appendEncodedPath("tree").appendPath("primary:").appendPath("document").appendPath("primary:Android/data").build()
Run Code Online (Sandbox Code Playgroud)

问题

  1. 如何直接向“Android”文件夹请求 Intent,让我真正访问它,并让我获得子文件夹及其内容?
  2. 还有另一种选择吗?也许使用 adb 和/或 root,我可以授予 SAF 访问此特定文件夹的权限?

Poi*_*ull 13

以下是它在 X-plore 中的工作原理:

当 on 时Build.VERSION.SDK_INT>=30,[Internal storage]/Android/data 不可访问,javaFile.canRead()File.canWrite()返回 false,因此我们需要切换到此文件夹内文件的备用文件系统(也可能还有 obb)。

您已经知道存储访问框架的工作原理,因此我将详细说明需要完成的工作。

您致电ContentResolver.getPersistedUriPermissions()以了解您是否已经拥有此文件夹的保存权限。最初你没有它,所以你请求用户许可:

要请求访问,请使用startActivityForResultwith Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).putExtra(DocumentsContract.EXTRA_INITIAL_URI, DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Android")) Here 您设置的EXTRA_INITIAL_URI选择器应直接在主存储上的 Android 文件夹上启动,因为我们要访问 Android 文件夹。当您的应用以 API30 为目标时,picker 将不允许选择存储的根目录,并且通过获得对 Android 文件夹的权限,您可以使用一个权限请求来处理里面的文件夹dataobb文件夹。

当用户通过 2 次点击确认时,onActivityResult您将在数据中获得 Uri,该数据应为content://com.android.externalstorage.documents/tree/primary%3AAndroid. 进行必要的检查以验证用户是否确认了正确的文件夹。然后调用contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION)保存权限,你就准备好了。

所以我们回到ContentResolver.getPersistedUriPermissions(),其中包含已授予权限的列表(可能还有更多),您在上面授予的权限如下所示:(UriPermission {uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid, modeFlags=3}与您在 中获得的 Uri 相同onActivityResult)。迭代列表getPersistedUriPermissions以找到感兴趣的 uri,如果找到可以使用它,否则请求用户授权。

现在,您想要处理ContentResolverDocumentsContract使用此“树”uri 以及 Android 文件夹内文件的相对路径。这是列出data文件夹的示例: data/是相对于授予的“树”uri 的路径。使用DocumentsContract.buildChildDocumentsUriUsingTree()(列出文件)或DocumentsContract.buildDocumentUriUsingTree()(用于处理单个文件)构建最终 uri ,例如:DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri), DocumentsContract.getTreeDocumentId(treeUri)+"/data/"),您将获得 uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid/document/primary%3AAndroid%2Fdata%2F/children适合列出数据文件夹中的文件。现在调用ContentResolver.query(uri, ...)并处理数据Cursor以获取文件夹列表。

你与其他SAF功能的读/写/重命名/移动/删除工作类似的方式/创建,你可能已经知道,使用ContentResolver或方法DocumentsContract

一些细节:

  • 它不需要 android.permission.MANAGE_EXTERNAL_STORAGE
  • 它适用于目标 API 29 或 30
  • 它仅适用于主存储,不适用于外部 SD 卡
  • 对于数据文件夹内的所有文件,您需要使用 SAF(java 文件不起作用),只需使用相对于 Android 文件夹的文件层次结构
  • 未来谷歌可能会修补他们“安全”意图中的这个漏洞,并且在一些安全更新后这可能不起作用

编辑:示例代码,基于 Cheticamp Github 示例。该示例显示了“Android”文件夹的每个子文件夹的内容(和文件数):

class MainActivity : AppCompatActivity() {
    private val handleIntentActivityResult =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode != Activity.RESULT_OK)
                return@registerForActivityResult
            val directoryUri = it.data?.data ?: return@registerForActivityResult
            contentResolver.takePersistableUriPermission(
                directoryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            )
            if (checkIfGotAccess())
                onGotAccess()
            else
                Log.d("AppLog", "you didn't grant permission to the correct folder")
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(findViewById(R.id.toolbar))
        val openDirectoryButton = findViewById<FloatingActionButton>(R.id.fab_open_directory)
        openDirectoryButton.setOnClickListener {
            openDirectory()
        }
    }

    private fun checkIfGotAccess(): Boolean {
        return contentResolver.persistedUriPermissions.indexOfFirst { uriPermission ->
            uriPermission.uri.equals(androidTreeUri) && uriPermission.isReadPermission && uriPermission.isWritePermission
        } >= 0
    }

    private fun onGotAccess() {
        Log.d("AppLog", "got access to Android folder. showing content of each folder:")
        @Suppress("DEPRECATION")
        File(Environment.getExternalStorageDirectory(), "Android").listFiles()?.forEach { androidSubFolder ->
            val docId = "$ANDROID_DOCID/${androidSubFolder.name}"
            val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(androidTreeUri, docId)
            val contentResolver = this.contentResolver
            Log.d("AppLog", "content of:${androidSubFolder.absolutePath} :")
            contentResolver.query(childrenUri, null, null, null)
                ?.use { cursor ->
                    val filesCount = cursor.count
                    Log.d("AppLog", "filesCount:$filesCount")
                    val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                    val mimeIndex = cursor.getColumnIndex("mime_type")
                    while (cursor.moveToNext()) {
                        val displayName = cursor.getString(nameIndex)
                        val mimeType = cursor.getString(mimeIndex)
                        Log.d("AppLog", "  $displayName isFolder?${mimeType == DocumentsContract.Document.MIME_TYPE_DIR}")
                    }
                }
        }
    }

    private fun openDirectory() {
        if (checkIfGotAccess())
            onGotAccess()
        else {
            val primaryStorageVolume = (getSystemService(STORAGE_SERVICE) as StorageManager).primaryStorageVolume
            val intent =
                primaryStorageVolume.createOpenDocumentTreeIntent().putExtra(EXTRA_INITIAL_URI, androidUri)
            handleIntentActivityResult.launch(intent)
        }
    }

    companion object {
        private const val ANDROID_DOCID = "primary:Android"
        private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
        private val androidUri = DocumentsContract.buildDocumentUri(
            EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
        )
        private val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
            EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @androiddeveloper [此处](https://github.com/Cheticamp/SAFWalker) 是一个小应用程序,演示如何遍历“数据”目录。如您所知,`File().listFiles()`将为您提供“../Android/”的顶级内容,因此您应该能够使用应用程序中的逻辑来遍历“obb”以及您在那里找到的任何其他东西。 (2认同)