【发布时间】:2019-01-01 18:22:42
【问题描述】:
背景
我希望能够通过FileProvider 将一些文件(通过发送意图)共享为单个压缩文件,但无需实际创建此文件。
对于意图,您只需添加ArrayList<Uri> 作为参数,如下所示:
ArrayList<Uri> uris = MyFileProvider.prepareFileProviderFiles(...)
sharingIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
问题
FileProvider 可用于将真实文件传送到外部应用程序。
我不想让一些垃圾文件(压缩文件,仅用于共享)在我的应用程序中毫无目的地停留,以防使用它们的应用程序由于某种原因完成、崩溃或停止.
我发现了什么
根据FileProvider的API,我应该实现真正的文件处理:
默认情况下,FileProvider 会自动返回 与 content:// Uri 关联的文件的 ParcelFileDescriptor。至 获取 ParcelFileDescriptor,调用 ContentResolver.openFileDescriptor。 要覆盖此方法,您必须提供自己的子类 文件提供者。
所以它返回一个 ParcelFileDescriptor,但是根据创建 ParcelFileDescriptor 的所有函数,我需要一个真实的文件:
问题
是否可以让它提供一个实际上并不存在但实际上是不同文件的压缩文件?也许是压缩文件的流?
如果这不可能,我有什么办法可以避免这些垃圾文件?这意味着我肯定知道删除我过去共享的压缩文件是安全的吗?
如果连这都不可能,我怎么能决定什么时候可以删除它们?只是将它们放在缓存文件夹中?我记得缓存文件夹并没有真正自动被操作系统很好地处理,需要时删除旧文件。还是不对吗?
由于我尝试了很长时间,我只会接受一个我可以测试自己的可行解决方案。
编辑:根据答案below,我制作了一个小样本here。这是它的代码:
清单
...
<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, ""))
}
}
【问题讨论】:
标签: android-intent android-fileprovider