Bootstrap

Android 9.0 sdCard文件读写

目录

背景描述:

解决方案:

处理过程中遇到的问题(代码中需要替换的点)

结尾:


背景描述:

9.0之后,Google合入一笔patch,去掉了WRITE_MEDIA_STORAGE权限中的sdcard_rw。导致之前的文件读写方式无法对sdcard生效

http://androidxref.com/9.0.0_r3/search?q=&defs=&refs=&path=&hist=86684240eb5753bb97c2cfc93d1d25fa1870f8f1&project=art&project=bionic&project=bootable&project=build&project=compatibility&project=cts&project=dalvik&project=developers&project=development&project=device&project=external&project=frameworks&project=hardware&project=kernel&project=libcore&project=libnativehelper&project=packages&project=pdk&project=platform_testing&project=prebuilts&project=sdk&project=system&project=test&project=toolchain&project=tools

解决方案:

DocumentFile

//申请目录权限
private void getSdCardPermission(){
        StorageManager mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
        boolean canFindPath=false;
        //获取所有挂载的设备(内部sd卡、外部sd卡、挂载的U盘)
        List<StorageVolume> volumes = mStorageManager.getStorageVolumes();
        try {
            Class<?> storageVolumeClazz = Class
                    .forName("android.os.storage.StorageVolume");
            //通过反射调用系统hide的方法
            Method getPath = storageVolumeClazz.getMethod("getPath");
            Method isRemovable = storageVolumeClazz.getMethod("isRemovable");
            for (int i = 0; i < volumes.size(); i++) {
                StorageVolume storageVolume = volumes.get(i);//获取每个挂载的StorageVolume
                //通过反射调用getPath、isRemovable
                storagePath = (String) getPath.invoke(storageVolume); //获取路径
                boolean isRemovableResult = (boolean) isRemovable.invoke(storageVolume);//是否可移除
                String description = storageVolume.getDescription(this);
                android.util.Log.d(TAG, " i=" + i + " ,storagePath=" + storagePath
                        + " ,isRemovableResult=" + isRemovableResult +" ,description="+description);
                //        i=0 ,storagePath=/storage/emulated/0 ,isRemovableResult=false ,description=内部共享存储空间
                //        i=1 ,storagePath=/storage/B8EA-15F1 ,isRemovableResult=true ,description=SD卡
                if (filePath.contains(storagePath)){
                    this.storageVolume=storageVolume;
                    canFindPath=true;
                    break;
                }
            }
        } catch (Exception e) {
            android.util.Log.d("jason", " e:" + e);
        }
        if (!canFindPath){
            this.storageVolume=null;
            Toast.makeText(this,"filePath not find",Toast.LENGTH_SHORT).show();
            return;
        }

        if (storageVolume!=null&&canFindPath){
            Intent intent = storageVolume.createAccessIntent(getDirectoryName("DCIM"));//
            permissionPath=storagePath + File.separator+"DCIM";
            startActivityForResult(intent, OPEN_DIRECTORY_REQUEST_CODE);
        }
    }

//请求成功之后,权限持久化
getContentResolver().takePersistableUriPermission(uriForPermissionPath,
                        Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

//持久化之后的权限可以通过下面方法获取所有已经获得授权的uriPermission
List<UriPermission> persistedUriPermissions = context.getContentResolver().getPersistedUriPermissions();
//获取授权的uri
Uri uri = persistedUriPermissions.get(0).getUri();
//根据uri获取DocumentFile对象
DocumentFile documentFile= DocumentFile.fromTreeUri(context, parentUri);

//创建目录或者文件
//获取权限路径之后的未知目录结构 /100MEDIA/VIDEO0001_01.mp4.hyperlapse
String replace = outPath.replace(permissionPath, "");
//循环取得各层级目录名,判断是否存在并创建
String[] split = replace.split(File.separator);
for (int i = 0; i <split.length ; i++) {
    if (TextUtils.isEmpty(split[i])){
        continue;
    }
    DocumentFile documentFileTmp = documentFile.findFile(split[i]);
    if (documentFileTmp!=null&&documentFileTmp.exists()){
        documentFile=documentFileTmp;
        if (i==split.length -1){
            return documentFile;
        }
        continue;
    }
    if (split[i].contains(".")&&i==split.length -1){//文件
        documentFile = documentFile.createFile(null, split[i]);//创建文件
        if (documentFile!=null){
            Uri writeUri = documentFile.getUri();
            return documentFile;
        }
    }else{//目录
        DocumentFile directory = documentFile.createDirectory(split[i]);
        if (directory!=null){
            documentFile=directory;
        }
    }
}
//文件读写
Uri uri = documentFile.getUri();//获取文件的uri
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "rw");//通过uri获取ParcelFileDescriptor对象
FileDescriptor fileDescriptor = pfd.getFileDescriptor();//得到文件描述
FileInputStream fis = new FileInputStream(fileDescriptor);//文件输入流
FileOutputStream fos = new FileOutputStream(fileDescriptor);//文件输出流
//之后就可以进行正常的文件读写

处理过程中遇到的问题(代码中需要替换的点)

1、权限获取方式

2、文件/目录的创建、删除、重命名方式

3、其他文件相关的类的初始化或者方法调用,不能直接传入path,需要改为FileDescriptor。
 

3.1、MediaMuxer(@NonNull FileDescriptor fd, @Format int format)//初始化时使用FileDescriptor

3.2、RandomAccessFile raf = new RandomAccessFile(ori, "rws");//不支持,无法初始化RandomAccessFile

//*****Open file channel to read byte

FileChannel fc = raf.getChannel();

3.3、MediaExtractor extractor = new MediaExtractor();

extractor.setDataSource(FileDescriptor fd);//setDataSource方法需要传入FileDescriptor

3.4、MediaMetadataRetriever retriever = new MediaMetadataRetriever();

 if (!inExtStorage){

        retriever.setDataSource(pfd.getFileDescriptor());//setDataSource方法需要传入FileDescriptor

}else{

        retriever.setDataSource(mContext, Uri.parse(path));

}

3.5、MovieCreator.build(FileChannel in);//入参的获取方式需要通过DocumentFile,不能直接通过路径

in = new FileInputStream(filePath).getChannel();

DocumentFile documentFileByPath = FileUtils.getDocumentFileByPath(filePath);

pfd = context.getContentResolver().openFileDescriptor(documentFileByPath.getUri(), "rw");

in = new FileInputStream(pfd.getFileDescriptor()).getChannel();

3.6、WritableByteChannel fc = null;//获取方式不能通过path,类似与3.5

3.5和3.6:都属于FileInputStream、FileOutputStream、FileDescriptor、FileChannel的获取方式

结尾:

以上是目前的进展,简单的做一个总结. 

;