Android使用DocumentFile读写外置存储的问题
最近在维护项目,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回调里,添加上面这两行代码,就不用每次都弹授权,影响体验了
到此这篇关于Android使用DocumentFile读写外置存储的问题的文章就介绍到这了,更多相关Android DocumentFile读写外置存储内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!
赞 (0)