Android文件系统管理——版本内外存所指差异,获得外接SD/U盘路径,在SD卡与U盘间传送文件,两天辛酸泪,收藏不迷路

在开发一个文件管理系统的路上,总有坑在等着你。

前情提示:因为该系统的使用方需要严格保密文件,导致它失去了无线传输功能,只能通过外设传输文件。

在没接触这个功能之前,我想大家应该都觉得手机自带的存储空间叫内部存储,SD卡叫外部存储。印象里存储的划分就应该这样简简单单清清楚楚。(但是别再化简了啊,内部等于内部存储那就过分了点。)

我们有这样的印象也不奇怪,就连我的某为手机上显示的也是内部存储128G。经过一番查找,总结一下就是,Android4.4之前,手机自身的存储卡叫内部存储,扩展的SD卡叫外部存储,有没有印象,以前我们还经常把这sd卡用卡针捅出来?但是不知不觉SD卡悄悄的消失了。

从Android4.4开始,机器的机身存储已经扩展到好多G了,现在看下我的手机是Android9,你可能更高。然后概念就开始混淆了,不管手机界面怎么显示,但是在系统的内部,这些扩展出来的内存空间都被称为外部存储!

肯定有人问,那我再外接SD卡怎么办,虽然现在SD卡少见,但是很多机身都有留地方给我们再扩展SD卡,别急我这边总结一下。

我们可以先把手机自带的机身存储叫作A,外接SD卡叫作B,那么其实就是在4.4之前,A=内部存储,B=外部存储。

那在4.4之后,机身存储扩展到很大的情况下,我们可以认为A被分成了a1和a2,外接SD卡还是B,a1 = 内部存储,a2+sd卡=外部存储。这样明白了吗?其实也可以认为a2就是被集成到了手机机身的另一张“SD卡”,划分的时候当然要把它算成外部存储了。

概念分清楚了,来说操作。因为内部存储其实不大,一般而言是不会将数据往里面写的,之前我做别的东西的时候,都是操作/storage/emulated/0/Android/data/packname/files 这个目录,那这个目录什么意思呢,简单的说/storage/emulated/0=a2,这样明白了吧。而后面/Android/data/packname/files这个目录是在你手机安装这个应用后会自动创建的,在你删除这个软件后也会自动删除,而获取这个路径的函数就是
getExternalFilesDir("").getAbsolutePath()。你看看External,外部外部!(虽然如果打印设备的描述系统显示的还是内部存储,可能是怕把用户弄混了,但是大家一定要记得a1和a2他们不是亲兄弟啊)

因为这次要操作的是外接的SD卡和U盘,我也是等到设备到了才开始弄。磨刀不误砍柴工,我们先捋清楚思路,首先先把文件管理系统管理的文件存放在SD卡中,然后实现可以导入到U盘和从U盘中导出到SD卡中,也就是数据的拷贝传递了。

那我当然要先获得外接的这张SD卡的路径了,还是那个问题a2和B才是亲兄弟。实际上你如果去获取外部设备的路径,你一定会把a2和B都拿到,可能做一些字符匹配也可以拿到B,但是也许就有点为了这个机身定制化了,然后我发现可以利用Java的反射机制去拿到android.os.storage.StorageVolume类以及对应的方法,这个类被隐藏了,通过反射可以获得所有存储空间也就是(storage Volume),然后我们再通过其中的一个参数 is_removable,来判定是否能够移除,a2都卖给a1了,a2怎么能走,只有B还能走。我就是通过这个条件来筛选出来B。

	private String getAppRootOfSdCardRemovable() {       
	     if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
	          return null;
	      }
	      StorageManager mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
	      Class<?> storageVolumeClazz = null;
	      try {
	          storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
	          Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
	          Method getPath = storageVolumeClazz.getMethod("getPath");
	          Method isRemovable = storageVolumeClazz.getMethod("isRemovable");
	          Object result = getVolumeList.invoke(mStorageManager);
	          final int length = Array.getLength(result);
	          for (int i = 0; i < length; i++) {
	              Object storageVolumeElement = Array.get(result, i);
	              String path = (String) getPath.invoke(storageVolumeElement);
	              if ((Boolean) isRemovable.invoke(storageVolumeElement)) {
	                  return path;
	              }
	          }
	      } catch (Exception e) {
	          e.printStackTrace();
	      }
	      return null;
	  }

通过这个方法,就能获取那个能移动的SD也就是B的路径了。

接下来,我要去获得U盘的路径了,依葫芦画瓢,就照着上面的思路,我们可以看到上面获得的存储空间是有数量的,那是不是如果加入U盘,length就会从2变成3?到这里就不得不吐槽一下,样机上只有一个OTG接口,插了U盘就不能插电脑,我抱着你就得放下手中的砖。真是让人欲哭无泪。只能用Toast来代替Log看看效果,不过不出所料,length变成3了,而且这个索引位置上的设备正是刚接入的U盘。

就这样也把U盘的路径获得了,本来还美滋滋的,然后就开始了长达一天的不知所措,也不知道为什么反正就是U盘一直没复制成功,但是Toast的路径都是正确的,然后就在百度和轮换拔插间度过了许久,后来发现FileUtils,文件工具类复制文件的逻辑是,不存在文件夹,创建文件夹,然后开始复制。其实中间还是有问题的,比如你要是没有创建文件夹的权限,这里是无法报错的,所以我通过是不是复制成功来判断是有漏洞的,使用网上的工具类固然方便,但是还是要多看看有没有隐患。

大概知道自己的app是没有权限去写入U盘的,我又想到那我有权限去读取吗,我就用做好的功能换成U盘的根目录,去读,果然是能读到的。然后查了一下Android各版本的权限问题。大抵就是8.0之后,失去了WRITE_MEDIA_STORAGE权限,那怎么弄呢,网上查了很多方法,不过很多是修改底层代码或是升级软件成系统软件的,卑微的我,不敢去做这种操作,最后的解决方法是

@Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ... 在你需要的地方发起权限请求
    if (DocumentsUtils.checkWritableRootPath(getActivity(), rootPath)) {
        showOpenDocumentTree();
    }
    // ... }


private void showOpenDocumentTree() {
    Intent intent = null;
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
			// getActivity() 是获得你context,也可以在Fragment中发起
        StorageManager sm = getActivity().getSystemService(StorageManager.class);
 			// rootPath 就是你的外设的根目录,前面那个方法返回的Path就行
        StorageVolume volume = sm.getStorageVolume(new File(rootPath));
 
   if (volume != null) {
        intent = volume.createAccessIntent(null);
    }
}
	// 这里如果没搜到U盘它就会跳转到打开最近文件的目录,也可以根据自己需要修改比如改成Toast显示未检测到设备。
	//当然要是没给intent赋值要直接return的不然走到最后一行会报错的。
	if (intent == null) {
	    intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
	}
    startActivityForResult(intent, DocumentsUtils.OPEN_DOCUMENT_TREE_CODE);
}
 
  @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case DocumentsUtils.OPEN_DOCUMENT_TREE_CODE:
            if (data != null && data.getData() != null) {
                Uri uri = data.getData();
                DocumentsUtils.saveTreeUri(getActivity(), rootPath, uri);
            }
            break;
        default:
            break;
    }

附上DocumentsUtils


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);
        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;
    }
 
    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 {
                    if (src.getParent().equals(dest.getParent())) {
                        res = srcDoc.renameTo(dest.getName());
                    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        res = DocumentsContract.moveDocument(context.getContentResolver(),
                                srcDoc.getUri(),
                                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;
    }
 
    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();
            return true;
        } else {
            Log.e(TAG, "no write permission: " + rootPath);
        }
        return false;
    }
 
    public static boolean checkWritableRootPath(Context context, String rootPath) {
        File root = new File(rootPath);
        if (!root.canWrite()) {
 
            if (DocumentsUtils.isOnExtSdCard(root, context)) {
                DocumentFile documentFile = DocumentsUtils.getDocumentFile(root, true, context);
                return documentFile == null || !documentFile.canWrite();
            } else {
                SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context);
 
                String documentUri = perf.getString(rootPath, "");
 
                if (documentUri == null || documentUri.isEmpty()) {
                    return true;
                } else {
                    DocumentFile file = DocumentFile.fromTreeUri(context, Uri.parse(documentUri));
                    return !(file != null && file.canWrite());
                }
            }
        }
        return false;
    }
}

在申请到权限后,我这边已经是有权限写入了,两天心酸泪终于搞定了。

感谢

很多很多在网上分享资源的人,附上几个收益匪浅的地址
https://blog.csdn.net/qq_36467463/article/details/88691726
https://blog.csdn.net/csdn_aiyang/article/details/80665185
https://blog.csdn.net/c529283955/article/details/104266083

发布了16 篇原创文章 · 获赞 4 · 访问量 320

猜你喜欢

转载自blog.csdn.net/qq_39197781/article/details/105629174