在 WebView 中从相机或图库上传图像

Posted

技术标签:

【中文标题】在 WebView 中从相机或图库上传图像【英文标题】:Upload an Image from camera or gallery in WebView 【发布时间】:2013-03-21 11:40:59 【问题描述】:

此应用中的 WebView 会打开一个带有上传按钮的页面。

以下是允许打开对话框以从图库或相机上传图像的代码块。

在我的活动中,我有:

 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;        
    
   

在 onCreate 我有以下内容:

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

点击上传按钮后,我可以看到相机、图片库和文件资源管理器的选项。

文件资源管理器和图库正在按预期工作。问题是,当我使用相机拍照时,它没有上传到“选择文件”选项中,显示状态为“未选择文件”。

关于选择相机:

关于使用相机拍摄快照:返回并显示检查选项。

关于选择复选标记:

文件未上传:(在“选择文件”选项中

预期结果:

我检查了我是否拥有正确的写入权限,因此会生成一个名为“MyApp”的目录并将图片存储在其中(如果在点击网页上的上传按钮后调用相机拍摄)。

点击复选标记后,如何以编程方式告诉应用程序选择从相机拍摄的照片(存储在 MyApp 目录中)?

【问题讨论】:

是否调用了onActivityResultrequestCodeUri result 的值是多少? onActivityResult 未被调用... gist.github.com/chirag-v/5281337 另外,我的目标是 API 8 以上。出于某种原因,上面的代码不适用于我的 HTC 欲望 C (android 4.0.3)。所以,我在没有相机选项的情况下恢复到早期版本。 :( 如果没有调用该方法,添加CameraWrite_external_storage权限。此外,您还有很多可用于包的额外代码,请尝试使用我的代码。 我在清单中有相机和外部存储写入权限...&lt;uses-feature android:name="android.hardware.camera" android:required="false"/&gt; &lt;uses-permission android:name="android.permission.CAMERA" android:required="false"/&gt; &lt;uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /&gt;我试过你的代码,但它给了我一些错误。我在下面发表了评论。 【参考方案1】:

经过一番努力,我找到了一个代码,可用于从 5.0+ 设备的厨房和相机中获取文件

    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;

这是初始化和设置 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());

这是我的 ChomeClient() 方法

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


//这是我的 onActivityResult 方法来处理来自画廊或相机意图的数据

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

这是打开相机所需的权限

<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" />

注意:我的代码还包含运行 3.0+ 的设备的代码,但我从未测试过它们,上面的代码在 Lolipop、Marshmallow 和 Nougat 模拟器上工作。 还有一件事,如果您看到 Android System 的图标代替了相机,则意味着您的设备中有许多应用程序可以处理相机。

【讨论】:

在您的 onActivityResult 中有两个分支:一个用于 SDK >= 21,第二个用于 SDK @vojta sdk 20 适用于智能手表,即不适用的 KitKat Wear OS 嗨,我使用了你的代码,画廊工作正常,但对于相机,我遇到了两个问题。 1.如何实现权限请求,因为它们必须在相机显示之前请求。如果未授予权限,活动结果将返回错误。然后,如果显示并授予权限请求,我将无法直接转发到相机。现在的解决方案是再次按下上传按钮并拍照。 2.文件现在在缓存目录中生成,但在网站上图像没有到达并且出现一些“空白”图像。也许使用缓存是不可能的,但我不知道【参考方案2】:

我假设实际上调用了onActivityResult方法,但是第三个参数Intent intent为空。好像是 Nexus 手机的 bug。

但您可以将输出图像 uri 保存到私有变量并使用它来代替意图:

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;
    

在这段代码中,我将imageUri 变量添加到活动中,并在两种方法中都使用了它。

【讨论】:

@Chirag 当然它不会工作。你的openFileChooser 方法在哪里? 它提供了文件选择器对话框 ===> gist.github.com/chirag-v/5281599 但它缺少相机选项(仅文件资源管理器和图像库)。参见第 155 行以后.. @Chirag 你在那里做了什么?你根本没有使用我的方法showAttachmentDialog @Voretex,我用你的方法更新了要点showAttachmentDialog ==> gist.github.com/chirag-v/5281599 在单击选择文件按钮时出现文件选择器对话框,但缺少相机选项.... 我正在使用 android 4.0.3 在 HTC 渴望 C 上调试这个我的目的是支持 8 以上的所有 API【参考方案3】:

更新 6/18: 这似乎不适用于搭载 Android 4.2.1 的三星 Galaxy S2。这在带有 4.1.2 的 HTC One X+ 上运行良好。请注意。


我遇到了同样的问题。这是问题以及我是如何解决的。

问题:

openFileChooser被调用时,回调对象ValueCallback&lt;Uri&gt;正在被传递。当我们准备好文件时,这是对 Web 视图的实际回调。我们将此对象保存到mUploadMessage 并使用onActivityResult 中的mUploadMessage.onReceiveValue() 函数将文件返回给Webview。当您选择相机,点击图片,保存并返回 webview 活动时,我们的活动可能被回收,这意味着我们实际上丢失了回调对象mUploadMessage。因此,无法将文件传回 webview 进行上传。

修复:

修复涉及执行一些 javascript,因此在 webview 上启用 javascript。基本上,如果我们丢失了前一个回调对象,我们将获得另一个回调对象。

我们需要创建一个布尔字段“mUploadFileOnLoad”和三个字段。

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

当我们从相机返回活动时,onActivityResult 将被调用。如果重新构建活动,则 mUploadMessage 为空。因此,我们将参数保存到字段并将mUploadFileOnLoad 设置为true 并返回。 else部分很重要。

    @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
    

这个解决方案的重要部分在WebViewClientWebChromeClient

    new WebChromeClient() 

        //Other overloaded functions

        //See http://***.com/a/15423907/375093 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....
         

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

上面my_file是网页中&lt;input&gt;元素的id。

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

所以,总而言之,我们所做的是 - 当我们没有回调对象时,我们保存从其他活动接收到的数据并将 mUploadFileOnLoad 设置为 true 并等待页面加载。当页面加载时,我们使用 Javascript 调用文件选择器,因此我们得到一个回调对象。由于我们已经有了结果,我们调用onActivityResult 并返回。 onActivityResult 现在有一个来自 webview 的回调。

【讨论】:

感谢分享。我已经实施了您的解决方案。但是在 onActivityResult 中,无论是我通过相机拍摄图像还是从图库中选择 img,它总是在两种情况下都返回 -1 @QadirHussain -1 表示操作成功,即相机活动已捕获图像或图库已选择图片。见developer.android.com/reference/android/app/…。对于这个解决方案,您需要检查回调对象是否为空,然后调用 onActivityResult。更新上面的代码更清晰【参考方案4】:

对不起我的英语。

这是一个解决方案。

首先,您像这样定义文件成员。

public File mTempFile;

你的打开文件选择器没问题。

onActivityResult方法太重要了。

相机应用不返回 URL,但 ValueCallback 必须有 URL。

从 mTempFile 获取 URI。

这是工作。

我以前喜欢这个。

if ( mTempFile.exists() ) 

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

 else 

    mUploadMessage.onReceiveValue(result);
    mUploadMessage = null;

如果存在称为相机的 mTempFile,则来自图库的其他情况。

【讨论】:

为我做了诀窍:这完全是关于正确解析文件路径【参考方案5】:

确保清单文件中没有android:launchMode="singleInstance"

【讨论】:

酷,但为什么不呢?请详细说明你的答案。

以上是关于在 WebView 中从相机或图库上传图像的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Android 7.0 中从相机或图库中选择要裁剪的图像?

从图库上传图像或单击移动相机流星 android 应用程序中的图像

将相机图像上传到 Firebase

如何在片段中从相机捕获图像,

从相机和图库上传在所有版本中都无法正常工作

文件上传选项以从相机拍摄图像或从图库中选择不适用于 Mozilla Firefox 中的 Web 应用程序