天天看點

Android WebView那些坑之上傳檔案

Android WebView那些坑之上傳檔案

最近公司項目需要在

WebView

上調用手機系統相冊來上傳圖檔,開發過程中發現在很多機器上無法正常喚起系統相冊來選擇圖檔。

解決問題之前我們先來說說

WebView

上傳檔案的邏輯:當我們在Web頁面上點選選擇檔案的控件(

<input type="file">

)時,會回調

WebChromeClient

下的

openFileChooser()

(5.0及以上系統回調

onShowFileChooser()

)。這個時候我們在

openFileChooser

方法中通過

Intent

打開系統相冊或者支援該

Intent

的第三方應用來選擇圖檔。like this:

public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, String capture) {
    uploadMessage = valueCallback;
       openImageChooserActivity();
}

private void openImageChooserActivity() {
    Intent i = new Intent(Intent.ACTION_GET_CONTENT);
    i.addCategory(Intent.CATEGORY_OPENABLE);
    i.setType("image/*");
    startActivityForResult(Intent.createChooser(i,
                "Image Chooser"), FILE_CHOOSER_RESULT_CODE);
}           

複制

最後我們在

onActivityResult()

中将選擇的圖檔内容通過

ValueCallback

onReceiveValue

方法傳回給

WebView

,然後通過js上傳。代碼如下:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == FILE_CHOOSER_RESULT_CODE) {
        Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
        if (uploadMessage != null) {
            uploadMessage.onReceiveValue(result);
            uploadMessage = null;
        }
    }
}           

複制

PS:

ValueCallbacks

WebView

元件通過

openFileChooser()

或者

onShowFileChooser()

提供給我們的,它裡面包含了一個或者一組

Uri

,然後我們在

onActivityResult()

裡将

Uri

傳給

ValueCallbacks

onReceiveValue()

方法,這樣

WebView

就知道我們選擇了什麼檔案。

到這裡你可能要問了,說了這麼多還是沒解釋為什麼在很多機型上無法喚起系統相冊或者第三方app來選擇圖檔啊?!這是因為為了最求完美的Google攻城獅們對

openFileChooser

做了多次修改,在5.0上更是将回調方法該為了

onShowFileChooser

。是以為了解決這一問題,相容各個版本,我們需要對

openFileChooser()

進行重載,同時針對5.0及以上系統提供

onShowFileChooser()

方法:

webview.setWebChromeClient(new WebChromeClient() {

        // For Android < 3.0
        public void openFileChooser(ValueCallback<Uri> valueCallback) {
            ***
        }

        // For Android  >= 3.0
        public void openFileChooser(ValueCallback valueCallback, String acceptType) {
            ***
        }

        //For Android  >= 4.1
        public void openFileChooser(ValueCallback<Uri> valueCallback,
                String acceptType, String capture) {
            ***
        }

        // For Android >= 5.0
        @Override
        public boolean onShowFileChooser(WebView webView,
                ValueCallback<Uri[]> filePathCallback,
                WebChromeClient.FileChooserParams fileChooserParams) {
            ***
            return true;
        }
    });           

複制

大家應該注意到

onShowFileChooser()

中的

ValueCallback

包含了一組

Uri(Uri[])

,是以針對5.0及以上系統,我們還需要對

onActivityResult()

做一點點處理。這裡不做描述,最後我再貼上完整代碼。

當處理完這些後你以為就萬事大吉了?!當初我也這樣天真,但當我們打好release包測試的時候卻又發現沒法選擇圖檔了!!!真是坑了個爹啊!!!無奈去翻

WebChromeClient

的源碼,發現

openFileChooser()

是系統API,我們的release包是開啟了混淆的,是以在打包的時候混淆了

openFileChooser()

,這就導緻無法回調

openFileChooser()

了。

/**
 * Tell the client to open a file chooser.
 * @param uploadFile A ValueCallback to set the URI of the file to upload.
 *      onReceiveValue must be called to wake up the thread.a
 * @param acceptType The value of the 'accept' attribute of the input tag
 *         associated with this file picker.
 * @param capture The value of the 'capture' attribute of the input tag
 *         associated with this file picker.
 *
 * @deprecated Use {@link #showFileChooser} instead.
 * @hide This method was not published in any SDK version.
 */
@SystemApi
@Deprecated
public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture) {
    uploadFile.onReceiveValue(null);
}           

複制

解決方案也很簡單,直接不混淆

openFileChooser()

就好了。

-keepclassmembers class * extends android.webkit.WebChromeClient{
        public void openFileChooser(...);
}           

複制

支援關于上傳檔案的所有坑都填完了,最後附上完整源碼:

(源碼位址:https://github.com/BaronZ88/WebViewSample)

public class MainActivity extends AppCompatActivity {

    private ValueCallback<Uri> uploadMessage;
    private ValueCallback<Uri[]> uploadMessageAboveL;
    private final static int FILE_CHOOSER_RESULT_CODE = 10000;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WebView webview = (WebView) findViewById(R.id.web_view);
        assert webview != null;
        WebSettings settings = webview.getSettings();
        settings.setUseWideViewPort(true);
        settings.setLoadWithOverviewMode(true);
        settings.setJavaScriptEnabled(true);
        webview.setWebChromeClient(new WebChromeClient() {

            // For Android < 3.0
            public void openFileChooser(ValueCallback<Uri> valueCallback) {
                uploadMessage = valueCallback;
                openImageChooserActivity();
            }

            // For Android  >= 3.0
            public void openFileChooser(ValueCallback valueCallback, String acceptType) {
                uploadMessage = valueCallback;
                openImageChooserActivity();
            }

            //For Android  >= 4.1
            public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, String capture) {
                uploadMessage = valueCallback;
                openImageChooserActivity();
            }

            // For Android >= 5.0
            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
                uploadMessageAboveL = filePathCallback;
                openImageChooserActivity();
                return true;
            }
        });
        String targetUrl = "file:///android_asset/up.html";
        webview.loadUrl(targetUrl);
    }

    private void openImageChooserActivity() {
        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("image/*");
        startActivityForResult(Intent.createChooser(i, "Image Chooser"), FILE_CHOOSER_RESULT_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == FILE_CHOOSER_RESULT_CODE) {
            if (null == uploadMessage && null == uploadMessageAboveL) return;
            Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
            if (uploadMessageAboveL != null) {
                onActivityResultAboveL(requestCode, resultCode, data);
            } else if (uploadMessage != null) {
                uploadMessage.onReceiveValue(result);
                uploadMessage = null;
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void onActivityResultAboveL(int requestCode, int resultCode, Intent intent) {
        if (requestCode != FILE_CHOOSER_RESULT_CODE || uploadMessageAboveL == null)
            return;
        Uri[] results = null;
        if (resultCode == Activity.RESULT_OK) {
            if (intent != null) {
                String dataString = intent.getDataString();
                ClipData clipData = intent.getClipData();
                if (clipData != null) {
                    results = new Uri[clipData.getItemCount()];
                    for (int i = 0; i < clipData.getItemCount(); i++) {
                        ClipData.Item item = clipData.getItemAt(i);
                        results[i] = item.getUri();
                    }
                }
                if (dataString != null)
                    results = new Uri[]{Uri.parse(dataString)};
            }
        }
        uploadMessageAboveL.onReceiveValue(results);
        uploadMessageAboveL = null;
    }
}           

複制

源碼位址:https://github.com/BaronZ88/WebViewSample

如果大家喜歡這一系列的文章,歡迎關注我的知乎專欄和GitHub。
  • 知乎專欄:https://zhuanlan.zhihu.com/baron
  • GitHub:https://github.com/BaronZ88