Webview上傳文件的那些坑

要說Android中最厲害的組件莫過于Webview 了,夸張點(diǎn)說把這個(gè)組件放在屏幕上就可以算作一個(gè)簡單地瀏覽器應(yīng)用了。但你若認(rèn)為這就萬事大吉了,可太小看Webview這個(gè)磨人的妖精了,下面單就上傳文件的這個(gè)坑來做展開。

從零開始

我們?cè)趚ml中寫入一個(gè)簡單的Webview組件:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"                             android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

     <WebView
          android:id="@+id/webview"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:layout_margin="5dp"></WebView>

 </RelativeLayout>

然后在Java代碼中使用其加載一個(gè)能夠提供上傳服務(wù)的URL:

WebView webview = (WebView) findViewById(R.id.webview);
webview.loadUrl(A_UPLOAD_URL);

之后,要加網(wǎng)絡(luò)權(quán)限:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

如果想讓W(xué)ebview能夠訪問本地資源,SD卡的讀寫權(quán)限也是避免不了的:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

最后,我們運(yùn)行,會(huì)發(fā)現(xiàn)根本不能訪問本地資源。Why?

讓我們來填補(bǔ)第一個(gè)坑:

支持上傳文件

Webview執(zhí)行上傳操作的邏輯是這樣的:首先準(zhǔn)備上傳時(shí)會(huì)回調(diào)WebChromeClient類下的openFileChooser方法,在這個(gè)方法中給我們機(jī)會(huì)發(fā)起Intent來打開支持提供文件的第三方應(yīng)用,最后在onActivityResult回調(diào)中將第三方應(yīng)用提供的內(nèi)容通過一個(gè)叫做ValueCallback的參數(shù)返回給Webview(詳細(xì)點(diǎn)來說:ValueCallback是在openFileChooser方法里由webview提供給我們的,里面包裹一個(gè)Uri,我們?cè)?code>onActivityResult里將選中的Uri反饋給ValueCallback,這時(shí)候相當(dāng)于Webview就知道我們選擇了什么文件),因此,我們需要為Webview設(shè)置一個(gè)提供openFileChooser方法的WebChromeClient,這個(gè)方法在不同版本的Android中參數(shù)是不同的,為此我們一般需要寫三個(gè)重載函數(shù),大致像這個(gè)樣子:

private ValueCallback<Uri> mUploadMessage;
    //設(shè)置`WebChromeClient`:
webview.setWebChromeClient(new WebChromeClient(){
     public void openFileChooser(ValueCallback<Uri> uploadMsg) {
            Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg)");
            mUploadMessage = uploadMsg;
            Intent i = new Intent(Intent.ACTION_GET_CONTENT);
            i.addCategory(Intent.CATEGORY_OPENABLE);
            i.setType("*/*");
            MainActivity.this.startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
      }

      public void openFileChooser( ValueCallback uploadMsg, String acceptType ) {
            Log.d(TAG, "openFileChoose( ValueCallback uploadMsg, String acceptType )");
            mUploadMessage = uploadMsg;
            Intent i = new Intent(Intent.ACTION_GET_CONTENT);
            i.addCategory(Intent.CATEGORY_OPENABLE);
            i.setType("*/*");
            MainActivity.this.startActivityForResult(
                    Intent.createChooser(i, "File Browser"),
                    FILECHOOSER_RESULTCODE);
      }
      public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture){
            Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg, String acceptType, String capture)");
            mUploadMessage = uploadMsg;
            Intent i = new Intent(Intent.ACTION_GET_CONTENT);
            i.addCategory(Intent.CATEGORY_OPENABLE);
            i.setType("*/*");
            MainActivity.this.startActivityForResult( Intent.createChooser( i, "File Browser" ), MainActivity.FILECHOOSER_RESULTCODE );
        }
});
    
//onActivityResult回調(diào)   
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode==FILECHOOSER_RESULTCODE)
         {
                if (null == mUploadMessage && null == mUploadCallbackAboveL) return;
                 Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
                 if (mUploadMessage != null) {
                    mUploadMessage.onReceiveValue(result);
                    mUploadMessage = null;
               }
          }
}

還有重要的一點(diǎn):如果這個(gè)上傳操作涉及到JS操作,別忘記對(duì)Webview開啟對(duì)JS的支持:

 WebSettings settings = webview.getSettings();
settings.setJavaScriptEnabled(true);

這樣,打個(gè)debug包測試看以下,不出意外我們的Webview應(yīng)該可以支持上傳操作了。

別高興得太早,如果這個(gè)時(shí)候產(chǎn)品要將release包推向市場,當(dāng)你把release包交給產(chǎn)品時(shí),你會(huì)發(fā)現(xiàn)你的Webview又不能上傳了,什么情況?

請(qǐng)聽Webview上傳操作的第二個(gè)坑。

支持release版

debug版是好的,為什么release就不行了呢?準(zhǔn)確的說,開啟了混淆的release包是不可以的,究其原因在于,openFileChooser方法并不是WebChromeClient的對(duì)外開放的方法,因此這個(gè)方法會(huì)被混淆,解決辦法也比較簡單,只需要在混淆文件里控制一下即可:

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

好了,我們的Webview可以作為應(yīng)用內(nèi)的一個(gè)部分對(duì)外發(fā)布了,等等,有5.0以上用戶反映用不了?納尼????

別回心,來看看這第三個(gè)坑。

支持5.0

在5.0發(fā)布后,Android人家說了,這次我們回調(diào)的不是openFileChooser方法,而是onShowFileChooser方法,并且上文提到的ValueCallback參數(shù)里包裹著不再是Uri,而是Uri數(shù)組,因此我們必須為5.0+的機(jī)器做適配,大致思路如下:

webview.setWebChromeClient(new WebChromeClient(){
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
     ...
}

public void openFileChooser( ValueCallback uploadMsg, String acceptType ) {
       ...
}

public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture){
                ...
}
        
// For Android 5.0+
public boolean onShowFileChooser (WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
         mUploadCallbackAboveL = filePathCallback;
         Intent i = new Intent(Intent.ACTION_GET_CONTENT);
         i.addCategory(Intent.CATEGORY_OPENABLE);
         i.setType("*/*");
         MainActivity.this.startActivityForResult(
                    Intent.createChooser(i, "File Browser"),
                    FILECHOOSER_RESULTCODE);
         return true;
        }
});
    
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if(requestCode==FILECHOOSER_RESULTCODE)
    {
        if (null == mUploadMessage && null == mUploadCallbackAboveL) return;
        Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
        if (mUploadCallbackAboveL != null) {
            onActivityResultAboveL(requestCode, resultCode, data);
        }
        else  if (mUploadMessage != null) {
            mUploadMessage.onReceiveValue(result);
            mUploadMessage = null;
        }
    }
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void onActivityResultAboveL(int requestCode, int resultCode, Intent data) {
    if (requestCode != FILECHOOSER_RESULTCODE
            || mUploadCallbackAboveL == null) {
        return;
    }

    Uri[] results = null;
    if (resultCode == Activity.RESULT_OK) {
        if (data == null) {

        } else {
            String dataString = data.getDataString();
            ClipData clipData = data.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)};
        }
    }
    mUploadCallbackAboveL.onReceiveValue(results);
    mUploadCallbackAboveL = null;
    return;
}

如上,我們的Webview應(yīng)該就可以適應(yīng)5.0+的機(jī)器了。

參考代碼

如果你需要上如完整代碼,可以參考這個(gè)DEMO:https://gitcafe.com/saymagic/Webviewdemo

總結(jié)

根據(jù)我自己的測試,上面的參考代碼成功跑通了如下幾個(gè)機(jī)型:魅族5.0.1、YunOS5.0、小米4.4.4、小米4.3、摩托4.4.4。不過對(duì)于Android這種從2.0、3.0、4.0、5.0都對(duì)Webview做手腳并且不保持向下兼容的作法,我只想說在逗寶寶們玩?

綜上,也許你會(huì)放松些,不管怎樣我們總算有了比較完美的解決辦法,但別急,如上代碼在4.4.0機(jī)子上依舊會(huì)失效的,為什么呢?當(dāng)時(shí)Android說在Webview中上傳文件不安全,我們先取消,換句話說,如果不是一些第三方良(惡)心廠商對(duì)Webview從底層做修改,單從應(yīng)用層即使你改出花來也不會(huì)支持上傳操的!

本文地址:http://blog.saymagic.tech/2015/11/08/webview-upload.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評(píng)論 25 708
  • 前言:隨著市場需求的不斷變化,原生安卓已經(jīng)無法滿足客戶的需要了,現(xiàn)在很多app都在使用Android和h5的交互實(shí)...
    AWeiLoveAndroid閱讀 26,583評(píng)論 21 194
  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,384評(píng)論 0 17
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評(píng)論 19 139
  • 云銷雨霽,彩徹區(qū)明。落霞與孤鶩齊飛,秋水共長天一色——《滕王閣序》王勃 2017-01-04,大理雨后晚霞甚美,然...
    墨者閱讀 935評(píng)論 0 1

友情鏈接更多精彩內(nèi)容