PictureSelector 2.0 Android Q 适配之旅。

PictureSelector 至从2016年12月底提交第一个版本以来时至今日总共也已提交80多个版本,经过不断的改良和适配现在也相对趋于稳定了并在Github上收获超7900个star,再这也要感谢各位同学的鼓励和提供的一些意见。

由于Google Android Q正式版预计在2019年8月份前后推出,所以针对AndroidQ的适配已经迫在眉睫了,其中Android Q一项比较重要的变更就是在外部存储设备中为每个应用提供了一个“隔离存储沙盒”。任何其他应用都无法直接访问您应用的沙盒文件,由于文件是您应用的私有文件,因此您不再需要任何权限即可在外部存储设备中访问和保存自己的文件。
由于PictureSelector是一款图片选择器所以自然只对存储这一块需要适配,如您需要了解更多Q相关的特性请参考 官网地址

先介绍一下沙盒机制下的三种存储方式

1、context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);// 图片存储路径
2、context.getExternalFilesDir(Environment.DIRECTORY_MOVIES);// 视频存储路径
3、context.getExternalFilesDir(Environment.DIRECTORY_DIRECTORY_MUSIC);//音频存储路径

作为一款图片选择器,沙盒机制这个操作对PictureSelector肯定是影响比较大,为了验证有哪些问题方便适配我们把targetSdkVersion改成>=29然后在Q版本机型上运行后发现果然PictureSelector基本是处于不能使用的尴尬局面,然后也收到了很多小伙伴发来的邮件和信息报bug那么废话就不多说了,下面就讲一下PictureSelector 2.0是如何适配Android Q;问题一、图片加载失败无法正常显示图片 因为MediaStore.MediaColumns.DATA 字段在Q版本被标记已过时,原因上面有提到过任何其他应用都无法直接访问您应用的沙盒文件;所以在Q上此字段返回的类似/storage/emulated/0/DCIM/Camera/IMG_20200105_19064545.jpg将无权限访问,即使你用Glide这些优秀的第三方框架也显示失败,你在Logcat过滤一下Glide日志就会发现会报如下异常:
```java.io.FileNotFoundException: open failed: EACCES (Permission 
    denied) ```意思是找不到文件&无权限访问
    
这个解决方案也很简单我们只需要通过``` MediaStore.Files.FileColumns._ID```拿到资源的Id进行拼接转成Uri的方式content://media/external/file/20246就能正常显示图片,代码如下:

  long id = data.getLong(data.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID));
    String path = isAndroidQ ? getRealPathAndroid_Q(id) : data.getString(data.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA));

    /**
     * 适配Android Q
     *
     * @param id
     * @return
     */
    private String getRealPathAndroid_Q(long id) {
        return QUERY_URI.buildUpon().appendPath(ValueOf.toString(id)).build().toString();
    }


问题二、Q版本中调用系统拍照或录视频如何保存至系统相册?

    Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    File cameraFile = new File("存储路径");
    Uri imageUri = PictureFileUtils.parUri(this, cameraFile);
    cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);


 在非Android Q版本下我们调用系统拍照按上面方式就可以了,但在Q版本上这个```File cameraFile = new File("存储路径");```就有讲究了,如果你直接传公共目录地址肯定是不行的因为你没权限访问,你只能传应用内地址```ctx.getExternalFilesDir(Environment.DIRECTORY_PICTURES);```把图片存储在应用沙盒内,但我们是需要存储到系统相册中所以显然这也不是我们所期望的,所以我们只能通过Uri来处理,这个Uri就不能通过parUri(this, cameraFile);来获得了,可以通过以下方式创建拍照或录视频Uri;
 
 

/**
     * 创建一条图片地址uri,用于保存拍照后的照片
     *
     * @param context
     * @return 图片的uri
     */
    @Nullable
    public static Uri createImageUri(final Context context) {
        final Uri[] imageFilePath = {null};
        String status = Environment.getExternalStorageState();
        String time = ValueOf.toString(System.currentTimeMillis());
        // ContentValues是我们希望这条记录被创建时包含的数据信息
        ContentValues values = new ContentValues(3);
        values.put(MediaStore.Images.Media.DISPLAY_NAME, DateUtils.getCreateFileName("IMG_"));
        values.put(MediaStore.Images.Media.DATE_TAKEN, time);
        values.put(MediaStore.Images.Media.MIME_TYPE, PictureMimeType.MIME_TYPE_IMAGE);
        // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
        if (status.equals(Environment.MEDIA_MOUNTED)) {
            values.put(MediaStore.Images.Media.RELATIVE_PATH, PictureMimeType.DCIM);
            imageFilePath[0] = context.getContentResolver()
                    .insert(MediaStore.Images.Media.getContentUri("external"), values);
        } else {
            imageFilePath[0] = context.getContentResolver()
                    .insert(MediaStore.Images.Media.getContentUri("internal"), values);
        }
        return imageFilePath[0];
    }
    

 

/**
     * 创建一条视频地址uri,用于保存录制的视频
     *
     * @param context
     * @return 视频的uri
     */
    @Nullable
    public static Uri createVideoUri(final Context context) {
        final Uri[] imageFilePath = {null};
        String status = Environment.getExternalStorageState();
        String time = ValueOf.toString(System.currentTimeMillis());
        // ContentValues是我们希望这条记录被创建时包含的数据信息
        ContentValues values = new ContentValues(3);
        values.put(MediaStore.Video.Media.DISPLAY_NAME, DateUtils.getCreateFileName("VID_"));
        values.put(MediaStore.Video.Media.DATE_TAKEN, time);
        values.put(MediaStore.Video.Media.MIME_TYPE, PictureMimeType.MIME_TYPE_VIDEO);
        // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
        if (status.equals(Environment.MEDIA_MOUNTED)) {
            values.put(MediaStore.Video.Media.RELATIVE_PATH, PictureMimeType.DCIM);
            imageFilePath[0] = context.getContentResolver()
                    .insert(MediaStore.Video.Media.getContentUri("external"), values);
        } else {
            imageFilePath[0] = context.getContentResolver()
                    .insert(MediaStore.Video.Media.getContentUri("internal"), values);
        }
        return imageFilePath[0];
    }    

 

将生成的Uri传入 cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);这样在Q手机上拍照或录的视频则可以保存到/storage/emulated/0/DCIM/Camera/目录下了。

问题三、使用PictureSelector 2.0在Q机型上传图片失败?

java.io.FileNotFoundException: open failed: EACCES (Permission denied) 无权限访问或文件找不到,这同样也是因为我们在选择公共目录下的一些图片资源上传时你并没有访问权限所以也自然不能使用它进行上传操作,其实我们在上面讲到MediaStore.MediaColumns.DATA```已过时问题时你点开源码就会发现其实已经告诉了我们替代方案了

        /**
         *
         *   @deprecated Apps may not have filesystem permissions to 
         *   directly
         *   access this path. Instead of trying to open this path
         *   directly, apps should use
         *   {@link ContentResolver#openFileDescriptor(Uri, String)}
         *   to gain access.
         */

通过ContentResolver#openFileDescriptor(Uri, String);方法获得访问权限,PictureSelector的解决方法是
通过getContentResolver().openFileDescriptor(uri,"r");获得文件的访问权限然后将其拷贝到自己应用沙盒中然后再进行上传操作,代码如下:

    ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri,"r");
    FileDescriptor fileDescriptor =  parcelFileDescriptor.getFileDescriptor();
    FileInputStream inputStream = new  FileInputStream(fileDescriptor); 
    boolean copyFileSuccess = copyFile(inputStream, outFile);  
     if (copyFileSuccess) 
     {
        Log.i(TAG,"Copy File Success");           
     }
     
   
    /**
     * Copy File
     *
     * @param fileInputStream
     * @param outFile
     * @return
     * @throws IOException
     */
    public static boolean copyFile(FileInputStream fileInputStream, File outFile) throws 
    IOException {
        if (fileInputStream == null) {
            return false;
        }
        FileChannel inputChannel = null;
        FileChannel outputChannel = null;
        try {
            inputChannel = fileInputStream.getChannel();
            outputChannel = new FileOutputStream(outFile).getChannel();
            inputChannel.transferTo(0, inputChannel.size(), outputChannel);
            inputChannel.close();
            return true;
        } catch (Exception e) {
            return false;
        } finally {
            if (inputChannel != null) inputChannel.close();
            if (outputChannel != null) outputChannel.close();
        }
    }  


   以上就是PictureSelector2.0关于Android Q的适配一些方案,如果您有更好方案也欢迎提出参考;
   
   另外:
   有一些小伙伴在Q机型上使用PictureSelector2.0时总是问我为什么需要一个拷贝文件至应用沙盒内的操
   作?说自己通过Uri转Path也能拿到真实路径进行上传,在这里我想说的是Uri转Path其实早在AndroidQ未出
   来之前就一直可以的,这也仅仅是路径之间的相互转换而已并没有访问的权限,如果有,那么只能说明你应用
   targetSdkVersion没有改成>=29或没有开启沙盒机制而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章