45

I was using the <input type="file"> on the android webview. I got it working thanks to this thread: File Upload in WebView

But the accepted answer (or any other) no longer works with android 4.4 kitkat webview.

Anybody knows how to fix it?

It doesn't work with target 18 either.

I've looking some android 4.4 source code and it seems that the WebChromeClient hasn't changed, but I think the setWebChromeClient no longer works on the kitkat webview, or at least not the openFileChooser function.

4

9 回答 9

30

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

于 2013-11-13T19:17:15.107 回答
21

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Do not copy any HTML and JavaScript files anywhere, we will add it to your website later.

  6. 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.

  1. 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.

  2. Add READ_EXTERNAL_STORAGE permission to your app.

  3. 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.

于 2014-05-04T16:00:33.907 回答
8

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
  1. Add activity Class variables

    private ValueCallback mFilePathCallback; private XWalkView mXwalkView

  2. 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.

  3. 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);
    
    }
    
  4. The MediaUtility class can be found at
    Get real path from URI, Android KitKat new storage access framework
    See Paul Burke's answer

  5. 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.");
    }
    

    }

  6. 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"

于 2015-03-23T13:21:27.170 回答
5

File Chooser in a webview works now in the latest Android release 4.4.3.

Tried it myself using Nexus 5.

于 2014-06-25T08:59:07.123 回答
1

Despite the fact that Kitkat version is not compatible with webview type=file form field,we could use webview's addJavascriptInterface method to accomplish file upload task. Server side should judge android's version,if it is lower than 4.4,use WebViewChromeClient private methods,if 4.4 or above,let the server call android method to communicate with each other(ex. upload file content async)

// code

webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new WebViewJavaScriptInterface(this), "app");


Here is a link that might help...

call-android-methods-from-javascript

于 2014-09-13T06:42:49.837 回答
1

I built my own solution for this problem without using any libraries, Cordova plugins or custom WebViews, and it is working properly in all Android versions.

This solution involves using some very simple Javascript to communicate between your website in the WebView and the Android app, and performing file selection and upload directly from your Android application, removing all the openFileChooser(), showFileChooser() and onShowFileChooser() WebChromeClient methods.

First step is to trigger a javascript console message from the website when the user clicks a file input, writing a unique code, that will be used to upload the file with a unique name or path. For example, concatenating the full datetime with a huge random number:

<input type="file" name="user_file" onclick="console.log('app.upload=20170405173025_456743538912');">

Then your app can read this messages overriding WebChromeClient's onConsoleMessage() method, detect that message, read the code, and trigger file selection:

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;
    }
});

For file selection, you can just use a simple Android ACTION_PICK intent like this:

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 );
}

After the user selects a file (or not) you can receive the file Uri, and translate it to a real file path with:

@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);
}

The uploadSelectedFile method, simply creates an object with all the information inside (filePath, fileUploadCode, and WebView), and triggers an AsyncTask that uploads the file using that information, and updates the WebView when it's done:

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);
}

The AsyncTask receives the request object with all the information, and uses MultipartUtility to build a multipart request and send it to the server easily. You can get lots of Java Multipart Utilities from many places, one of them is here: 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;
    }

Now we have uploaded the file to the server and we can update our website using Javascript again from our onPostExecute method in our AsyncTask. The most important thing is to set the file code in a hidden field in your form, so you can get that code when the user sends the form. You can also show a message in the site or even easily show the uploaded image (if it is an image):

@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");
    }
}

Now the Android part is done, and you need to work server side to receive the file you upload through the Android app AsyncTask and save it wherever you need.

You will also have to process your website form when the user sends it, and do whatever you need with the file the user uploaded from the app. To do that, you will get the file code in the form (we completed that in a field in onPostExecute()), and you have to use that file code to find the file the app has uploaded to your server. To achieve that, you could save the file in a path using that code as the filename, or save the code and the path where you uploaded the file in a database.

This solution only relies on elements that are available and compatible with all Android versions, so it should work on any device (and I haven't received users complains about this anymore).

If you have multiple file inputs in the same page, you can send a field number or extra identifier along with the unique file code in the initial javascript message, pass that identifier around in all the app code, and use it to update the right elements in onPostExecute().

I have modified the actual code a bit here, so if anything fails, it will probably be a typo or some minor detail when renaming a couple of things.

It's quite a lot of information to process so if anyone needs any clarification or have suggestions or corrections please tell me.

于 2017-04-17T01:31:38.580 回答
0

Kitkat s new file browser is going equally crazy on Chrome, seeing how WebView is now using Chromium it might be a related issue. I found that uploading files straight from the camera works, but not from the 'Images' folder. If you were to upload from 'Gallery' instead, the same files are accessible. Gah.

Seems like a fix is ready but awaiting release:

https://code.google.com/p/chromium/issues/detail?id=278640

于 2013-12-04T21:28:38.680 回答
0

if you are going to just add a webview wrapper around your website and launch it as an app, just don't look around with default android webview, one way or other it is a huge headache..for me two things didn't work out 1. Input file 2. Stripe checkout integeration (which uses advanced JS API)

What I did to come out of darkness

Just created a sample App using Cordova. It is much more simple that we think.

  1. Install Cordova from its official page, build a sample app.
  2. It will give you an apk file. ( You got it)
  3. You go to www folder from the created app and open index.js, find and replace the line with onDeviceReady: function() { window.open('http://yourdomain.com/yourpage')} Run the app again, it will open the site

  4. Now the master step. So far Cordova using nothing but a moderate webview. Everything ought to change one day. Add crosswalk plugin to your Cordova app, it will replace the sluggish webview with a brand new full fledged chromium view https://crosswalk-project.org/documentation/cordova.html

  5. run cordova clean, then cordova build --release to cleanup the old build.

  6. Open the config.xml inside the inside the app directory given by Cordva app and add <allow-navigation href="http://yourdomain.com/*" />
  7. Run the app again. Magic!.
于 2016-06-25T02:09:13.253 回答
-1

When use crosswalk-webview-21.51.546.7 and select the picture via Camera. In onActivityResult() the intent.getData() is null. It means upload image by camera can not work.

于 2016-11-15T13:11:29.287 回答