WebView 的 input 上传照片的兼容问题

问题

前几天接到的一个需求,是关于第三方理财产品的 H5 上传照片问题。 对方说他们的新的需求,需要接入方配合上传资产照片的需求,测试之后发现我们这边的 app 端,IOS 端上传没有问题,而 Android 端则点击没有任何反应。 对方 H5 调用的方式是通过<input type='file' accept='image/*'/>的方式调用,本来以为这个问题很简单,就是 app 端没有设置相机权限,造成的点击无反应情况,而实际上加了之后发现,并非简单的权限问题。

解决问题

因为 Android 的版本碎片问题,很多版本的 WebView 都对唤起函数有不同的支持。 我们需要重写WebChromeClient下的openFileChooser()(5.0 及以上系统回调onShowFileChooser())。我们通过 Intent 在openFileChooser()中唤起系统相机和支持 Intent 的相关 app。 在系统相机或者相关 app 中一顿操作之后,当返回 app 的时候,我们在onActivityResult()中将选择好的图片通过ValueCallback的onReceiveValue方法返回给 WebView。

附上代码:

1.首先是重写各个版本的WebChromeClient的支持

webView.setWebChromeClient(new WebChromeClient() {
  //For Android 3.0+
  public void openFileChooser(ValueCallback<Uri> uploadMsg) {
      selectImage();
      mUM = uploadMsg;
      Intent i = new Intent(Intent.ACTION_GET_CONTENT);
      i.addCategory(Intent.CATEGORY_OPENABLE);
      i.setType("*/*");
      MyBaseWebViewActivity.this.startActivityForResult(Intent.createChooser(i, "File Chooser"), FCR);
  }

  // For Android 3.0+, above method not supported in some android 3+ versions, in such case we use this
  public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
      selectImage();
      mUM = uploadMsg;
      Intent i = new Intent(Intent.ACTION_GET_CONTENT);
      i.addCategory(Intent.CATEGORY_OPENABLE);
      i.setType("*/*");
      MyBaseWebViewActivity.this.startActivityForResult(
              Intent.createChooser(i, "File Browser"),
              FCR);
  }

  //For Android 4.1+
  public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
      selectImage();
      mUM = uploadMsg;
      Intent i = new Intent(Intent.ACTION_GET_CONTENT);
      i.addCategory(Intent.CATEGORY_OPENABLE);
      i.setType("*/*");
      MyBaseWebViewActivity.this.startActivityForResult(Intent.createChooser(i, "File Chooser"), MyBaseWebViewActivity.FCR);
  }

  //For Android 5.0+
  public boolean onShowFileChooser(
          WebView webView, ValueCallback<Uri[]> filePathCallback,
          WebChromeClient.FileChooserParams fileChooserParams) {
      selectImage();
      if (mUMA != null) {
          mUMA.onReceiveValue(null);
      }
      mUMA = filePathCallback;
      Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
      if (takePictureIntent.resolveActivity(MyBaseWebViewActivity.this.getPackageManager()) != null) {
          File photoFile = null;
          try {
              photoFile = createImageFile();
              takePictureIntent.putExtra("PhotoPath", mCM);
          } catch (IOException ex) {
              Log.e(TAG, "Image file creation failed", ex);
          }
          if (photoFile != null) {
              mCM = "file:" + photoFile.getAbsolutePath();
              filePath = 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("*/*");
      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, FCR);
      return true;
  }
});

2.选完照片之后

/**
* 打开图库,同时处理图片
*/private void selectImage() {
   compressPath = Environment.getExternalStorageDirectory().getPath() + "/QWB/temp";
   File file = new File(compressPath);
   if (!file.exists()) {
       file.mkdirs();
   }
   compressPath = compressPath + File.separator + "compress.png";
   File image = new File(compressPath);
   if (image.exists()) {
       image.delete();
   }}// Create an image fileprivate File createImageFile() throws IOException {
   @SuppressLint("SimpleDateFormat") String timeStamp = DateUtils.nowTimeDetail();
   String imageFileName = "img_" + timeStamp + "_";
   File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
   return File.createTempFile(imageFileName, ".jpg", storageDir);}private String mCM;private String filePath = "";private ValueCallback<Uri> mUM;private ValueCallback<Uri[]> mUMA;private final static int FCR = 1;String compressPath = "";@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent intent) {
   super.onActivityResult(requestCode, resultCode, intent);
   if (Build.VERSION.SDK_INT >= 21) {
       Uri[] results = null;
       //Check if response is positive
       if (resultCode == Activity.RESULT_OK) {
           if (requestCode == FCR) {
               if (null == mUMA) {
                   return;
               }
               if (intent == null) {
                   //Capture Photo if no image available
                   if (mCM != null) {
                       // results = new Uri[]{Uri.parse(mCM)};
                       results = new Uri[]{afterChosePic(filePath, compressPath)};
                   }
               } else {
                   String dataString = intent.getDataString();
                   if (dataString != null) {
                       results = new Uri[]{Uri.parse(dataString)};
                       LogUtil.d("tag", intent.toString());//                            String realFilePath = getRealFilePath(Uri.parse(dataString));//                            results = new Uri[]{afterChosePic(realFilePath, compressPath)};
                   }
               }
           }
       }
       mUMA.onReceiveValue(results);
       mUMA = null;
   } else {
       if (requestCode == FCR) {
           if (null == mUM) return;
           Uri result = intent == null || resultCode != RESULT_OK ? null : intent.getData();
           mUM.onReceiveValue(result);
           mUM = null;
       }
   }}/**
* 选择照片后结束
*/private Uri afterChosePic(String oldPath, String newPath) {
   File newFile;
   try {
       newFile = FileUtils.compressFile(oldPath, newPath);
   } catch (Exception e) {
       e.printStackTrace();
       newFile = null;
   }
   return Uri.fromFile(newFile);
}

3.工具类

public class FileUtils {
    /**
     * 把图片压缩到200K
     *
     * @param oldpath
     *            压缩前的图片路径
     * @param newPath
     *            压缩后的图片路径
     * @return
     */
    public static File compressFile(String oldpath, String newPath) {
        Bitmap compressBitmap = FileUtils.decodeFile(oldpath);
        Bitmap newBitmap = ratingImage(oldpath, compressBitmap);
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        newBitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
        byte[] bytes = os.toByteArray();

        File file = null ;
        try {
            file = FileUtils.getFileFromBytes(bytes, newPath);
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            if(newBitmap != null ){
                if(!newBitmap.isRecycled()){
                    newBitmap.recycle();
                }
                newBitmap  = null;
            }
            if(compressBitmap != null ){
                if(!compressBitmap.isRecycled()){
                    compressBitmap.recycle();
                }
                compressBitmap  = null;
            }
        }
        return file;
    }

    private static Bitmap ratingImage(String filePath,Bitmap bitmap){
        int degree = readPictureDegree(filePath);
        return rotaingImageView(degree, bitmap);
    }

    /**
     *  旋转图片
     * @param angle
     * @param bitmap
     * @return Bitmap
     */
    public static Bitmap rotaingImageView(int angle , Bitmap bitmap) {
        //旋转图片 动作
        Matrix matrix = new Matrix();;
        matrix.postRotate(angle);
        System.out.println("angle2=" + angle);
        // 创建新的图片
        Bitmap resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0,
                bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        return resizedBitmap;
    }

    /**
     * 读取图片属性:旋转的角度
     * @param path 图片绝对路径
     * @return degree旋转的角度
     */
    public static int readPictureDegree(String path) {
        int degree  = 0;
        try {
            ExifInterface exifInterface = new ExifInterface(path);
            int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    degree = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    degree = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    degree = 270;
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return degree;
    }

    /**
     * 把字节数组保存为一个文件
     *
     * @param b
     * @param outputFile
     * @return
     */
    public static File getFileFromBytes(byte[] b, String outputFile) {
        File ret = null;
        BufferedOutputStream stream = null;
        try {
            ret = new File(outputFile);
            FileOutputStream fstream = new FileOutputStream(ret);
            stream = new BufferedOutputStream(fstream);
            stream.write(b);
        } catch (Exception e) {
            // log.error("helper:get file from byte process error!");
            e.printStackTrace();
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    // log.error("helper:get file from byte process error!");
                    e.printStackTrace();
                }
            }
        }
        return ret;
    }

    /**
     * 图片压缩
     *
     * @param fPath
     * @return
     */
    public static Bitmap decodeFile(String fPath) {
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;
        opts.inDither = false; // Disable Dithering mode
        opts.inPurgeable = true; // Tell to gc that whether it needs free
        opts.inInputShareable = true; // Which kind of reference will be used to
        BitmapFactory.decodeFile(fPath, opts);
        final int REQUIRED_SIZE = 400;
        int scale = 1;
        if (opts.outHeight > REQUIRED_SIZE || opts.outWidth > REQUIRED_SIZE) {
            final int heightRatio = Math.round((float) opts.outHeight                    / (float) REQUIRED_SIZE);
            final int widthRatio = Math.round((float) opts.outWidth                    / (float) REQUIRED_SIZE);
            scale = heightRatio < widthRatio ? heightRatio : widthRatio;//
        }
        Log.i("scale", "scal ="+ scale);
        opts.inJustDecodeBounds = false;
        opts.inSampleSize = scale;
        Bitmap bm = BitmapFactory.decodeFile(fPath, opts).copy(Bitmap.Config.ARGB_8888, false);
        return bm;
    }



    /**
     * 创建目录
     * @param path
     */
    public static void setMkdir(String path)
    {
        File file = new File(path);
        if(!file.exists())
        {
            file.mkdirs();
            Log.e("file", "目录不存在  创建目录    ");
        }else{
            Log.e("file", "目录存在");
        }
    }

    /**
     * 获取目录名称
     * @param url
     * @return FileName
     */
    public static String getFileName(String url)
    {
        int lastIndexStart = url.lastIndexOf("/");
        if(lastIndexStart!=-1)
        {
            return url.substring(lastIndexStart+1, url.length());
        }else{
            return null;
        }
    }

    /**
     * 删除该目录下的文件
     *
     * @param path
     */
    public static void delFile(String path) {
        if (!TextUtils.isEmpty(path)) {
            File file = new File(path);
            if (file.exists()) {
                file.delete();
            }
        }
    }
}

4.需要注意的问题

在打 release 包的时候,因为混淆的问题,点击又会没有反应,这是因为openFileChooser()是系统 api,所以需要在混淆是不混淆该方法。

-keepclassmembers class * extends android.webkit.WebChromeClient{
public void openFileChooser(...);
}

当点击拍照之后,如果相机是横屏拍照的话,当拍照结束之后跳回 app 的时候,会导致 app 端当前的 webView 页面销毁并重新打开,需要在androidManifest.xml中当前 Activity 添加:

android:configChanges="orientation|keyboardHidden|screenSize"

原文发布于微信公众号 - 非著名程序员(non-famous-coder)

原文发表时间:2017-09-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术小黑屋

Start InstalledAppDetails Activity With a Specific Package Name

Here is the javadoc of android.provider.Settings.ACTION_APPLICATION_DETAILS_SETT...

18620
来自专栏程序员互动联盟

【Android基础】利用Intent在Activity之间传递数据

前言: 上一篇文章给大家聊了Intent的用法,如何用Intent启动Activity和隐式Intent,这一篇文章给大家聊聊如何利用Intent在Activi...

28560
来自专栏Android学习之路

相机和相册选取图片并剪裁

14860
来自专栏一个会写诗的程序员的博客

android 在一个应用中启动另一个应用android 在一个应用中启动另一个应用

在程序开发过程当中,常遇到需要启动另一个应用程序的情况,比如在点击软件的一个按钮可以打开地图软件。

10840
来自专栏潇涧技术专栏

Android Development Code Snippets

9110
来自专栏程序员叨叨叨

一个SingleTask与跳转传值引发的血案

后来想到,Activity A使用了SingleTask的launchMode,猜想可能跟这个有关,在执行界面跳转的时候,不会生成新的Activity A实例,...

9510
来自专栏everhad

安卓Task和Back Stack

概述 一个Activity允许用户完成一些操作,甚至,Android中设计Activity为组件的形式,这样,多个Activity——甚至是其它App的Acti...

21290
来自专栏分享达人秀

Intent 属性详解(下)

上一期学习了Intent的前三个属性,本期接着学习其余四个属性,以及Android系统常用内置组件的启动。 四、Data和Type属性 Dat...

26850
来自专栏梦里茶室

几种改变Activity回退栈默认行为的Intent Flag

FLAG_与LaunchMode相比最大的不同是临时性 1.FLAG_ACTIVITY_NEW_TASK: Developer.android.com的说法: ...

24470
来自专栏上善若水

009android初级篇之APP中使用系统相机相册等集成应用

这是第一种方式 在启动相机前先指定好图片的文件位置,通知intent,同时也保留在成员变量中。然后在函数中,可以直接打开该文件

15840

扫码关注云+社区

领取腾讯云代金券