Làm cách nào để sử dụng API truy cập thẻ SD mới được trình bày cho Android 5.0 (Lollipop)?


118

Lý lịch

Trên Android 4.4 (KitKat), Google đã hạn chế quyền truy cập vào thẻ SD.

Kể từ Android Lollipop (5.0), các nhà phát triển có thể sử dụng một API mới yêu cầu người dùng xác nhận để cho phép truy cập vào các thư mục cụ thể, như được viết trên bài đăng trên Google-Groups này .

Vấn đề

Bài viết hướng bạn đến hai trang web:

Điều này trông giống như một ví dụ bên trong (có lẽ sẽ được hiển thị trên các bản trình diễn API sau này), nhưng khá khó để hiểu điều gì đang xảy ra.

Đây là tài liệu chính thức của API mới, nhưng nó không cho biết đủ chi tiết về cách sử dụng nó.

Đây là những gì nó cho bạn biết:

Nếu bạn thực sự cần toàn quyền truy cập vào toàn bộ cây con của tài liệu, hãy bắt đầu bằng cách khởi chạy ACTION_OPEN_DOCUMENT_TREE để cho phép người dùng chọn một thư mục. Sau đó chuyển getData () kết quả vào fromTreeUri (Context, Uri) để bắt đầu làm việc với cây do người dùng chọn.

Khi bạn điều hướng cây các thể hiện DocumentFile, bạn luôn có thể sử dụng getUri () để lấy Uri đại diện cho tài liệu cơ bản cho đối tượng đó, để sử dụng với openInputStream (Uri), v.v.

Để đơn giản hóa mã của bạn trên các thiết bị chạy KITKAT hoặc phiên bản cũ hơn, bạn có thể sử dụng fromFile (Tệp) để mô phỏng hành vi của DocumentsProvider.

Những câu hỏi

Tôi có một số câu hỏi về API mới:

  1. Bạn thực sự sử dụng nó như thế nào?
  2. Theo bài đăng, hệ điều hành sẽ nhớ rằng ứng dụng đã được cấp quyền truy cập các tệp / thư mục. Làm cách nào để kiểm tra xem bạn có thể truy cập các tệp / thư mục hay không? Có chức năng nào trả về cho tôi danh sách các tệp / thư mục mà tôi có thể truy cập không?
  3. Bạn xử lý vấn đề này trên Kitkat như thế nào? Nó có phải là một phần của thư viện hỗ trợ không?
  4. Có màn hình cài đặt trên hệ điều hành cho biết ứng dụng nào có quyền truy cập vào tệp / thư mục nào không?
  5. Điều gì xảy ra nếu một ứng dụng được cài đặt cho nhiều người dùng trên cùng một thiết bị?
  6. Có tài liệu / hướng dẫn nào khác về API mới này không?
  7. Quyền có thể bị thu hồi không? Nếu vậy, có một ý định nào đó đang được gửi đến ứng dụng không?
  8. Yêu cầu quyền có hoạt động đệ quy trên một thư mục đã chọn không?
  9. Việc sử dụng quyền cũng cho phép người dùng có cơ hội chọn nhiều lựa chọn theo lựa chọn của người dùng? Hay ứng dụng cần cho biết cụ thể mục đích cho phép tệp / thư mục nào?
  10. Có cách nào trên trình giả lập để thử API mới không? Ý tôi là, nó có phân vùng thẻ SD, nhưng nó hoạt động như bộ nhớ ngoài chính, vì vậy tất cả quyền truy cập vào nó đều đã được cấp (sử dụng một quyền đơn giản).
  11. Điều gì xảy ra khi người dùng thay thế thẻ SD bằng một thẻ khác?

FWIW, AndroidPolice đã có một bài viết nhỏ về điều này ngày hôm nay.
fattire

@fattire Cảm ơn bạn. nhưng họ đang hiển thị về những gì tôi đã đọc. Đó là một tin tốt.
nhà phát triển android

32
Mỗi lần một hệ điều hành mới đi ra, họ làm cho cuộc sống của chúng ta phức tạp hơn ...
Phantômaxx

@Funkystein đúng. Ước gì họ làm được điều đó trên Kitkat. Bây giờ có 3 loại hành vi cần xử lý.
nhà phát triển android

1
@Funkystein Tôi không biết. Tôi đã sử dụng nó một thời gian dài trước đây. Tôi nghĩ việc kiểm tra này không tệ lắm. Bạn phải nhớ họ là những con người quá, vì vậy họ có thể mắc sai lầm và thay đổi suy nghĩ của họ bất cứ lúc nào ...
android phát triển

Câu trả lời:


143

Rất nhiều câu hỏi hay, hãy cùng tìm hiểu. :)

Làm thế nào để bạn sử dụng nó?

Đây là một hướng dẫn tuyệt vời để tương tác với Khung truy cập lưu trữ trong KitKat:

https://developer.android.com/guide/topics/providers/document-provider.html#client

Tương tác với các API mới trong Lollipop rất giống nhau. Để nhắc người dùng chọn một cây thư mục, bạn có thể khởi chạy một ý định như sau:

    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, 42);

Sau đó, trong onActivityResult (), bạn có thể chuyển Uri do người dùng chọn sang lớp trợ giúp DocumentFile mới. Dưới đây là một ví dụ nhanh liệt kê các tệp trong thư mục đã chọn, sau đó tạo một tệp mới:

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

Uri được trả về DocumentFile.getUri()đủ linh hoạt để sử dụng với các API nền tảng khác nhau. Ví dụ, bạn có thể chia sẻ nó bằng cách sử dụng Intent.setData()với Intent.FLAG_GRANT_READ_URI_PERMISSION.

Nếu bạn muốn truy cập Uri đó từ mã gốc, bạn có thể gọi ContentResolver.openFileDescriptor()và sau đó sử dụng ParcelFileDescriptor.getFd()hoặc detachFd()lấy số nguyên bộ mô tả tệp POSIX truyền thống.

Làm cách nào để kiểm tra xem bạn có thể truy cập các tệp / thư mục hay không?

Theo mặc định, Uris được trả về thông qua ý định của Khung truy cập lưu trữ không được duy trì khi khởi động lại. Nền tảng này "cung cấp" khả năng duy trì quyền, nhưng bạn vẫn cần phải "lấy" quyền nếu bạn muốn. Trong ví dụ của chúng tôi ở trên, bạn sẽ gọi:

    getContentResolver().takePersistableUriPermission(treeUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION |
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

Bạn luôn có thể tìm ra những khoản tài trợ lâu dài mà ứng dụng của bạn có quyền truy cập thông qua ContentResolver.getPersistedUriPermissions()API. Nếu bạn không cần quyền truy cập vào Uri tồn tại nữa, bạn có thể giải phóng nó bằng ContentResolver.releasePersistableUriPermission().

Cái này có trên KitKat không?

Không, chúng tôi không thể thêm chức năng mới vào các phiên bản cũ hơn của nền tảng.

Tôi có thể xem những ứng dụng nào có quyền truy cập vào tệp / thư mục không?

Hiện không có giao diện người dùng nào hiển thị điều này, nhưng bạn có thể tìm thấy chi tiết trong phần "Quyền của Uri được cấp" của adb shell dumpsys activity providersđầu ra.

Điều gì xảy ra nếu một ứng dụng được cài đặt cho nhiều người dùng trên cùng một thiết bị?

Việc cấp quyền cho Uri được tách biệt trên cơ sở mỗi người dùng, giống như tất cả các chức năng nền tảng đa người dùng khác. Có nghĩa là, cùng một ứng dụng chạy dưới hai người dùng khác nhau không có sự cấp phép trùng lặp hoặc được chia sẻ với Uri.

Quyền có thể bị thu hồi không?

Trình hỗ trợ DocumentProvider có thể thu hồi quyền bất kỳ lúc nào, chẳng hạn như khi tài liệu dựa trên đám mây bị xóa. Cách phổ biến nhất để khám phá các quyền bị thu hồi này là khi chúng biến mất khỏiContentResolver.getPersistedUriPermissions() được đề cập ở trên.

Quyền cũng bị thu hồi bất cứ khi nào dữ liệu ứng dụng bị xóa đối với một trong hai ứng dụng liên quan đến việc cấp.

Yêu cầu quyền có hoạt động đệ quy trên một thư mục đã chọn không?

Đúng, ACTION_OPEN_DOCUMENT_TREE mục đích cung cấp cho bạn quyền truy cập đệ quy vào cả tệp và thư mục hiện có và mới được tạo.

Điều này có cho phép nhiều lựa chọn không?

Đúng, nhiều lựa chọn đã được hỗ trợ kể từ KitKat và bạn có thể cho phép nó bằng cách thiết lập EXTRA_ALLOW_MULTIPLEkhi bắt đầu ACTION_OPEN_DOCUMENTý định của mình . Bạn có thể sử dụng Intent.setType()hoặc EXTRA_MIME_TYPESthu hẹp các loại tệp có thể được chọn:

http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT

Có cách nào trên trình giả lập để thử API mới không?

Đúng, thiết bị lưu trữ dùng chung chính sẽ xuất hiện trong bộ chọn, ngay cả trên trình giả lập. Nếu ứng dụng của bạn chỉ sử dụng truy cập Khung lưu trữ để truy cập lưu trữ chia sẻ, bạn không còn cần đến READ/WRITE_EXTERNAL_STORAGEquyền ở tất cả và có thể loại bỏ chúng hoặc sử dụngandroid:maxSdkVersion tính năng để chỉ yêu cầu họ trên các phiên bản nền tảng cũ.

Điều gì xảy ra khi người dùng thay thế thẻ SD bằng một thẻ khác?

Khi có liên quan đến phương tiện vật lý, UUID (chẳng hạn như số sê-ri FAT) của phương tiện cơ bản luôn được ghi vào Uri trả về. Hệ thống sử dụng điều này để kết nối bạn với phương tiện mà người dùng đã chọn ban đầu, ngay cả khi người dùng hoán đổi phương tiện giữa nhiều vị trí.

Nếu người dùng đổi sang thẻ thứ hai, bạn sẽ cần phải nhắc để có quyền truy cập vào thẻ mới. Vì hệ thống ghi nhớ các khoản cấp trên cơ sở mỗi UUID, bạn sẽ tiếp tục có quyền truy cập đã cấp trước đó vào thẻ gốc nếu người dùng nạp lại thẻ sau.

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


2
Tôi hiểu rồi. vì vậy quyết định là thay vì thêm nhiều API đã biết (của Tệp), hãy sử dụng một API khác. ĐỒNG Ý. Bạn có thể vui lòng trả lời các câu hỏi khác (được viết trong bình luận đầu tiên)? BTW, cảm ơn bạn đã trả lời tất cả những điều này.
nhà phát triển android

7
@JeffSharkey Có cách nào để cung cấp cho OPEN_DOCUMENT_TREE gợi ý vị trí bắt đầu không? Người dùng không phải lúc nào cũng là người giỏi nhất trong việc điều hướng đến những gì ứng dụng cần quyền truy cập.
Justin

2
Có cách nào, cách thay đổi phương thức Last Modified Timestamp ( setLastModified () trong lớp File) không? Nó thực sự là nền tảng cho các ứng dụng như trình lưu trữ.
Quark

1
Giả sử bạn có một Uri tài liệu thư mục được lưu trữ và bạn muốn liệt kê các tệp trong đó sau này sau khi khởi động lại thiết bị. DocumentFile.fromTreeUri luôn liệt kê các tệp gốc, bất kể Uri mà bạn cung cấp (thậm chí là Uri con), vì vậy làm cách nào để tạo DocumentFile mà bạn có thể gọi các tệp danh sách, trong đó DocumentFile không đại diện cho tệp gốc hoặc SingleDocument.
AndersC

1
@JeffSharkey Làm cách nào để sử dụng URI này trong MediaMuxer, vì nó chấp nhận một chuỗi làm đường dẫn tệp đầu ra? MediaMuxer (java.lang.String, int
Petrakeas

45

Trong dự án Android của tôi trên Github, được liên kết bên dưới, bạn có thể tìm thấy mã làm việc cho phép viết trên thẻ extSdC trong Android 5. Nó giả định rằng người dùng cấp quyền truy cập vào toàn bộ Thẻ SD và sau đó cho phép bạn ghi ở mọi nơi trên thẻ này. (Nếu bạn chỉ muốn có quyền truy cập vào các tệp đơn lẻ, mọi thứ trở nên dễ dàng hơn.)

Đoạn mã chính

Kích hoạt Khung truy cập lưu trữ:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}

Xử lý phản hồi từ Khung truy cập lưu trữ:

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

Nhận outputStream cho tệp thông qua Khung truy cập lưu trữ (sử dụng URL được lưu trữ, giả sử rằng đây là URL của thư mục gốc của thẻ SD bên ngoài)

DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
    getContentResolver().openOutputStream(targetDocument.getUri());

Điều này sử dụng các phương pháp trợ giúp sau:

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

Tham chiếu đến mã đầy đủ

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

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


1
Bạn có thể vui lòng đặt nó trong một dự án thu nhỏ, chỉ xử lý thẻ SD không? Ngoài ra, bạn có biết làm cách nào tôi có thể kiểm tra xem tất cả các kho lưu trữ bên ngoài có sẵn cũng có thể truy cập được hay không, để tôi không yêu cầu sự cho phép của họ mà không cần làm gì không?
nhà phát triển android

1
Ước gì tôi có thể ủng hộ điều này nhiều hơn. Tôi đã đi được nửa chặng đường với giải pháp này và nhận thấy điều hướng Tài liệu quá tệ đến mức tôi nghĩ rằng mình đã làm sai. Tốt để có một số xác nhận về điều này. Cảm ơn Google ... không có gì.
Anthony

1
Có, để ghi trên SD bên ngoài, bạn không thể sử dụng cách tiếp cận Tệp thông thường nữa. Mặt khác, đối với các phiên bản Android cũ hơn và SD chính, Tệp vẫn là cách tiếp cận hiệu quả nhất. Do đó, bạn nên sử dụng một lớp tiện ích tùy chỉnh để truy cập tệp.
Jörg Eisfeld

15
@ JörgEisfeld: Tôi có một ứng dụng sử dụng File254 lần. Bạn có thể tưởng tượng việc sửa chữa đó không ?? Android đang trở thành cơn ác mộng đối với các nhà phát triển với sự thiếu tương thích ngược hoàn toàn. Tôi vẫn không tìm thấy bất kỳ nơi nào mà họ giải thích tại sao Google lại đưa ra tất cả các quyết định ngu ngốc này về bộ nhớ ngoài. Một số cho rằng "bảo mật", nhưng tất nhiên là vô nghĩa vì bất kỳ ứng dụng nào cũng có thể làm xáo trộn bộ nhớ trong. Tôi đoán là cố gắng buộc chúng tôi sử dụng các dịch vụ đám mây của họ. Rất may, rễ giải quyết vấn đề ... ít nhất cho Android <6 ....
Luis A. Florit

1
ĐỒNG Ý. Kỳ diệu, sau khi tôi khởi động lại điện thoại của tôi, nó hoạt động :)
sancho21

0

Nó chỉ là một câu trả lời bổ sung.

Sau khi bạn tạo một tệp mới, bạn có thể cần phải lưu vị trí của nó vào cơ sở dữ liệu của mình và đọc nó vào ngày mai. Bạn có thể đọc, lấy lại nó bằng cách sử dụng phương pháp này:

/**
 * Get {@link DocumentFile} object from SD card.
 * @param directory SD card ID followed by directory name, for example {@code 6881-2249:Download/Archive},
 *                 where ID for SD card is {@code 6881-2249}
 * @param fileName for example {@code intel_haxm.zip}
 * @return <code>null</code> if does not exist
 */
public static DocumentFile getExternalFile(Context context, String directory, String fileName){
    Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/" + directory);
    DocumentFile parent = DocumentFile.fromTreeUri(context, uri);
    return parent != null ? parent.findFile(fileName) : null;
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS && resultCode == RESULT_OK) {
        int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
        getContentResolver().takePersistableUriPermission(data.getData(), takeFlags);
        String sdcard = data.getDataString().replace("content://com.android.externalstorage.documents/tree/", "");
        try {
            sdcard = URLDecoder.decode(sdcard, "ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        // for example, sdcardId results "6312-2234"
        String sdcardId = sdcard.substring(0, sdcard.indexOf(':'));
        // save to preferences if you want to use it later
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        preferences.edit().putString("sdcard", sdcardId).apply();
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.