簡體   English   中英

如何使用為 Android 5.0 (Lollipop) 提供的新 SD 卡訪問 API?

[英]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 的問題:

  1. 你如何真正使用它?
  2. 根據帖子,操作系統會記住該應用程序被授予訪問文件/文件夾的權限。 您如何檢查是否可以訪問文件/文件夾? 是否有一個函數可以返回我可以訪問的文件/文件夾列表?
  3. 你如何處理 Kitkat 上的這個問題? 它是支持庫的一部分嗎?
  4. 操作系統上是否有設置屏幕顯示哪些應用程序可以訪問哪些文件/文件夾?
  5. 如果在同一設備上為多個用戶安裝了一個應用程序,會發生什么情況?
  6. 是否有關於這個新 API 的任何其他文檔/教程?
  7. 可以撤銷權限嗎? 如果是這樣,是否有發送到應用程序的意圖?
  8. 會在選定的文件夾上遞歸地請求權限嗎?
  9. 使用該權限是否還允許用戶通過用戶選擇進行多項選擇? 或者應用程序是否需要專門告訴意圖允許哪些文件/文件夾?
  10. 模擬器上有沒有辦法嘗試新的 API? 我的意思是,它有 SD 卡分區,但它作為主要的外部存儲工作,因此已經授予了對它的所有訪問權限(使用簡單的權限)。
  11. 當用戶將 SD 卡更換為另一張 SD 卡時會發生什么?

很多好問題,讓我們深入研究。:)

你如何使用它?

這是一個很好的教程,用於在 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()釋放它。

這在 KitKat 上可用嗎?

不,我們不能向舊版本的平台追溯添加新功能。

我可以查看哪些應用可以訪問文件/文件夾嗎?

目前沒有顯示此內容的 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

模擬器上有沒有辦法嘗試新的 API?

是的,主共享存儲設備應該出現在選擇器中,即使是在模擬器上。 如果您的應用僅使用存儲訪問框架來訪問共享存儲,則您根本不再需要READ/WRITE_EXTERNAL_STORAGE權限可以刪除它們或使用android:maxSdkVersion功能僅在舊平台版本上請求它們。

當用戶用另一張 SD 卡替換 SD 卡時會發生什么?

當涉及物理介質時,底層介質的UUID(如FAT序列號)總是被燒入返回的Uri中。 系統使用它來將您連接到用戶最初選擇的媒體,即使用戶在多個插槽之間交換媒體。

如果用戶換入第二張卡,您需要提示獲取新卡的訪問權限。 由於系統會根據每個 UUID 記住授權,因此如果用戶稍后重新插入,您將繼續擁有先前授予的對原始卡的訪問權限。

http://en.wikipedia.org/wiki/Volume_serial_number

在下面鏈接的 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();
}

完整代碼參考

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/SettingsFragment.java#L521

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM