Android DocumentFile读写外置存储
雪飘碧鸳 人气:0最近在维护项目,app遇到安装在高版本的Android时,以往直接授权和new File(path)的形式不再支持,日志也是说Permission denied。。。。。好吧,换为DocumentFile。
经过一番操作,也终于实再对存储目录的读和写了,下面记录一下:
首先建一个DocumentFile的Utils类:
import android.annotation.TargetApi; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.ParcelFileDescriptor; import android.preference.PreferenceManager; import android.provider.DocumentsContract; import android.util.Log; import androidx.documentfile.provider.DocumentFile; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.List; public class DocumentsUtils { private static final String TAG = DocumentsUtils.class.getSimpleName(); public static final int OPEN_DOCUMENT_TREE_CODE = 8000; private static List<String> sExtSdCardPaths = new ArrayList<>(); private DocumentsUtils() { } public static void cleanCache() { sExtSdCardPaths.clear(); } /** * Get a list of external SD card paths. (Kitkat or higher.) * * @return A list of external SD card paths. */ @TargetApi(Build.VERSION_CODES.KITKAT) private static String[] getExtSdCardPaths(Context context) { if (sExtSdCardPaths.size() > 0) { return sExtSdCardPaths.toArray(new String[0]); } for (File file : context.getExternalFilesDirs("external")) { if (file != null && !file.equals(context.getExternalFilesDir("external"))) { int index = file.getAbsolutePath().lastIndexOf("/Android/data"); if (index < 0) { Log.w(TAG, "Unexpected external file dir: " + file.getAbsolutePath()); } else { String path = file.getAbsolutePath().substring(0, index); try { path = new File(path).getCanonicalPath(); } catch (IOException e) { // Keep non-canonical path. } sExtSdCardPaths.add(path); } } } if (sExtSdCardPaths.isEmpty()) sExtSdCardPaths.add("/storage/sdcard1"); return sExtSdCardPaths.toArray(new String[0]); } /** * Determine the main folder of the external SD card containing the given file. * * @param file the file. * @return The main folder of the external SD card containing this file, if the file is on an SD * card. Otherwise, * null is returned. */ @TargetApi(Build.VERSION_CODES.KITKAT) private static String getExtSdCardFolder(final File file, Context context) { String[] extSdPaths = getExtSdCardPaths(context); try { for (int i = 0; i < extSdPaths.length; i++) { if (file.getCanonicalPath().startsWith(extSdPaths[i])) { return extSdPaths[i]; } } } catch (IOException e) { return null; } return null; } /** * Determine if a file is on external sd card. (Kitkat or higher.) * * @param file The file. * @return true if on external sd card. */ @TargetApi(Build.VERSION_CODES.KITKAT) public static boolean isOnExtSdCard(final File file, Context c) { return getExtSdCardFolder(file, c) != null; } /** * Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5). * If the file is not * existing, it is created. * * @param file The file. * @param isDirectory flag indicating if the file should be a directory. * @return The DocumentFile */ public static DocumentFile getDocumentFile(final File file, final boolean isDirectory, Context context) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { return DocumentFile.fromFile(file); } String baseFolder = getExtSdCardFolder(file, context); // Log.i(TAG,"lum_ baseFolder " + baseFolder); boolean originalDirectory = false; if (baseFolder == null) { return null; } String relativePath = null; try { String fullPath = file.getCanonicalPath(); if (!baseFolder.equals(fullPath)) { relativePath = fullPath.substring(baseFolder.length() + 1); } else { originalDirectory = true; } } catch (IOException e) { return null; } catch (Exception f) { originalDirectory = true; //continue } String as = PreferenceManager.getDefaultSharedPreferences(context).getString(baseFolder, null); Uri treeUri = null; if (as != null) treeUri = Uri.parse(as); if (treeUri == null) { return null; } // start with root of SD card and then parse through document tree. DocumentFile document = DocumentFile.fromTreeUri(context, treeUri); if (originalDirectory) return document; String[] parts = relativePath.split("/"); for (int i = 0; i < parts.length; i++) { DocumentFile nextDocument = document.findFile(parts[i]); if (nextDocument == null) { if ((i < parts.length - 1) || isDirectory) { nextDocument = document.createDirectory(parts[i]); } else { nextDocument = document.createFile("image", parts[i]); } } document = nextDocument; } return document; } public static boolean mkdirs(Context context, File dir) { boolean res = dir.mkdirs(); if (!res) { if (DocumentsUtils.isOnExtSdCard(dir, context)) { DocumentFile documentFile = DocumentsUtils.getDocumentFile(dir, true, context); res = documentFile != null && documentFile.canWrite(); } } return res; } public static boolean delete(Context context, File file) { boolean ret = file.delete(); if (!ret && DocumentsUtils.isOnExtSdCard(file, context)) { DocumentFile f = DocumentsUtils.getDocumentFile(file, false, context); if (f != null) { ret = f.delete(); } } return ret; } public static boolean canWrite(File file) { boolean res = file.exists() && file.canWrite(); if (!res && !file.exists()) { try { if (!file.isDirectory()) { res = file.createNewFile() && file.delete(); } else { res = file.mkdirs() && file.delete(); } } catch (IOException e) { e.printStackTrace(); } } return res; } public static boolean canWrite(Context context, File file) { boolean res = canWrite(file); if (!res && DocumentsUtils.isOnExtSdCard(file, context)) { DocumentFile documentFile = DocumentsUtils.getDocumentFile(file, true, context); res = documentFile != null && documentFile.canWrite(); } return res; } /** * 重命名 * @param context * @param src * @param dest * @return */ public static boolean renameTo(Context context, File src, File dest) { boolean res = src.renameTo(dest); if (!res && isOnExtSdCard(dest, context)) { DocumentFile srcDoc; if (isOnExtSdCard(src, context)) { srcDoc = getDocumentFile(src, false, context); } else { srcDoc = DocumentFile.fromFile(src); } DocumentFile destDoc = getDocumentFile(dest.getParentFile(), true, context); if (srcDoc != null && destDoc != null) { try { Log.i("renameTo", "src.getParent():" + src.getParent() + ",dest.getParent():" + dest.getParent()); if (src.getParent().equals(dest.getParent())) {//同一目录 res = srcDoc.renameTo(dest.getName()); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {//不同一目录 Uri renameSrcUri = DocumentsContract.renameDocument(context.getContentResolver(),//先重命名 srcDoc.getUri(), dest.getName()); res = DocumentsContract.moveDocument(context.getContentResolver(),//再移动 renameSrcUri, srcDoc.getParentFile().getUri(), destDoc.getUri()) != null; } } catch (Exception e) { e.printStackTrace(); } } } return res; } public static InputStream getInputStream(Context context, File destFile) { InputStream in = null; try { if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) { DocumentFile file = DocumentsUtils.getDocumentFile(destFile, false, context); if (file != null && file.canWrite()) { in = context.getContentResolver().openInputStream(file.getUri()); } } else { in = new FileInputStream(destFile); } } catch (FileNotFoundException e) { e.printStackTrace(); } return in; } public static OutputStream getOutputStream(Context context, File destFile) { OutputStream out = null; try { if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) { DocumentFile file = DocumentsUtils.getDocumentFile(destFile, false, context); if (file != null && file.canWrite()) { out = context.getContentResolver().openOutputStream(file.getUri()); } } else { out = new FileOutputStream(destFile); } } catch (FileNotFoundException e) { e.printStackTrace(); } return out; } /** * 获取文件流 * @param context * @param destFile 目标文件 * @param mode May be "w", "wa", "rw", or "rwt". * @return */ public static OutputStream getOutputStream(Context context, File destFile, String mode) { OutputStream out = null; try { if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) { DocumentFile file = DocumentsUtils.getDocumentFile(destFile, false, context); if (file != null && file.canWrite()) { out = context.getContentResolver().openOutputStream(file.getUri(), mode); } } else { out = new FileOutputStream(destFile, mode.equals("rw") || mode.equals("wa")); } } catch (FileNotFoundException e) { e.printStackTrace(); } return out; } public static FileDescriptor getFileDescriptor(Context context, File destFile) { FileDescriptor fd = null; try { if (/*!canWrite(destFile) && */isOnExtSdCard(destFile, context)) { DocumentFile file = DocumentsUtils.getDocumentFile(destFile, false, context); if (file != null && file.canWrite()) { ParcelFileDescriptor out = context.getContentResolver().openFileDescriptor(file.getUri(), "rw"); fd = out.getFileDescriptor(); } } else { RandomAccessFile file = null; try { file = new RandomAccessFile(destFile, "rws"); file.setLength(0); fd = file.getFD(); } catch (Exception e){ e.printStackTrace(); } finally { if (file != null) { try { file.close(); } catch (IOException e) { e.printStackTrace(); } } } } } catch (FileNotFoundException e) { e.printStackTrace(); } return fd; } public static boolean saveTreeUri(Context context, String rootPath, Uri uri) { DocumentFile file = DocumentFile.fromTreeUri(context, uri); if (file != null && file.canWrite()) { SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context); perf.edit().putString(rootPath, uri.toString()).apply(); Log.e(TAG, "save uri" + rootPath); return true; } else { Log.e(TAG, "no write permission: " + rootPath); } return false; } /** * 返回true表示没有权限 * @param context * @param rootPath * @return */ public static boolean checkWritableRootPath(Context context, String rootPath) { File root = new File(rootPath); if (!root.canWrite()) { Log.e(TAG,"sd card can not write:" + rootPath + ",is on extsdcard:" + DocumentsUtils.isOnExtSdCard(root, context)); if (DocumentsUtils.isOnExtSdCard(root, context)) { DocumentFile documentFile = DocumentsUtils.getDocumentFile(root, true, context); if (documentFile != null) { Log.i(TAG, "get document file:" + documentFile.canWrite()); } return documentFile == null || !documentFile.canWrite(); } else { SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context); String documentUri = perf.getString(rootPath, ""); Log.i(TAG,"lum_2 get perf documentUri:" + documentUri); if (documentUri == null || documentUri.isEmpty()) { return true; } else { DocumentFile file = DocumentFile.fromTreeUri(context, Uri.parse(documentUri)); if (file != null) Log.i(TAG,"lum get perf documentUri:" + file.canWrite()); return !(file != null && file.canWrite()); } } }else{ Log.e(TAG,"sd card can write..."); } return false; } }
然后在app启动的地方检查下是否需要授权才能操作:
if (DocumentsUtils.checkWritableRootPath(this, StringUtils.STORAGE_PATH)) { //检查sd卡路径是否有 权限 没有显示dialog showOpenDocumentTree(); } private void showOpenDocumentTree() { Log.e("showOpenDocumentTree", "start check sd card..."); Intent intent = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { StorageManager sm = getSystemService(StorageManager.class); StorageVolume volume = sm.getStorageVolume(new File(StringUtils.STORAGE_PATH)); if (volume != null) { intent = volume.createAccessIntent(null); } } Log.e("showOpenDocumentTree", "intent=" + intent); if (intent == null) { intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); } startActivityForResult(intent, DocumentsUtils.OPEN_DOCUMENT_TREE_CODE); } //................................ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case DocumentsUtils.OPEN_DOCUMENT_TREE_CODE: if (data != null && data.getData() != null) { Uri uri = data.getData(); DocumentsUtils.saveTreeUri(this, StringUtils.STORAGE_PATH, uri); Log.i(TAG,"DocumentsUtils.OPEN_DOCUMENT_TREE_CODE : " + uri); } break; } super.onActivityResult(requestCode, resultCode, data); }
按以上代码,是可以实现对外置存储卡进行读和写操作了,只要机器没关机,多关打开关闭app,都不会弹下面这个授权框:
但是-----如果关机再重新开机,就要重新授权,这样很麻烦,用户体验也极其不好,所以又查询了很多资料,最后在stackoverflow上找到办法,关键是下面这句:
grantUriPermission(getPackageName(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
是在ActivityResult回调里,添加上面这两行代码,就不用每次都弹授权,影响体验了
加载全部内容