Android4.4之后的外置SD卡文件读写的解决方法

原因

在Android4.4之后,普通应用就没有外置SD卡的写权限了,对于要操作外置SD的应用来说就是个灾难了。
我最近做的功能是要对视频和图片进行加锁,无法写就无法锁了。怎么解决呢?先百度Google大家都在说这个问题,但没有找到好的解决办法,然后我就去看看其它应用怎么做的。找几个需要控制SD卡的应用,ES文件浏览器。
在写外置SD卡文件时会弹出这样一个界面:
Alt text
点击选择进入系统的一个文件目录界面:
Alt text

点击“选择SD”卡,回来就可以对文件进行操作了。

探索

然后我就去反编译看看它是怎么操作的。我反编译了另一个文件管理器的应用,和这个也是差不多,但代码混淆较少,可以看到很多细节。
用ApkTool反编译找到Dialog的提示语,再到代码找到Dialog界面。ApkTool得到的代码比较乱,就用的dex2jar反编译得到的代码,保存用Android Studio打开看。

看到点击Dialog确实后的操作是:
this.val$context.startActivityForResult(new Intent(“android.intent.action.OPEN_DOCUMENT_TREE”), this.val$requestCode);

这个Intent就是进入文件目录,再找回来在OnActivityResult的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case SDUtils.REQ_PICK_SDCARD_PATH /*8712*/:
if (resultCode == RESULT_OK) {
Uri treeUri = data.getData();
if (!":".equals(treeUri.getPath().substring(treeUri.getPath().length() - 1)) || treeUri.getPath().contains("primary")) {
SDUtils sdUtils = new SDUtils();
sdUtils.getSdCardUriDialog(SelectVideoActivity.this);
} else {
Logger.get().e("RESULT_OK");
SDUtils.setSdCardUriPreferences(SelectVideoActivity.this, treeUri.toString());
lockVideo();
}
}
}
super.onActivityResult(requestCode, resultCode, data);
}

这开始就是贴的是我改完后的自己代码了,清楚点,都是差不多意思。回来成功选择后,先判断是不是SD卡,不是就继续弹Dialog,是就保存treeUri,就是外置SD的文档Uri。
这个Uri和普通Uri是不一样的,打印出来是这样的:
content://com.android.externalstorage.documents/tree/6635-3265%3A
中间有个tree。

再去找文件操作时是怎么样的,发现外置SD卡都是通过DocumentFile进行文件写的。先根据之前保存的SD卡uri获取根DocumentFile,在一层一层获取子DocumentFile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static DocumentFile getDocumentFilePath(Context context, String path, boolean createDirectories) {
DocumentFile document = DocumentFile.fromTreeUri(context, Uri.parse(SDUtils.getSdCardUriPreferences(context)));
Logger.get().e( document.getName()+":"+SDUtils.getSdCardUriPreferences(context));

String[] parts = path.split(ZIP_FILE_SEPARATOR);
for (int i = 3; i < parts.length; i++) {
DocumentFile nextDocument = document.findFile(parts[i]);
if (nextDocument == null) {
if (i < parts.length - 1) {
if (createDirectories) {
nextDocument = document.createDirectory(parts[i]);
} else {
return null;
}
} else {
nextDocument = document.createFile("image", parts[i]);
}
}
document = nextDocument;
}
return document;
}

path是要获取文件的路径,createDirectories是是否要新建,false用于获取DocumentFile执行删除操作,true用于新建文件。遍历不从0开始是因为path的是绝对路径,前面SD卡的路径不需要,从SD卡的第一层目录开始遍历。
有了思路就可以更好的找方法了,再在google搜DocumentFile,SD卡相关的,在stackOverFlow找到了这个:
http://stackoverflow.com/questions/26744842/how-to-use-the-new-sd-card-access-api-presented-for-lollipop
改写了其提供的一些方法,下面的copyFile就是。target可写就直接写,android5通过DucumentFile获取outStream,4.4由contentResolve通过普通Uri获取到输出流。然后写到输出流中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public static boolean copyFile(Context context, final File source, final File target) {
FileInputStream inStream = null;
OutputStream outStream = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inStream = new FileInputStream(source);
if (isWritable(target)) {
// standard way
outStream = new FileOutputStream(target);
inChannel = inStream.getChannel();
outChannel = ((FileOutputStream) outStream).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
outStream = null;
} else if (isAndroid5()) {
// Storage Access Framework
DocumentFile targetDocument = getDocumentFilePath(context, target.getAbsolutePath(), true);
if (targetDocument != null) {
outStream = context.getContentResolver().openOutputStream(targetDocument.getUri());
}
} else if (isKitkat()) {
// Workaround for Kitkat ext SD card
Uri uri = getUriFromFile(context, target.getAbsolutePath());
outStream = context.getContentResolver().openOutputStream(uri);
} else {
return false;
}

if (outStream != null) {
// Both for SAF and for Kitkat, write to output stream.
byte[] buffer = new byte[4096]; // MAGIC_NUMBER
int bytesRead;
while ((bytesRead = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, bytesRead);
}
}

} catch (Exception e) {
Logger.get().e("Error when copying file from " + source.getAbsolutePath() + " to " + target.getAbsolutePath(), e);
return false;
} finally {
try {
inStream.close();
} catch (Exception e) {
// ignore exception
}
try {
outStream.close();
} catch (Exception e) {
// ignore exception
}
try {
inChannel.close();
} catch (Exception e) {
// ignore exception
}
try {
outChannel.close();
} catch (Exception e) {
// ignore exception
}
}
return true;
}

总结

其实就是通过DocumentFile的方法实现对外置SD卡文件的操作,关键是步骤获取TreeUri,通过DocumentFile.fromTreeUri方法获取到想要找的文件树对象,从而实现文件操作。