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

23

背景

Android 10和11存在各种存储限制, 这也包括一个新的权限(MANAGE_EXTERNAL_STORAGE)来访问所有文件(但它并不允许访问真正的所有文件),而之前的存储权限被减少以仅授予访问媒体文件的权限:

  1. 应用程序可以自由地访问“media”子文件夹。
  2. 应用程序永远无法访问“data”子文件夹,特别是内容。
  3. 对于“obb”文件夹,如果允许应用程序安装应用,则可以访问它(将文件复制到其中)。否则就不能。
  4. 使用USB或root,您仍然可以访问它们,并且作为最终用户,您可以通过内置文件管理器应用程序“文件”访问它们。

问题

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

这意味着可能仍然有一种方法可以访问它,但问题在于出于某种原因我无法制作一个做到同样的事情的样本。

我发现和尝试过的东西

它似乎针对 API 29(Android 10),并且尚未使用新的权限,而且它具有标志 requestLegacyExternalStorage。我不知道他们使用的同样的技巧是否适用于目标 API 30,但我可以说,在我的情况下,在 Pixel 4 上运行 Android 11,它运行良好。

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

  1. 我制作了一个示例 POC,目标是 Android API 29,并授予了所有类型的存储权限,包括遗留标志。

  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)

我尝试了各种标志组合。

  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: https://dev59.com/uek5XIcBkEYKwwoY49sn
    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")
}

所以使用DocumentFile.fromTreeUri,我仍然只能得到无用的“media”文件夹,而使用File类,我只能看到还有“data”和“obb”文件夹,但仍然无法访问它们的内容...所以这根本不起作用。
后来我发现另一个使用此技巧的应用程序,名为“MiXplorer”。在此应用程序上,它无法直接请求“Android”文件夹(也许它甚至没有尝试),但是一旦您允许它,它确实授予您对其及其子文件夹的完全访问权限。而且,它针对API 30进行了定位,这意味着它之所以不起作用,仅仅是因为您的目标是API 29。
我注意到(有人给我写信)通过对代码进行一些更改,我可以单独请求每个子文件夹的访问权限(即请求“data”和请求“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()

然而,一旦您获得了访问权限,您将无法通过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()

问题

  1. 我如何直接向“Android”文件夹请求一个意图,以便实际让我访问它,并让我获取子文件夹及其内容?
  2. 是否有其他替代方法?也许使用adb和/或root,我可以授予SAF对此特定文件夹的访问权限?

我可以授予 SAF 访问这个特定文件夹的权限吗?不对。你不能授予 SAF 任何东西。相反,应该庆幸 SAF 授予了你访问权限。 - blackapps
我看到并引用了以下代码:// Provide read access to files and sub-directories in the user-selected // directory. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);。嗯,这个文档中有一个错误。将其删除,你就会发现。再想一想:如果没有WRITE标志,它怎么能运行呢? - blackapps
还是不行。我试过没有,我试了FLAG_GRANT_WRITE_URI_PERMISSION,我两个都试了。所有这些选项都给了我相同的结果。另外,由于某种原因,有时候它没有显示给我“使用此文件夹”按钮,我不得不跳转到另一个文件夹然后再返回来。看起来像是文件选择器界面上的奇怪问题。您能否也试一下? - android developer
好的,但是就像我现在写的那样,即使没有它们也没有帮助。你能检查一下吗? - android developer
2
你看过这个错误报告吗?它提到了关于Amaze文件管理器的GitHub存储库上的一些讨论,可能会对你有所帮助。 - Cheticamp
显示剩余25条评论
3个回答

25
以下是X-plore的工作方式:
Build.VERSION.SDK_INT>=30 时,[Internal storage]/Android/data 文件夹不可访问,java 中的 File.canRead() 或者 File.canWrite() 返回 false,因此我们需要使用替代文件系统来处理该文件夹中的文件(以及可能的 obb 文件)。
你已经知道了存储访问框架的工作原理,所以我将详细说明需要做什么。
你可以调用 ContentResolver.getPersistedUriPermissions() 来查看是否已经保存了该文件夹的权限。最初你没有该权限,因此需要请求用户授权:
要请求访问权限,请使用 startActivityForResultIntent(Intent.ACTION_OPEN_DOCUMENT_TREE).putExtra(DocumentsContract.EXTRA_INITIAL_URI, DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Android"))。 在这里,你将使用 EXTRA_INITIAL_URI 将选择器直接设置为主存储上的 Android 文件夹,因为我们想要访问 Android 文件夹。当你的应用程序针对 API30 时,选择器不会允许选择存储的根目录,并且通过获取对 Android 文件夹的许可,你可以使用一个权限请求来处理其中的 dataobb 文件夹。
当用户通过两次点击确认后,在 onActivityResult 中,你将获得数据中的 Uri,该 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}(与 onActivityResult 中获取的 Uri 相同)。遍历从 getPersistedUriPermissions 获取的列表,找到感兴趣的 Uri,如果找到,请使用它,否则请请求用户授权。
现在您想要使用此“tree”uri和Android文件夹内文件的相对路径与ContentResolverDocumentsContract一起工作。以下是列出data文件夹的示例:data/是相对于授予的“tree”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中的数据以获取文件夹列表。
以类似的方式,您可以使用ContentResolverDocumentsContract的方法来读取/写入/重命名/移动/删除/创建其他SAF功能,这可能已经是您所知道的。
一些细节:
  • 它不需要android.permission.MANAGE_EXTERNAL_STORAGE
  • 它适用于目标API 29或30
  • 它仅适用于主存储,而不适用于外部SD卡
  • 对于数据文件夹中的所有文件,您需要使用SAF(java File将无法工作),只需使用相对于Android文件夹的文件层次结构即可
  • 在未来,Google可能会在其“安全”意图中修补此漏洞,并且在某些安全更新后可能无法使用

编辑:基于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
        )
    }
}

1
@androiddeveloper 这绝对是你正在寻找的答案。这里有一个函数的要点,一旦允许访问"../Android",就会记录条目在"../Android/data"中。(代码可能更紧凑,但它演示了这个概念。) - Cheticamp
2
@androiddeveloper 这里是一个小应用程序,演示了如何遍历“data”目录。正如您所知,File().listFiles()将为您提供“../Android/”的顶级内容,因此您应该能够使用应用程序中的逻辑来遍历“obb”以及您在那里找到的任何其他内容。 - Cheticamp
1
@Cheticamp 非常感谢!我根据您的写作和我的发现更新了当前答案,提供了一个简化的示例。顺便说一句,由于有太多文件夹,您的样本对我来说冻结了,即使它即将显示内容,由于暗色主题,它也显示了暗色背景上的暗色文本... - android developer
1
@Cheticamp 希望 Google 会忽略这个问题,或者需要很久才能“修复”它。 - android developer
1
@androiddeveloper你好,我想要读取初始URI中的“Android / media”文件夹,这个功能已经正常工作。但是问题在于,如果我选择像“Android / media / any_app_package_name”这样的路径并尝试从“Android / media / any_app_package_name”读取数据,那么它就无法正常工作.....我不知道为什么。你有任何想法吗? - pratik vekariya
显示剩余13条评论

1

好的,我尝试了这段代码,它在Android API 29、三星Galaxy 20FE上运行良好:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE_STORAGE_ACCESS) {
        Uri treeUri = null;
        // Get Uri from Storage Access Framework.
        treeUri = data.getData();

        // Persist URI in shared preference so that you can use it later.
        // Use your own framework here instead of PreferenceUtil.
        MySharedPreferences.getInstance(null).setFileURI(treeUri);

        // Persist access permissions.
        final int takeFlags = data.getFlags()
                & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        getContentResolver().takePersistableUriPermission(treeUri, takeFlags);

        createDir(DIR_PATH);


        finish();
    }
}

private void createDir(String path) {
    Uri treeUri = MySharedPreferences.getInstance(null).getFileURI();

    if (treeUri == null) {
        return;
    }

    // start with root of SD card and then parse through document tree.
    DocumentFile document = DocumentFile.fromTreeUri(getApplicationContext(), treeUri);
    document.createDirectory(path);
}

我是从按钮的onClick事件中调用这个函数:

Button btnLinkSd = findViewById(R.id.btnLinkSD);
    btnLinkSd.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            triggerStorageAccessFramework();
        }
    });

在用户界面中,我点击“显示内部存储”,然后导航到Android目录并点击允许。之后,在调试时,如果我尝试列出Android下的所有文件,则会得到Data中所有目录的列表。如果这就是您要寻找的内容。 enter image description here enter image description here enter image description here

最后,在调试中得到的结果为: enter image description here


4
问题(标题中已经提到)出在 Android API 30(Android 11)上。在 Android 10 上,无法访问 Android 文件夹及其子文件夹的限制不存在。请在 Android 10 上尝试。 - android developer
很抱歉,我没有安卓11,但我相信它应该仍然可以工作,因为它与遗留标志或其他任何东西都无关。 - Dan Baruch
有一个模拟器已经存在了相当长的时间... - android developer
是的,但它们已被root,我认为它们不会有效。没问题,我会在Android 11上检查一下。 - Dan Baruch
没关系。根据他们给我的内容,我制作了一个样本。 - android developer
显示剩余2条评论

1

"在Android 11上测试的Java版本" 这将从assets文件夹复制文件到android/data/xxx内的任何目录

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class MainActivity extends AppCompatActivity 
{
   private final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents";
    private final String ANDROID_DOCID =
            "primary:Android/data/xxxxFolderName";
    Uri uri;
    Uri treeUri;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button b=findViewById(R.id.ok);
        uri = DocumentsContract.buildDocumentUri(
                EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
                ANDROID_DOCID
        );
        treeUri = DocumentsContract.buildTreeDocumentUri(
                EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
                ANDROID_DOCID
        );
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            openDirectory();
        }

    }

    private Boolean checkIfGotAccess() {
        List<UriPermission> permissionList = getContentResolver().getPersistedUriPermissions();
        for (int i = 0; i < permissionList.size(); i++) {
            UriPermission it = permissionList.get(i);
            if (it.getUri().equals(treeUri) && it.isReadPermission())
                return true;
        }
        return false;
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    private void openDirectory() {

        if (checkIfGotAccess()) {
            copyFile(treeUri);
            //return;
        }
        Intent intent =
                getPrimaryVolume().createOpenDocumentTreeIntent()
                        .putExtra(EXTRA_INITIAL_URI, uri);
        ActivityResultLauncher<Intent> handleIntentActivityResult = registerForActivityResult(
                new ActivityResultContracts.StartActivityForResult(),
                result -> {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        if (result.getData() == null || result.getData().getData() == null)
                            return;
                        Uri directoryUri = result.getData().getData();
                        getContentResolver().takePersistableUriPermission(
                                directoryUri,
                                Intent.FLAG_GRANT_READ_URI_PERMISSION
                        );
                        if (checkIfGotAccess())
                            copyFile(treeUri);
                        else
                            Log.d("AppLog", "you didn't grant permission to the correct folder");
                    }
                });
        handleIntentActivityResult.launch(intent);

    }


    @RequiresApi(api = Build.VERSION_CODES.N)
    private StorageVolume getPrimaryVolume() {
        StorageManager sm = (StorageManager) getSystemService(STORAGE_SERVICE);
        return sm.getPrimaryStorageVolume();
    }

    private void copyFile(Uri treeUri) {
        AssetManager assetManager = getAssets();

        OutputStream out;
        DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
        String extension = "ini";
        try {
            InputStream inn = assetManager.open("xxxxfileName.ini");
            assert pickedDir != null;
           DocumentFile existing = pickedDir.findFile("xxxxfileName.ini");
           if(existing!=null)
               existing.delete();
            DocumentFile newFile = pickedDir.createFile("*/" + extension, "EnjoyCJZC.ini");
            assert newFile != null;
            out = getContentResolver().openOutputStream(newFile.getUri());
            byte[] buffer = new byte[1024];
            int read;
            while ((read = inn.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
            inn.close();
            out.flush();
            out.close();
        } catch (Exception fnfe1) {
            fnfe1.printStackTrace();
        }
    }

}

复制文件?首先我需要能够在那里读取。获取每个文件夹中有哪些文件。另外,不幸的是我认为在Android 12上,Google“修复”了这个问题,因此开发人员不能再使用这个解决方法了:( - android developer

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