Android/Kotlin/Jetpack Compose: File Sharing using FileProvider and Android ShareSheet

Android provides two ways for users to share data between apps:

In this article, let’s see how we can share a file from our app to another app. Specifically, here is a list of things we will be looking at

In additional to that, I will also share with you how to retrieve the correct MIME type for your contents as well as some errors I have encountered and the way I solved it.

Overview

In order for us to share a file from our app to another app, we will need to provide the receiving application a Uri that points to the file data. Specifically, the Uri needs to be in the form of content://Uri , which allows us to grant read and write access using temporary access permissions, instead of file:///Uri , which is what we normally have when saving a file to a local storage or cached directory.

There are two (recommended) ways to do this:

In this article, we will be taking a detail look at the first approach.

Specify FileProvider

FileProvider is a basically a subclass of ContentProvider that facilitates secure sharing of files by creating a content://Uri for a file instead of a file:///Uri .

This class is part of the AndroidX Core Library. You should have it included automatically but if not, head to build.gradle.kts(Module:app) and add the following under dependencies.

implementation("androidx.core:core-ktx:1.9.0")

Add to Manifest

In order to use FileProvider within our code, we will first need to add to our AndroidManifest.xml the element that specifies the FileProvider class, the authority , and the XML file name that specifies the directories we want to share.

 android:name="androidx.core.content.FileProvider" 
android:authorities="com.example.filepreviewdemo.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />

Note here. The authority you specified should consist of the app’s android:package value with the string fileprovider appended to it.

Specify sharable directories and Available Files

A FileProvider can only generate a content URI for files in directories that is specified beforehand.

First of all, create a filepaths.xml inside res/xml directory. Within this file, we will specify the directories that contain the files we want to share, using child elements of the element.

For example, to share a subdirectory downloads of the files/ directory in the internal storage area, ie: the File you want to share is created like following

val path = File(context.filesDir.toString() + "/downloads")
var file = File(path, "your_file_name.extension")

and add the path segment mydownloads to content URIs:

The element can have multiple children, each specifying a different directory to share.

In addition to the element we have above, here is a list of the available ones:

Generating the Content URI for a File

Now that we have everything we need in order to use FileProvider set up, we can generate the content URI for a file by simply call getUriForFile() .

val path = File(context.filesDir.toString() + "/downloads")
var imageFileTemp = File(path, "image.jpg")
val contentUri: Uri = FileProvider.getUriForFile(
LocalContext.current,
"com.example.filepreviewdemo.fileprovider",
imageFileTemp)

Two things to keep in mind here!

The subdirectory that you create your file in (in my example above, the downloads folder) should be one of those you specified in filepaths.xml .

The authority (the second parameter) that you are passing into FileProvider.getUriForFile should be the same as that in your AndroidManifest.xml that we specified in the section above.

Sharing File

Android ShareSheet

This method is primarily designed for sending content outside your app and/or directly to another user. For example, sharing a URL with a friend.

Since we already have our Content URI, this can be done as simple as the following.

val sendIntent: Intent = Intent().apply action = Intent.ACTION_SEND 
putExtra(Intent.EXTRA_STREAM, contentUri)
type = "image/jpg"
>
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(context, shareIntent, null)

To allow the user to choose which app receives the intent, and the permission to access the content, I have also added sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .

And this will trigger a bottom modal presentation with a list of relevant app suggestions

Android intent resolver

This is best suited for passing data to the next stage of a well-defined task. For example, opening a PDF from your app and letting users pick their preferred viewer.

This can simply be achieve by making some minor modification to what we have above.

val sendIntent: Intent = Intent().apply action = Intent.ACTION_SEND 
putExtra(Intent.EXTRA_STREAM, contentUri)
type = "image/jpg"
>
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(context, sendIntent, null)

Use the right MIME type

One really important thing to keep in mind while creating your Intent is to specified the correct MIME type for your contents.

Here are a few common MIME types:

Although you can use a MIME type of */* , you should try your best to specify the type since most of the receiving apps aren’t able to receive any kind of content.

To retrieve MIME type from a URL:

fun getMimeType(url: String): String val extension = MimeTypeMap.getFileExtensionFromUrl(url); 
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
Log.d("type", type.toString())
return if (type.isNullOrBlank()) "*/*"
> else type
>
>

And if you are getting your file content from an Http request, you can also retrieve the MIME type from the response header

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response

fun getMimeType(response: Response): String var contentType = response.header("Content-Type")
contentType?.let val mediaType = it.toMediaType()
return "$/$"
>
return "*/*"
>

That’s it! We can now share files within our app to other apps!

Open File Directly

I have got couple questions regarding to use ACTION_VIEW or ACTION_DEFAULT to open a file using the default app, for example, Google photos for image, with the problem being that the target app cannot display the data.

Therefore, I decided to add this part to the article since those two actions work a little different from ACTION_SEND .

Instead of adding the contentUri by putExtra(Intent.EXTRA_STREAM, contentUri) , we will need to use setDataAndType like following.

val sendIntent = Intent()
sendIntent.setAction(Intent.ACTION_DEFAULT)
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
sendIntent.setDataAndType(contentUri,getMimeType(it.toURI().toString()))

startActivity(context, sendIntent, null)

With this approach, a ShareSheet will not be displayed to the user and the contentUri will simply be opened directly using the default app.

Example

For example, by adding it to the example we have done in this article where we download file data from Http Request, Save it to Local Storage and Preview using WebView, we can now share what we have downloaded and previewing with other apps!

class FilePreviewDemoViewModel(): ViewModel() private var url = "" 
private var fileName = ""
private var imageFile: File? = null
private var script = """
var meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=0.5, maximum-scale=0.5, user-scalable=no';
document.head.appendChild(meta)
"""

constructor(url: String, fileName: String) : this() this.url = url
this.fileName = fileName
>

@Composable
fun previewFile() var isLoading by rememberSaveable < mutableStateOf(true) >
val context = LocalContext.current

LaunchedEffect(null) saveFileData(context = context)
isLoading = false
>

if (isLoading || imageFile == null) return
>
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
) Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 15.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) Image(
painter = painterResource(id = R.drawable.xmark),
contentDescription = "clock_rotate_left",
modifier = Modifier
.width(18.dp)
.height(18.dp)
)
Text(
text = "title",
style = MaterialTheme.typography.headlineSmall,
fontSize = 20.sp,
modifier = Modifier.padding(0.dp, 0.dp, 0.dp, 2.dp),
fontWeight = FontWeight.Bold

)
Image(
painter = painterResource(id = R.drawable.ellipsis),
contentDescription = "clock_rotate_left",
modifier = Modifier
.width(20.dp)
.height(20.dp)
.clickable < shareFile(context) >,
)
>

Box(
modifier = Modifier
.fillMaxWidth()
) AndroidView(
factory = ::WebView,
update = < webView ->
webView.webViewClient = object : WebViewClient() override fun onPageFinished(view: WebView?, url: String?) super.onPageFinished(view, url)
view?.evaluateJavascript(script, null)
>
>
webView.settings.allowContentAccess = true
webView.settings.allowFileAccess = true
webView.settings.useWideViewPort = true
webView.settings.domStorageEnabled = true
webView.settings.javaScriptEnabled = true

webView.loadUrl(imageFile. toURI().toString())

>,
)
>
>
>

private suspend fun saveFileData(context: Context) val okHttpClient = OkHttpClient()

val request = Request.Builder()
.url(url)
.build()
Log.d("saving", request.toString())

return withContext(Dispatchers.IO) try val response: Response = okHttpClient.newCall(request).execute()
val inputStream = response.body?.byteStream()

// internal
val path = File(context.filesDir.toString() + "/downloads")
if (!path.exists())
path.mkdirs()

var imageFileTemp = File(path, fileName)
if (imageFileTemp.exists()) imageFileTemp.delete()
>

val fos = FileOutputStream(imageFileTemp)
fos.write(inputStream?.readBytes())
fos.flush()
fos.close()

imageFile = imageFileTemp

> catch (e: IOException) e.printStackTrace()
>
>
>

private fun shareFile(context: Context) imageFile?.let val contentUri: Uri = FileProvider.getUriForFile(context, "jp.co.collabostyle.collaboflow.fileprovider", it)
Log.d("content uri", contentUri.toString())

val sendIntent: Intent = Intent().apply action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, contentUri)
type = getMimeType(it.toURI().toString())
>
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(context, shareIntent, null)
>
>

private fun getMimeType(url: String): String val extension = MimeTypeMap.getFileExtensionFromUrl(url);
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
Log.d("type", type.toString())
return if (type.isNullOrBlank()) "*/*"
> else type
>
>
>


@Preview(showBackground = true)
@Composable
fun PikachuPreview() PreviewTheme val url = "https://www.pngall.com/wp-content/uploads/5/Pokemon-Pikachu-PNG-Free-Download.png"
val fileName = "pikachu.jpg"

var fileViewModel = FilePreviewDemoViewModel(url = url, fileName = fileName)
fileViewModel.previewFile()
>
>

Extra Bonus

Here are some errors that you might encounter (at least I did)