5

We only get a tree URI when a user selects a folder using the storage access framework. The docs mention that starting Android 11, we can use raw file paths to access files. I need this so that I can use an existing native library. But I can't find any documentation of getting the raw path given a tree URI. How to get file path given a tree URI on Android 11?

Note that all of the existing answers on stackoverflow are hacks and they are not even working now. I need an official API for this.

Ganesh Bhambarkar
  • 930
  • 1
  • 8
  • 9
  • 1
    You do not have file access to anything you receive from Storage Access Framework. That link is for Media files (i.e., via `MediaStore`) – ianhanniballake Apr 15 '20 at 03:07

5 Answers5

5

For APIs >= 29 the storage access options have changed so drastically that even having a file path will not be of much use due to the restrictions, required permissions, etc.

The documentation for API 30 (Andorid 11) stating that the older File API can be used, refers to the fact that such API can now be used to access/work with files located in the "shared storage" folders, so DCIM, Music, etc. Out of these locations the File API will not work, neither in Android 10.

Note that the File API in Android 11 and all future versions is much slower with a penalty hit, because has been rewritten into a wrapper, so is no longer a File system API. Underneath now delegates all the operations to the MediaStore. If performance is important then is better to use directly the MediaStore API.

I would recommend to work only with Uris / File Descriptors via the ContentResolver, and get away from real file paths. Any workaround will be temporary and harder to maintain with each new Android version.

The next sample (a bit hacky) can help to resolve a file path in APIs 29 & 30; not tested below 29. Is not official so there is no guaranty it will work for all use cases, and I recommend against using it for production purposes. In my tests the resolved path works fine with the MediaScannerConnection, but it may need adjustments to handle other requirements.

@Nullable
public static File getFile(@NonNull final Context context, @NonNull final DocumentFile document)
{
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
    {
        return null;
    }

    try
    {
        final List<StorageVolume> volumeList = context
                .getSystemService(StorageManager.class)
                .getStorageVolumes();

        if ((volumeList == null) || volumeList.isEmpty())
        {
            return null;
        }

        // There must be a better way to get the document segment
        final String documentId      = DocumentsContract.getDocumentId(document.getUri());
        final String documentSegment = documentId.substring(documentId.lastIndexOf(':') + 1);

        for (final StorageVolume volume : volumeList)
        {
            final String volumePath;

            if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q)
            {
                final Class<?> class_StorageVolume = Class.forName("android.os.storage.StorageVolume");

                @SuppressWarnings("JavaReflectionMemberAccess")
                @SuppressLint("DiscouragedPrivateApi")
                final Method method_getPath = class_StorageVolume.getDeclaredMethod("getPath");

                volumePath = (String)method_getPath.invoke(volume);
            }
            else
            {
                // API 30
                volumePath = volume.getDirectory().getPath();
            }

            final File storageFile = new File(volumePath + File.separator + documentSegment);

            // Should improve with other checks, because there is the
            // remote possibility that a copy could exist in a different
            // volume (SD-card) under a matching path structure and even
            // same file name, (maybe a user's backup in the SD-card).
            // Checking for the volume Uuid could be an option but
            // as per the documentation the Uuid can be empty.

            final boolean isTarget = (storageFile.exists())
                    && (storageFile.lastModified() == document.lastModified())
                    && (storageFile.length() == document.length());

            if (isTarget)
            {
                return storageFile;
            }
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }

    return null;
}
PerracoLabs
  • 16,449
  • 15
  • 74
  • 127
  • 2
    What is and how to pass : DocumentFile document ? – Noor Hossain Feb 19 '21 at 18:20
  • What is Document file? – Md. Rejaul Karim Mar 11 '21 at 05:03
  • @PerracoLabs using above method i got only path at media folder "/storage/emulated/0/Android/media". But my tree uri is "content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia/document/primary%3AAndroid%2Fmedia%2Fcom.whatsapp.w4b%2FWhatsApp%20Business%2FMedia%2F.Statuses%2F249c5c394fd348378627e31c1cfc332c.jpg", Any idea about how i can get file from above uri? – pratik vekariya Aug 10 '21 at 06:52
  • @pratikvekariya As said in the answer the code may or may not work. With the latest Android versions 11 and 12 I would think it shouldn't work at all anymore, as in newer Android versions the OS uses sandboxing, meaning that files are in their own private space and not visible/shareable between apps. – PerracoLabs Aug 10 '21 at 08:00
  • 1
    yeah right. I got file uri correct also that's working, set that image uri in ImageView, Image is displaying. So is there in solution to copy that uri file to another location? I have tried in many different ways but not working. Do you have any idea? Thanks for reply. – pratik vekariya Aug 10 '21 at 08:46
3
fun getFilePath(context: Context, contentUri: Uri): String? {
    try {
        val filePathColumn = arrayOf(
            MediaStore.Files.FileColumns._ID,
            MediaStore.Files.FileColumns.TITLE,
            MediaStore.Files.FileColumns.SIZE,
            MediaStore.Files.FileColumns.DATE_ADDED,
            MediaStore.Files.FileColumns.DISPLAY_NAME,
        )

        val returnCursor = contentUri.let { context.contentResolver.query(it, filePathColumn, null, null, null) }

        if (returnCursor != null) {

            returnCursor.moveToFirst()
            val nameIndex = returnCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
            val name = returnCursor.getString(nameIndex)
            val file = File(context.cacheDir, name)
            val inputStream = context.contentResolver.openInputStream(contentUri)
            val outputStream = FileOutputStream(file)
            var read: Int
            val maxBufferSize = 1 * 1024 * 1024
            val bytesAvailable = inputStream!!.available()

            val bufferSize = min(bytesAvailable, maxBufferSize)
            val buffers = ByteArray(bufferSize)

            while (inputStream.read(buffers).also { read = it } != -1) {
                outputStream.write(buffers, 0, read)
            }

            inputStream.close()
            outputStream.close()
            return file.absolutePath
        }
        else
        {
            Log.d("","returnCursor is null")
            return null
        }
    }
    catch (e: Exception) {
        Log.d("","exception caught at getFilePath(): $e")
        return null
    }
}

SimpleStorage didnt work on real device ( Samsung Galaxy Tab S7 FE Android 11 (R) ) but it was okay for emulator. Anyway i modified this code and its working perfectly now.

Its creating file in cache thats why its working perfectly with Android 11.

I am picking PDF via this function. Then i am passing the selected uri in onActivityResult or ActivityResultLauncher.

fun pickPDF(activityResult: ActivityResultLauncher<Intent>){
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.type = "application/pdf"
    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    activityResult.launch(intent)
}



pdfPickerResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult? ->
        if (result != null) {
            val intent = result.data
            if (intent != null) {
                val uri = intent.data
                if (uri != null) {
                    requireContext().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    val path = FileFinder.getFilePath(requireContext(),uri)

                    if (path != null && path.length > 3)
                    {
                        //Do your implementation.
                    }
                    else
                    {
                        Log.d("","PDF's path is null")
                    }
                }
            }
        }
    }

So ultimately we are getting Filepath from Uri on Android 11. If you want to do same thing for image etc you have to play with filePathColumn, intent values.

What about permissions ?

<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

Add these flags in manifest file.

After that we need to ask permission in runtime.

fun checkAndRequestPermissions(context: Activity): Boolean {
    val extStorePermission = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
    val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
    val mediaLocationPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_MEDIA_LOCATION)
    val listPermissionsNeeded: MutableList<String> = ArrayList()

    if (cameraPermission != PackageManager.PERMISSION_GRANTED) {
        listPermissionsNeeded.add(Manifest.permission.CAMERA)
    }
    if (extStorePermission != PackageManager.PERMISSION_GRANTED) {
        listPermissionsNeeded.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
    }
    if (extStorePermission != PackageManager.PERMISSION_GRANTED) {
        listPermissionsNeeded.add(Manifest.permission.READ_EXTERNAL_STORAGE)
    }
    if (mediaLocationPermission != PackageManager.PERMISSION_GRANTED) {
        listPermissionsNeeded.add(Manifest.permission.ACCESS_MEDIA_LOCATION)
    }
    if (extStorePermission != PackageManager.PERMISSION_GRANTED) {
        listPermissionsNeeded.add(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
    }
    if (listPermissionsNeeded.isNotEmpty()) {
        ActivityCompat.requestPermissions(context, listPermissionsNeeded.toTypedArray(), REQUEST_ID_MULTIPLE_PERMISSIONS)
        return false
    }

    return true
}

You can use this function. But we not done yet. We are still dont have full access.

We are going redirect user to app settings with that function.

private fun requestAllFilesAccessPermission(){
    if (!Environment.isExternalStorageManager()) {
        Snackbar.make(findViewById(android.R.id.content), "Permissin Needed", Snackbar.LENGTH_INDEFINITE)
            .setAction("Settings") {
                try {
                    val uri: Uri = Uri.parse("package:" + BuildConfig.APPLICATION_ID)
                    val intent =
                        Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, uri)
                    startActivity(intent)
                } catch (ex: Exception) {
                    val intent = Intent()
                    intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
                    startActivity(intent)
                }
            }.show()
    }
}
1

How to get file path given a tree URI on Android 11?

Sorry, but that is not an option.

I need this so that I can use an existing native library

Have the native library work with a filesystem location that your app can use directly on Android 10+ and on older devices (e.g., getFilesDir(), getExternalFilesDir()).

The docs mention that starting Android 11, we can use raw file paths to access files.

Yes, but:

  • That will not work on Android 10, and
  • The Storage Access Framework (e.g., ACTION_OPEN_DOCUMENT_TREE) is not involved

You would use APIs like getDirectory() on StorageVolume to work with storage volumes (external and removable) via Android 11's "all files access". See this blog post for more.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • the blog says "With no permissions, your app can create files of any type in the Documents/ and Downloads/ directories. Your app can then manipulate those files afterwards" - so, how can I get the path for document directiory for Android 11 ? – Noor Hossain Feb 17 '21 at 17:49
  • 1
    @NoorHossain: I believe that I used `Environment.getExternalStoragePublicDirectory()`, despite its deprecation, for my testing. – CommonsWare Feb 17 '21 at 17:57
  • Thanks for quick reply, I have also found that before I commented you, but I am afraid , is there any side effects that will make problems in future for using deprecation codes ? - if no, so, I can use, because, One of my published app I am stack here. In this point what to do ? – Noor Hossain Feb 17 '21 at 18:15
  • Environment.getExternalStoragePublicDirectory() --- in Android 11, when app uninstalled and install again, the app cant open its old file creating by its own. "cant access" message appeared. Is there any way, that it know the old files creating by its own even after uninstall and install again ( in 11) ? – Noor Hossain Feb 19 '21 at 18:08
  • 1
    @NoorHossain: "Is there any way, that it know the old files creating by its own even after uninstall and install again ( in 11) ?" -- no, sorry. – CommonsWare Feb 19 '21 at 19:09
1

You can with SimpleStorage:

val file = DocumentFileCompat.fromUri(context, treeUri);
file.absolutePath // => e.g. /storage/emulated/0/MyFolder/MyFile.mp4
file.basePath // => e.g. MyFolder/MyFile.mp4
Anggrayudi H
  • 14,977
  • 11
  • 54
  • 87
1

On my application compiled with androidSdkTarget:29 i can share pdf generated, on Android 10 and 11, saving file in this path:

val workSpaceDir = if (isExternalStorageWritable) {
    if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q){
        File(Globals.application.getExternalFilesDir(null))
    } else {
        File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).path + "/" + Globals.application.getString(R.string.app_name))
    }
} else {
    File(Globals.application.filesDir)
}

At the moment Environment.getExternalStoragePublicDirectory it's deprecated but still to work. This is the only way that i have found.

I have the follow permissions on my android manifest:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Here how i use it:

val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(
    Uri.fromFile(workSpaceDir + filename),
    Constants.ContentType.ApplicationPdf
)

intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
try {
    Globals.currentActivity.startActivity(intent)
} catch (e: ActivityNotFoundException) {
    Toast.makeText(
        Globals.currentActivity,
        "No PDF viewer installed.",
        Toast.LENGTH_LONG
    ).show()
}
jedi
  • 839
  • 12
  • 33