Search code examples
androidfile-uploadandroid-webview

How to upload file by content uri in WebView


Typical Scenario

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.

The problem

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?

Approach 1

This approach works fine, if uri is generated using Uri.fromFile() as below:

...
Uri uri = Uri.fromFile(file);
...

Approach 2

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:

FileUploadProvider.java

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);
    }
    ...
}

Approach 3

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;
}
...

Approach 4

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?

Example

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".

enter image description here

enter image description here

enter image description here

enter image description here


Solution

  • Solution

    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.

    Limitation

    This approach is working only for Android API higher than 25.