4

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.

Samuel T. Chou
  • 521
  • 6
  • 31

7 Answers7

4

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

alpertign
  • 296
  • 1
  • 4
  • 13
  • "Context uris does not have path or scheme." that might be the part I miss! Can you elaborate? Like the reference, or is there any chance that we can add path/scheme back to that Uri? (ex. the one stated in the question) – Samuel T. Chou Feb 23 '22 at 02:33
  • @SamuelT.Chou You can check [this] (https://developer.android.com/reference/android/content/ContentUris) documentation. 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_ – alpertign Feb 23 '22 at 06:21
  • 1
    @SamuelT.Chou "is there any chance that we can add path/scheme back to that Uri?" -> by using the function above you can create a temporary file and yo can extract uri and path like this: `val tempFile: File = createFileFromContentUri(contentUri)` - `val filePath = tempFile.path` and `val fileUri = tempFile.getUri()`. So you don't have to edit content uri. Adding scheme to content uri probably not gonna work – alpertign Feb 23 '22 at 06:29
  • 1
    Well IMO, re-creating a file temporally and use that instead seems a little bit overkill (since we already have the file). But the info you provided is very helpful to understand the whole thing. I suggest you to edit the comments into your answer, then I'll consider accept it. – Samuel T. Chou Feb 23 '22 at 06:56
  • 1
    "re-creating a file temporally and use that instead seems a little bit overkill" -> same idea. The same question asked [here](https://commonsware.com/community/t/get-file-from-uri/624) . And here is a useful [link](https://commonsware.com/blog/2016/03/16/how-publish-files-via-content-uri.html) describes file-uri relations quite well – alpertign Feb 23 '22 at 07:40
1

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)
                )

ExtremeMultiman
  • 112
  • 1
  • 4
0

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

Adnan Bashir
  • 645
  • 4
  • 8
  • Yeah I saw this before, tried it, and still not working. The answer focus on the file created by `Context` -provided directory; but in my situation, I'm using `MediaStore`. – Samuel T. Chou Feb 15 '22 at 07:13
  • If you can help me change save-style from `MediaStore` into `Context`-provided directory, though might not answering the question, but still helps me. I read and tried a lot, it seems that `Context` ones could not really create a file in `Download` folder...? – Samuel T. Chou Feb 15 '22 at 07:16
0

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.

blackapps
  • 8,011
  • 2
  • 11
  • 25
  • I'm doing the "share-file" action as official document written, which seems only accepts uri. https://developer.android.com/training/sharing/send#send-binary-content Then I should only look for Uri. How could I "just read the uri from the stream"? (which is opened from uri...?) – Samuel T. Chou Feb 15 '22 at 07:25
  • Can you edit your post and tell that you wanna share a file? As now we can only read rhat you wanna read a file. Confusing as you can see. – blackapps Feb 15 '22 at 09:10
  • I want to know how can I read `Uri` of `File` from `FileProvider` -- which I stated in title and First section. If you answer the part of how I can share file not by `Uri`, I would be appreciate, but not close the question (or mark as accepted answer). That path-reading is a puzzle I'd like to solve now. – Samuel T. Chou Feb 15 '22 at 10:39
  • Oh, I understand which part confuses you now. I made it clear, hope the description is clear enough now. – Samuel T. Chou Feb 15 '22 at 10:44
  • ??? I see nothing changed. You still dont start your post with telling that you wanna share a file. Also the title of your post does not say such. – blackapps Feb 15 '22 at 10:47
  • File-sharing is not the point I want to deal with (because the error is not there). I'm more like looking for answer of how to get `Uri` of a specific file, or the correct combination of `MediaStore` and `FileProvider`. – Samuel T. Chou Feb 15 '22 at 10:57
  • I totally have no idea what you want. You wrote an unreadable post. Please rewrite it totally. And remember: it makes no sense to use FileProvider in combination with a MediaStore uri. You have been said that before. – blackapps Feb 15 '22 at 11:00
  • Well, I _didn't_ say that `FileProvider` should not be used with a `MediaStore` uri. Maybe you can elaborate that part? That could be an answer. – Samuel T. Chou Feb 16 '22 at 03:49
0
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);

    }
Adnan Bashir
  • 645
  • 4
  • 8
  • Sorry, but `DownloadManager` doesn't match my need. My API uses `POST` method with request body, which is not allowed by `DownloadManager`. – Samuel T. Chou Feb 15 '22 at 07:28
0

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.

Samuel T. Chou
  • 521
  • 6
  • 31
0

Don't use same name in xml file paths. Android seems take the first path matches the provided name.

Ayman Al-Absi
  • 2,630
  • 24
  • 22