https://busy.im/post/android-sdcard-write/

最近升级到 Android 9.0 后,发现文件管理器在写入外置 SD 卡时出现了写入失败的问题,定位到 File.canWrite() 方法,发现返回了 false。经过讨论追踪定位,发现是由于 Google 的一个更改导致的:

diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index 04006b1..3021555 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -62,7 +62,6 @@<permission name="android.permission.WRITE_MEDIA_STORAGE" ><group gid="media_rw" />
-        <group gid="sdcard_rw" /></permission><permission name="android.permission.ACCESS_MTP" >
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index a0cb722..940d19f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -20936,9 +20936,6 @@if (Process.isIsolated(uid)) {return Zygote.MOUNT_EXTERNAL_NONE;}
-                if (checkUidPermission(WRITE_MEDIA_STORAGE, uid) == PERMISSION_GRANTED) {
-                    return Zygote.MOUNT_EXTERNAL_DEFAULT;
-                }if (checkUidPermission(READ_EXTERNAL_STORAGE, uid) == PERMISSION_DENIED) {return Zygote.MOUNT_EXTERNAL_DEFAULT;}

这里的修改移除了 WRITE_MEDIA_STORAGE 权限相关权限,导致了外部 SD 卡存储不可写的问题。

平台签名应用受影响

这个修改对系统应用影响较大,在 9.0 之前的平台,申请了 WRITE_MEDIA_STORAGE 的权限后,平台签名的应用就可以通过 java.io.File 接口写入外置 SD 卡了。但是这个修改之后,想要写入外置 SD 卡,就需要像第三方应用一样,使用 DocumentFile 的接口,可以阅读 API 文档 存储访问框架 和 使用作用域目录访问 。

参考 google 的这个 bug ,平台类的应用,如文件管理器、相机、图库甚至 MediaProvider 都会出现外置 SD 卡只能读不可写,即写入失败的问题,因为这些系统应用都没有适配 DocumentProvider 的写入方式。

DocumentFile 适配方案

1. 请求写入外置 SD 卡权限

早 在 Android 4.4,Android 就已经加入了存储访问框架,外置 SD 卡的访问由 DocumentsUI (com.android.documentsui) 提供支持,经过 5.0 版本的完善以及 7.0 的改进,目前有两种请求外置 SD 卡写入权限的交互方法:

  • Android 7.0 之前,使用 ACTION_OPEN_DOCUMENT_TREE 跳转到 DocumentsUI 的存储选择界面,之后用户手动打开外置存储并选择

  • Android 7.0 及之后,使用 StorageVolume.createAccessIntent(null) 跳转到权限写入提示框。(这个提示框也是 DocumentsUI 提供的,只是对之前的交互做了改进,避免繁琐的用户操作)

检查权限界面的属性,会发现这个权限提示框其实是 com.android.documentsui/com.android.documentsui.ScopedAccessActivity

也就是说 DocumentsUI 为了简化权限请求的流程,已经特意做了一个权限的提示框。

而 StorageVolume.createAccessIntent(String directoryName) 可以传入众多媒体类型,包括音乐、图片、电影、文档等,如果传入参数为 null ,则表示整个外置存储分区。

Parameters  
directoryName String: must be one of Environment.DIRECTORY_MUSIC, Environment.DIRECTORY_PODCASTS, Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MOVIES, Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_DCIM, or Environment.DIRECTORY_DOCUMENTS, or null to request access to the entire volume.
Returns  
Intent intent to request access, or null if the requested directory is invalid for that volume.

权限请求及处理

权限请求需要在 Activity 或者 Fragment 中发起,同时在 onActivityResult 中捕获返回的 Uri,这个 Uri 可以保存在本地存储中,方便再次调用。请求的代码封装如下:

@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) {StorageManager sm = getActivity().getSystemService(StorageManager.class);StorageVolume volume = sm.getStorageVolume(new File(rootPath));if (volume != null) {intent = volume.createAccessIntent(null);}}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;}
}

这里的 rootPath 是上下文中传入的外置 sd 卡根目录,如 /storage/0000-0000 这样的路径,可以通过 context.getExternalFilesDirs("external") 方法获取到。DocumentsUtils 工具类的实现方法见下文。

其中 DocumentsUtils.checkWritableRootPath() 方法用来检查 SD 卡根目录是否有写入权限,如果没有则跳转到权限请求;DocumentsUtils.saveTreeUri() 方法保存返回的 Uri 信息到本地存储,以便之后查询。

2. DocumentFile 文件操作封装

由于之前应用使用了 java.io.File 接口操作外置 SD 卡文件,期望对代码的修改量最小,则最好的方式是对已有的 File 操作再做一次封装。

由于 Android 9.0 之前系统应用默认是可以通过 java.io.File 接口写入外置 SD卡 的,而如果作为公开市场第三方应用却在 4.4 之后就不可写,而且有的厂商定制版本 Android 9.0 外置 SD 卡也是可以直接写入而不需要 DocumentFile 接口,DocumentFile 接口也没有 java.io.File 效率高。

所以最好的办法是先检查是否有文件写入权限,如果有写入权限,则直接使用 File 接口操作,如果没有权限再检查文件是否在外置 SD 卡,如果文件在 SD 卡则使用 DocumentFile 接口操作。

封装的工具类 DocumentsUtils 方法说明,不兼容 表示没有封装 DocumentFile 操作:

DocumentsUtils 公共方法 功能描述
void cleanCache() 清除路径缓存,建议插拔 sd 卡后调用
boolean isOnExtSdCard(File file, Context c) 文件路径是否在外置 SD 卡上
DocumentFile getDocumentFile(final File file, final boolean isDirectory, Context context) 从 File 转到 DocumentFile
boolean mkdirs(Context context, File dir) 创建文件夹
boolean delete(Context context, File file) 删除文件
boolean canWrite(File file) File 文件是否可写(如果文件不存在,则尝试创建文件再删除检查写入权限)不兼容
boolean canWrite(Context context, File file) 文件是否可写
boolean renameTo(Context context, File src, File dest) 文件重命名
boolean saveTreeUri(Context context, String rootPath, Uri uri) 保存 path 和 uri 到本地存储
boolean checkWritableRootPath(Context context, String rootPath) 检查路径是否可写,不可写返回 true
InputStream getInputStream(Context context, File destFile) 获取 InputStream,可用于读操作
OutputStream getOutputStream(Context context, File destFile) 获取 OutputStream,可用于写操作

封装的工具类 DocumentsUtils.java 内容如下:

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;}
}

参考

Media process should run with “write” access.

[Developer Preview Android P]WRITE_MEDIA_STORAGE is not working for system apps to access the secondary storage.

AmazeFileManager/FileUtil.java

Android 外置 SD 卡写入权限问题相关推荐

  1. Android P 外置 SD 卡写入权限问题

    概述 Android 9.0 后,发现文件管理器在写入外置 SD 卡时出现了写入失败的问题,定位到 File.canWrite() 方法,发现返回了 false.经过讨论追踪定位,发现是由于 Goog ...

  2. Android Studio SD卡访问权限及asserts文件夹下的文件操作

    Android Studio SD卡访问权限 1.在 AndroidManifext.xml 中添加如下代码 <uses-permission android:name="androi ...

  3. 外置存储权限在哪打开_安卓手机外置sd卡的权限怎么打开?

    展开全部 在2.x的版本中,在manifest中配e69da5e887aa3231313335323631343130323136353331333365633962置的权限android.permi ...

  4. Android向SD卡写入文件

    1.检查是否有读写sdcard的权限 (1)首先要在AndroidManifest.xml加入 <uses-permission android:name="android.permi ...

  5. android 外置sd卡,安卓手机,让你的外置sd卡瞬间变成内置sd卡!

    喜欢玩游戏的都知道,一个大型游戏的数据包少说几百M,多则1g以上,再加上机子里必须装在内置sd里的一些软件,几个游戏一装内存基本上就满了, 而且最令人头疼的是安卓的机子目前只能默认识别机子自身自带的内 ...

  6. Android对外置sd卡的权限问题(上)

    作者:许勇权 在调查图库中关于在内外存置卡之间移动/复制操作时,写了一个小程序测试在内外存储卡操作的可行性和性能问题,发现第三方应用无法访问外置存储卡. 调查后得知,在2.x的版本中,android手 ...

  7. android 读写sd卡的权限设置

    在Android中,要模拟SD卡,要首先使用adb的mksdcard命令来建立SD卡的镜像,如何建立,大家上网查一下吧,应该很容易找到,这里不说这个问题. 但是在应用程序执行起来以后,我们可以看到sd ...

  8. android访问SD卡的权限

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses ...

  9. saf java_[原创]Android Storage Access Framework(SAF)框架实现外置SD卡的写入(JAVA层与JNI层HOOK)...

    1. 前言 之前折腾了了一下MINE模拟器,发现SDL全是在JNI层fopen操作的,而安卓的SAF则是JAVA层通过DocumentFile和docUri来实现写入的.一种方法是通过去的File D ...

最新文章

  1. 全文翻译(全文合集):TVM: An Automated End-to-End Optimizing Compiler for Deep Learning
  2. 三菱工业机器人rv6s_FANUC机器人控制器—维护三要素
  3. 基于.Net Core开发现代化Web应用程序系列课程和文章
  4. italic与oblique的区别
  5. OpenCV--图像内轮阔填充
  6. C# 利用类名字符串调用并执行类方法
  7. 【kafka】Apache Kafka 中的事务
  8. 简直要吐槽!!enable-migrations fails on x64 Projects
  9. pandas作图_pandas绘图
  10. 【每日算法Day 91】求解数组中出现次数超过1/3的那个数
  11. 台式计算机显示器的分辨率,台式电脑分辨率多少合适,测试电脑分辨率
  12. 【推荐五款ssh连接工具】
  13. 网络小说海外“走红”的启示
  14. 小程序todolist
  15. Pisces的属性配置文件加载
  16. 在3ds max中,什么是PBR材质?
  17. 怎么把win10退回win7系统
  18. java造成capturing lambda后需要注意的事情
  19. ETH2.0 Serenity中网络的详细介绍
  20. 真是没有预料到,一款推送全国公考信息的app开发用了一年时间

热门文章

  1. 手机电脑都能用,将照片转成PDF的免费方法
  2. VC程序里判断系统是64位还是32位的正确方法
  3. 集成支付宝,跳转到支付宝后显示的不是支付页面
  4. 通过计数器完成工厂可视化看板的开发
  5. 梅科尔工作室--梁嘉莹-鸿蒙笔记3
  6. bellman算法流程
  7. 【高效复习】计算机网络重要概念总结
  8. 微型计算机接口课程设计报告,《微机接口技术》课程设计报告(范文).doc
  9. 全键盘模式,目前按center key 和LSK时候会进入menu 菜单,期望按center键进入编辑
  10. 开发一个App来为你的女神“化妆”!