In order to upload a file using WebView
, it's typically needed to override WebChromeClient
, and start a file chooser Activity
for result:
...
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
mFilePathCallback = filePathCallback;
Intent intent = new Intent(MainActivity.this, FilePickerActivity.class);
startActivityForResult(intent, 1);
return true;
}
});
...
Then, once file is selected (for simplicity, only a single file can be selected at a time), onActivityResult()
is called with a file uri stored in a data
object. So, the uri is retrieved and handed over to filePathCallback
and the file gets uploaded:
...
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == 1) {
mFilePathCallback.onReceiveValue(new Uri[] {data.getData()});
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
...
Basically, filePathCallback
requires a uri to a file so that to upload it.
What if I have only an InputStream
that contains the data I need to upload rather than a file with a URI? And I cannot save that data to a file so that to generate a URI (for security reasons). Is there a way to upload the data as a file in such case? In particular, can I convert an InputStream
to a content URI and then upload it?
This approach works fine, if uri is generated using Uri.fromFile()
as below:
...
Uri uri = Uri.fromFile(file);
...
If I implement my own ContentProvider
and override openFile
there in such a way, that it uses ParcelFileDescriptor.open()
to create a ParcelFileDescriptor
, then uploading a file based on a uri provided by getContentUri(...)
is working without problems:
public class FileUploadProvider extends ContentProvider {
public static Uri getContentUri(String name) {
return new Uri.Builder()
.scheme("content")
.authority(PROVIDER_AUTHORITY)
.appendEncodedPath(name)
.appendQueryParameter(OpenableColumns.DISPLAY_NAME, name)
.build();
}
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
File file = new File(getContext().getApplicationContext().getCacheDir(),
TextUtils.join(File.separator, segments));
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
...
}
However, if I create a ParcelFileDescriptor
with help of ParcelFileDescriptor.createPipe()
, then file upload never finishes, so it basically doesn't work:
...
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
File file = new File(getContext().getApplicationContext().getCacheDir(),
TextUtils.join(File.separator, segments));
InputStream inputStream = new FileInputStream(file);
ParcelFileDescriptor[] pipe;
try {
pipe = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
throw new RuntimeException(e);
}
ParcelFileDescriptor readPart = pipe[0];
ParcelFileDescriptor writePart = pipe[1];
try {
IOUtils.copy(inputStream, new ParcelFileDescriptor.AutoCloseOutputStream(writePart));
} catch (IOException e) {
throw new RuntimeException(e);
}
return readPart;
}
...
To make matters worse, if I create a ParcelFileDescriptor
with help of MemoryFile
, then file upload never finishes as well:
...
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
File file = new File(getContext().getApplicationContext().getCacheDir(),
TextUtils.join(File.separator, segments));
try {
MemoryFile memoryFile = new MemoryFile(file.getName(), (int) file.length());
byte[] fileBytes = Files.readAllBytes(file.toPath());
memoryFile.writeBytes(fileBytes, 0, 0, (int) file.length());
Method method = memoryFile.getClass().getDeclaredMethod("getFileDescriptor");
FileDescriptor fileDescriptor = (FileDescriptor) method.invoke(memoryFile);
Constructor<ParcelFileDescriptor> constructor = ParcelFileDescriptor.class.getConstructor(FileDescriptor.class);
ParcelFileDescriptor parcelFileDescriptor = constructor.newInstance(fileDescriptor);
return parcelFileDescriptor;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
...
Why is file upload not working for Approach 3 and Approach 4?
I created a sample app to showcase my issue here.
Below are steps to follow to reproduce it. First log in to your gmail account and click "create new message".
My colleagues have found a solution to this with help of StorageManager
- please read more about the component here.
First, create a callback that handles file system requests from ProxyFileDescriptor
in FileUploadProvider
:
private static class CustomProxyFileDescriptorCallback extends ProxyFileDescriptorCallback {
private ByteArrayInputStream inputStream;
private long length;
public CustomProxyFileDescriptorCallback(File file) {
try {
byte[] fileBytes = Files.readAllBytes(file.toPath());
length = fileBytes.length;
inputStream = new ByteArrayInputStream(fileBytes);
} catch (IOException e) {
// do nothing here
}
}
@Override
public long onGetSize() {
return length;
}
@Override
public int onRead(long offset, int size, byte[] out) {
inputStream.skip(offset);
return inputStream.read(out,0, size);
}
@Override
public void onRelease() {
try {
inputStream.close();
} catch (IOException e) {
//ignore this for now
}
inputStream = null;
}
}
Then, create a file descriptor using StorageManager
that will read the InputStream
with help of the callback above.
...
StorageManager storageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
ParcelFileDescriptor descriptor;
try {
descriptor = storageManager.openProxyFileDescriptor(
ParcelFileDescriptor.MODE_READ_ONLY,
new CustomProxyFileDescriptorCallback(file),
new Handler(Looper.getMainLooper()));
} catch (IOException e) {
throw new RuntimeException(e);
}
return descriptor;
...
Please find the full code on github here.
This approach is working only for Android API higher than 25
.