最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

android - How to best serve a virtual file from a content provider? - Stack Overflow

programmeradmin3浏览0评论

So I have written a custom ContentProvider that serves a file that is created whenever requested using a content URI. This file does not exist previous to the request and combines several other files into one zip archive, sort of like an export mechanism.

I could write this file to disk, and then just pass the result of ParcelFileDescriptor.open(File) to the requesting app, but this leaves an artifact on disk that effectively is a redundant copy that becomes out of date fast. I also don't know of a mechanism that allows me to remove this file once it has been fully consumed, so I'd have to implement some sort of mechanism that deletes those files eventually.

Instead, I want to pass a buffer directly without the indirection of writing to disk first. I ended up using ParcelFileDescriptor.createPipe() to directly stream the contents using input/output streams using an extra thread. This seems to work fine for most cases, but Gmail out of all apps just seems to immediately close the InputStream, resulting in an broken pipe exception and Gmail complaining about an empty file:

@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
    long id = Long.parseLong(Objects.requireNonNull(uri.getPathSegments().get(0)));
    try {
        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
        ParcelFileDescriptor output = pipe[1];
        new Thread(() -> {
            try (ParcelFileDescriptor.AutoCloseOutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(output)) {
                serializeZipFile(out, id);
            } catch (IOException e) {
                Log.e(TAG, "Error during zip serialization.", e);
            }
        }).start();
        return pipe[0];
    } catch (IOException e) {
        Log.e(TAG, "Error while trying to create pipe", e);
        throw new FileNotFoundException("Failed to create pipe");
    }
}

After digging through some documentation I found StorageManager#openProxyFileDescriptor(int, ProxyFileDescriptorCallback, Handler), that seems to roughly do what I want. However I'm using a minimum SDK of 24 and this method was introduced in SDK 26, so for the SDK >= 26 case I added this code which pleased Gmail:

StorageManager storageManager = Objects.requireNonNull(getContext()).getSystemService(StorageManager.class);
ByteArrayOutputStream out = new ByteArrayOutputStream();
serializeZipFile(out, id);
return storageManager.openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY, new ProxyFileDescriptorCallback() {
    private final byte[] bytes = out.toByteArray();

    @Override
    public long onGetSize() {
        return bytes.length;
    }

    @Override
    public int onRead(long offset, int size, byte[] data) {
        if (offset >= bytes.length) {
            return 0;
        }
        int intOffset = Math.toIntExact(offset);
        int realSize = Math.min(Math.min(size, bytes.length - intOffset), data.length);
        System.arraycopy(bytes, intOffset, data, 0, realSize);
        return realSize;
    }

    @Override
    public void onRelease() {}
}, new Handler(Looper.getMainLooper()));

Both of these approaches seem really clunky and unnecessarily complicated to me. For the Gmail issue I just ended up excluding Gmail from handling my Intent on SDKs < 26, but it seems like there just has to be a better way to do this?

发布评论

评论列表(0)

  1. 暂无评论