在android webview(android 4.4,kitkat)中inputHTML文件
我正在使用android webview上的<input type="file">
。 我得到它的工作感谢这个线程: 在WebView中的file upload
但接受的答案(或任何其他)不再适用于android 4.4 kitkat webview。
任何人都知道如何解决它?
它也不适用于目标18。
我看了一些Android 4.4的源代码,似乎WebChromeClient没有改变,但我认为setWebChromeClient
不再工作在kitkat webview,或至less不是openFileChooser
函数。
更新2:有一个更简单的插件与phonegap / cordova一起使用
https://github.com/MaginSoft/MFileChooser
更新:与Cesidio DiBenedetto插件的示例项目
https://github.com/jcesarmobile/FileBrowserAndroidTest
我在android开源项目上打开了一个问题 ,答案是:
状态:WorkingAsItended
不幸的是,openFileChooser不是一个公共的API。 我们正在开发未来Android版本的公共API。
对于那些使用phonegap / cordova的人来说,这个解决方法是发布在bug跟踪器上的:
Cesidio DiBenedetto添加了评论 – 28 / Mar / 14 01:27
嘿,我一直在遇到这个问题,所以我暂时写了一个Cordova FileChooser插件给一个“创可贴”。 基本上,在Android 4.4(KitKat)中,正如前面的评论所述,文件对话框并没有打开。 然而,onclick事件仍然被触发,所以你可以调用FileChooser插件来打开一个文件对话框,一旦select,你可以设置一个包含文件完整path的variables。 在这一点上,你可以使用FileTransfer插件上传到你的服务器,并钩进onprogress事件来显示进度。 此插件主要针对Android 4.4进行configuration,所以我build议继续使用早期版本的Android的本机文件对话框。 这个插件可能存在问题,因为我没有在许多设备上完全testing所有可能的场景,但是我已经将它安装在Nexus 5上,并且工作正常。
没有testing,因为我build立了我自己的解决方法
铬开发者的评论
我们将在下一个主要版本中为WebViewClient添加一个公共API来处理文件请求。
看来他们现在认为它是一个错误,他们正在修复它
我设法在我的应用程序中实现提到的Cesidio DiBenedetto的解决方法 。 它工作的很好,但对于以前从未使用过PhoneGap / Cordove的人来说(比如我)可能有点棘手。 所以这是我在实施的时候放在一起的。
Apache Cordova是一个平台,可让您使用Web技术构build多平台移动应用程序。 其主要特点是它将本地API导出到JavaScript,因此提供了一种在网站和本地应用程序之间进行通信的方式。 典型的PhoneGap / Cordova应用程序是一个静态网站,与Cordova图层捆绑在一个APK中。 但是您可以使用Cordova来显示远程网站,这就是我们的情况。
解决方法如下:我们使用CordovaWebView
来显示我们的网站,而不是标准的WebView
。 当用户点击浏览select文件时,我们使用标准的JavaScript(jQuery …)捕获点击,并使用Cordova API,我们激活了Cesidio DiBenedetto的filechooser插件在本机端,这将打开一个不错的文件浏览器。 当用户select一个文件时,文件将从我们上传到我们的networking服务器的地方发回到JavaScript端。
重要的是要知道,你需要添加cordova支持你的网站。 好的,现在的实际情况是…
首先,您必须将Cordova添加到现有的应用程序中。 我遵循这个文件 。 有些步骤我不清楚,所以我会试着解释一下:
-
下载并提取cordova以外的应用程序,并build立cordova-3.4.0.jar所述。 它可能会第一次失败,因为local.properties文件丢失。 您将被指示如何在错误输出中创build它; 您只需将其指向您用来构buildAndroid应用程序的SDK即可。
-
将编译的jar文件复制到你的应用lib目录,并将jar作为库添加。 如果你像我一样使用Android Studio,只要确保在build.gradle的
dependencies
compile fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
。 然后只需点击同步项目与gradle文件button,你会没事的。 -
您不必创build/res/xml/main.xml文件。 您可以像对待标准WebView一样对待CordovaWebView,以便将其直接放到布局文件中。
-
现在只需按照原始文档中的步骤5-7将自己的
Activity
CordobaWebView
将要运行的地方。 检查下载的Cordova包中的/framework/src/org/apache/cordova/CordovaActivity.java
是个好主意。 你可以简单地复制大部分需要实现的方法。 6.步骤对于我们的目的来说非常重要,因为它将使用filechooser插件。 -
不要在任何地方复制任何HTML和JavaScript文件,我们稍后将其添加到您的网站。
-
不要忘记复制config.xml文件(你不必改变它)。
要在CordovaWebView
加载您的网站,只需将其url传递给cwv.loadUrl()
而不是Config.getStartUrl()
。
其次,你必须添加FileChooser插件到你的应用程序。 由于我们没有使用标准的Cordova设置,所以我们不能只按照自述文件中的说明打cordova plugin add
,我们必须手动添加它。
-
下载存储库并将源文件复制到您的应用程序。 确保res文件夹的内容转到您的应用程序res文件夹。 您现在可以忽略JavaScript文件。
-
将
READ_EXTERNAL_STORAGE
权限添加到您的应用程序。 -
将以下代码添加到/res/xml/config.xml中 :
<feature name="FileChooser"> <param name="android-package" value="com.cesidiodibenedetto.filechooser.FileChooser" /> </feature>
现在是时候添加cordova支持您的网站。 这听起来比较简单,你只需要把cordova.js链接到你的网站上,然而,有两件事要知道。
首先,每个平台(Android,iOS,WP)都有自己的cordova.js ,因此请确保使用Android版本(可以在/ framework / assets / www中下载的Cordova包中find它)。
其次,如果要从CordovaWebView
和标准浏览器(桌面或移动设备)访问您的网站,只有在CordovaWebView
显示页面时才加载cordova.js是一个好主意。 我发现了几种检测CordovaWebView
方法,但是下面的方法适用于我。 以下是您网站的完整代码:
function getAndroidVersion(ua) { var ua = ua || navigator.userAgent; var match = ua.match(/Android\s([0-9\.]*)/); return match ? parseFloat(match[1]) : false; }; if (window._cordovaNative && getAndroidVersion() >= 4.4) { // We have to use this ugly way to get cordova working document.write('<script src="/js/cordova.js" type="text/javascript"></script>'); }
请注意,我们也正在检查Android版本。 这个解决方法仅适用于KitKat。
在这一点上,你应该能够从你的网站手动调用FileChooser插件。
var cordova = window.PhoneGap || window.Cordova || window.cordova; cordova.exec(function(data) {}, function(data) {}, 'FileChooser', 'open', [{}]);
这应该打开文件浏览器,让你select一个文件。 请注意,这只能在事件deviceready被触发后才能完成。 要testing它,只需使用jQuery将此代码绑定到某个button即可。
最后一步是把这一切放在一起,让上传表单工作。 要做到这一点,您可以简单地按照自述文件中所述的Cesidio DiBenedetto的说明进行操作。 当用户在FileChooser中select文件时,文件path将返回到另一个Cordova插件FileTransfer用于执行实际上传的JavaScript端。 这意味着文件是上传到本地,而不是在CordovaWebView
(如果我理解正确的话)。
我不想把另一个Cordova插件添加到我的应用程序,我也不确定它如何与cookie一起工作(我需要发送cookie请求,因为只有经过身份validation的用户可以上传文件),所以我决定做这是我的方式。 我修改了FileChooser插件,所以它不会返回path,而是整个文件。 所以当用户select一个文件时,我读取它的内容,使用base64
进行编码,将其作为JSON传递给客户端,在那里解码并使用JavaScript将其发送到服务器。 它的工作原理,但有一个明显的缺点,因为base64是相当CPU要求,所以应用程序可能会冻结一点点时,大file upload。
要做到这一点,首先将此方法添加到FileUtils :
public static byte[] readFile(final Context context, final Uri uri) throws IOException { File file = FileUtils.getFile(context, uri); return org.apache.commons.io.FileUtils.readFileToByteArray(file); }
请注意,它使用Apache Commons库,所以不要忘记包含它或实现文件读取其他方式,不需要外部库。
接下来,修改FileChooser.onActivityResult方法以返回文件内容而不是其path:
// Get the URI of the selected file final Uri uri = data.getData(); Log.i(TAG, "Uri = " + uri.toString()); JSONObject obj = new JSONObject(); try { obj.put("filepath", FileUtils.getPath(this.cordova.getActivity(), uri)); obj.put("name", FileUtils.getFile(this.cordova.getActivity(), uri).getName()); obj.put("type", FileUtils.getMimeType(this.cordova.getActivity(), uri)); // attach the actual file content as base64 encoded string byte[] content = FileUtils.readFile(this.cordova.getActivity(), uri); String base64Content = Base64.encodeToString(content, Base64.DEFAULT); obj.put("content", base64Content); this.callbackContext.success(obj); } catch (Exception e) { Log.e("FileChooser", "File select error", e); this.callbackContext.error(e.getMessage()); }
最后,这是您在网站上使用的代码(需要jQuery):
var cordova = window.PhoneGap || window.Cordova || window.cordova; if (cordova) { $('form.fileupload input[type="file"]', context).on("click", function(e) { cordova.exec( function(data) { var url = $('form.fileupload', context).attr("action"); // decode file from base64 (remove traling = first and whitespaces) var content = atob(data.content.replace(/\s/g, "").replace(/=+$/, "")); // convert string of bytes into actual byte array var byteNumbers = new Array(content.length); for (var i = 0; i < content.length; i++) { byteNumbers[i] = content.charCodeAt(i); } var byteContent = new Uint8Array(byteNumbers); var formData = new FormData(); var blob = new Blob([byteContent], {type: data.type}); formData.append('file', blob, data.name); $.ajax({ url: url, data: formData, processData: false, contentType: false, type: 'POST', success: function(data, statusText, xhr){ // do whatever you need } }); }, function(data) { console.log(data); alert("error"); }, 'FileChooser', 'open', [{}]); }); }
那么,就是这样。 我花了好几个小时才搞定这个工作,所以我分享我的知识,希望能帮助别人。
如果有人仍然在寻找解决scheme,使用kitkat上的webview文件input。
当在Android 4.4上点击时,openFileChooser不会被调用
https://code.google.com/p/android/issues/detail?id=62220
一个叫做Crosswalk的基于铬的库可以用来解决这个问题
https://crosswalk-project.org/documentation/downloads.html
脚步
1.将从上述链接下载的xwalk_core_library android项目作为库导入到您的项目中
2.在您的布局xml中添加以下内容
<org.xwalk.core.XWalkView android:id="@+id/webpage_wv" android:layout_width="match_parent" android:layout_height="match_parent" />
3.在您的活动的onCreate方法中,执行以下操作
mXwalkView = (XWalkView) context.findViewById(R.id.webpage_wv); mXwalkView.setUIClient(new UIClient(mXwalkView)); mXwalkView.load(navigateUrl, null); //navigate url is your page URL
-
添加活动类variables
私人ValueCallback mFilePathCallback; 私人XWalkView mXwalkView
-
文件input对话框现在应该显示出来。 但是,您需要提供callback来获取文件并将其发送给服务器。
-
您将需要重写您的活动onActivityResult
public void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (mXwalkView != null) { if (mFilePathCallback != null) { Uri result = intent == null || resultCode != Activity.RESULT_OK ? null : intent.getData(); if (result != null) { String path = MediaUtility.getPath(getActivity(), result); Uri uri = Uri.fromFile(new File(path)); mFilePathCallback.onReceiveValue(uri); } else { mFilePathCallback.onReceiveValue(null); } } mFilePathCallback = null; } mXwalkView.onActivityResult(requestCode, resultCode, intent); }
-
MediaUtility类可以在
从URI获取真正的path,Android KitKat新的存储访问框架
见Paul Burke的回答 -
要获取mFilePathCallback的数据对象, 请在您的活动中创build一个子类
class UIClient extends XWalkUIClient { public UIClient(XWalkView xwalkView) { super(xwalkView); } public void openFileChooser(XWalkView view, ValueCallback<Uri> uploadFile, String acceptType, String capture) { super.openFileChooser(view, uploadFile, acceptType, capture); mFilePathCallback = uploadFile; Log.d("fchooser", "Opened file chooser."); }
}
-
你们都完成了。 fileupload现在应该工作。 不要忘记将Crosswalk所需的权限添加到您的清单。
使用权限android:name =“android.permission.ACCESS_FINE_LOCATION”
使用权限android:name =“android.permission.ACCESS_NETWORK_STATE”
使用权限android:name =“android.permission.ACCESS_WIFI_STATE”
使用权限android:name =“android.permission.CAMERA”
使用权限android:name =“android.permission.INTERNET”
使用权限android:name =“android.permission.MODIFY_AUDIO_SETTINGS”
使用权限android:name =“android.permission.RECORD_AUDIO”
使用权限android:name =“android.permission.WAKE_LOCK”
使用权限android:name =“android.permission.WRITE_EXTERNAL_STORAGE”
在webview中的文件select器现在可以在最新的Android 4.4.3版本中运行。
尝试使用Nexus 5自己。
尽pipeKitkat
版本与webview
type = file表单字段不兼容,我们可以使用webview的addJavascriptInterface
方法来完成file upload任务。 服务器端应该判断android的版本,如果它低于4.4
,使用WebViewChromeClient
私有方法,如果4.4或更高版本,让服务器调用android方法相互通信(例如上传文件内容asynchronous)
//代码
webView.getSettings().setJavaScriptEnabled(true); webView.addJavascriptInterface(new WebViewJavaScriptInterface(this), "app");
这是一个链接,可能有帮助…
呼叫Android的方法-从JavaScript的
我为这个问题build立了我自己的解决scheme,而不使用任何库,Cordova插件或自定义WebViews,并且它在所有Android版本中都能正常工作。
这个解决scheme涉及到使用一些非常简单的Javascript在WebView和Android应用程序之间进行通信,并从Android应用程序直接执行文件select和上传, 删除所有openFileChooser(),showFileChooser()和onShowFileChooser() WebChromeClient方法。
第一步是在用户点击一个文件input时,从网站触发一个javascript控制台消息 , 编写一个唯一的代码 ,用来上传带有唯一名称或path的文件。 例如,将完整的date时间与一个巨大的随机数连接起来:
<input type="file" name="user_file" onclick="console.log('app.upload=20170405173025_456743538912');">
然后,您的应用程序可以读取此消息,覆盖WebChromeClient的onConsoleMessage()方法,检测该消息,读取代码并触发文件select :
webview.setWebChromeClient(new WebChromeClient() { // Overriding this method you can read messages from JS console. public boolean onConsoleMessage(ConsoleMessage message){ String messageText = message.message(); // Check if received message is a file upload and get the unique code if(messageText.length()>11 && messageText.substring(0,11).equals("app.upload=")) { String code = messageText.substring(11); triggerFileUploadSelection(code); return true; } return false; } });
对于文件select ,你可以像这样使用一个简单的Android ACTION_PICKE意图 :
public void triggerFileUploadSelection(String code){ // For Android 6.0+ you must check for permissions in runtime to read from external storage checkOrRequestReadPermission(); // Store code received from Javascript to use it later (code could also be added to the intent as an extra) fileUploadCode = code; // Build a simple intent to pick any file, you can replace "*/*" for "image/*" to upload only images if needed Intent filePickerIntent = new Intent(Intent.ACTION_PICK); filePickerIntent.setType("*/*"); // FILE_UPLOAD_CODE is just any unique integer request code to identify the activity result when the user selects the file startActivityForResult( Intent.createChooser(filePickerIntent, getString(R.string.chooseFileToUpload) ), FILE_UPLOAD_CODE ); }
用户select一个文件(或不),你可以接收文件Uri ,并将其转换为一个真正的文件path :
@Override public void onActivityResult (int requestCode, int resultCode, Intent data) { if(requestCode==FILE_UPLOAD_CODE) { if(data != null && resultCode == RESULT_OK){ // user selected a file try{ Uri selectedFileUri = data.getData(); if(selectedFileUri!=null) { // convert file URI to a real file path with an auxiliary function (below) String filePath = getPath(selectedFileUri); if(filePath!=null) { // I got the file path, I can upload the file to the server (I pass webview as an argument to be able to update it when upload is completed) uploadSelectedFile(getApplicationContext(), filePath, fileUploadCode, webview); }else{ showToastFileUploadError(); } }else{ showToastFileUploadError(); } }catch (Exception e){ e.printStackTrace(); showToastFileUploadError(); } }else{ // user didn't select anything } } } // I used this method for images, and it uses MediaStore.Images so you should probably // use another method to get the path from the Uri if you are working with any kind of file public String getPath(Uri uri) { String[] projection = { MediaStore.Images.Media.DATA }; Cursor cursor = managedQuery(uri, projection, null, null, null); if(cursor==null)return null; int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); return cursor.getString(column_index); }
uploadSelectedFile方法只是简单地创build一个包含filePath,fileUploadCode和WebView中所有信息的对象,并触发一个AsyncTask来上传使用该信息的文件 ,并在完成后更新WebView:
public static void uploadSelectedFile(Context c, String filePath, String code, WebView webView){ // YOU CAN SHOW A SPINNER IN THE WEB VIEW EXECUTING ANY JAVASCRIPT YOU WANT LIKE THIS: webView.loadUrl("javascript: Element.show('my_upload_spinner');void 0"); // void 0 avoids at the end avoids some browser redirection problems when executing javascript commands like this // CREATE A REQUEST OBJECT (you must also define this class with those three fields and a response field that we will use later): FileUploadRequest request = new FileUploadRequest(filePath, code, webView); // Trigger an async task to upload the file, and pass on the request object with all needed data FileUploadAsyncTask task = new FileUploadAsyncTask(); task.execute(request); }
AsyncTask接收包含所有信息的请求对象,并使用MultipartUtility构build多部分请求,并将其轻松发送到服务器 。 您可以从许多地方获得大量的Java Multipart Utilities,其中之一就在这里: http : //www.codejava.net/java-se/networking/upload-files-by-sending-multipart-request-programmatically
public class FileUploadAsyncTask extends AsyncTask<FileUploadRequest, Void, FileUploadRequest> { @Override protected FileUploadRequest doInBackground(FileUploadRequest... requests) { FileUploadRequest request = requests[0]; try { // Generate a multipart request pointing to the URL where you will receive uploaded file MultipartUtility multipart = new MultipartUtility("http://www.example.com/file_upload.php", "UTF-8"); // Add a field called file_code, to send the code to the server script multipart.addFormField("file_code", request.code); // Add the file to the request multipart.addFilePart("file_path", new File(request.filePath)); // Send the request to the server and get the response and save it back in the request object request.response = multipart.finish(); // response from server. } catch (IOException e) { request.response = "FAILED"; e.printStackTrace(); } return request; }
现在我们已经把file upload到服务器上了,我们可以在我们的AsyncTask的onPostExecute方法中使用Javascript来更新我们的网站。 最重要的是将文件代码设置在表单的隐藏字段中 ,以便用户发送表单时可以获取该代码。 您也可以在网站上显示消息,甚至可以轻松显示上传的图像(如果是图像):
@Override protected void onPostExecute(FileUploadRequest request) { super.onPostExecute(request); // check for a response telling everything if upload was successful or failed if(request.response.equals("OK")){ // set uploaded file code field in a hidden field in your site form (ESSENTIAL TO ASSOCIATE THE UPLOADED FILE WHEN THE USER SENDS THE WEBSITE FORM) request.webView.loadUrl("javascript: document.getElementById('app_upload_code_hidden').value = '"+request.code+"';void 0"); // Hide spinner (optional) //request.webView.loadUrl("javascript: Element.hide('my_upload_spinner');void 0"); // show a message in the website, or the uploaded image in an image tag setting the src attribute // to the URL of the image you just uploaded to your server. // (you must implement your own fullUrl method in your FileUploadRequest class) // request.webView.loadUrl("javascript: document.getElementById('app_uploaded_image').src = '"+request.fullUrl()+"';void 0"); // request.webView.loadUrl("javascript: Element.show('app_uploaded_image');void 0"); } }
现在Android部分已经完成,您需要通过Android应用程序AsyncTask工作服务器端来接收您上传的文件 ,并将其保存到您需要的任何地方。
用户发送邮件时 ,您还必须处理您的网站表单 ,并根据用户从应用中上传的文件进行任何操作。 要做到这一点,您将获得表单中的文件代码(我们在onPostExecute()中的一个字段中完成了该代码),并且必须使用该文件代码来查找应用程序上传到服务器的文件 。 为了达到这个目的,你可以用这个代码作为文件名保存在一个path中,或者把代码和你上传文件的path保存到数据库中。
这个解决scheme只依赖于可用的元素,并且与所有Android版本兼容,所以它可以在任何设备上工作(而且我还没有收到用户对此的抱怨)。
如果在同一页面中有多个文件input ,则可以在初始JavaScript消息中发送一个字段编号或额外的标识符以及唯一的文件代码,并在所有应用程序代码中传递该标识符,并使用它来更新正确的元素在onPostExecute()中。
我在这里修改了一些实际的代码,所以如果有什么失败的话,在重命名一些东西的时候可能会是一个错字或者一些细节。
这是相当多的信息处理,所以如果任何人需要任何澄清或有build议或更正,请告诉我。
Kitkat的新文件浏览器在Chrome上也同样疯狂,看到WebView如何使用Chromium,这可能是一个相关的问题。 我发现直接从相机上传文件的工作,而不是从'图像'文件夹。 如果您要从“图库”上传,则可以访问相同的文件。 嘎。
看起来像一个修复已经准备好,但等待释放:
如果你打算只在你的网站上添加一个webview wrapper并作为一个应用程序启动它,只是不要使用默认的android webview来查看,这种方式或者其他的方式是一个非常头痛的问题。对于我来说,两件事情都不起作用out 1.input文件2. Stripe checkout integeration(使用高级的JS API)
我做了什么来从黑暗中走出来
刚刚使用Cordova创build了一个示例应用程序。 我们认为它更简单。
- 从官方页面安装Cordova,构build一个示例应用程序。
- 它会给你一个apk文件。 ( 你说对了)
-
你从创build的应用程序去www文件夹并打开index.js,find并replace行onDeviceReady:function(){window.open(' http://yourdomain.com/yourpage ')}再次运行应用程序,它将打开该网站
-
现在,主步骤。 到目前为止cordova只使用一个温和的webview。 一切都应该改变一天。 将Crosswalk插件添加到您的Cordova应用程序,它将用一个全新的完整铬视图replace呆滞的webview https://crosswalk-project.org/documentation/cordova.html
-
运行
cordova clean
,然后cordova build --release
清理旧的build设。 - 打开Cordva应用程序内部的config.xml文件,添加
<allow-navigation href="http://yourdomain.com/*" />
- 再次运行应用程序。 魔法!。
当使用crosswalk-webview-21.51.546.7并通过相机select图片。 在onActivityResult() the intent.getData()
为null
。 这意味着通过相机上传图片无法正常工作。