在Android 4.4(KitKat)上,Google已严格限制访问SD卡。
从Android Lollipop(5.0)开始,开发人员可以使用新的API,要求用户确认允许访问特定文件夹,如this Google-Groups post所示。
该帖子指示您访问两个网站:
这看起来像是一个内部示例(可能稍后会在API演示中显示),但很难理解发生了什么。
这是新API的官方文档,但它没有详细说明如何使用它。
这是它告诉你的:
如果您确实需要完全访问整个文档子树, 首先启动ACTION_OPEN_DOCUMENT_TREE让用户选择一个 目录。然后将生成的getData()传递给fromTreeUri(Context, Uri)开始使用用户选择的树。
在导航DocumentFile实例树时,您始终可以使用 getUri()获取表示底层文档的Uri 该对象,用于openInputStream(Uri)等。
要在运行KITKAT或更早版本的设备上简化代码,您可以 使用fromFile(File)模拟DocumentsProvider的行为。
我对新API有几个问题:
答案 0 :(得分:134)
很多好问题,让我们深入研究。:)
这是一个与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文件描述符整数。
默认情况下,通过存储访问框架返回的Uris意图在重新启动后 保持不变。该平台“提供”持久许可的能力,但如果您需要,您仍需要“获取”权限。在上面的示例中,您将调用:
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
您始终可以通过ContentResolver.getPersistedUriPermissions()
API找出您的应用有权访问的内容。如果您不再需要访问持久性Uri,可以使用ContentResolver.releasePersistableUriPermission()
释放它。
不,我们无法追溯性地向平台的旧版本添加新功能。
目前没有显示此功能的用户界面,但您可以在adb shell dumpsys activity providers
输出的“授予的Uri权限”部分找到详细信息。
Uri权限授予是基于每个用户隔离的,就像所有其他多用户平台功能一样。也就是说,在两个不同用户下运行的相同应用程序没有重叠或共享的Uri权限授予。
支持DocumentProvider可以随时撤消权限,例如删除基于云的文档时。发现这些已撤销权限的最常见方式是它们从上述ContentResolver.getPersistedUriPermissions()
中消失。
每当为授权中涉及的任一应用清除应用数据时,权限也会被撤销。
是的,ACTION_OPEN_DOCUMENT_TREE
意图使您可以递归访问现有和新创建的文件和目录。
是的,自KitKat以来一直支持多项选择,您可以在启动EXTRA_ALLOW_MULTIPLE
意图时设置ACTION_OPEN_DOCUMENT
来允许它。您可以使用Intent.setType()
或EXTRA_MIME_TYPES
来缩小可以选择的文件类型:
http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT
是的,主要的共享存储设备应该出现在选择器中,即使在模拟器上也是如此。如果您的应用仅使用存储访问框架来访问共享存储,则您不再需要READ/WRITE_EXTERNAL_STORAGE
权限 并可以将其删除或使用android:maxSdkVersion
功能仅请求它们在较旧的平台版本上。
当涉及物理媒体时,底层媒体的UUID(例如FAT序列号)总是被刻录到返回的Uri中。系统使用此功能将您连接到用户最初选择的媒体,即使用户在多个插槽之间交换媒体也是如此。
如果用户换入第二张卡,则需要提示您访问新卡。由于系统会以每UUID为基础记住授权,因此如果用户稍后重新插入,您将继续获得以前授予的对原始卡的访问权限。
答案 1 :(得分:40)
在下面链接的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();
}
和
答案 2 :(得分:0)
这只是一个补充答案。
创建新文件后,您可能需要将其位置保存到数据库中并明天读取。您可以使用以下方法再次检索它:
/**
* 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();
}
}