日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

Android 外置 SD 卡写入权限问题

發布時間:2024/3/13 Android 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android 外置 SD 卡写入权限问题 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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?
directoryNameString: 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?
Intentintent 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 卡写入权限问题的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。