因為最近項目需要,涉及到 SD卡 的讀寫操作,然而申請
<!-- 讀寫權限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
權限只能對 SD卡 進行讀操作,而沒有寫權限,也就是說,Android 在某個版本中對 SD卡 的讀寫權限進行了限制。后在 StackoverFlow 上找到一篇相關問答,解了心中疑惑。在此,對該問答進行翻譯並附上相關 Demo,已做備忘。
原文地址:How to use the new SD card access API presented for Android 5.0 (Lollipop)?
問
背景
在 Android 4.4(KitKat) 中,Google 對 SD卡 的訪問已經做了嚴格的限制。
在 Android 5.0(Lollipop) 中,開發者可以使用 新API 要求用戶對某個指定的文件夾進行訪問授權,詳見:Android 4.4 Samsung Galaxy s4 external sd card is now read only, Remove or option to edit non app files.(譯者注:開頭挺搞笑的,都是開發者吐槽 Google 對 SD卡 做了限制)
問題
上述文章中有兩個鏈接:
-
此鏈接中代碼看起來更像是內部示例(可能會在以后的 API Demo 中出現),但是真的很難理解這部分代碼的意圖。
-
http://developer.android.com/reference/android/support/v4/provider/DocumentFile.html
這是 新API 的官方文檔,但是並沒有多少如何使用的細節。(譯者注:這份文檔其實還是有很多內容的,后面會具體細講。至於為什么會有這種差別,可能作者提問時,該文檔尚未完善吧~)
If you really do need full access to an entire subtree of documents, start by launching ACTION_OPEN_DOCUMENT_TREE to let the user pick a directory. Then pass the resulting getData() into fromTreeUri(Context, Uri) to start working with the user selected tree.
As you navigate the tree of DocumentFile instances, you can always use getUri() to obtain the Uri representing the underlying document for that object, for use with openInputStream(Uri), etc.
To simplify your code on devices running KITKAT or earlier, you can use fromFile(File) which emulates the behavior of a DocumentsProvider.
對於新 API 我有以下問題:
- 新 API 的正確使用方式?
- 根據文檔,系統會記錄 app 被授予訪問權限的文件和文件夾。那么,我該如何檢測我對某個文件或者文件夾是否有訪問權限?是否有方法獲取可訪問的文件或文件夾列表呢?
- 在 Android 4.4 上如何處理這個問題?Support Library 是否包含相應的解決方案
- 系統中是否有對應的界面可以查看哪些 App 可以訪問哪些文件。
- 在多用戶的設備上授權該如何處理?
- 是否有其它關於新 API 的文檔?
- 對 SD卡 的授權是否可以被取消?如果是,那對應的意圖是什么?
- 對於文件夾授權是否是遞歸授權?指代文件夾內還嵌套有文件夾。
- SD 授權是否支持多選?或該應用程序需要專門告訴意圖要允許的文件/文件夾嗎?
- 模擬器可以測試新 API 嘛?我的意思是,模擬器具有 SD 卡的分區,但它的作用是主要的外部存儲,簡單使用
android.permission.WRITE_EXTERNAL_STORAGE
是否足夠? - 當用戶替換 SD卡 是會發生什么?
來自 Jeff Sharkey 的回答
這些問題問的都非常好,讓我們來深入挖掘下
如何使用新的 API
在 Kitkat 中有一份非常好的關於與 Storage Access Framework
交互的文檔:Document provider.
新 API 的使用與之很相似。通過發送以下 Intent ,讓用戶在文檔樹(Directory Tree
)中選擇授權目錄。
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 搭配使用。例如,你可以通過 Inetnt.setData()
將 Uri 分享出去,不過得將 Intent 的 flag 設置為 Intent.FLAG_GRANT_READ_URI_PERMISSION
。
如何檢測是否對某個文件/文件夾有訪問權限
默認情況下,通過 Storage Access Framework
獲取的 Uri 授權並不是永久的,設備重啟后就會消失。不過,系統提供了相關的接口讓授權永久化,如果需要的話可自行設置。在上述代碼,你可以如此設置:
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
之后,你就可以通過 ContentResolver.getPersistedUriPermissions()
來獲取 APP 已經被永久授予權限的 Uri。如果不在需要某個 Uri 的權限,可以通過 ContentResolver.releasePersistableUriPermission()
來釋放。
能否在 Kitkat 中使用
不能,因為該 API 是在 Lollipop 中添加的
能否知道有哪些 APP 擁有該權限
能。但是目前是沒有 UI 界面的,你得通過 adb shell dumpsys activity providers
來獲取。
在多用戶的設備上授權該如何處理?
與多用戶系統的其它功能一樣,Uri 授權也是用戶獨立的。因此,同一個 APP 的 Uri 授權對每個用戶是透明的。
授權是否可以被取消?
DocumentProvider 支持隨時撤銷授權。取消授權最常見的方法就是通過上面提到 ContentResolver.releasePersistableUriPermission()
。
當清除應用的數據時,應用相關的授權也都會被清除。
對於文件夾授權是否是遞歸授權的?
是的,通過 ACTION_OPEN_DOCUMENT_TREE
的 Intent 獲取到授權之后,對該 Uri 下的所有文件都有讀寫權限。
授權是否支持多選操作?
從 Android 4.4(Kitkat) 起就支持了。您可以在啟動 ACTION_OPEN_DOCUMENT
Intent 時通過設置 EXTRA_ALLOW_MULTIPLE
來實現。您可以通過使用 Intent.setType()
或者 EXTRA_MIME_TYPES
來設置可選文件類型。具體參考:ACTION_OPEN_DOCUMENT
是否可以在模擬器上嘗試新 API
可以的。如果你的 APP 只使用 Storage Access Framework
訪問共享存儲,你甚至不再需要 READ/WRITE_EXTERNAL_STORAGE
權限或者使用 android:maxSdkVersion
在較舊的版本上使用它們。
當用戶替換 SD卡 時會發生什么?
當涉及物理介質時,底層媒體的 UUID(例如FAT序列號)總是被燒錄到返回的 Uri 中。The system uses this to connect you to the media that the user originally selected, even if the user swaps the media around between multiple slots.(翻譯不了)
如果用戶替換了新的 SD卡,您需要重新申請 SD卡 授權。 由於系統會記住基於每個UUID的授權,如果用戶以后重新插入,您將繼續先前授予對原始卡的訪問權限。
參考:磁盤序列號