当前位置 博文首页 > 文章内容

    Android使用DocumentFile读写外置存储的问题

    作者:shunshunshun18 栏目:未分类 时间:2021-12-28 15:24:35

    本站于2023年9月4日。收到“大连君*****咨询有限公司”通知
    说我们IIS7站长博客,有一篇博文用了他们的图片。
    要求我们给他们一张图片6000元。要不然法院告我们

    为避免不必要的麻烦,IIS7站长博客,全站内容图片下架、并积极应诉
    博文内容全部不再显示,请需要相关资讯的站长朋友到必应搜索。谢谢!

    另祝:版权碰瓷诈骗团伙,早日弃暗投明。

    相关新闻:借版权之名、行诈骗之实,周某因犯诈骗罪被判处有期徒刑十一年六个月

    叹!百花齐放的时代,渐行渐远!



    最近在维护项目,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回调里,添加上面这两行代码,就不用每次都弹授权,影响体验了