1. 程式人生 > >Webview上傳檔案的那些坑

Webview上傳檔案的那些坑

要說Android中最厲害的元件莫過於Webview 了,誇張點說把這個元件放在螢幕上就可以算作一個簡單地瀏覽器應用了。但你若認為這就萬事大吉了,可太小看Webview這個磨人的妖精了,下面單就上傳檔案的這個坑來做展開。

從零開始

我們在xml中寫入一個簡單的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程式碼中使用其載入一個能夠提供上傳服務的URL:

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

之後,要加網路許可權:

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

如果想讓Webview能夠訪問本地資源,SD卡的讀寫許可權也是避免不了的:

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

最後,我們執行,會發現根本不能訪問本地資源。Why?

讓我們來填補第一個坑:

支援上傳檔案

Webview執行上傳操作的邏輯是這樣的:首先準備上傳時會回撥 WebChromeClient 類下的 openFileChooser 方法,在這個方法中給我們機會發起Intent來開啟支援提供檔案的第三方應用,最後在 onActivityResult 回撥中將第三方應用提供的內容通過一個叫做 ValueCallback 的引數返回給Webview(詳細點來說:ValueCallback是在 openFileChooser 方法裡由webview提供給我們的,裡面包裹一個Uri,我們在onActivityResult 裡將選中的Uri反饋給 ValueCallback ,這時候相當於Webview就知道我們選擇了什麼檔案),因此,我們需要為Webview設定一個提供 openFileChooser 方法的 WebChromeClient ,這個方法在不同版本的Android中引數是不同的,為此我們一般需要寫三個過載函式,大致像這個樣子:

private ValueCallback<Uri> mUploadMessage;
//設定`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回撥   
@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;
     }
}
   	}

還有重要的一點:如果這個上傳操作涉及到JS操作,別忘記對Webview開啟對JS的支援:

WebSettings settings = webview.getSettings();

settings.setJavaScriptEnabled(true);

這樣,打個debug包測試看以下,不出意外我們的Webview應該可以支援上傳操作了。

別高興得太早,如果這個時候產品要將release包推向市場,當你把release包交給產品時,你會發現你的Webview又不能上傳了,什麼情況?

請聽Webview上傳操作的第二個坑。

支援release版

debug版是好的,為什麼release就不行了呢?準確的說,開啟了混淆的release包是不可以的,究其原因在於, openFileChooser 方法並不是 WebChromeClient 的對外開放的方法,因此這個方法會被混淆,解決辦法也比較簡單,只需要在混淆檔案裡控制一下即可:

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

好了,我們的Webview可以作為應用內的一個部分對外發布了,等等,有5.0以上使用者反映用不了?納尼????

別迴心,來看看這第三個坑。

支援5.0

在5.0釋出後,Android人家說了,這次我們回撥的不是 openFileChooser 方法,而是 onShowFileChooser 方法,並且上文提到的 ValueCallback 引數裡包裹著不再是Uri,而是Uri陣列,因此我們必須為5.0+的機器做適配,大致思路如下:

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應該就可以適應5.0+的機器了。

參考程式碼

總結

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

綜上,也許你會放鬆些,不管怎樣我們總算有了比較完美的解決辦法,但別急,如上程式碼在4.4.0機子上依舊會失效的,為什麼呢?當時Android說在Webview中上傳檔案不安全,我們先取消,換句話說,如果不是一些第三方良(惡)心廠商對Webview從底層做修改,單從應用層即使你改出花來也不會支援上傳操的!