【问题标题】:Is it possible to provide a zipped file in FileProvider, of a file that doesn't really exist?是否可以在 FileProvider 中提供一个实际上并不存在的文件的压缩文件?
【发布时间】: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 的所有函数,我需要一个真实的文件:

问题

  1. 是否可以让它提供一个实际上并不存在但实际上是不同文件的压缩文件?也许是压缩文件的流?

  2. 如果这不可能,我有什么办法可以避免这些垃圾文件?这意味着我肯定知道删除我过去共享的压缩文件是安全的吗?

  3. 如果连这都不可能,我怎么能决定什么时候可以删除它们?只是将它们放在缓存文件夹中?我记得缓存文件夹并没有真正自动被操作系统很好地处理,需要时删除旧文件。还是不对吗?

由于我尝试了很长时间,我只会接受一个我可以测试自己的可行解决方案。


编辑:根据答案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


    【解决方案1】:

    是的,有可能。

    1. 将 FileProvider 复制到您的代码中(您需要它来使用一些私有方法 - 使它们受到保护)。创建扩展 FileProvider 的类。

    2. public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) 中使用 ParcelFileDescriptor.createReliablePipe()(或 ParcelFileDescriptor.createPipe() 用于旧版 android)来创建管道和一对 ParcelFileDescriptor:readFd 和 writeFd。

      李>
    3. 创建一个单独的线程并使用它来压缩和写入文件到 writeFd FileDescriptor。

    4. 返回另一个 ParcelFileDescriptor (readFd) 进行读取。

    我的实现在这里: https://github.com/Babay88/AndroidCodeSamplesB/blob/master/ShareZipped/src/main/java/ru/babay/codesamples/sharezip/


    编辑:下面的代码: (但你最好在 github 上查看实现,因为该类扩展了一点点定制的 FileProvider)

    /**
     * File provider intended to zip files on-the-fly.
     * It can send files (just like FileProvider) and zip files.
     *
     * Use {@link ZipableFileProvider#getUriForFile(Context, String, File, boolean)}
     * to create an URI.
     *
     */
    //@SuppressWarnings("ALL")
    public class ZipableFileProvider extends FileProvider {
    
        static final String TAG = "ZipableFileProvider";
    
        /**
         * Just like {@link FileProvider#getUriForFile}, but will create an URI for zipping wile while sending
         * @param context
         * @param authority
         * @param file
         * @param zipFile
         * @return
         */
    
        public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
                                        @NonNull File file, boolean zipFile) {
            Uri uri = getUriForFile(context, authority, file);
            if (zipFile) {
                return new Uri.Builder()
                        .scheme(uri.getScheme())
                        .authority(uri.getAuthority())
                        .encodedPath(uri.getPath())
                        .encodedQuery("zip").build();
            }
            return uri;
        }
    
        @Override
        public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
            File file = getFileForUri(uri);
            // if file does not exist -- let parent class handle that
            if (file.exists() && isZip(uri)) {
                if (file.exists()) {
                    try {
                        return startZippedPipe(file);
                    } catch (IOException e) {
                        Log.e(TAG, "openFile: ", e);
                    }
                }
            }
            return super.openFile(uri, mode);
        }
    
        private boolean isZip(@NonNull Uri uri) {
            return "zip".equals(uri.getQuery());
        }
    
        @Override
        public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
                            @Nullable String[] selectionArgs,
                            @Nullable String sortOrder) {
            // ContentProvider has already checked granted permissions
            File file = mStrategy.getFileForUri(uri);
    
            if (projection == null) {
                projection = COLUMNS;
            }
    
            String[] cols = new String[projection.length];
            Object[] values = new Object[projection.length];
            int i = 0;
            for (String col : projection) {
                if (OpenableColumns.DISPLAY_NAME.equals(col)) {
                    cols[i] = OpenableColumns.DISPLAY_NAME;
                    values[i++] = file.getName() + (isZip(uri) ? ".zip" : "");
                } else if (OpenableColumns.SIZE.equals(col)) {
                    // return size of original file; zip-file might differ
                    cols[i] = OpenableColumns.SIZE;
                    values[i++] = file.length();
                }
            }
    
            cols = copyOf(cols, i);
            values = copyOf(values, i);
    
            final MatrixCursor cursor = new MatrixCursor(cols, 1);
            cursor.addRow(values);
            return cursor;
        }
    
        public static ParcelFileDescriptor startZippedPipe(File file) throws IOException {
            ParcelFileDescriptor[] pipes = Build.VERSION.SDK_INT >= 19 ?
                    ParcelFileDescriptor.createReliablePipe() :
                    ParcelFileDescriptor.createPipe();
            new Thread(() -> doZipFile(pipes[1], file)).start();
            return pipes[0];
        }
    
        @RequiresApi(api = Build.VERSION_CODES.KITKAT)
        private static ParcelFileDescriptor startZippedSocketPair(File file) throws IOException {
            ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createReliableSocketPair();
            new Thread(() -> doZipFile(pipes[1], file)).start();
            return pipes[0];
        }
    
        /**
         * zips and sends a file to a ParcelFileDescriptor writeFd
         *
         * Note that some apps (like Telegram) receives the file at once.
         * Other apps (like Gmail) open the file you share, read some kb and close it,
         * and reopen it later (when you really send the email).
         * So, it's OK if "Broken pipe" exception thrown.
         *
         * @param writeFd
         * @param inputFile
         */
        private static void doZipFile(ParcelFileDescriptor writeFd, File inputFile) {
            long start = System.currentTimeMillis();
            byte[] buf = new byte[1024];
            int writtenSize = 0;
            try (FileInputStream iStream = new FileInputStream(inputFile);
                 ZipOutputStream zipStream = new ZipOutputStream(new FileOutputStream(writeFd.getFileDescriptor()))) {
    
                zipStream.putNextEntry(new ZipEntry(inputFile.getName()));
                int amount;
                while (0 <= (amount = iStream.read(buf))) {
                    zipStream.write(buf, 0, amount);
                    writtenSize += amount;
                }
    
                zipStream.closeEntry();
                zipStream.close();
                iStream.close();
                writeFd.close();
    
                if (BuildConfig.DEBUG)
                    Log.d(TAG, "doZipFile: done. it took ms: " + (System.currentTimeMillis() - start));
            } catch (IOException e) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    try {
                        writeFd.closeWithError(e.getMessage());
                    } catch (IOException e1) {
                        Log.e(TAG, "doZipFile: ", e1);
                    }
                }
                if (BuildConfig.DEBUG)
                    Log.d(TAG, "doZipFile: written: " + writtenSize, e);
            }
        }
    }
    

    您可以对一个 zip 文件中的多个文件执行相同的操作。您只需要:

    1. 创建一个可用于提取文件列表的 Uri。 (注意:如果你在 Uri 查询中使用'.',你会失败。我不知道为什么。我在我的代码中编码 Base64 文件名)

    2. 将所有 zip 文件写入一个 zip 流。

    https://github.com/Babay88/AndroidCodeSamplesB/blob/master/ShareZipped/src/main/java/ru/babay/codesamples/sharezip/ZipFilesProvider.java中实现

    共享和压缩文件的示例活动代码(一个或多个): https://github.com/Babay88/AndroidCodeSamplesB/blob/master/sharedzipexample/src/main/java/ru/babay/sharedzipexample/MainActivity.java

    【讨论】:

    • 看起来很有希望!能否请您展示一个完整的样本?
    • 由于某种原因,我无法构建和运行该应用程序。怎么来的?我已经在你的存储库网站上写过这个:github.com/Babay88/AndroidCodeSamplesB/issues/1
    • 是的,您不能运行库项目。它不包括活动。您确定我们需要示例活动吗?
    • 那么你怎么知道它在不使用它的情况下工作?总是,当一个人准备一个示例库时,会制作一个 POC 来演示和测试它的工作原理......
    • 好的。 :) 是的,样本很大。但是......这是解决方案的关键部分。而且...我重新设计了共享,以便为每个文件应用内容根。现在感觉不错。
    猜你喜欢
    • 1970-01-01
    • 2019-05-05
    • 1970-01-01
    • 1970-01-01
    • 2013-10-28
    • 1970-01-01
    • 2016-06-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多