在开发一个文件管理系统的路上,总有坑在等着你。
前情提示:因为该系统的使用方需要严格保密文件,导致它失去了无线传输功能,只能通过外设传输文件。
在没接触这个功能之前,我想大家应该都觉得手机自带的存储空间叫内部存储,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