HTML file input in android webview (android 4.4, kitkat)
Update 2: There is a simpler plugin to use with phonegap/cordova
https://github.com/MaginSoft/MFileChooser
Update: Sample project with Cesidio DiBenedetto plugin
https://github.com/jcesarmobile/FileBrowserAndroidTest
I opened an issue on the android open source project and the answer was:
Status: WorkingAsIntended
unfortunately, openFileChooser is not a public API. We are working on a public API in future releases of Android.
For those using phonegap/cordova, this workaround was posted on the bug tracker:
Cesidio DiBenedetto added a comment - 28/Mar/14 01:27
Hey all, I've been experiencing this issue as well so I wrote a Cordova FileChooser plugin to a "band-aid" for the time being. Basically, in Android 4.4(KitKat), as mentioned in previous comments, the file dialog is not opened. However the onclick event is still fired on so you can call the FileChooser plugin to open a file dialog and upon selection, you can set a variable that contains the full path to the file. At this point, you can use the FileTransfer plugin to upload to your server and hook into the onprogress event to show progress. This plugin is mainly configured for Android 4.4 so I would recommend to continue to use the native file dialogs for earlier versions of Android. There might be issues with the plugin as I have not fully tested all possible scenarios on many devices, but I have installed it on a Nexus 5 and it worked fine.
https://github.com/cdibened/filechooser
Not tested it because I built my own workaround
A comment form a chromium developer
We will be a adding a public API to WebViewClient in next major release to handle file requests.
It seems they consider it as a bug now and they are going to fix it
I managed to implement the mentioned Cesidio DiBenedetto's workaround in my app. It's working great but for someone who's never used PhoneGap/Cordove before (like me) it might be a bit tricky. So here is a little howto I put together while I was implementing it.
Apache Cordova is a platform that lets you build multiplatform mobile apps just using web technologies. The key feature is that it exports native API to JavaScript and therefore provides a way to communicate between the website and the native application. Typical PhoneGap/Cordova app is a static website which is bundled together with the Cordova layer in one APK. But you can use Cordova to display a remote website and that is our case.
The workaround works as follows: Instead of the standard WebView
we use CordovaWebView
to display our website. When user clicks browse to select a file, we catch that click using standard JavaScript (jQuery...) and using Cordova API we activate Cesidio DiBenedetto's filechooser plugin on the native side which will open a nice file browser. When user selects a file, the file is sent back to the JavaScript side from where we upload it to our webserver.
Important thing to know is that you need to add Cordova support to you website. Okay, now the actual howto...
Firstly, you have to add Cordova to your existing app. I followed this documentation. Some steps were unclear to me so I'll try to explain more:
Download and extract Cordova somewhere outside your app and build cordova-3.4.0.jar as described. It will probably fail for the first time as local.properties file is missing. You'll be instructed how to create it in the error output; you just have to point it to the SDK you use to build your android app.
Copy the compiled jar file to your app lib directory and add the jar as a library. If you use Android Studio like me, just make sure you have
compile fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
independencies
inside build.gradle. Then just hit Sync project with gradle files button and you'll be fine.You don't have to create the /res/xml/main.xml file. You can treat the CordovaWebView the same way you treat the standard WebView so you can put it directly to your layout file.
Now just follow steps 5-7 in the original documentation to put together your own
Activity
where theCordobaWebView
will be running. It's a good idea to check the/framework/src/org/apache/cordova/CordovaActivity.java
in the Cordova package you downloaded. You can simply copy most of the methods required to be implemented. The 6. step is really crucial for our purpose as it will let as use the filechooser plugin.Do not copy any HTML and JavaScript files anywhere, we will add it to your website later.
Don't forget to copy the config.xml file (you don't have to change it).
To load your website in the CordovaWebView
, simply pass its url to cwv.loadUrl()
instead of Config.getStartUrl()
.
Secondly, you have to add the FileChooser plugin to your app. As we are not using the standard Cordova setup we can't just hit cordova plugin add
as instructed in the readme, we have to add it manually.
Download the repository and copy the source files to your app. Make sure that the content of res folder goes to your app res folder. You can ignore the JavaScript file for now.
Add
READ_EXTERNAL_STORAGE
permission to your app.Add following code to /res/xml/config.xml:
<feature name="FileChooser">
<param name="android-package" value="com.cesidiodibenedetto.filechooser.FileChooser" />
</feature>
Now is the time to add Cordova support to your website. It's simpler than it sounds, you just have to link cordova.js to your website, however, there are two things to know about.
First, each platform (Android, iOS, WP) has its own cordova.js, so make sure you use the Android version (you can find it in the Cordova package you downloaded in /framework/assets/www).
Second, if your website is going to be accessed from both CordovaWebView
and standard browsers (desktop or mobile) it's generally a good idea to load cordova.js only when the page is displayed in CordovaWebView
. I found several ways to detect CordovaWebView
but the following one worked for me. Here is the complete code for your website:
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>');
}
Note that we're also checking the Android version. This workaround is required only for KitKat.
At this point you should be able to manually invoke the FileChooser plugin from your website.
var cordova = window.PhoneGap || window.Cordova || window.cordova;
cordova.exec(function(data) {}, function(data) {}, 'FileChooser', 'open', [{}]);
This should open the file browser and let you pick a file. Note that this can be done only after the event deviceready is fired. To test it, just bind this code to some button using jQuery.
The final step is to put this all together and get the upload form working. To achieve this, you can simply follow Cesidio DiBenedetto's instructions described in the README. When user picks the file in the FileChooser, the filepath is returned back to the JavaScript side from where another Cordova plugin, FileTransfer, is used to perform the actual upload. That means that the file is uploaded on the native side, not in CordovaWebView
(if I understand it correctly).
I didn't feel like adding another Cordova plugin to my application and I also wasn't sure how it would work with cookies (I need to send cookies with the request because only authenticated users are allowed to upload files) so I decided to do it my way. I modified the FileChooser plugin so it doesn't return the path but the whole file. So when user picks a file, I read its content, encode it using base64
, pass it as JSON to the client side where I decode it and send it using JavaScript to the server. It works but there is an obvious downside as base64 is quite CPU demanding so the app may freeze a little bit when large files are uploaded.
To do it may way, first add this method to 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);
}
Note that it uses Apache Commons library so don't forget to include it or implement file reading some other way that doesn't require the external library.
Next, modify FileChooser.onActivityResult method to return file content instead of its 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());
}
And finally, this is the code you'll use on your website (jQuery is required):
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', [{}]);
});
}
Well, that's all. It took me several hours to get this working so I'm sharing my knowledge with a humble hope that it might help somebody.
If anyone is still looking for a solution to file input using a webview on kitkat.
openFileChooser not called when is clicked on android 4.4
https://code.google.com/p/android/issues/detail?id=62220
A chromium based library called Crosswalk can be used to solve this
https://crosswalk-project.org/documentation/downloads.html
Steps
1. Import the xwalk_core_library android project downloaded from the above link into your project as a library
2. In your layout xml add the following
<org.xwalk.core.XWalkView
android:id="@+id/webpage_wv"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
3. In onCreate method of your activity, do the following
mXwalkView = (XWalkView) context.findViewById(R.id.webpage_wv);
mXwalkView.setUIClient(new UIClient(mXwalkView));
mXwalkView.load(navigateUrl, null); //navigate url is your page URL
-
Add activity Class variables
private ValueCallback mFilePathCallback; private XWalkView mXwalkView
The file input dialog should now show up. However you would need to provide callbacks to get the file and send it to the sever.
-
You would need to override onActivityResult of your activity
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); }
The MediaUtility class can be found at
Get real path from URI, Android KitKat new storage access framework
See Paul Burke's answer-
To get the data object for mFilePathCallback, create a subclass in your activity
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."); }
}
-
You're all done. The fileupload should now work. Don't forget to add the permissions required by Crosswalk to your manifest.
uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"
uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
uses-permission android:name="android.permission.CAMERA"
uses-permission android:name="android.permission.INTERNET"
uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"
uses-permission android:name="android.permission.RECORD_AUDIO"
uses-permission android:name="android.permission.WAKE_LOCK"
uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"