是否可以在 FileProvider 中提供一个并不真正存在的文件的压缩文件?

and*_*per 5 android-intent android-fileprovider

背景

我希望能够通过FileProvider将一些文件(通过发送意图)共享为单个压缩文件,但无需实际创建该文件。

对于意图,您所做的就是添加ArrayList<Uri>作为参数,如下所示:

ArrayList<Uri> uris = MyFileProvider.prepareFileProviderFiles(...)
sharingIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
Run Code Online (Sandbox Code Playgroud)

问题

FileProvider 可用于将真实文件传递给外部应用程序。

我不想让我的应用程序中的一些垃圾文件(压缩的文件,仅用于共享)毫无目的地保留下来,以防使用它们的应用程序因某种原因完成、崩溃或停止。

我发现了什么

根据 FileProvider 的 API,我应该实现真正的文件处理:

默认情况下,FileProvider 自动返回与 content:// Uri 关联的文件的 ParcelFileDescriptor。要获取 ParcelFileDescriptor,请调用 ContentResolver.openFileDescriptor。要重写此方法,您必须提供您自己的 FileProvider 子类。

所以它返回一个 ParcelFileDescriptor ,但是根据创建 ParcelFileDescriptor 的所有函数,我需要一个真实的文件:

问题

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

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

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

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


编辑:根据下面的答案,我在这里做了一个小样本。这是它的代码:

显现

...
    <provider
        android:name=".ZipFilesProvider"
        android:authorities="${applicationId}.zip_file_provider"
        android:exported="false"
        android:grantUriPermissions="true"/>
Run Code Online (Sandbox Code Playgroud)

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()
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

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, ""))
    }
}
Run Code Online (Sandbox Code Playgroud)

bab*_*bay 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以对一个 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