AndroidQ 沙箱适配多媒体文件(小结)
综述
所有内容的访问变化见下图:
外部媒体文件的扫描,读取和写入
最容易被踩坑的应该是,对外部媒体文件,照片,视频,图片的读取或写入。
扫描
首先是扫描。扫描依然是使用queryMediaStore的方式。一句话介绍MediaStore,MediaStore就是Android系统中的一个多媒体数据库。代码如下图所示,以搜索本地视频为例子:
protectedListdoInBackground(Void...params){ mContentResolver=context.getContentResolver(); String[]mediaColumns={MediaStore.Video.Media._ID,MediaStore.Video.Media.DATA, MediaStore.Video.Media.TITLE,MediaStore.Video.Media.MIME_TYPE, MediaStore.Video.Media.DISPLAY_NAME,MediaStore.Video.Media.SIZE, MediaStore.Video.Media.DATE_ADDED,MediaStore.Video.Media.DURATION, MediaStore.Video.Media.WIDTH,MediaStore.Video.Media.HEIGHT}; CursormCursor=mContentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,mediaColumns, null,null,MediaStore.Video.Media.DATE_ADDED); if(mCursor==null){ returnnull; } //注意,DATA数据在AndroidQ以前代表了文件的路径,但在AndroidQ上该路径无法被访问,因此没有意义。 ixData=mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA); ixMime=mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE); //ID是在AndroidQ上读取文件的关键字段 ixId=mCursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID); ixSize=mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE); ixTitle=mCursor.getColumnIndexOrThrow(MediaStore.Video.Media.TITLE); allImages=newArrayList (); mTotalVideoCount=0; mCursor.moveToLast(); while(mCursor.moveToPrevious()){ if(addVideo(mCursor)==0){ continue; }elseif(addVideo(mCursor)==1){ break; } } mCursor.close(); returnallImages; }
既然data不可用,就需要知晓id的使用方式,首先是使用id拼装出contenturi,如下所示:
publicgetRealPath(Stringid){ returnMediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build().toString(); }
Image同理换成MediaStore.Images。
读取和写入
其次,是读取contenturi。这里需要注意Filefile=newFile(contentUri);是无法获取到文件的。file.exist()为false。
那么就产生两个问题:1.如何确定ContentUri形式的文件存在2.如何读取或写入文件。
首先,对于ContentUri的读取,必须借助于ContentResolver。
其次,对于1,没有找到Google文档中提供比较容易的API,只能采用打开FileDescriptor是否成功的形式,代码如下所示:
publicbooleanisContentUriExists(Contextcontext,Uriuri){ if(null==context){ returnfalse; } ContentResolvercr=context.getContentResolver(); try{ AssetFileDescriptorafd=cr.openAssetFileDescriptor(uri,"r"); if(null==afd){ iterator.remove(); }else{ try{ afd.close(); }catch(IOExceptione){ } } }catch(FileNotFoundExceptione){ returnfalse; } returntrue; }
这种方法最大的问题即是,对应于一个同步I/O调用,易造成线程等待。因此,目前对于MediaStore中扫描出来的文件可能不存在的情况,没有直接的好方法可以解决过滤。
对于问题2,如1所示,可以借助ContentUri从ContentResolver里面拿到AssetFileDescriptor,然后就可以拿到InputSteam或OutputStream,那么接下来的读取和写入就非常自然,如下所示:
publicstaticvoidcopy(Filesrc,ParcelFileDescriptorparcelFileDescriptor)throwsIOException{ FileInputStreamistream=newFileInputStream(src); try{ FileOutputStreamostream=newFileOutputStream(parcelFileDescriptor.getFileDescriptor()); try{ IOUtil.copy(istream,ostream); }finally{ ostream.close(); } }finally{ istream.close(); } } publicstaticvoidcopy(ParcelFileDescriptorparcelFileDescriptor,Filedst)throwsIOException{ FileInputStreamistream=newFileInputStream(parcelFileDescriptor.getFileDescriptor()); try{ FileOutputStreamostream=newFileOutputStream(dst); try{ IOUtil.copy(istream,ostream); }finally{ ostream.close(); } }finally{ istream.close(); } } publicstaticvoidcopy(InputStreamist,OutputStreamost)throwsIOException{ byte[]buffer=newbyte[4096]; intbyteCount=0; while((byteCount=ist.read(buffer))!=-1){//循环从输入流读取buffer字节 ost.write(buffer,0,byteCount);//将读取的输入流写入到输出流 } }
保存媒体文件到公共区域
这里仅以Video示例,Image、Downloads基本类似:
publicstaticUriinsertVideoIntoMediaStore(Contextcontext,StringfileName){ ContentValuescontentValues=newContentValues(); 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"); Uriuri=context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,contentValues); returnuri; }
这里所做的,只是往MediaStore里面插入一条新的记录,MediaStore会返回给我们一个空的ContentUri,接下来问题就转化为往这个ContentUri里面写入,那么应用上一节所述的代码即可实现。
Video的Thumbnail问题
在AndroidQ上已经拿不到Video的Thumbnail路径了,又由于没有暴露Video的Thumbnail的id,导致了Video的Thumbnail只能使用实时获取Bitmap的方法,如下所示:
privateBitmapgetThumbnail(ContentResolvercr,longvideoId)throwsThrowable{ returnMediaStore.Video.Thumbnails.getThumbnail(cr,videoId,MediaStore.Video.Thumbnails.MINI_KIND, null); }
可以进去看AndroidSDK的实现,其中最关键的部分是:
Stringcolumn=isVideo?"video_id=":"image_id="; c=cr.query(baseUri,PROJECTION,column+origId,null,null); if(c!=null&&c.moveToFirst()){ bitmap=getMiniThumbFromFile(c,baseUri,cr,options); if(bitmap!=null){ returnbitmap; } }
进一步再进去看,可以发现直接就把Video/Image文件打开计算Thumbnail。
privatestaticBitmapgetMiniThumbFromFile( Cursorc,UribaseUri,ContentResolvercr,BitmapFactory.Optionsoptions){ Bitmapbitmap=null; UrithumbUri=null; try{ longthumbId=c.getLong(0); StringfilePath=c.getString(1); thumbUri=ContentUris.withAppendedId(baseUri,thumbId); ParcelFileDescriptorpfdInput=cr.openFileDescriptor(thumbUri,"r"); bitmap=BitmapFactory.decodeFileDescriptor( pfdInput.getFileDescriptor(),null,options); pfdInput.close(); }catch(FileNotFoundExceptionex){ Log.e(TAG,"couldn'topenthumbnail"+thumbUri+";"+ex); }catch(IOExceptionex){ Log.e(TAG,"couldn'topenthumbnail"+thumbUri+";"+ex); }catch(OutOfMemoryErrorex){ Log.e(TAG,"failedtoallocatememoryforthumbnail" +thumbUri+";"+ex); } returnbitmap; }
这个API毫无疑问设计的非常不合理,没有暴露Thumbnail的系统缓存给开发者,造成了每次都要重新I/O计算的极大耗时。强烈呼吁AndroidQ的正式版能修正这个API设计缺陷。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。