适配Android Q上读取多媒体文件

Android Q版本出来也有一段时间了,但是大部分我们都没有去适配过它,首选说一下Android Q版,最大的亮点集中在隐私安全和智能交互两方面,其中在隐私安全方面Android Q增加了外部存储策略变更、位置权限的后台访问限制、后台应用(不限于摄像头、麦克风等)的启动限制、设备识别码限制权限收敛举措,其将限制应用在安卓设备上接受通话日志和短信权限的能力,并不再通过安卓通讯录API提供联系人互动数据,这些举措都是针对社交属性的权限进行的。这里说再多的解释还不如大家去官方文档看的更加详细,我这里只记录下自已适配Q版有关于读取系统多媒体文件的心得(参考)。

  • 官方文档

  • 内部存储访问变化:
    适配Android Q上读取多媒体文件_第1张图片

  • 多媒体文件(图片,视频等)
    1.使用MediaStore来获取图片和视频,然后通过Cursor来得到多媒体文件的相关信息

         ``MediaStore.Images.Media``表示媒体文件为图片
         ``MediaStore.Images.Media``表示媒体文件为视频
    

图片:

 Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;

                String[] projection = {MediaStore.Images.Media._ID,
                        MediaStore.Images.Media.DATA,
                        MediaStore.Images.Media.DATE_ADDED,
                        MediaStore.Images.Thumbnails.DATA
                };
                //全部图片
                String where = MediaStore.Images.Media.MIME_TYPE + "=? or "
                        + MediaStore.Images.Media.MIME_TYPE + "=? or "
                        + MediaStore.Images.Media.MIME_TYPE + "=?";
                //指定格式
                String[] whereArgs = {"image/jpeg", "image/png", "image/jpg"};
                //查询
                Cursor mCursor = activity.getContentResolver().query(
                        mImageUri, projection, null, null,
                        null);
                if (mCursor != null) {
                    while (mCursor.moveToNext()) {
                        // 获取图片的路径
                        int thumbPathIndex = mCursor.getColumnIndex(MediaStore.Images.Thumbnails.DATA);
                        int timeIndex = mCursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED);
                        int pathIndex = mCursor.getColumnIndex(MediaStore.Images.Media.DATA);
                        int id = mCursor.getColumnIndex(MediaStore.Images.Media._ID);

                        Long date = mCursor.getLong(timeIndex)*1000;
                        String filepath,thumbPath;
                        //适配Android Q
                        if(Build.VERSION.SDK_INT == Build.VERSION_CODES.P){
                             thumbPath  =MediaStore.Images.Media
                                    .EXTERNAL_CONTENT_URI
                                    .buildUpon()
                                    .appendPath(String.valueOf(mCursor.getInt(id))).build().toString();
                            filepath = thumbPath;
                            if(FileManagerUtils.isContentUriExists(MSApplication.getmContext(), Uri.parse(filepath))){
                                MediaData fi = new MediaData(id,MediaConstant.IMAGE,filepath,"",getPhotoUri(mCursor),date,"",false);
                                mediaBeen.add(fi);
                            }
                        }else{
                             thumbPath = mCursor.getString(thumbPathIndex);
                             filepath = mCursor.getString(pathIndex);
                            //判断文件是否存在,存在才去加入
                            boolean b = FileManagerUtils.fileIsExists(filepath);
                            if (b) {
                                File f = new File(filepath);
                                MediaData fi = new MediaData(id,MediaConstant.IMAGE,filepath,thumbPath,null,date,f.getName(),false);
                                mediaBeen.add(fi);
                            }
                        }
                    }
                    mCursor.close();
                }

视频:

final List videoList = new ArrayList<>();
                Uri mVideoUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                String[] projection = {MediaStore.Video.Thumbnails._ID
                        , MediaStore.Video.Thumbnails.DATA
                        , MediaStore.Video.Media.DURATION
                        , MediaStore.Video.Media.SIZE
                        , MediaStore.Video.Media.DATE_ADDED
                        , MediaStore.Video.Media.DISPLAY_NAME
                        , MediaStore.Video.Media.DATE_MODIFIED};
                //全部视频
                String where = MediaStore.Images.Media.MIME_TYPE + "=? or "
                        + MediaStore.Video.Media.MIME_TYPE + "=? or "
                        + MediaStore.Video.Media.MIME_TYPE + "=? or "
                        + MediaStore.Video.Media.MIME_TYPE + "=? or "
                        + MediaStore.Video.Media.MIME_TYPE + "=? or "
                        + MediaStore.Video.Media.MIME_TYPE + "=? or "
                        + MediaStore.Video.Media.MIME_TYPE + "=? or "
                        + MediaStore.Video.Media.MIME_TYPE + "=? or "
                        + MediaStore.Video.Media.MIME_TYPE + "=?";
                String[] whereArgs = {"video/mp4", "video/3gp", "video/aiv", "video/rmvb", "video/vob", "video/flv",
                        "video/mkv", "video/mov", "video/mpg"};
                Cursor mCursor = activity.getContentResolver().query(mVideoUri,
                        projection, null, null, MediaStore.Video.Media.DATE_ADDED + " DESC ");

                if (mCursor != null) {
                    while (mCursor.moveToNext()) {
                        // 获取视频的路径
                        int videoId = mCursor.getInt(mCursor.getColumnIndex(MediaStore.Video.Media._ID));
                        String path;
                        if(Build.VERSION.SDK_INT == Build.VERSION_CODES.P){
                            path =MediaStore.Video.Media
                                    .EXTERNAL_CONTENT_URI
                                    .buildUpon()
                                    .appendPath(String.valueOf(videoId)).build().toString();
                        }else{
                            path = mCursor.getString(mCursor.getColumnIndex(MediaStore.Video.Media.DATA));
                        }
                        long duration = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Video.Media.DURATION));
                        long size = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Video.Media.SIZE)) / 1024; //单位kb
                        if (size < 0) {
                            //某些设备获取size<0,直接计算
                            Log.e("dml", "this video size < 0 " + path);
                            size = new File(path).length() / 1024;
                        }
                        String displayName = mCursor.getString(mCursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME));
                        //用于展示相册初始化界面
                        int timeIndex = mCursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED);
                        Long date = mCursor.getLong(timeIndex) *1000;
                        //需要判断当前文件是否存在  一定要加,不然有些文件已经不存在图片显示不出来。这里适配Android Q
                        synchronized (activity) {
                            boolean fileIsExists;
                            if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
                                fileIsExists = FileManagerUtils.isContentUriExists(MSApplication.getmContext(), Uri.parse(path));
                                if (fileIsExists) {
                                    videoList.add(new MediaData(videoId, MediaConstant.VIDEO, path, "", getVideoUri(mCursor), duration, date, displayName, false));
                                }
                            } else {
                                fileIsExists = FileManagerUtils.fileIsExists(path);
                                if (fileIsExists) {
                                    videoList.add(new MediaData(videoId, MediaConstant.VIDEO, path, path, null, duration, date, displayName, false));
                                }
                            }
                        }
                    }
                    mCursor.close();
                }

2.使用Content Uri
在以上代码中我们可以看到都适配了Andorid Q,这是为什么呢?因为DATA 数据在 Android Q 以前代表了文件的路径,但在 Android Q上该路径无法被访问,因此没有意义。所以既然DATA不可用,那么我们可以用ID 在 Android Q 上读取文件,即使用ID拼装出Content Uri,如1中的代码:

 图片:
      int id = mCursor.getColumnIndex(MediaStore.Images.Media._ID);
      String thumbPath  = MediaStore.Images.Media
                                    .EXTERNAL_CONTENT_URI
                                    .buildUpon()
                                    .appendPath(String.valueOf(mCursor.getInt(id))).build().toString();
 视频:
 	  int videoId = mCursor.getInt(mCursor.getColumnIndex(MediaStore.Video.Media._ID));
      String path = MediaStore.Video.Media
                                    .EXTERNAL_CONTENT_URI
                                    .buildUpon()
                                    .appendPath(String.valueOf(videoId)).build().toString();

3.读取和写入

  • 在Andorid Q 中,当我们通过Content Uri拿到路径之后,是无法通过File来判断文件是否存在,即file.exist()会总是为False。所以我们借助于ContentResolver来判断

    public static boolean isContentUriExists(Context context, Uri uri){
        if (null == context) {
            return false;
        }
        ContentResolver cr = context.getContentResolver();
        try {
            AssetFileDescriptor afd = cr.openAssetFileDescriptor(uri, "r");
            if (null == afd) {
                return false;
            } else {
                try {
                    afd.close();
                } catch (IOException e) {
                }
            }
        } catch (FileNotFoundException e) {
            return false;
        }

        return true;
    }

这种方法最大的问题即是,对应于一个同步 I/O 调用,易造成线程等待。因此,目前对于 MediaStore 中扫描出来的文件可能不存在的情况,没有直接的好方法可以解决过滤。

  • 在读取和写入时,我们可以借助 Content Uri 从 ContentResolver 里面拿到 AssetFileDescriptor,然后就可以拿到 InputSteam 或 OutputStream,那么接下来的读取和写入就非常自然,如下所示:
public static void copy(File src, ParcelFileDescriptor parcelFileDescriptor) throws IOException {
    FileInputStream istream = new FileInputStream(src);
    try {
        FileOutputStream ostream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
        try {
            IOUtil.copy(istream, ostream);
        } finally {
            ostream.close();
        }
    } finally {
        istream.close();
    }
}

public static void copy(ParcelFileDescriptor parcelFileDescriptor, File dst) throws IOException {
    FileInputStream istream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());
    try {
        FileOutputStream ostream = new FileOutputStream(dst);
        try {
            IOUtil.copy(istream, ostream);
        } finally {
            ostream.close();
        }
    } finally {
        istream.close();
    }
}
    
    
public static void copy(InputStream ist, OutputStream ost) throws IOException {
    byte[] buffer = new byte[4096];
    int byteCount = 0;
    while ((byteCount = ist.read(buffer)) != -1) {  // 循环从输入流读取 buffer字节
        ost.write(buffer, 0, byteCount);        // 将读取的输入流写入到输出流
    }
}
  • 保存媒体文件到公共区域,这里仅以 Video 示例,Image、Downloads 基本类似:
public static Uri insertVideoIntoMediaStore(Context context, String fileName) {
    ContentValues contentValues = new ContentValues();
    contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);
    contentValues.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
    contentValues.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");

    Uri uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
    return uri;
}

4.关于视频Video的缩略图问题,在 Android Q 上已经拿不到 Video 的 Thumbnail 路径了,又由于没有暴露 Video 的 Thumbnail 的 id ,导致了 Video 的 Thumbnail 只能使用实时获取 Bitmap 的方法,即

private Bitmap getThumbnail(ContentResolver cr, long videoId) throws Throwable {
    return MediaStore.Video.Thumbnails.getThumbnail(cr, videoId, MediaStore.Video.Thumbnails.MINI_KIND,
            null);
}

5.在3中我们需要通过FileInputStream 来出来文件,比如加载到ImageView,我感觉有点麻烦,所以这里我是用了Glide来加载Uri,这样也就没有和流相关的操作了。

public static Uri getPhotoUri(Cursor cursor) {
        return getMediaUri(cursor, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    }

    public static Uri getVideoUri(Cursor cursor) {
        return getMediaUri(cursor, MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
    }

    public static Uri getMediaUri(Cursor cursor, Uri uri) {
        String id = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
        return Uri.withAppendedPath(uri, id);
    }

加载:

Glide.with(XXApplication.getmContext())
                        .load(thumbPathUri)
                        .apply(options)
                        .into(iv_item_image);

你可能感兴趣的:(android,View,异常)