Upload an Image from camera or gallery in WebView
Asked Answered
O

6

33

WebView in this app opens a page with upload button.

Page in webview with upload button

Below is the code block that allows to open a dialog box to upload image from gallery or camera.

Within my Activity I have:

 private WebView wv;  

//make HTML upload button work in Webview   
 private ValueCallback<Uri> mUploadMessage;  
 private final static int FILECHOOSER_RESULTCODE=1;

 @Override  
 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {  
  if(requestCode==FILECHOOSER_RESULTCODE)  
  {  
   if (null == mUploadMessage) return;  
            Uri result = intent == null || resultCode != RESULT_OK ? null  
                    : intent.getData();  
            mUploadMessage.onReceiveValue(result);  
            mUploadMessage = null;        
  }  
 }  

Within onCreate I have the following:

    wv.setWebChromeClient(new WebChromeClient()  {
        private Uri imageUri;   

        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType )  {      
             File imageStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "MyApp");
            // Create the storage directory if it does not exist
            if (! imageStorageDir.exists()){
                imageStorageDir.mkdirs();                  
            }
            File file = new File(imageStorageDir + File.separator + "IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg");  
            imageUri = Uri.fromFile(file); 

            final List<Intent> cameraIntents = new ArrayList<Intent>();
            final Intent captureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
            final PackageManager packageManager = getPackageManager();
            final List<ResolveInfo> listCam = packageManager.queryIntentActivities(captureIntent, 0);
            for(ResolveInfo res : listCam) {
                final String packageName = res.activityInfo.packageName;
                final Intent i = new Intent(captureIntent);
                i.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
                i.setPackage(packageName);
                i.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                cameraIntents.add(i);

            }


            mUploadMessage = uploadMsg; 
            Intent i = new Intent(Intent.ACTION_GET_CONTENT);  
            i.addCategory(Intent.CATEGORY_OPENABLE);  
            i.setType("image/*"); 
            Intent chooserIntent = Intent.createChooser(i,"Image Chooser");
            chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[]{}));
            MainActivity.this.startActivityForResult(chooserIntent,  FILECHOOSER_RESULTCODE); 
        }

I am able to see option for camera, image gallery and file explorer on clicking Upload button. Camera, gallery and file explorer upload option

File explorer and Gallery is working as expected. The problem is that, when I take a picture using camera, it is not uploaded in the "choose file" option which shows status "No file chosen".

ON SELECTING CAMERA:

camera

ON TAKING SNAPSHOT USING CAMERA: back and check options appear.

snapshot using camera

ON CHOOSING CHECK MARK:

FILE IS NOT UPLOADED :( IN "CHOOSE FILE" OPTION

enter image description here

WHAT IS EXPECTED:

image uploaded

I checked that I have the proper writing permission and hence a directory named "MyApp" is generated and the picture is stored within it (if taken by invoking camera after clicking upload button on webpage).

How to programatically tell the application to choose picture taken from camera (that was stored in MyApp directory) after hitting check mark?

Oshaughnessy answered 31/3, 2013 at 2:43 Comment(8)
Is onActivityResult called? What are the values of requestCode and Uri result?Bioclimatology
onActivityResult is not called...Oshaughnessy
gist.github.com/chirag-v/5281337 Also, I'm target API 8 onwards. For some reason the code above was not working on my HTC desire C (android 4.0.3). So, I reverted back to earlier version without option for camera. :(Oshaughnessy
If the method is not called, add the Camera and Write_external_storage permissions. Also you have lot of extra code which works with packages, try my code instead.Bioclimatology
I have Camera amd external storage write permission in manifest... <uses-feature android:name="android.hardware.camera" android:required="false"/> <uses-permission android:name="android.permission.CAMERA" android:required="false"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> I tried your code but it gives me a few errors. I've commented below.Oshaughnessy
@Oshaughnessy can you please share the code of HTML, and Javascrpt if any here? thanks. i m confused how to use thisLandrum
@Chirag: Cannot create a generic array of Parcelable error found in your code . I didn't get any idea how to show both gallery and camera action on choose file option. Can you please help me.Damalas
Please do you have a github for this ? I need it for my app. ThanksVariant
K
15

After struggling a lot I found a code that works for taking files from galley and camera from 5.0+ devices

    private ValueCallback<Uri> mUploadMessage;
private Uri mCapturedImageURI = null;
private ValueCallback<Uri[]> mFilePathCallback;
private String mCameraPhotoPath;
private static final int INPUT_FILE_REQUEST_CODE = 1;
private static final int FILECHOOSER_RESULTCODE = 1;



private File createImageFile() throws IOException {
    // Create an image file name
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    String imageFileName = "JPEG_" + timeStamp + "_";
    File storageDir = Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES);
    File imageFile = File.createTempFile(
            imageFileName,  /* prefix */
            ".jpg",         /* suffix */
            storageDir      /* directory */
    );
    return imageFile;
}

this is initialization and setting webview

     mWebView= (WebView) findViewById(R.id.webview);
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.getSettings().setPluginState(WebSettings.PluginState.OFF);
        mWebView.getSettings().setLoadWithOverviewMode(true);
        mWebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
        mWebView.getSettings().setUseWideViewPort(true);
        mWebView.getSettings().setUserAgentString("Android Mozilla/5.0 AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30");
        mWebView.getSettings().setAllowFileAccess(true);
        mWebView.getSettings().setAllowFileAccess(true);
        mWebView.getSettings().setAllowContentAccess(true);
        mWebView.getSettings().supportZoom();
        mWebView.loadUrl(Common.adPostUrl);

        mWebView.setWebViewClient(new WebViewClient() {
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // do your handling codes here, which url is the requested url
                // probably you need to open that url rather than redirect:
                if ( url.contains(".pdf")){
                    Intent intent = new Intent(Intent.ACTION_VIEW);
                    intent.setDataAndType(Uri.parse(url), "application/pdf");
                    try{
                        view.getContext().startActivity(intent);
                    } catch (ActivityNotFoundException e) {
                        //user does not have a pdf viewer installed
                    }
                } else {
                    mWebView.loadUrl(url);
                }
                return false; // then it is not handled by default action
            }


            @Override
            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {

Log.e("error",description);
            }


            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {        //show progressbar here

                super.onPageStarted(view, url, favicon);
            }

            @Override
            public void onPageFinished(WebView view, String url) {
          //hide progressbar here

            }

        });
        mWebView.setWebChromeClient(new ChromeClient());

and here is my ChomeClient() method

public class ChromeClient extends WebChromeClient {

    // For Android 5.0
    public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> filePath, WebChromeClient.FileChooserParams fileChooserParams) {
        // Double check that we don't have any existing callbacks
        if (mFilePathCallback != null) {
            mFilePathCallback.onReceiveValue(null);
        }
        mFilePathCallback = filePath;

        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            // Create the File where the photo should go
            File photoFile = null;
            try {
                photoFile = createImageFile();
                takePictureIntent.putExtra("PhotoPath", mCameraPhotoPath);
            } catch (IOException ex) {
                // Error occurred while creating the File
                Log.e(Common.TAG, "Unable to create Image File", ex);
            }

            // Continue only if the File was successfully created
            if (photoFile != null) {
                mCameraPhotoPath = "file:" + photoFile.getAbsolutePath();
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                        Uri.fromFile(photoFile));
            } else {
                takePictureIntent = null;
            }
        }

        Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
        contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
        contentSelectionIntent.setType("image/*");

        Intent[] intentArray;
        if (takePictureIntent != null) {
            intentArray = new Intent[]{takePictureIntent};
        } else {
            intentArray = new Intent[0];
        }

        Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
        chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
        chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser");
        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);

        startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE);

        return true;

    }

    // openFileChooser for Android 3.0+
    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {

        mUploadMessage = uploadMsg;
        // Create AndroidExampleFolder at sdcard
        // Create AndroidExampleFolder at sdcard

        File imageStorageDir = new File(
                Environment.getExternalStoragePublicDirectory(
                        Environment.DIRECTORY_PICTURES)
                , "AndroidExampleFolder");

        if (!imageStorageDir.exists()) {
            // Create AndroidExampleFolder at sdcard
            imageStorageDir.mkdirs();
        }

        // Create camera captured image file path and name
        File file = new File(
                imageStorageDir + File.separator + "IMG_"
                        + String.valueOf(System.currentTimeMillis())
                        + ".jpg");

        mCapturedImageURI = Uri.fromFile(file);

        // Camera capture image intent
        final Intent captureIntent = new Intent(
                android.provider.MediaStore.ACTION_IMAGE_CAPTURE);

        captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mCapturedImageURI);

        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
        i.addCategory(Intent.CATEGORY_OPENABLE);
        i.setType("image/*");

        // Create file chooser intent
        Intent chooserIntent = Intent.createChooser(i, "Image Chooser");

        // Set camera intent to file chooser
        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS
                , new Parcelable[] { captureIntent });

        // On select image call onActivityResult method of activity
        startActivityForResult(chooserIntent, FILECHOOSER_RESULTCODE);


    }

    // openFileChooser for Android < 3.0
    public void openFileChooser(ValueCallback<Uri> uploadMsg) {
        openFileChooser(uploadMsg, "");
    }

    //openFileChooser for other Android versions
    public void openFileChooser(ValueCallback<Uri> uploadMsg,
                                String acceptType,
                                String capture) {

        openFileChooser(uploadMsg, acceptType);
    }

}

//here is my onActivityResult method to handle data from gallery or camera intent

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

        if (requestCode != INPUT_FILE_REQUEST_CODE || mFilePathCallback == null) {
            super.onActivityResult(requestCode, resultCode, data);
            return;
        }

        Uri[] results = null;

        // Check that the response is a good one
        if (resultCode == Activity.RESULT_OK) {
            if (data == null) {
                // If there is not data, then we may have taken a photo
                if (mCameraPhotoPath != null) {
                    results = new Uri[]{Uri.parse(mCameraPhotoPath)};
                }
            } else {
                String dataString = data.getDataString();
                if (dataString != null) {
                    results = new Uri[]{Uri.parse(dataString)};
                }
            }
        }

        mFilePathCallback.onReceiveValue(results);
        mFilePathCallback = null;

    } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
        if (requestCode != FILECHOOSER_RESULTCODE || mUploadMessage == null) {
            super.onActivityResult(requestCode, resultCode, data);
            return;
        }

        if (requestCode == FILECHOOSER_RESULTCODE) {

            if (null == this.mUploadMessage) {
                return;

            }

            Uri result = null;

            try {
                if (resultCode != RESULT_OK) {

                    result = null;

                } else {

                    // retrieve from the private variable if the intent is null
                    result = data == null ? mCapturedImageURI : data.getData();
                }
            } catch (Exception e) {
                Toast.makeText(getApplicationContext(), "activity :" + e,
                        Toast.LENGTH_LONG).show();
            }

            mUploadMessage.onReceiveValue(result);
            mUploadMessage = null;

        }
    }

    return;
}

and here is the permissions required to open camera

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA2" /> // for new versions api 21+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

Note: My code also contains the code for devices running 3.0+ also but I never tested them , the above code worked on Lolipop, Marshmallow and Nougat emulators. One more thing , if you see and icon of Android System in place of Camera it means you have many apps available in your device to handle camera.

Katheykathi answered 6/2, 2017 at 12:24 Comment(8)
In your onActivityResult there are two branches: one for SDK >= 21, the second one for SDK <= 19. What if SDK == 20?Prothesis
@Prothesis sdk 20 is for smart watches i.e., KitKat Wear OS where this is not applicableKatheykathi
Hi, I used your code and gallery is working fine, but for camera I'm struggling with two issues. 1. how to implement the permission requests, because they have to be requested before camera is shown. Activity Result will return an error if permission is not granted. Then if permission request is shown and granted, I'm unable to directly forward to camera. Solution now is to press on upload button again and take picture. 2. file is generated in cache directory now but on the website the image does not arrive and some "blank" image appears. Maybe using cache is not possible, but i dont knowProjector
FYI - This code has worked fine for me, I had to adjust just one little thing. That if/else we have on the onActivityResult, sometimes the dataString is null but the path with the picture isn't. So I had to add another else, and process the image Uri there.Chacha
Thank you. Add permission check/request for CAMERA, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE in the beginning of onShowFileChooser(), then move following code in the onShowFileChooser() to an independent function for the result of permission result to call. This solution still works!Phenomenalism
@4gus71n, can you please explain the else part you used, because my camera is still not working after the code mentioned in the answer. thanksReadily
Hi @YasarKhalid Can you please mention what line you are referring that is not working for you? This code is a bit old now so you may try giving permissions directly by going to settings and then check, may be that helps.Katheykathi
Can you update this answer for API 33 and above? WRITE_EXTERNAL_STORAGE is not feasible to be added anymore.Saline
B
11

I suppose that the onActivityResult method is actually called, but the 3rd parameter Intent intent is null. It seems that it is a bug of Nexus phones.

But you can save the output image uri to the private variable and use it instead of the intent:

private Uri imageUri;

private void showAttachmentDialog(ValueCallback<Uri> uploadMsg) {
    this.mUploadMessage = uploadMsg;

    File imageStorageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "TestApp");
    if (!imageStorageDir.exists()) {
        imageStorageDir.mkdirs();
    }
    File file = new File(imageStorageDir + File.separator + "IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg");
    this.imageUri = Uri.fromFile(file); // save to the private variable

    final Intent captureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
    captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);

    Intent i = new Intent(Intent.ACTION_GET_CONTENT);
    i.addCategory(Intent.CATEGORY_OPENABLE);
    i.setType("image/*");

    Intent chooserIntent = Intent.createChooser(i, "Image Chooser");
    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Parcelable[] { captureIntent });

    this.startActivityForResult(chooserIntent, FILECHOOSER_RESULTCODE);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    if (requestCode == FILECHOOSER_RESULTCODE) {
        if (null == this.mUploadMessage) {
            return;
        }

        Uri result;
        if (resultCode != RESULT_OK) {
            result = null;
        } else {
            result = intent == null ? this.imageUri : intent.getData(); // retrieve from the private variable if the intent is null
        }

        this.mUploadMessage.onReceiveValue(result);
        this.mUploadMessage = null;
    }
}

In this code I added the imageUri variable to the activity and used it in both methods.

Bioclimatology answered 31/3, 2013 at 15:50 Comment(8)
@Oshaughnessy Of couse it won't work. Where is your openFileChooser method?Bioclimatology
With this it gives the File Chooser Dialog ===> gist.github.com/chirag-v/5281599 but it lacks the option for camera, (only file explorer and image gallery). See line 155 onwards..Oshaughnessy
@Oshaughnessy What did you do there? You didn't use my method showAttachmentDialog at all.Bioclimatology
@Voretex, I updated the gist with your method showAttachmentDialog ==> gist.github.com/chirag-v/5281599 The file chooser dialog appears on clciking choose file button but camera option is missing....Oshaughnessy
I am debugging this on HTC desire C with android 4.0.3 My intention is to support all API above 8Oshaughnessy
Manifest file also has all permissions for camera and writing on SD card, see line 12, 13 and 14 in this gist ==> gist.github.com/chirag-v/5281816Oshaughnessy
I referred to this question https://mcmap.net/q/453403/-android-webview-openfilechooser-termination/1331135 for the code in my postOshaughnessy
+1 to @vorrtex . Had the exact same problem and now it works fine.The trick with the private variable saved the daySlover
I
7

Update 6/18: This doesn't seem to work on Samsung Galaxy S2 with Android 4.2.1. This worked fine on HTC One X+ with 4.1.2. Be cautioned.


I faced the same problem. Here's the problem and how I solved it.

Problem:

When openFileChooser is called, the call back object ValueCallback<Uri> is being passed. This is the actual call back to web view when we have a file ready for it. We save this object to mUploadMessage and use mUploadMessage.onReceiveValue() function in onActivityResult to return the file to Webview. While you choose camera, click a picture, save it and return to the webview activity, our activity might get recycled, which means we actually lose the call back object mUploadMessage. And hence, the file cannot be passed back to webview for upload.

Fix:

Fix involves executing some javascript, so enable javascript on webview. Basically, we will get another call back object if we lost the previous one.

We need to create a boolean field 'mUploadFileOnLoad' and three fields.

    private int mReturnCode;
    private int mResultCode;
    private Intent mResultIntent;
    private boolean mUploadFileOnLoad = false;

When we return to our activity from camera, onActivityResult will be called. If activity is reconstructed, mUploadMessage is null. So, we will save the parameters to fields and set mUploadFileOnLoad to true and return. The else part is very important.

    @Override
    protected void onActivityResult(int requestCode, 
                                    int resultCode,
                                    Intent intent) 
    {  
      //if the callback object has been recycled      
      if(null==mUploadMessage)
      {
        //Save the result
        mReturnCode = requestCode;
        mResultCode = resultCode;
        mResultIntent = intent;
        //remember to invoke file upload using Javascript
        mUploadFileOnLoad = true;
        return;
      }else
        mUploadFileOnLoad = false;
      //rest of the code
    }

The important parts of this solution are in WebViewClient and WebChromeClient

    new WebChromeClient() {

        //Other overloaded functions

        //See https://mcmap.net/q/126305/-file-upload-in-webview for full explanation
        //The undocumented magic method override
        //Eclipse will swear at you if you try to put @Override here
        // For Android < 3.0
        public void openFileChooser(ValueCallback<Uri> uploadMsg) {
            //If we lost the callback object
            if(mUploadFileOnLoad)
            {
                mUploadMessage = uploadMsg;
                //use the saved result objects to invoke onActivityResult
                onActivityResult(mReturnCode, mResultCode, mResultIntent);
                return;
            }
         //Rest of the code....
         }

and

        new WebViewClient() {
        @Override
        public void onPageFinished(WebView view, String url) {
            if(mUploadFileOnLoad)
            {
               webview.loadUrl("javascript:document.getElementById('my_file').click()");
            }
        }

In the above, my_file is the id of the <input> element in the web page.

<input type="file" id="my_file">

So, in summary, what we did is - When we don't have a callback object, we save the data received from other activity and set mUploadFileOnLoad to true and wait for page to load. When page loads, we use Javascript to invoke the file chooser so we get a callback object. Since we already have results, we invoke onActivityResult and return. onActivityResult now has a callback from the webview.

Impetrate answered 5/6, 2013 at 19:50 Comment(2)
thanks for sharing this. I have implemented your solution. But in the onActivityResult it always return me -1 in both conditions either i m taking image via camera or selecting img from galleryLandrum
@QadirHussain -1 means the operation succeeded, i.e, either the camera activity has captured the image or the gallery has selected the picture. See developer.android.com/reference/android/app/… . For this solution, you need to check if the callback object becomes null and then invoke onActivityResult. Updating above code for more clarityImpetrate
C
4

I am sorry for my English.

This is a solution.

The first, you define file members like this.

public File mTempFile;

your's open file chooser is ok.

onActivityResult method is so important.

the camera app does not return URL, but ValueCallback must have URL.

Get URI from mTempFile.

this is work.

I use to like this.

if ( mTempFile.exists() ) {

    mUploadMessage.onReceiveValue(Uri.fromFile(mTempFile));
    mUploadMessage = null;

} else {

    mUploadMessage.onReceiveValue(result);
    mUploadMessage = null;
}

If mTempFile is exist that was called camera, other case from gallery.

Clements answered 22/4, 2014 at 1:5 Comment(1)
Did the trick for me: it is all about correct parse of file pathWooded
O
3

Make sure you do NOT have android:launchMode="singleInstance" in manifest file

Oz answered 14/5, 2013 at 12:40 Comment(1)
Cool, but why not? Please elaborate on your answer.Albur
A
0

For Camera need to update below code

Old Code:

// Continue only if the File was successfully created
if (photoFile != null) {
       mCameraPhotoPath = "file:" + photoFile.getAbsolutePath();
       takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT,
                        Uri.fromFile(photoFile));
} else {
       takePictureIntent = null;
}

New Code:

if (photoFile != null) {
     mCM = "file:" + photoFile.getAbsolutePath();
     takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(requireActivity(), requireActivity().getPackageName() + ".provider", photoFile));
} else {
       takePictureIntent = null;
}
Aldercy answered 17/5 at 11:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.