Lets say I\'m developing a chat app that is able to share with others ANY kind of files (no mimetype restriction): like images, videos, documents, but also compressed files like
More than 10 months have passed and yet not a satisfying answer for me have been made. So I'll answer my own question.
As @CommonsWare states in a comment, "get MediaStore.Downloads.INTERNAL_CONTENT_URI or MediaStore.Downloads.EXTERNAL_CONTENT_URI and save a file by using Context.getContentResolver.insert()" is supposed to be the solution. I double checked and found out this is true and I was wrong saying it doesn't work. But...
I found it tricky to use ContentResolver and I was unable to make it work properly. I'll make a separate question with it but I kept investigating and found a somehow satisfying solution.
MY SOLUTION:
Basically you have to download to any directory owned by your app and then copy to Downloads folder.
Configure your app:
Add provider_paths.xml to xml resource folder
In your manifest add a FileProvider:
Prepare to download files to any directory your app owns, such as getFilesDir(), getExternalFilesDir(), getCacheDir() or getExternalCacheDir().
val privateDir = context.getFilesDir()
Download file taking its progress into account (DIY):
val downloadedFile = myFancyMethodToDownloadToAnyDir(url, privateDir, fileName)
Once downloaded you can make any threatment to the file if you'd like to.
Copy it to Downloads folder:
//This will be used only on android P-
private val DOWNLOAD_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val finalUri : Uri? = copyFileToDownloads(context, downloadedFile)
fun copyFileToDownloads(context: Context, downloadedFile: File): Uri? {
val resolver = context.contentResolver
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, getName(downloadedFile))
put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(downloadedFile))
put(MediaStore.MediaColumns.SIZE, getFileSize(downloadedFile))
}
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
} else {
val authority = "${context.packageName}.provider"
val destinyFile = File(DOWNLOAD_DIR, getName(downloadedFile))
FileProvider.getUriForFile(context, authority, destinyFile)
}?.also { downloadedUri ->
resolver.openOutputStream(downloadedUri).use { outputStream ->
val brr = ByteArray(1024)
var len: Int
val bufferedInputStream = BufferedInputStream(FileInputStream(downloadedFile.absoluteFile))
while ((bufferedInputStream.read(brr, 0, brr.size).also { len = it }) != -1) {
outputStream?.write(brr, 0, len)
}
outputStream?.flush()
bufferedInputStream.close()
}
}
}
Once in download folder you can open file from app like this:
val authority = "${context.packageName}.provider"
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(finalUri, getMimeTypeForUri(finalUri))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
try {
context.startActivity(Intent.createChooser(intent, chooseAppToOpenWith))
} catch (e: Exception) {
Toast.makeText(context, "Error opening file", Toast.LENGTH_LONG).show()
}
//Kitkat or above
fun getMimeTypeForUri(context: Context, finalUri: Uri) : String =
DocumentFile.fromSingleUri(context, finalUri)?.type ?: "application/octet-stream"
//Just in case this is for Android 4.3 or below
fun getMimeTypeForFile(finalFile: File) : String =
DocumentFile.fromFile(it)?.type ?: "application/octet-stream"
Pros:
Downloaded files survives to app uninstallation
Also allows you to know its progress while downloading
You still can open them from your app once moved, as the file still belongs to your app.
write_external_storage permission is not required for Android Q+, just for this purpose:
Cons:
If this approach is enough for you then give it a try.