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'])
in dependencies
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 the CordobaWebView
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.