Background
I wish to be able to share some files (via a send intent) as a single compressed file, via FileProvider, but without actually creating this file.
For the intent, all you do is add the ArrayList<Uri>
as a parameter, as such:
ArrayList<Uri> uris = MyFileProvider.prepareFileProviderFiles(...)
sharingIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
The problem
A FileProvider can be used to deliver real files to outside apps.
I don't want to have some junk files (the compressed ones, which are meant just for sharing) in my app staying around without any purpose, in case the app that used them has finished, crashed or stopped for some reason.
What I've found
According to the API of FileProvider, I'm supposed to implement real files handling:
By default, FileProvider automatically returns the ParcelFileDescriptor for a file associated with a content:// Uri. To get the ParcelFileDescriptor, call ContentResolver.openFileDescriptor. To override this method, you must provide your own subclass of FileProvider.
So it returns a ParcelFileDescriptor, but according to all of the functions to create ParcelFileDescriptor , I need a real file:
The questions
Is it possible to have it offer a file that doesn't really exist, but is actually a compressed one of a different file/s? A stream of the zipped file, perhaps?
If this is not possible, is there any way for me to avoid having those junk files? Meaning that I would know for sure that it's safe to delete the compressed file/s that I've shared in the past?
If even that isn't possible, how could I decide when it's ok to delete them? Just putting them in the cache folder? I remember that the cache folder doesn't really automatically get handled nicely by the OS, removing old files when needed. Isn't it still correct?
Since I tried this for a long time, I will only accept a working solution that I can test myself.
EDIT: based on the answer below, I've made a tiny sample here. This is its code:
manifest
...
<provider
android:name=".ZipFilesProvider"
android:authorities="${applicationId}.zip_file_provider"
android:exported="false"
android:grantUriPermissions="true"/>
ZipFilesProvider.kt
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.pm.ProviderInfo
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import android.provider.OpenableColumns
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.concurrent.thread
class ZipFilesProvider : ContentProvider() {
override fun onCreate() = true
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?) = 0
override fun delete(uri: Uri, arg1: String?, arg2: Array<String>?) = 0
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun getType(uri: Uri) = ZIP_FILE_MIME_TYPE
override fun attachInfo(context: Context, info: ProviderInfo) {
super.attachInfo(context, info)
authority = info.authority
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
val filesPathsToCompress = getFilesPathsToCompress(uri)
filesPathsToCompress.forEach { if (!it.exists()) throw FileNotFoundException(it.absolutePath) }
val pipes = if (Build.VERSION.SDK_INT >= 19) ParcelFileDescriptor.createReliablePipe() else ParcelFileDescriptor.createPipe()
thread {
val writeFd = pipes[1]
try {
ZipOutputStream(FileOutputStream(writeFd.fileDescriptor)).use { zipStream: ZipOutputStream ->
filesPathsToCompress.forEach {
zipStream.putNextEntry(ZipEntry(it.name))
FileInputStream(it).copyTo(zipStream)
zipStream.closeEntry()
}
zipStream.close()
writeFd.close()
}
} catch (e: IOException) {
e.printStackTrace()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
writeFd.closeWithError(e.message)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
return pipes[0]
}
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
val filesPathsToCompress = getFilesPathsToCompress(uri)
val fileToCompressInto = uri.encodedPath!!.substringAfter("/")
val columnNames = projection ?: arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
val ret = MatrixCursor(columnNames)
val values = arrayOfNulls<Any>(columnNames.size)
for (i in columnNames.indices) {
when (columnNames[i]) {
MediaStore.MediaColumns.DISPLAY_NAME -> values[i] = fileToCompressInto
MediaStore.MediaColumns.SIZE -> {
var totalFilesSize = 0L
filesPathsToCompress.forEach { totalFilesSize += it.length() }
values[i] = totalFilesSize
}
}
}
ret.addRow(values)
return ret
}
companion object {
lateinit var authority: String
const val ZIP_FILE_MIME_TYPE = "application/zip"
private fun getFilesPathsToCompress(uri: Uri): HashSet<File> {
val filesPathsToCompress = HashSet<File>(uri.queryParameterNames.size)
uri.queryParameterNames.forEach {
val path = uri.getQueryParameters(it)[0]// alternative: String(Base64.decode(uri.getQueryParameters(it)[0], Base64.URL_SAFE))
filesPathsToCompress.add(File(path))
}
return filesPathsToCompress
}
fun prepareFilesToShareAsZippedFile(filesToCompress: Collection<String>, zipFileName: String): Uri {
val builder = Uri.Builder().scheme("content").authority(authority).encodedPath(zipFileName)
for ((index, filePath) in filesToCompress.withIndex())
builder.appendQueryParameter(index.toString(), filePath)// alternative: String(Base64.encode(filePath.toByteArray(), Base64.URL_SAFE)))
return builder.build()
}
}
}
MainActivity.kt
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import java.io.File
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val installedPackages = packageManager.getInstalledPackages(0)
val filesToCompress = ArrayList<String>()
val maxFiles = 3
var maxTotalSize = 10 * 1024L * 1024L
for (installedPackage in installedPackages) {
val filePath = installedPackage.applicationInfo.publicSourceDir
val file = File(filePath)
val fileSize = file.length()
if (maxTotalSize - fileSize >= 0) {
maxTotalSize-= fileSize
filesToCompress.add(filePath)
if (filesToCompress.size >= maxFiles)
break
}
}
val uri = ZipFilesProvider.prepareFilesToShareAsZippedFile(filesToCompress, "someZipFile.zip")
val intent = Intent(Intent.ACTION_SEND).setType(ZipFilesProvider.ZIP_FILE_MIME_TYPE).putExtra(Intent.EXTRA_STREAM, uri)
startActivity(Intent.createChooser(intent, ""))
}
}