使用范围
使用文中方式封装的DocumentFile
工具类适用范围极为广阔,不仅可以操作Android10-Android13(目前最新版)的Android/data
目录,还可以操作外置TF卡中的文件,以及手机外接U盘中的文件,理论支持一切连接手机的外部储存硬件
添加权限
读取和写入外部储存不需要多说,所有文件访问权限好像是能提升SAF框架的读写速度
1 2 3 4 5 6
| <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
DocumentFileUtils
新建一个DocumentFileUtils类,使用这个类来对DocumentFile进行读写操作
常量
所需的一些常量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private final Context context;
private final String URI_HEAD;
private final String permissionPath; private final String permissionUriStr;
public final static String PRIMARY_STORAGE; public final static String ANDROID_PATH; public final static String ANDROID_DATA_PATH; public final static String ANDROID_OBB_PATH;
{ URI_HEAD = "content://com.android.externalstorage.documents/tree/"; }
static { PRIMARY_STORAGE = Environment.getExternalStorageDirectory().getAbsolutePath(); ANDROID_PATH = PRIMARY_STORAGE + "/Android"; ANDROID_DATA_PATH = ANDROID_PATH + "/data"; ANDROID_OBB_PATH = ANDROID_PATH + "/obb"; }
|
添加和移除斜杠
然后为了防止传入的目录绝对正确,我们要先对目录的前后添加斜杠“/”,在后续的操作中会用到移除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
private String addSlash(String permissionDir) { if (!permissionDir.startsWith("/")) { permissionDir = "/" + permissionDir; }
if (!permissionDir.endsWith("/")) { permissionDir = permissionDir + "/"; } return permissionDir; }
private String removeSlash(String path) { if (path.startsWith("/")) { path = path.substring(1); }
if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); }
return path; }
|
将目录地址转换为Uri地址
再将目录地址转换为Uri地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
|
private String pathToUri(String path) { path = addSlash(path);
String pathHead = PRIMARY_STORAGE.substring(0, PRIMARY_STORAGE.indexOf("/", 1) + 1); if (!path.startsWith(pathHead)) return null;
String pathContent = path.substring(pathHead.length());
String primaryPath = PRIMARY_STORAGE.substring(pathHead.length());
if (pathContent.startsWith(primaryPath)) pathContent = "primary" + pathContent.substring(primaryPath.length());
String rootPathName = pathContent.substring(0, pathContent.indexOf("/"));
pathContent = pathContent.substring(rootPathName.length() + 1);
if (pathContent.endsWith("/")) pathContent = pathContent.substring(0, pathContent.length() - 1);
pathContent = pathContent.replaceAll("/", "%2F");
return URI_HEAD + rootPathName + "%3A" + pathContent; }
|
写构造器
在构造器中要得到请求权限的目录地址、请求权限的目录的uri地址、上下文,请求码可以暂时不传
1. Context context: 上下文
2. String permissionDir: 需要请求权限的目录,下文中称为“权限目录”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
public static DocumentFileUtils create(Context context, String permissionDir) { return new XAFUtil(context, permissionDir); }
private DocumentFileUtils(Context context, String permissionDir) { this.permissionPath = addSlash(permissionDir); this.permissionUriStr = pathToUri(permissionDir); this.context = context;
if (this.permissionUriStr == null) Log.e(TAG, "DocumentFileUtils: root directory permissionDir field"); }
|
权限判断
判断权限目录是否拥有访问权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public boolean isPermission() { if (permissionUriStr == null) Log.e(TAG, "isPermission: root directory path field");
Uri uriPath = Uri.parse(permissionUriStr);
DocumentFile documentFile = DocumentFile.fromTreeUri(this.context, uriPath); if (documentFile != null) { return documentFile.canWrite(); } return false; }
|
判断是否拥有所有文件访问权限,我个人感觉这是一个很操蛋的名字,既然叫所有文件访问权限,我们拥有这个权限之后又不能访问所有文件,例如Android/data
1 2 3 4 5 6 7 8 9
|
public boolean isManagerExternalPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return Environment.isExternalStorageManager(); } return true; }
|
申请权限
申请权限目录的访问权限
这里注意一个困扰了我很久的细节问题,app的gradle文件中的最大sdk尽量不要超过29,从30开始将无法获得某些目录的权限例如storage/emulated/0
和storage/emulated/0/Android
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
|
public void requestPermission(Activity activity, int requestCode) { requestPermission(activity, null, requestCode); }
public void requestPermission(Fragment fragment, int requestCode) { requestPermission(null, fragment, requestCode); }
private void requestPermission(Activity activity, Fragment fragment, int requestCode) { if (permissionUriStr == null) { Log.e(TAG, "requestPermission: permission directory path field"); return; }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { Log.e(TAG, "requestPermission: sdk version too low"); return; }
this.requestCode = requestCode; Uri uriPath = Uri.parse(permissionUriStr); DocumentFile documentFile = DocumentFile.fromTreeUri(this.context, uriPath); if (documentFile != null) { Intent intent = createIntent(documentFile); if (activity != null) { activity.startActivityForResult(intent, requestCode); } else { fragment.startActivityForResult(intent, requestCode); } } else { Log.e(TAG, "requestPermission: " + permissionUriStr + " not exists"); } }
@RequiresApi(api = Build.VERSION_CODES.O) private Intent createIntent(DocumentFile documentFile) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.getUri()); return intent; }
|
申请权限之后需要进行保存权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
@SuppressLint("WrongConstant") public void savePermission(int requestCode, Intent intent) { if (intent == null) return; if (this.requestCode == requestCode) { Uri uri = intent.getData(); if (uri != null) { DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri); if (documentFile != null && documentFile.canWrite()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { this.context.getContentResolver().takePersistableUriPermission(uri, intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)); } else { Log.e(TAG, "savePermission: sdk version too low"); } } else { Log.e(TAG, "savePermission: no write permission"); } } else { Log.e(TAG, "savePermission: data uri field"); } } else { Log.e(TAG, "savePermission: requestCode field"); } }
|
请求所有文件访问权限
1 2 3 4 5 6 7 8 9 10 11 12
|
@SuppressLint("InlinedApi") public void requestManagerExternalPermission(Activity activity, int requestCode) { Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); intent.setData(Uri.parse("package:" + activity.getPackageName())); activity.startActivityForResult(intent, requestCode); }
|
获取DocumentFile对象
获取某文件DocumentFile对象
在这一步操作中,首先在传入地址的头尾添加斜杠,不管它是文件或文件夹
然后将传入的路径转换为Uri地址,当Uri地址错误,或者Uri地址不属于权限目录的Uri地址时返回null
如果传入的目录是权限目录直接return出去就好,如果是其子文件或子目录,则需要进行剔除相同部分,然后根据其不同的类型得到不同的DocumentFile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
|
public DocumentFile getDocumentFile(String filePath, boolean isFile) { filePath = addSlash(filePath);
String _uriPathStr = pathToUriStr(filePath);
if (_uriPathStr == null || !_uriPathStr.startsWith(permissionUriStr)) return null;
DocumentFile documentFile = DocumentFile.fromTreeUri(context, Uri.parse(permissionUriStr));
if (_uriPathStr.equals(permissionUriStr)) return documentFile;
String pathContent = filePath.substring(this.permissionPath.length()); return getDocumentFile(documentFile, pathContent, isFile); }
public DocumentFile getDocumentFile(DocumentFile documentFile, String filePath, boolean isFile) { if (documentFile == null) return null;
filePath = removeSlash(filePath);
if (TextUtils.isEmpty(filePath)) return documentFile;
String[] pathArr = filePath.split("/");
DocumentFile[] documentFiles = documentFile.listFiles(); if (pathArr.length > 0) { filePath = filePath.substring(pathArr[0].length()); for (DocumentFile _documentFile : documentFiles) { if (_documentFile.getName() != null && _documentFile.getName().equals(pathArr[0])) { return getDocumentFile(_documentFile, filePath, isFile); } } if (pathArr.length == 1 && isFile) { return documentFile.createFile("", pathArr[0]); } else { DocumentFile createDir = documentFile.createDirectory(pathArr[0]); return getDocumentFile(createDir, filePath, isFile); } } return documentFile; }
|
创建、删除、重命名
文件和文件夹的创建、删除、重命名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
public DocumentFile createDirectory(String folderPath) { return getDocumentFile(folderPath, false); }
public DocumentFile createFile(String filePath) { return getDocumentFile(filePath, true); }
public boolean deleteFile(String filePath, boolean isFile) { return getDocumentFile(filePath, isFile).delete(); }
public boolean renameFile(String filePath, boolean isFile, String newName) { return getDocumentFile(filePath, isFile).renameTo(newName); }
|
文件复制
文件复制有三种方式:File复制到DocumentFile,DocumentFile复制到File,DocumentFile复制到DocumentFile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
public void copyFile(DocumentFile fromFile, File toFile) { try { InputStream inputStream = context.getContentResolver().openInputStream(fromFile.getUri()); FileOutputStream fileOutputStream = new FileOutputStream(toFile); copy(inputStream, fileOutputStream); } catch (IOException e) { e.printStackTrace(); } }
public void copyFile(File fromFile, DocumentFile toFile) { try { FileInputStream fileInputStream = new FileInputStream(fromFile); OutputStream outputStream = context.getContentResolver().openOutputStream(toFile.getUri()); copy(fileInputStream, outputStream); } catch (IOException e) { e.printStackTrace(); } }
public void copyFile(DocumentFile fromFile, DocumentFile toFile) { try { InputStream inputStream = context.getContentResolver().openInputStream(fromFile.getUri()); OutputStream outputStream = context.getContentResolver().openOutputStream(toFile.getUri()); copy(inputStream, outputStream); } catch (IOException e) { e.printStackTrace(); } }
private void copy(InputStream inputStream, OutputStream outputStream) { try { byte[] buffer = new byte[1024]; int len; while ((len = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, len); } } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (outputStream != null) { try { outputStream.flush(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
|
获取流
ParcelFileDescriptor用于在某些场景中直接向DocumentFile中写入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
public InputStream getInputStream(String filePath) { DocumentFile documentFile = getDocumentFile(filePath, true); return getInputStream(documentFile); }
public InputStream getInputStream(DocumentFile documentFile) { try { return context.getContentResolver().openInputStream(documentFile.getUri()); } catch (FileNotFoundException e) { e.printStackTrace(); } return null; }
public OutputStream getOutputStream(String filePath) { DocumentFile documentFile = getDocumentFile(filePath, true); return getOutputStream(documentFile); }
public OutputStream getOutputStream(DocumentFile documentFile) { try { return context.getContentResolver().openOutputStream(documentFile.getUri()); } catch (FileNotFoundException e) { e.printStackTrace(); } return null; }
public ParcelFileDescriptor getFileDescriptor(DocumentFile documentFile, String openMode) { Uri uri = documentFile.getUri(); try { return context.getContentResolver().openFileDescriptor(uri, openMode); } catch (FileNotFoundException e) { e.printStackTrace(); } return null; }
|
使用说明
基础功能实现大体如下,注意调试时顶部的目录地址确定后,调试期间一定不要随意改动,因为很有可能改动后的目录地址没有申请权限,申请权限后点击按钮即可展示效果
DocumentFileUtils类由于没有对Uri写死,所以可以进行任何DocumentFile操作,例如Android/data目录,外置TF卡,SD卡等

参考:从共享存储空间访问文档和其他文件
开源地址:https://github.com/xxinPro/XAFUtil