Android: FileProvider.getUriForFile reading MediaStore-generated file?

Viewed 871

TL;DR

I wanna get(read/generate) file Uri -- with path like below -- by FileProvider, but don't know how:

val file = File("/external/file/9359") // how do I get its Uri then?

All I know was FileProvider.getUriForFile method, but it throws exception.

What I was doing

I was trying to mock a download progress -- create a file in Download folder, and then pass it to ShareSheet to let user do whatever it want to do.

What I have done:

  1. Create file in Download by using MediaStore and ContentResolver.
  2. Have a ShareSheet util function.
  3. Registered FileProvider and filepath.xml.

In my case, I want to share the file via ShareSheet function, which requires

  • The Uri of File

but the usual file.toUri() will throw exception above SDK 29. Hence I change it into FileProvider.getUriForFile() as Google-official recommended.

The Direct Problem Code

val fileUri: Uri = FileProvider.getUriForFile(context, "my.provider", file)

Will throw exception:

java.lang.IllegalArgumentException: Failed to find configured root that contains /external/file/9359

My File Creation Code

val fileUri: Uri? = ContentValues().apply {
    put(MediaStore.MediaColumns.DISPLAY_NAME, "test.txt")
    put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // create file in download directory.
        put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
    }
}.let { values ->
    context.contentResolver.insert(MediaStore.Files.getContentUri("external"), values)
}
if (fileUri == null) return

context.contentResolver.openOutputStream(fileUri)?.use { fileOut ->
    fileOut.write("Hello, World!".toByteArray())
}

val file = File(fileUri.path!!) // File("/external/file/9359")

I can assure the code is correct because I can see the correct file in Download folder.

My FileProvider Setup

I have registered provider in AndroidManifest.xml:

<application>
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="my.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepath" />
    </provider>
</application>

with filepath.xml below:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="external"
        path="." />
</paths>

I've also tried so many path variants, one by one:

<external-files-path name="download" path="Download/" />
<external-files-path name="download" path="." />
<external-path name="download" path="Download/" />
<external-path name="download" path="." />
<external-path name="external" path="." />
<external-files-path name="external_files" path="." />

I also even registered external storage reading permissions:

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

which part did I do wrong?

Edit 1

After posting, I thought the File initialization might be the problem:

File(fileUri.path!!)

So I changed it to

File(fileUri.toString())

but still not working.

Their content difference is below btw:

fileUri.path       -> /external/file/9359
(file.path)        -> /external/file/9359
error              -> /external/file/9359

fileUri.toString() -> content://media/external/file/9359
(file.path)        -> content:/media/external/file/9359
error              -> /content:/media/external/file/9359

Edit 2

What I originally wanted to achieve is sending binary data to other app. As Official-documented, it seems only accept nothing but file Uri.

I'd be appreciate if there's any other way to achieve this, like share the File directly, etc.

But what I'm wondering now is simple -- How do I make FileProvider available to get/read/generate file Uri like /external/file/9359 or so on? This might comes in help not only this case, and seems like a more general/basic knowledge to me.

6 Answers

Content uris does not have file-path or scheme. You should create an input stream and by using this inputstream create a temporary file. You can extract a file-uri or path from this file. Here is a function of mine to create a temporary file from content-uri:

 fun createFileFromContentUri(fileUri : Uri) : File{

    var fileName : String = ""

    fileUri.let { returnUri ->
        requireActivity().contentResolver.query(returnUri,null,null,null)
    }?.use { cursor ->
        val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
        cursor.moveToFirst()
        fileName = cursor.getString(nameIndex)
    }
    
    //  For extract file mimeType
    val fileType: String? = fileUri.let { returnUri ->
        requireActivity().contentResolver.getType(returnUri)
    }

    val iStream : InputStream = requireActivity().contentResolver.openInputStream(fileUri)!!
    val outputDir : File = context?.cacheDir!!
    val outputFile : File = File(outputDir,fileName)
    copyStreamToFile(iStream, outputFile)
    iStream.close()
    return  outputFile
}

fun copyStreamToFile(inputStream: InputStream, outputFile: File) {
    inputStream.use { input ->
        val outputStream = FileOutputStream(outputFile)
        outputStream.use { output ->
            val buffer = ByteArray(4 * 1024) // buffer size
            while (true) {
                val byteCount = input.read(buffer)
                if (byteCount < 0) break
                output.write(buffer, 0, byteCount)
            }
            output.flush()
        }
    }
   }

--Edit--

Content Uri has path but not file path. For example;

content Uri path: content://media/external/audio/media/710 ,

file uri path : file:///sdcard/media/audio/ringtones/nevergonnagiveyouup.mp3

By using the function above you can create a temporary file and you can extract uri and path like this:

val tempFile: File = createFileFromContentUri(contentUri)  
val filePath = tempFile.path  
val fileUri = tempFile.getUri() 

After using tempFile delete it to prevent memory issues(it will be deleted because it is written in cache ):

tempFile.delete()

There is no need to edit content uri. Adding scheme to content uri probably not gonna work

First of all in recent Android you normally don't get the file path from the Uri, cause you might have only temporary access to it, you usually copy the content from it's stream using context.contentResolver.openInputStream(externalUri) to a file you can then use after.

By doing this you are putting a file in a directory not owned by your application, so FileProvider might not be needed and you could just pass the uri you got from MediaStore insertion.

The correct way to do use FileProvider would be putting the file in one of yours FileProvider paths without MediaStore so let's say you have

<provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepath" />
</provider>

and

<external-files-path name="external" path="shared" />

you would create file in that path

val sharedFolder=context.getExternalFilesDir("shared")
val fileName="test.txt"

File(sharedFolder,fileName).outputStream()?.use { fileOut ->
    fileOut.write("Hello, World!".toByteArray())
}

val fileProviderUri = FileProvider.getUriForFile(
                    context,
                    BuildConfig.APPLICATION_ID + ".provider",
                    File(sharedFolder,fileName)
                )
//example intent for viewing file, to change if you need to pass to another application
                context.startActivity(
                    Intent(Intent.ACTION_VIEW).setData(fileProviderUri )
                        .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                )

bro try this one may it works for you , change your path.xml file to this ;

<?xml version="1.0" encoding="utf-8"?>
<paths>
  <external-path
    name="external"
    path="." />
  <external-files-path
    name="external_files"
    path="." />
  <cache-path
    name="cache"
    path="." />
  <external-cache-path
    name="external_cache"
    path="." />
<files-path
    name="files"
    path="." />
</paths>

or check this one also if behind solution not working

https://stackoverflow.com/a/42516202/7905787

content:/media/external/file/9370

You can just read that mediastore uri by opening a stream for it:

InputStream is = getContentResolver().openInpurStream(uri);

Then read from the stream.

If you wanna share the file then use this uri as is.

No need to use FileProvider if you have already a nice uri.

public  void image_download(Context context, String uRl, String bookNmae) {

        File direct = new File(Environment.getExternalStorageDirectory()
                + "/Islam Video App/books");

        if (!direct.exists()) {
            direct.mkdirs();
        }

        DownloadManager mgr = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);

        Uri downloadUri = Uri.parse(uRl);
        DownloadManager.Request request = new DownloadManager.Request(
                downloadUri);

        request.setAllowedNetworkTypes(
                DownloadManager.Request.NETWORK_WIFI
                        | DownloadManager.Request.NETWORK_MOBILE)
                .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
                .setAllowedOverRoaming(false).setTitle(bookNmae)
                .setDestinationInExternalPublicDir("/Islam Video App/books", bookNmae + ".pdf");

        mgr.enqueue(request);

    }

Time has passed and I finally figured it out -- kinda, I guess?

I'd leave what I know so far here. Correct me if anything wrong.

TL; DR

For a file generated like:

val file = File("/external/file/9359")

There is NO WAY to get its (full) uri back, even by FileProvider.getUriForFile.

In fact -- "/external/file/9359" is not a standard file uri. I'd explain more below.

Let's talk about few things:

  1. What (exactly) is FileProvider?
  2. What does MediaStore do?
  3. How to combine 2 of them?
  4. How do I do the download-process finally?

For a quicker, briefer version of file/uri/path concepts, please view the answer of @Alperen Acikgoz.

Concept of FileProvider

As the official ref doc stated:

FileProvider is a special subclass of ContentProvider that facilitates secure sharing of files associated with an app by creating a content://Uri for a file instead of a file:///Uri.

So, what FileProvider does is actually very clear -- it just transform the uri, making it more secure to use and share.

An original File uri might looks like this:

file:///sdcard/media/audio/ringtones/nevergonnagiveyouup.mp3

Then FileProvider might change it to something like

content://my.app.fileprovider/sercet-bgm/nevergonnagiveyouup.mp3

That's all what a FileProvider does.

It actually DOESN'T fix the Uri, nor generate/get a File. All it does is to transform a (legal) file Uri into a more-secure one.

Also note that FileProvider comes from androidx.

Using of MediaStore

The official guides of media files and official ref doc is also very clear.

Long story short, the MediaStore series (combines with ContentResolver) provides app a way to access/update/create a media file, such as Image, Video, Audio, or even any File, in specific folders like Download, DCIM/, etc.

The MediaStore series are like a protocal between app and Android system. And the file created/modified can be shared/accessed by ANY OTHER APP. (Or user itself)

The result of MediaStore series usually comes with a Uri:

val values = ContentValues() // set something inside

val fileUri = context.contentResolver.insert(MediaStore.Files.getContentUri("external"), values)

Note that the fileUri above would be something like this:

// fileUri.toString()
"content://media/external/file/9359"

The MediaStore series (along with ContentResolver) are Android built-in feature.

FileProvider and MediaStore

So, what's the relation between FileProvider and MediaStore series?

The answer is NOTHING. They don't really have anything related.

  1. FileProvider solves the problem of deprecated, less-secure file:/// uri.
  2. MediaStore provides a way to access media file, giving uri like content://media/external/...

So if you are looking for how to combine 2 of them, just give up. They don't work together.

Bonus: The Download Process

So how do I archive the download process, the original goal?

I just gave up the MediaStore series. Instead, I use the method @Alperen Acikgoz mentioned-- byte-to-byte copy to create a file:

// create file
val file: File? = context.getExternalFilesDir(null)?.let { File(it, fileName) }
// Write File: Simple Copy Process
val iStream: InputStream // any file from local, downloading, etc.
file?.outputStream().use { outputStream ->
    var bytesCopied = 0
    val bufferLengthBytes = 1024
    val buffer = ByteArray(bufferLengthBytes)
    try {
        var bytes = iStream.read(buffer)
        while (bytes >= 0) {
            outputStream.write(buffer, 0, bytes)
            bytesCopied += bytes
            bytes = iStream.read(buffer)
            val progress = bytesCopied * 1f / length // if needed
        }
    } catch (e: IOException) {
        // failed writing. Might be insufficient storage.
    }
}
iStream.close()

To share it, use FileProvider to pass to Share Dialog:

val file: File
assertNotNull(file)
val fileUri: Uri = FileProvider.getUriForFile(context, "my.app.provider", file)

// from 
val shareIntent: Intent = Intent().apply {
    action = Intent.ACTION_SEND
    putExtra(Intent.EXTRA_STREAM, fileUri)
    type = "image/jpeg" // or any other type, depending on file.
}
startActivity(Intent.createChooser(shareIntent, null))

Some notes about the process above:

  1. Yes, the file would have 2 copies at the time. But it would be cleared after memory released.
  2. You may use MediaStore series to create a file instead. (If you don't need byte-to-byte creation)
  3. The file will NOT exist in Download/ folder by default. To do so, use MediaStore to insert one (by copying)

That's all I know so far. Hope this helps the future googlers.

Related