如何在 WebView 中通过内容 uri 上传文件

How to upload file by content uri in WebView

典型场景

为了使用 WebView 上传文件,通常需要覆盖 WebChromeClient,并为结果启动文件选择器 Activity

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

然后,一旦选择了文件(为简单起见,一次只能选择一个文件),将调用 onActivityResult() 并使用存储在 data 对象中的文件 uri。因此,检索 uri 并将其移交给 filePathCallback 并上传文件:

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

基本上,filePathCallback 需要 uri 文件才能上传。

问题

如果我只有 InputStream 包含我需要上传的数据而不是带有 URI 的文件怎么办?而且我无法将该数据保存到文件中以生成 URI(出于安全原因)。在这种情况下,有没有办法将数据作为文件上传?特别是,我可以将 InputStream 转换为内容 URI 然后上传吗?

方法一

如果 uri 是使用 Uri.fromFile() 生成的,则此方法工作正常,如下所示:

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

方法二

如果我实现自己的 ContentProvider 并以这种方式覆盖 openFile,它使用 ParcelFileDescriptor.open() 创建 ParcelFileDescriptor,然后上传基于文件getContentUri(...) 提供的 uri 工作正常:

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

方法 3

但是,如果我在 ParcelFileDescriptor.createPipe() 的帮助下创建了一个 ParcelFileDescriptor,那么文件上传永远不会完成,所以它基本上不起作用:

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

方法 4

更糟糕的是,如果我在 MemoryFile 的帮助下创建 ParcelFileDescriptor,那么文件上传也永远不会完成:

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

为什么文件上传不适用于 方法 3方法 4

例子

我创建了一个示例应用来展示我的问题 here

以下是重现它的步骤。首先登录到您的gmail帐户并点击“创建新邮件”。

解决方案

我的同事在 StorageManager 的帮助下找到了解决此问题的方法 - 请阅读有关该组件的更多信息 here

首先,创建一个回调来处理 FileUploadProvider 中来自 ProxyFileDescriptor 的文件系统请求:

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

然后,使用 StorageManager 创建一个文件描述符,它将在上述回调的帮助下读取 InputStream

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

请在 github here 上找到完整代码。

限制

此方法仅适用于 Android API 高于 25