在Android 4.4(KitKat)上,Google限制了对SD卡的访问。
从Android Lollipop(5.0)开始,开发人员可以使用新的API要求用户确认是否允许访问特定文件夹,如此Google-Groups帖子中所述。
问题
该帖子指导您访问两个网站:
https://android.googlesource.com/platform/development/+/android-5.0.0_r2/samples/Vault/src/com/example/android/vault/VaultProvider.java #258
这看起来像一个内部示例(可能稍后在API演示中显示),但是很难理解发生了什么。
http://developer.android.com/reference/android/support/v4/provider/DocumentFile.html
这是新API的正式文档,但没有提供足够的详细信息如何使用它。
这是告诉您的内容:
如果确实需要完全访问整个docume子树, nts,
首先启动ACTION_OPEN_DOCUMENT_TREE,让用户选择一个
目录。然后将得到的getData()传递到fromTreeUri(Context,
Uri)以开始使用用户选择的树。
在浏览DocumentFile实例的树时,始终可以使用
getUri()获取表示该对象的基础文档的Uri,以便与openInputStream(Uri)等配合使用。
为了简化在运行KITKAT或更早版本的设备上的代码,您可以
使用fromFile(File)来模拟DocumentsProvider的行为。
问题
我对新API有一些疑问:
您如何真正使用它?
根据该帖子,操作系统将记住该应用已被授予访问文件/文件夹的权限。如何检查是否可以访问文件/文件夹?是否有一个函数可以向我返回我可以访问的文件/文件夹列表?
您如何在Kitkat上处理此问题?它是支持库的一部分吗?
操作系统上是否有一个设置屏幕,可以显示哪些应用程序可以访问哪些文件/文件夹?设备?
是否有关于此新API的其他文档/教程?
可以撤消权限吗?如果是这样,是否有意图发送给应用程序?
是否会在选定的文件夹上递归请求许可工作?
使用许可还可以使用户有机会通过以下方式进行多项选择:用户的选择?还是应用程序需要明确告知意图允许哪些文件/文件夹?
模拟器上是否有办法尝试新的API?我的意思是,它具有SD卡分区,但可以用作主要的外部存储,因此已经获得了对其的所有访问权限(使用简单的权限)。
当用户用另一张SD卡替换SD卡时会发生什么?
#1 楼
很多好问题,让我们深入探讨。:)如何使用它?
这是与KitKat中的存储访问框架进行交互的出色教程:
https://developer.android.com/guide/topics/providers/document-provider.html#client
与Lollipop中的新API交互非常相似。要提示用户选择目录树,可以启动这样的意图:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, 42);
然后在onActivityResult()中,可以传递用户选择的Uri到新的DocumentFile帮助器类。这是一个简单的示例,列出了所选目录中的文件,然后创建了一个新文件:
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (resultCode == RESULT_OK) {
Uri treeUri = resultData.getData();
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
// List all existing files inside picked directory
for (DocumentFile file : pickedDir.listFiles()) {
Log.d(TAG, "Found file " + file.getName() + " with size " + file.length());
}
// Create a new file and write into it
DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel");
OutputStream out = getContentResolver().openOutputStream(newFile.getUri());
out.write("A long time ago...".getBytes());
out.close();
}
}
DocumentFile.getUri()
返回的Uri足够灵活,可以与不同的文件一起使用平台API。例如,可以使用Intent.setData()
和Intent.FLAG_GRANT_READ_URI_PERMISSION
共享它。如果要从本机代码访问该Uri,可以调用
ContentResolver.openFileDescriptor()
,然后使用ParcelFileDescriptor.getFd()
或detachFd()
获得传统的POSIX文件描述符整数。 如何检查是否可以访问文件/文件夹?
默认情况下,通过Storage Access Frameworks意图返回的Uris在重新启动后不会持久存在。该平台“提供”持久权限的能力,但是如果需要,您仍然需要“获得”该权限。在上面的示例中,您将调用:
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
您总是可以找出持久性授予您的应用程序通过
ContentResolver.getPersistedUriPermissions()
API可以访问的内容。如果您不再需要访问保留的Uri,则可以使用ContentResolver.releasePersistableUriPermission()
释放它。可以在KitKat上使用吗?
不,我们无法追溯添加新功能到该平台的旧版本。
我可以查看哪些应用程序可以访问文件/文件夹吗?
当前没有显示此内容的用户界面,但您可以在中找到详细信息
adb shell dumpsys activity providers
输出的“授予的Uri权限”部分。如果在同一设备上为多个用户安装一个应用程序,会发生什么情况?
Uri权限授予是按用户隔离的,就像所有其他多用户平台功能一样。也就是说,在两个不同用户下运行的同一应用程序没有重叠或共享的Uri权限授予。
可以撤消权限吗?
支持DocumentProvider可以随时撤消权限时间,例如删除基于云的文档时。发现这些已撤销权限的最常见方法是,当它们从上述
ContentResolver.getPersistedUriPermissions()
中消失时。 >是否要在选定的文件夹上递归请求许可权?是允许多选吗?是的,自KitKat支持多选,您可以通过在启动
ACTION_OPEN_DOCUMENT_TREE
目的时设置EXTRA_ALLOW_MULTIPLE
来允许选择。您可以使用ACTION_OPEN_DOCUMENT
或Intent.setType()
缩小可以选择的文件类型:http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT
模拟器上是否有办法尝试新的API?
是的,主要的共享存储设备应该出现在选择器中,即使在模拟器上也是如此。如果您的应用仅使用Storage Access Framework来访问共享存储,则根本不再需要
EXTRA_MIME_TYPES
权限,可以删除它们或使用READ/WRITE_EXTERNAL_STORAGE
功能仅在较旧的平台版本上请求它们。用户用另一张SD卡替换SD卡会发生什么情况?
当涉及到物理介质时,基础介质的UUID(例如FAT序列号)总是被刻录到返回的Uri中。即使用户在多个插槽之间交换介质,系统也会使用它将您连接到用户最初选择的介质。
如果用户交换第二张卡,则需要提示访问新卡。由于系统会根据每个UUID记住赠款,因此如果用户稍后重新插入原始卡,您将继续拥有先前授予的访问权。
http://en.wikipedia.org / wiki / Volume_serial_number
评论
我懂了。因此,我们决定使用一种不同的API,而不是向(文件的)已知API添加更多。好。您能否回答其他问题(写在第一条评论中)?顺便说一句,谢谢您回答所有这一切。
– Android开发人员
2014年11月7日20:11
@JeffSharkey是否可以为OPEN_DOCUMENT_TREE提供起始位置提示?用户并非总是最擅长导航到应用程序需要访问的内容。
–贾斯汀
2014年12月2日,0:02
有没有办法更改上次修改的时间戳(File类中的setLastModified()方法)?对于诸如归档器之类的应用程序,这确实是基础。
–夸克
2014-12-27 17:56
假设您有一个存储的文件夹文档Uri,并且想要在设备重启后的某个时候在其中列出文件。无论您给它提供的Uri(甚至是子Uri),DocumentFile.fromTreeUri始终列出根文件,因此,如何创建一个可以调用列表文件的DocumentFile,而DocumentFile既不表示根文件也不表示SingleDocument。
– AndersC
2015年1月3日,19:24
@JeffSharkey,因为它接受字符串作为输出文件路径,所以如何在MediaMuxer中使用此URI? MediaMuxer(java.lang.String,int
– Petrakeas
2015年2月2日在23:24
#2 楼
在下面链接的我在Github的Android项目中,您可以找到可以在Android 5中的extSdCard上进行写入的工作代码。它假定用户可以访问整个SD卡,然后您可以在该卡上的任何位置进行写入。 (如果您只希望访问单个文件,则事情会变得更加容易。)主要代码片段
触发存储访问框架:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
处理来自Storage Access Framework的响应:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) {
Uri treeUri = null;
if (resultCode == Activity.RESULT_OK) {
// Get Uri from Storage Access Framework.
treeUri = resultData.getData();
// Persist URI in shared preference so that you can use it later.
// Use your own framework here instead of PreferenceUtil.
PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri);
// Persist access permissions.
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
}
}
}
通过Storage Access Framework获取文件的outputStream(利用存储的URL(假设这是外部SD卡的根文件夹的URL)。
DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
getContentResolver().openOutputStream(targetDocument.getUri());
它使用以下帮助方法: />
public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) {
String baseFolder = getExtSdCardFolder(file);
if (baseFolder == null) {
return null;
}
String relativePath = null;
try {
String fullPath = file.getCanonicalPath();
relativePath = fullPath.substring(baseFolder.length() + 1);
}
catch (IOException e) {
return null;
}
Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard);
if (treeUri == null) {
return null;
}
// start with root of SD card and then parse through document tree.
DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri);
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 String getExtSdCardFolder(final File file) {
String[] extSdPaths = getExtSdCardPaths();
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;
}
/**
* 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() {
List<String> paths = new ArrayList<>();
for (File file : Application.getAppContext().getExternalFilesDirs("external")) {
if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) {
int index = file.getAbsolutePath().lastIndexOf("/Android/data");
if (index < 0) {
Log.w(Application.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.
}
paths.add(path);
}
}
}
return paths.toArray(new String[paths.size()]);
}
/**
* Retrieve the application context.
*
* @return The (statically stored) application context
*/
public static Context getAppContext() {
return Application.mApplication.getApplicationContext();
}
参考完整代码
https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/ de / jeisfeld / augendiagnoselib / fragments / SettingsFragment.java#L521
和
https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/ src / main / java / de / jeisfeld / augendiagnoselib / util / imagefile / FileUtil.java
评论
您能否将其放在仅处理SD卡的最小化项目中?另外,您知道如何检查所有可用的外部存储设备是否也都可以访问,以便我不会索要它们的许可吗?
– Android开发人员
15年7月27日在19:01
希望我能对此再投票。我在此解决方案中途走了一半,发现文档导航太糟糕了,以至于我认为自己做错了。很高兴对此有所确认。感谢Google ...
–安东尼
15年8月17日在13:29
是的,要在外部SD上进行写入,您将无法再使用常规的File方法。另一方面,对于较旧的Android版本和主SD,文件仍然是最有效的方法。因此,您应该使用自定义实用程序类进行文件访问。
–约尔格·埃斯费尔德
15年10月23日在21:18
@JörgEisfeld:我有一个使用File 254次的应用程序。您能想象解决这个问题吗? Android完全缺乏向后兼容性,正成为开发人员的噩梦。我仍然找不到他们可以解释Google为什么对外部存储做出所有这些愚蠢决定的地方。有人声称“安全性”,但当然是胡说八道,因为任何应用程序都可能破坏内部存储。我的猜测是试图迫使我们使用他们的云服务。幸运的是,至少在Android <6 ...上,生根解决了问题。
–路易斯·弗洛里特(Luis A. Florit)
2015年12月20日13:49
好。神奇的是,我重新启动手机后,它可以工作了:)
– sancho21
16-04-17在16:07
#3 楼
这只是一个补充性的答案。创建新文件后,您可能需要将其位置保存到数据库中,明天再阅读。您可以使用以下方法再次读取并检索它:
/**
* Get {@link DocumentFile} object from SD card.
* @param directory SD card ID followed by directory name, for example {@code 6881-2249:Download/Archive},
* where ID for SD card is {@code 6881-2249}
* @param fileName for example {@code intel_haxm.zip}
* @return <code>null</code> if does not exist
*/
public static DocumentFile getExternalFile(Context context, String directory, String fileName){
Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/" + directory);
DocumentFile parent = DocumentFile.fromTreeUri(context, uri);
return parent != null ? parent.findFile(fileName) : null;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS && resultCode == RESULT_OK) {
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(data.getData(), takeFlags);
String sdcard = data.getDataString().replace("content://com.android.externalstorage.documents/tree/", "");
try {
sdcard = URLDecoder.decode(sdcard, "ISO-8859-1");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// for example, sdcardId results "6312-2234"
String sdcardId = sdcard.substring(0, sdcard.indexOf(':'));
// save to preferences if you want to use it later
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
preferences.edit().putString("sdcard", sdcardId).apply();
}
}
评论
FWIW,AndroidPolice今天对此发表了一篇文章。@fattire谢谢。但他们显示的是我已经读过的内容。不过,这是个好消息。
每当出现新的操作系统时,它们都会使我们的生活更加复杂...
@Funkystein是的。希望他们在Kitkat上做到了。现在有3种类型的行为可以处理。
@Funkystein我不知道。我很久以前用过。我认为进行这项检查并不坏。您还必须记住他们也是人类,所以他们可能会犯错并时时改变主意...