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?