Bagaimana cara menggunakan API akses kartu SD baru yang disajikan untuk Android 5.0 (Lollipop)?

118

Latar Belakang

Pada Android 4.4 (KitKat), Google telah membatasi akses ke kartu SD.

Mulai Android Lollipop (5.0), pengembang dapat menggunakan API baru yang meminta pengguna mengonfirmasi untuk mengizinkan akses ke folder tertentu, seperti yang tertulis di postingan Google-Grup ini .

Masalah

Pos tersebut mengarahkan Anda untuk mengunjungi dua situs web:

Ini terlihat seperti contoh batin (mungkin akan ditampilkan di demo API nanti), tetapi cukup sulit untuk memahami apa yang sedang terjadi.

Ini adalah dokumentasi resmi dari API baru, tetapi tidak menjelaskan cukup detail tentang cara menggunakannya.

Inilah yang diberitahukannya kepada Anda:

Jika Anda benar-benar membutuhkan akses penuh ke seluruh subpohon dokumen, mulailah dengan meluncurkan ACTION_OPEN_DOCUMENT_TREE untuk memungkinkan pengguna memilih direktori. Kemudian berikan getData () yang dihasilkan ke fromTreeUri (Context, Uri) untuk mulai bekerja dengan pohon yang dipilih pengguna.

Saat Anda menavigasi pohon instance DocumentFile, Anda selalu bisa menggunakan getUri () untuk mendapatkan Uri yang mewakili dokumen yang mendasari objek itu, untuk digunakan dengan openInputStream (Uri), dll.

Untuk menyederhanakan kode Anda pada perangkat yang menjalankan KITKAT atau yang lebih lama, Anda dapat menggunakan fromFile (File) yang meniru perilaku DocumentsProvider.

Pertanyaan-pertanyaan

Saya punya beberapa pertanyaan tentang API baru:

  1. Bagaimana Anda benar-benar menggunakannya?
  2. Menurut postingan tersebut, OS akan mengingat bahwa aplikasi tersebut diberi izin untuk mengakses file / folder. Bagaimana Anda memeriksa apakah Anda dapat mengakses file / folder? Apakah ada fungsi yang mengembalikan daftar file / folder yang dapat saya akses?
  3. Bagaimana Anda menangani masalah ini di Kitkat? Apakah ini bagian dari pustaka dukungan?
  4. Apakah ada layar pengaturan pada OS yang menunjukkan aplikasi mana yang memiliki akses ke file / folder mana?
  5. Apa yang terjadi jika aplikasi dipasang untuk beberapa pengguna di perangkat yang sama?
  6. Apakah ada dokumentasi / tutorial lain tentang API baru ini?
  7. Bisakah izin dicabut? Jika ya, apakah ada maksud yang dikirim ke aplikasi?
  8. Apakah meminta izin bekerja secara rekursif pada folder yang dipilih?
  9. Akankah menggunakan izin juga memungkinkan untuk memberikan kesempatan kepada pengguna untuk melakukan beberapa pilihan berdasarkan pilihan pengguna? Atau apakah aplikasi perlu secara khusus memberi tahu maksud file / folder mana yang diizinkan?
  10. Apakah ada cara bagi emulator untuk mencoba API baru? Maksud saya, ini memiliki partisi kartu SD, tetapi berfungsi sebagai penyimpanan eksternal utama, jadi semua akses ke sana sudah diberikan (menggunakan izin sederhana).
  11. Apa yang terjadi jika pengguna mengganti kartu SD dengan yang lain?
pengembang android
sumber
FWIW, AndroidPolice punya sedikit artikel tentang hari ini.
menggemukkan
@fattire Terima kasih. tetapi mereka menunjukkan tentang apa yang sudah saya baca. Ini kabar baik.
Pengembang android
32
Setiap kali OS baru keluar, mereka membuat hidup kita lebih rumit ...
Phantômaxx
@Tokopedia benar. Berharap mereka melakukannya di Kitkat. Sekarang ada 3 jenis perilaku yang harus ditangani.
Pengembang android
1
@ Funkystein saya tidak tahu. Saya menggunakannya sejak lama. Tidak terlalu buruk untuk melakukan pemeriksaan ini, kurasa. Anda harus ingat bahwa mereka juga manusia, sehingga mereka dapat membuat kesalahan dan berubah pikiran dari waktu ke waktu ...
pengembang android

Jawaban:

143

Banyak pertanyaan bagus, mari kita gali. :)

Bagaimana kamu menggunakannya?

Berikut adalah tutorial bagus untuk berinteraksi dengan Storage Access Framework di KitKat:

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

Berinteraksi dengan API baru di Lollipop sangat mirip. Untuk meminta pengguna memilih pohon direktori, Anda bisa meluncurkan maksud seperti ini:

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

Kemudian di onActivityResult (), Anda bisa meneruskan Uri yang dipilih pengguna ke kelas helper DocumentFile baru. Berikut adalah contoh cepat yang mencantumkan file di direktori yang dipilih, lalu membuat file baru:

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 yang dikembalikan oleh DocumentFile.getUri()cukup fleksibel untuk digunakan dengan API platform yang berbeda. Misalnya, Anda dapat membagikannya Intent.setData()dengan Intent.FLAG_GRANT_READ_URI_PERMISSION.

Jika Anda ingin mengakses Uri tersebut dari kode native, Anda dapat memanggil ContentResolver.openFileDescriptor()dan kemudian menggunakan ParcelFileDescriptor.getFd()atau detachFd()untuk mendapatkan integer deskriptor file POSIX tradisional.

Bagaimana Anda memeriksa apakah Anda dapat mengakses file / folder?

Secara default, Uris yang dikembalikan melalui maksud Storage Access Frameworks tidak dipertahankan selama boot ulang. Platform "menawarkan" kemampuan untuk mempertahankan izin, tetapi Anda masih perlu "mengambil" izin jika Anda menginginkannya. Dalam contoh kami di atas, Anda akan menelepon:

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

Anda selalu dapat mengetahui hibah tetap yang dapat diakses aplikasi Anda melalui ContentResolver.getPersistedUriPermissions()API. Jika Anda tidak lagi memerlukan akses ke Uri yang bertahan, Anda dapat melepaskannya dengan ContentResolver.releasePersistableUriPermission().

Apakah ini tersedia di KitKat?

Tidak, kami tidak dapat menambahkan fungsionalitas baru secara retroaktif ke versi platform yang lebih lama.

Dapatkah saya melihat aplikasi apa yang memiliki akses ke file / folder?

Saat ini tidak ada UI yang menunjukkan hal ini, tetapi Anda dapat menemukan detailnya di bagian "Izin Uri yang Diberikan" pada adb shell dumpsys activity providerskeluaran.

Apa yang terjadi jika aplikasi dipasang untuk beberapa pengguna di perangkat yang sama?

Pemberian izin Uri diisolasi per pengguna, sama seperti semua fungsionalitas platform multi-pengguna lainnya. Artinya, aplikasi yang sama yang berjalan di bawah dua pengguna berbeda tidak memiliki izin Uri yang tumpang tindih atau dibagikan.

Bisakah izin dicabut?

DocumentProvider pendukung dapat mencabut izin kapan saja, seperti saat dokumen berbasis Internet dihapus. Cara paling umum untuk menemukan izin yang dicabut ini adalah ketika izin tersebut menghilang dari yang ContentResolver.getPersistedUriPermissions()disebutkan di atas.

Izin juga dicabut setiap kali data aplikasi dihapus untuk salah satu aplikasi yang terlibat dalam pemberian.

Apakah meminta izin bekerja secara rekursif pada folder yang dipilih?

Ya, ACTION_OPEN_DOCUMENT_TREEmaksudnya memberi Anda akses rekursif ke file dan direktori yang ada dan yang baru dibuat.

Apakah ini memungkinkan banyak pilihan?

Ya, beberapa pilihan telah didukung sejak KitKat, dan Anda bisa mengizinkannya dengan menyetel EXTRA_ALLOW_MULTIPLEsaat memulai ACTION_OPEN_DOCUMENTmaksud Anda . Anda dapat menggunakan Intent.setType()atau EXTRA_MIME_TYPESmempersempit jenis file yang dapat dipilih:

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

Apakah ada cara bagi emulator untuk mencoba API baru?

Ya, perangkat penyimpanan bersama utama seharusnya muncul di alat pilih, bahkan di emulator. Jika aplikasi Anda hanya menggunakan Storage Access Framework untuk mengakses penyimpanan bersama, Anda tidak lagi memerlukan READ/WRITE_EXTERNAL_STORAGEizin sama sekali dan dapat menghapusnya atau menggunakan android:maxSdkVersionfitur tersebut untuk hanya memintanya pada versi platform yang lebih lama.

Apa yang terjadi ketika pengguna mengganti SD-card dengan yang lain?

Jika media fisik terlibat, UUID (seperti nomor seri FAT) dari media yang mendasarinya selalu dibakar ke Uri yang dikembalikan. Sistem menggunakan ini untuk menghubungkan Anda ke media yang awalnya dipilih pengguna, meskipun pengguna menukar media di antara beberapa slot.

Jika pengguna menukar kartu kedua, Anda harus meminta untuk mendapatkan akses ke kartu baru. Karena sistem mengingat pemberian berdasarkan UUID, Anda akan terus mendapatkan akses yang sebelumnya diberikan ke kartu asli jika pengguna memasukkannya kembali nanti.

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

Jeff Sharkey
sumber
2
Saya melihat. jadi keputusannya adalah alih-alih menambahkan lebih banyak ke API (dari File) yang diketahui, gunakan yang lain. BAIK. Bisakah Anda menjawab pertanyaan lain (ditulis di komentar pertama)? BTW, terima kasih telah menjawab semua ini.
Pengembang android
7
@JeffSharkey Adakah cara untuk memberikan OPEN_DOCUMENT_TREE petunjuk lokasi awal? Pengguna tidak selalu yang terbaik dalam menavigasi ke aplikasi yang memerlukan akses.
Justin
2
Apakah ada cara, bagaimana mengubah metode Last Modified Timestamp ( setLastModified () di kelas File)? Ini sangat mendasar untuk aplikasi seperti pengarsip.
Quark
1
Katakanlah Anda memiliki dokumen folder tersimpan Uri dan Anda ingin daftar file nanti di beberapa titik setelah reboot perangkat. DocumentFile.fromTreeUri selalu mencantumkan file root, apa pun Uri yang Anda berikan (bahkan Uri anak), jadi bagaimana Anda membuat DocumentFile Anda bisa memanggil listfile, di mana DocumentFile tidak merepresentasikan root atau SingleDocument.
AndersC
1
@JeffSharkey Bagaimana URI ini digunakan di MediaMuxer, karena menerima string sebagai jalur file keluaran? MediaMuxer (java.lang.String, int
Petrakeas
45

Dalam proyek Android saya di Github, ditautkan di bawah, Anda dapat menemukan kode kerja yang memungkinkan untuk menulis di extSdCard di Android 5. Ini mengasumsikan bahwa pengguna memberikan akses ke seluruh Kartu SD dan kemudian memungkinkan Anda menulis di mana saja di kartu ini. (Jika Anda ingin memiliki akses hanya ke satu file, segalanya menjadi lebih mudah.)

Snipplet Kode Utama

Memicu Kerangka Akses Penyimpanan:

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

Menangani respons dari Storage Access Framework:

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

Mendapatkan outputStream untuk file melalui Storage Access Framework (memanfaatkan URL yang disimpan, dengan asumsi bahwa ini adalah URL dari folder root dari kartu SD eksternal)

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

Ini menggunakan metode helper berikut:

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

Referensi ke kode lengkap

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

dan

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

Jörg Eisfeld
sumber
1
Bisakah Anda memasukkannya ke dalam proyek yang diminimalkan, yang hanya menangani kartu SD? Selain itu, tahukah Anda bagaimana cara memeriksa apakah semua penyimpanan eksternal yang tersedia juga dapat diakses, sehingga saya tidak akan meminta izin mereka secara gratis?
Pengembang android
1
Seandainya saya bisa lebih suka ini. Saya setengah jalan ke solusi ini dan menemukan navigasi Dokumen sangat buruk sehingga saya pikir saya salah melakukannya. Senang mendapat konfirmasi tentang ini. Terima kasih Google ... untuk apa-apa.
Anthony
1
Ya, untuk menulis di SD eksternal Anda tidak dapat lagi menggunakan pendekatan File normal. Di sisi lain, untuk versi Android yang lebih lama dan untuk SD utama, File masih merupakan pendekatan yang paling efisien. Oleh karena itu, Anda harus menggunakan kelas utilitas khusus untuk akses file.
Jörg Eisfeld
15
@ JörgEisfeld: Saya memiliki aplikasi yang menggunakan File254 kali. Bisakah Anda bayangkan memperbaikinya ?? Android menjadi mimpi buruk bagi pengembang dengan kurangnya kompatibilitas ke belakang. Saya masih tidak menemukan tempat di mana mereka menjelaskan mengapa Google mengambil semua keputusan bodoh ini terkait penyimpanan eksternal. Beberapa mengklaim "keamanan", tetapi tentu saja tidak masuk akal karena aplikasi apa pun dapat mengacaukan penyimpanan internal. Tebakan saya adalah mencoba memaksa kami menggunakan layanan cloud mereka. Untungnya, rooting memecahkan masalah ... setidaknya untuk Android <6 ....
Luis A. Florit
1
BAIK. Ajaibnya, setelah saya me-restart telepon saya, itu berfungsi :)
sancho21
0

Itu hanyalah jawaban pelengkap.

Setelah Anda membuat file baru, Anda mungkin perlu menyimpan lokasinya ke database Anda dan membacanya besok. Anda dapat membaca mengambilnya lagi menggunakan metode ini:

/**
 * 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();
    }
}
Anggrayudi H
sumber