10

Background

There are various storage restrictions on Android 10 and 11, which also includes a new permission (MANAGE_EXTERNAL_STORAGE) to access all files (yet it doesn't allow access to really all files ) while the previous storage permission got reduced to grant access just to media files :

  1. Apps can reach the "media" sub folder freely.
  2. Apps can never reach "data" sub folder and especially the content.
  3. For "obb" folder, if the app was allowed to install apps, it can reach it (to copy files to there). Otherwise it can't.
  4. Using USB or root, you could still reach them, and as an end user you can reach them via the built-in file-manager app "Files".

The problem

I've noticed an app that somehow overcome this limitation (here) called "X-plore": Once you enter "Android/data" folder, it asks you to grant access to it (directly using SAF, somehow), and when you grant it, you can access everything in all folders of "Android" folder.

This means there might still be a way to reach it, but problem is that I couldn't make a sample that does the same, for some reason.

What I've found and tried

It seems this app targets API 29 (Android 10), and that it doesn't use the new permission yet, and that it has the flag requestLegacyExternalStorage. I don't know if the same trick they use will work when targeting API 30, but I can say that on my case, running on Pixel 4 with Android 11, it works fine.

So I tried to do the same:

  1. I made a sample POC that targets Android API 29, has storage permissions (of all kinds) granted, including the legacy flag.

  2. I tried to request access directly to "Android" folder (based on here), which sadly didn't work as it goes to some reason (kept going to DCIM folder, no idea why) :

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)

I tried various flags combinations.

  1. When launching the app, when I reach the "Android" folder myself manually as this didn't work well, and I granted the access to this folder just like on the other app.

  2. When getting the result, I try to fetch the files and folders in the path, but it fails to get them:

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://stackoverflow.com/q/56657639/878126
    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")
}

So using DocumentFile.fromTreeUri I could still get just "media" folder which is useless, and using the File class I could only see there are also "data" and "obb" folders, but still couldn't reach their contents...

So this didn't work well at all.

Later I've found out another app that uses this trick, called "MiXplorer". On this app, it failed to request "Android" folder directly (maybe it didn't even try), but it does grant you full access to it and its sub-folders once you allow it. And, it targets API 30, so this means it's not working just because you target API 29.

I've noticed (someone wrote me) that with some changes to the code, I could request access to each of the sub-folders separately (meaning a request for "data" and a new request for "obb"), but this is not what I see here, that apps do.

Meaning, to get to "Android" folder, I get use this Uri as a parameter for 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()

However, once you get an access to it, you won't be able to get the list of files from it, not via File, and not via SAF.

But, as I wrote, the weird thing is that if you try something similar, of getting to "Android/data" instead, you will be able to get its content:

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

The questions

  1. How can I request an Intent directly to "Android" folder that will actually let me access to it, and let me get the sub-folders and their contents?
  2. Is there another alternative for this? Maybe using adb and/or root, I could grant SAF access to this specific folder ?
android developer
  • 106,412
  • 122
  • 641
  • 1,128
  • `.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION... ` That does not makes sense. You cannot grand anything. Instead you should be glad that something is granted to you in onActivityResult. Better remove that code. – blackapps Jan 30 '21 at 12:13
  • `I could grant SAF access to this specific folder ?` Wrong. You cannot grand saf anything. Instead be glad saf grands you access. – blackapps Jan 30 '21 at 12:14
  • Quote: The app achieves that by targeting Android 10, which allows it to temporarily opt-out of scoped storage. – blackapps Jan 30 '21 at 12:23
  • The flag is mentioned on the docs too, so it shouldn't be removed: https://developer.android.com/training/data-storage/shared/documents-files . About "I could request", read the entire sentence, as I wrote about doing it via adb/root. As for the " temporarily opt-out" on the article, I don't think this is correct, as it's about the storage permission, not necessary about "Android" folder. Besides, my POC is also targetting API 29, so it still doesn't explain how it's done. – android developer Jan 30 '21 at 12:28
  • I see and quote `// Provide read access to files and sub-directories in the user-selected // directory. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);`. Well that is an error in that doc. Remove it and you will see. And think about it: how could it do without a WRITE flag then? – blackapps Jan 30 '21 at 13:27
  • Still didn't work. I tried without, I tried FLAG_GRANT_WRITE_URI_PERMISSION instead, I tried them both. All of these options get me the same result. Also, for some reason, sometimes it didn't show me the "use this folder" button, and I had to go to a different folder and back. Seems like weird issues on the folder-picker UI. Can you please try it out too? – android developer Jan 30 '21 at 13:35
  • I have seen such things before. But I do not put those flags on that intent to begin with. As said: makes no sense. At taking the permanent uri permissions just look which ones are offered to you. You will see that putting them has no effect at all. That doc is wrong. – blackapps Jan 30 '21 at 13:41
  • OK but as I wrote now, even without them it didn't help. Can you please check it out? – android developer Jan 30 '21 at 14:16
  • About targeting Android 10, seems the access to the folder is still possible even when targeting Android 11. That's because I've noticed MixPlorer succeeding in this. – android developer Jan 30 '21 at 14:34
  • I have no idea what i should check out. And indeed you should target 10 and add that extra flag. – blackapps Jan 30 '21 at 14:38
  • As I wrote, you can still target API 30 (Android 11) and it will still work. I noticed it on a different app. Therefore, the target API doesn't matter (at least for now). – android developer Jan 30 '21 at 14:55
  • ??? I have no idea what i should check. – blackapps Jan 30 '21 at 15:04
  • I've presented code. You say that you know how to do it and that I did it wrong. Please try either my code or show your own, of how to reach the folders. The question still is about how to reach the folders (and would be very nice to know how to have request to be directly to the folder, too). – android developer Jan 30 '21 at 15:21
  • ??? What did i say that i know how to do it? I never said such things. – blackapps Jan 30 '21 at 15:41
  • You wrote 3 things in the beginning saying that I'm wrong (and I showed I'm not, BTW), so this means you think you know what should be done. Please explain what should be done. – android developer Jan 30 '21 at 15:59
  • 2
    Have you seen [this](https://issuetracker.google.com/issues/178910699) bug report? It has some references to a discussion on the GitHub repository for [Amaze File Manager](https://github.com/TeamAmaze/AmazeFileManager) that may be of interest to you. – Cheticamp Feb 08 '21 at 18:42
  • @Cheticamp Thank you. The app you've presented doesn't seem to do it. But the bug report shows what I've found. So you think it's some loophole? Somehow Total Commander app can also reach there (though it's a bit uncomfortable to do so). – android developer Feb 08 '21 at 20:15
  • I think the intent is pretty clear to restrict access to those Android folders, so it looks like a loophole to me. It will be interesting to see the response to the bug report. I hope, as I am sure many, many others do, that there is some legal way for the user to permit access. – Cheticamp Feb 08 '21 at 20:26
  • I am going to retract that last comment. I don't think that it is a loophole. Take a look at [this comment](https://android.stackexchange.com/a/231482). SD Maid and "X-Plore" ask for permission to ../Android/... the same way, so I am thinking that it is standard method. Still, it will be interesting to see the response to the bug report I posted in my last comment. – Cheticamp Feb 08 '21 at 23:04
  • @Cheticamp I see. Why did he mention "root/data/media/0/Android/data" though? What is this? – android developer Feb 09 '21 at 09:55
  • I think that he is saying that those two paths point to the same place. Don't know specifically about "root/data/media/0/Android/data" though. – Cheticamp Feb 09 '21 at 11:36
  • @Cheticamp Do you know perhaps how to implement the usage of this "loophole" (whether it is as such or not) ? Can you please try ? I have no idea why&how some file manager apps seem to have had it automatically already, somehow, either. – android developer Feb 09 '21 at 14:15
  • https://developer.android.com/about/versions/11/privacy/storage – Abdelrahman Farag Feb 17 '21 at 21:26
  • @AbdelrahmanFarag Please read the question. Some apps succeeded getting there. – android developer Feb 17 '21 at 22:29
  • `How can I request an Intent directly to "Android" folder?` Full bounty? – blackapps Feb 25 '21 at 15:44
  • `looking for an answer from a reputable source. ` What do you mean by that? – blackapps Feb 25 '21 at 15:49
  • It seems you did not reed my first comment today. – blackapps Feb 25 '21 at 16:19
  • @blackapps I don't understand. – android developer Feb 25 '21 at 21:13
  • You are not interested in a solution only for question one. Well ok. No bounty for that. – blackapps Feb 25 '21 at 21:18
  • @blackapps I actually got it somehow (check updated question), but as I wrote, this isn't enough, because I want real access to it, like those apps do. I've updated the question to explain it better. Now this part is in question 1, because it's of the same thing. – android developer Feb 25 '21 at 22:00

2 Answers2

8

Here is how it works in X-plore:

When on Build.VERSION.SDK_INT>=30, [Internal storage]/Android/data is not accessible, java File.canRead() or File.canWrite() returns false, so we need to switch to alternative file system for files inside of this folder (and possibly also obb).

You already know how Storage access framework works, so I'll just give details about what needs to be done exactly.

You call ContentResolver.getPersistedUriPermissions() to find out if you already have saved permission for this folder. Initially you don't have it, so you ask user for permission:

To request access, use startActivityForResult with Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).putExtra(DocumentsContract.EXTRA_INITIAL_URI, DocumentsContract.buildDocumentUri("com.android.externalstorage.documents", "primary:Android")) Here you set with EXTRA_INITIAL_URI that picker shall start directly on Android folder on primary storage, because we want access to Android folder. When your app will target API30, picker won't allow to choose root of storage, and also by getting permission to Android folder, you can work with both data and obb folders inside, with one permission request.

When user confirms by 2 clicks, in onActivityResult you'll get Uri in data which should be content://com.android.externalstorage.documents/tree/primary%3AAndroid. Make needed checks to verify that user confirmed correct folder. Then call contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION) to save permission, and you're ready.

So we're back to ContentResolver.getPersistedUriPermissions(), which contains list of granted permissions (there may be more of them), the one you've granted above looks like this: UriPermission {uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid, modeFlags=3} (same Uri as you got in onActivityResult). Iterate the list from getPersistedUriPermissions to find uri of interest, if found work with it, otherwise ask user for grant.

Now you want to work with ContentResolver and DocumentsContract using this "tree" uri and your relative path to files inside of Android folder. Here is example to list data folder: data/ is path relative to granted "tree" uri. Build final uri using either DocumentsContract.buildChildDocumentsUriUsingTree() (to list files) or DocumentsContract.buildDocumentUriUsingTree() (for working with individual files), example: DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri), DocumentsContract.getTreeDocumentId(treeUri)+"/data/"), you'll get uri=content://com.android.externalstorage.documents/tree/primary%3AAndroid/document/primary%3AAndroid%2Fdata%2F/children suitable for listing files in data folder. Now call ContentResolver.query(uri, ...) and process data in Cursor to get folder listing.

Similar way you work with other SAF functionality to read/write/rename/move/delete/create, which you probably already know, using ContentResolver or methods of DocumentsContract.

Some details:

  • it doesn't need android.permission.MANAGE_EXTERNAL_STORAGE
  • it works on target API 29 or 30
  • it works only on primary storage, not for external SD cards
  • for all files inside of data folder, you need to use SAF (java File won't work), just use file hierarchy relative to Android folder
  • in future Google may patch this hole in their "security" intentions, and this may not work after some security update

EDIT: sample code, based on Cheticamp Github sample. The sample shows the content (and file-count) of each of the sub-folders of "Android" folder:

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
        )
    }
}
android developer
  • 106,412
  • 122
  • 641
  • 1,128
Pointer Null
  • 36,993
  • 13
  • 79
  • 106
  • This looks promising. Can you please make a sample app here, which holds the SAF granting, and prints the number of files in "Android" folder and for each of its sub-folders, for example? – android developer Mar 03 '21 at 23:14
  • You got plenty of advice on a complex topic. – loop Mar 03 '21 at 23:20
  • 1
    @androiddeveloper This is definitely the answer you are looking for. [Here](https://gist.github.com/Cheticamp/8c4ac4e596956a60311b492c456fbacd) is a gist with a function that will log entries in "../Android/data" once access is permitted to "../Android". (The code could be tighter, but it demonstrates the concept.) – Cheticamp Mar 04 '21 at 02:05
  • Sorry, no time for making a demo. You should be able to make it in own app with this information. – Pointer Null Mar 04 '21 at 07:48
  • @PointerNull I don't understand the part of when you got the Uri. It seems you don't use `DocumentFile.fromTreeUri(this, treeUri)` and then use `listFiles()` on it. I tried what you wrote instead of what I know of, and using `contentResolver.query` on the new Uri that you've made didn't result in the real content of "Android" folder. It showed me only "media" and another folder that I've added there, but no "data" and no "obb". – android developer Mar 04 '21 at 17:50
  • @androiddeveloper data and obb will not be listed as children of Android dir. Request them manually and you will see their contents. – artman Mar 04 '21 at 19:00
  • @artman Actually for some reason using File API on the path got me to list those folders. But from there I don't get how to get the children of "data" and "obb". The instructions aren't clear for me :( – android developer Mar 04 '21 at 19:12
  • 1
    @androiddeveloper [Here](https://github.com/Cheticamp/SAFWalker) is a small app that demonstrates how to walk the "data" directory. As you know, `File().listFiles()` will give you the top-level contents of "../Android/", so you should be able to use the logic in the app to walk "obb" as well as anything else you find there. – Cheticamp Mar 04 '21 at 19:18
  • @androiddeveloper grant SAF access to `Android` folder and query `ContentResolver` for `content://com.android.externalstorage.documents/tree/primary%3AAndroid/document/primary%3AAndroid%2Fdata/children`. It will make things clear – artman Mar 04 '21 at 19:19
  • 1
    @Cheticamp Thank you very much! Updated current answer to have a simplified sample based on what you wrote and what I've found. BTW, your sample froze for me due to too many folders, and even when it was about to show content, due to dark theme, it showed dark text on dark background ... – android developer Mar 04 '21 at 21:22
  • @androiddeveloper I'll take a look. You realize that this is just a vulnerability in Android 11 and it will likely be sealed up in the next release. – Cheticamp Mar 04 '21 at 21:43
  • @Cheticamp Hopefully Google will ignore this, or take forever to "fix" it. – android developer Mar 04 '21 at 22:07
0

Well, I tried this code and it works on Android API 29, Samsung 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);
}

I'm calling this from a button onClick:

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

In the UI, I'm pressing "show internal storage", I navigate to Android directory and press allow. After that, in debugging, if I try to list all files under android I'm getting a list of all directories in Data. If that's what you are looking for. enter image description here enter image description here enter image description here

And finally, results in debug: enter image description here

Dan Baruch
  • 526
  • 4
  • 11
  • 2
    The issue (written in the title too) is on Android API 30 (Android 11). On Android 10 the restriction of reaching the Android folder and subfolders doesn't exist. Please try on it – android developer Mar 03 '21 at 21:37
  • I'm afraid I don't have any android 11, but I believe it should still work, as it's not related to the legacy flag or anything. – Dan Baruch Mar 03 '21 at 22:08
  • There is an emulator that exists for quite some time... – android developer Mar 04 '21 at 00:46
  • Yes, but they are rooted I thought they won't be valid. No problem, I'll check it on Android 11 – Dan Baruch Mar 04 '21 at 05:33
  • Well, the emulator is not working for me here and it seems you already got a better answer so I'll skip checking it on Android 11 – Dan Baruch Mar 04 '21 at 09:28
  • The issue is about Android 11. Only then they added the restriction of reaching Android folder and seeing its content. Can you please provide a sample project that I can try, then? You can upload it to here if you wish : https://uploadfiles.io/ – android developer Mar 04 '21 at 17:48
  • Never mind. Made a sample based on what they wrote me. – android developer Mar 04 '21 at 21:35