[英]How to use the new SD card access API presented for Android 5.0 (Lollipop)?
在Android 4.4 (KitKat) 上,Google 對 SD 卡的訪問非常受限。
從 Android Lollipop (5.0) 開始,開發人員可以使用新的 API,要求用戶確認允許訪問特定文件夾,如這篇 Google-Groups 帖子中所述。
該帖子將指導您訪問兩個網站:
這看起來像是一個內部示例(也許稍后會在 API 演示中展示),但很難理解發生了什么。
這是新 API 的官方文檔,但沒有詳細說明如何使用它。
這是它告訴你的:
如果您確實需要對整個文檔子樹的完全訪問權限,請首先啟動 ACTION_OPEN_DOCUMENT_TREE 以讓用戶選擇一個目錄。 然后將結果 getData() 傳遞到 fromTreeUri(Context, Uri) 以開始處理用戶選擇的樹。
當您瀏覽 DocumentFile 實例的樹時,您始終可以使用 getUri() 來獲取表示該對象的底層文檔的 Uri,以便與 openInputStream(Uri) 等一起使用。
為了在運行 KITKAT 或更早版本的設備上簡化您的代碼,您可以使用 fromFile(File) 來模擬 DocumentsProvider 的行為。
我有幾個關於新 API 的問題:
很多好問題,讓我們深入研究。:)
這是一個很好的教程,用於在 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()
釋放它。
不,我們不能向舊版本的平台追溯添加新功能。
目前沒有顯示此內容的 UI,但您可以在adb shell dumpsys activity providers
輸出的“授予的 Uri 權限”部分中找到詳細信息。
Uri 權限授予是基於每個用戶的,就像所有其他多用戶平台功能一樣。 也就是說,在兩個不同用戶下運行的同一個應用程序沒有重疊或共享的 Uri 權限授予。
支持 DocumentProvider 可以隨時撤銷權限,例如刪除基於雲的文檔時。 發現這些撤銷權限的最常見方法是當它們從上面提到的ContentResolver.getPersistedUriPermissions()
中消失時。
每當清除涉及授權的任一應用程序的應用程序數據時,權限也會被撤銷。
是的, ACTION_OPEN_DOCUMENT_TREE
意圖使您可以遞歸訪問現有和新創建的文件和目錄。
是的,從 KitKat 開始支持多選,你可以通過在啟動ACTION_OPEN_DOCUMENT
意圖時設置EXTRA_ALLOW_MULTIPLE
來允許它。 您可以使用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 記住授權,因此如果用戶稍后重新插入,您將繼續擁有先前授予的對原始卡的訪問權限。
在下面鏈接的 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);
}
處理來自存儲訪問框架的響應:
@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);
}
}
}
通過存儲訪問框架獲取文件的 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();
}
和
SimpleStorage通過跨 API 級別簡化存儲訪問框架來幫助您。 它也適用於范圍存儲。 例如:
val fileFromExternalStorage = DocumentFileCompat.fromSimplePath(context, basePath = "Downloads/MyMovie.mp4")
val fileFromSdCard = DocumentFileCompat.fromSimplePath(context, storageId = "9016-4EF8", basePath = "Downloads/MyMovie.mp4")
使用這個庫可以更簡單地授予 SD 卡的 URI 權限、選擇文件和文件夾:
class MainActivity : AppCompatActivity() {
private lateinit var storageHelper: SimpleStorageHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
storageHelper = SimpleStorageHelper(this, savedInstanceState)
storageHelper.onFolderSelected = { requestCode, folder ->
// do stuff
}
storageHelper.onFileSelected = { requestCode, file ->
// do stuff
}
btnRequestStorageAccess.setOnClickListener { storageHelper.requestStorageAccess() }
btnOpenFolderPicker.setOnClickListener { storageHelper.openFolderPicker() }
btnOpenFilePicker.setOnClickListener { storageHelper.openFilePicker() }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
storageHelper.storage.onActivityResult(requestCode, resultCode, data)
}
override fun onSaveInstanceState(outState: Bundle) {
storageHelper.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
storageHelper.onRestoreInstanceState(savedInstanceState)
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.