Android 5.0 (Lollipop) için sunulan yeni SD kart erişim API'si nasıl kullanılır?


118

Arka fon

On Android 4.4 (KitKat), Google oldukça kısıtlı SD karta erişimi yaptı.

Android Lollipop (5.0) itibarıyla geliştiriciler, bu Google Grupları gönderisinde yazıldığı gibi, kullanıcıdan belirli klasörlere erişim izni vermesini onaylamasını isteyen yeni bir API kullanabilir .

Sorun

Gönderi sizi iki web sitesini ziyaret etmeye yönlendiriyor:

Bu, içsel bir örneğe benziyor (belki daha sonra API demolarında gösterilecek), ancak neler olduğunu anlamak oldukça zor.

Bu, yeni API'nin resmi belgeleridir, ancak nasıl kullanılacağı hakkında yeterince ayrıntı vermez.

İşte size söyledikleri:

Bir doküman alt ağacının tamamına gerçekten tam erişime ihtiyacınız varsa, kullanıcının bir dizin seçmesine izin vermek için ACTION_OPEN_DOCUMENT_TREE uygulamasını başlatarak başlayın. Sonra elde edilen getData () 'yı fromTreeUri' ye (Context, Uri) geçirerek kullanıcının seçtiği ağaçla çalışmaya başlayın.

DocumentFile örnekler ağacında gezinirken, openInputStream (Uri) vb. İle kullanmak üzere o nesnenin temel belgesini temsil eden Uri'yi elde etmek için her zaman getUri () kullanabilirsiniz.

Kodunuzu KITKAT veya önceki sürümlerini çalıştıran cihazlarda basitleştirmek için, DocumentsProvider davranışını taklit eden fromFile (File) öğesini kullanabilirsiniz.

Sorular

Yeni API hakkında birkaç sorum var:

  1. Onu gerçekten nasıl kullanıyorsun?
  2. Gönderiye göre, işletim sistemi uygulamaya dosyalara / klasörlere erişim izni verildiğini hatırlayacak. Dosyalara / klasörlere erişip erişemeyeceğinizi nasıl kontrol edersiniz? Bana erişebileceğim dosya / klasörlerin listesini döndüren bir işlev var mı?
  3. Kitkat'ta bu sorunu nasıl çözüyorsunuz? Destek kitaplığının bir parçası mı?
  4. İşletim sisteminde hangi uygulamaların hangi dosyalara / klasörlere erişimi olduğunu gösteren bir ayarlar ekranı var mı?
  5. Bir uygulama aynı cihazda birden çok kullanıcı için yüklenirse ne olur?
  6. Bu yeni API hakkında başka dokümantasyon / eğitim var mı?
  7. İzinler iptal edilebilir mi? Öyleyse, uygulamaya gönderilen bir niyet var mı?
  8. İzin istemek, seçilen bir klasörde yinelemeli olarak çalışır mı?
  9. İzni kullanmak, kullanıcıya kullanıcının seçimine göre çoklu seçim şansı da verir mi? Veya uygulamanın, hangi dosyalara / klasörlere izin vereceğini özellikle belirtmesi gerekiyor mu?
  10. Öykünücüde yeni API'yi denemenin bir yolu var mı? Demek istediğim, SD kart bölümü var, ancak birincil harici depolama olarak çalışıyor, bu nedenle ona tüm erişim zaten verilmiştir (basit bir izin kullanılarak).
  11. Kullanıcı SD kartı bir başkasıyla değiştirdiğinde ne olur?

FWIW, AndroidPolice bugün bununla ilgili küçük bir makale yayınladı .
fattire

@fattire Teşekkürler. ama zaten okuduklarımı gösteriyorlar. Yine de iyi bir haber.
android geliştiricisi

32
Her yeni işletim sistemi çıktığında, hayatımızı daha karmaşık hale getiriyorlar ...
Phantômaxx

@Funkystein doğru. Keşke bunu Kitkat'ta yapsalar. Şimdi ele alınacak 3 tür davranış var.
android geliştiricisi

1
@Funkystein bilmiyorum. Uzun zaman önce kullandım. Bu kontrolü yapmak o kadar da kötü değil sanırım. Onların da insan olduklarını hatırlamalısınız, böylece zaman zaman hata yapabilirler ve fikirlerini değiştirebilirler ...
android geliştiricisi

Yanıtlar:


143

Pek çok güzel soru, hadi araştıralım. :)

Nasıl kullanıyorsun?

KitKat'ta Depolama Erişim Çerçevesi ile etkileşim için harika bir öğretici:

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

Lollipop'ta yeni API'lerle etkileşim kurmak çok benzer. Kullanıcıdan bir dizin ağacı seçmesini istemek için aşağıdaki gibi bir amaç başlatabilirsiniz:

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

Ardından onActivityResult () öğenizde, kullanıcı tarafından seçilen Uri'yi yeni DocumentFile yardımcı sınıfına iletebilirsiniz. İşte seçilen dizindeki dosyaları listeleyen ve ardından yeni bir dosya oluşturan hızlı bir örnek:

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

Tarafından döndürülen URI DocumentFile.getUri(), farklı platform API'leri ile kullanılabilecek kadar esnektir. Örneğin, Intent.setData()ile paylaşabilirsiniz Intent.FLAG_GRANT_READ_URI_PERMISSION.

Bu Uri'ye yerel koddan erişmek istiyorsanız, çağırabilir ContentResolver.openFileDescriptor()ve ardından geleneksel bir POSIX dosya tanımlayıcısı tamsayısı elde etmek için ParcelFileDescriptor.getFd()veya kullanabilirsiniz detachFd().

Dosyalara / klasörlere erişip erişemeyeceğinizi nasıl kontrol edersiniz?

Varsayılan olarak, Depolama Erişim Çerçeveleri aracılığıyla döndürülen Uris , yeniden başlatmalarda kalıcı olmaz . Platform, izni sürdürme yeteneği "sunar", ancak isterseniz yine de izni "almanız" gerekir. Yukarıdaki örneğimizde şunu arayacaksınız:

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

Uygulamanızın ContentResolver.getPersistedUriPermissions()API aracılığıyla nelere erişim izni olduğunu her zaman öğrenebilirsiniz . Kalıcı bir URI'ya artık erişmeniz gerekmiyorsa, onu ile serbest bırakabilirsiniz ContentResolver.releasePersistableUriPermission().

Bu KitKat'ta mevcut mu?

Hayır, platformun eski sürümlerine geriye dönük olarak yeni işlevler ekleyemiyoruz.

Hangi uygulamaların dosyalara / klasörlere erişimi olduğunu görebilir miyim?

Şu anda bunu gösteren bir kullanıcı arayüzü yoktur, ancak ayrıntıları adb shell dumpsys activity providersçıktının "Verilen Uri İzinleri" bölümünde bulabilirsiniz .

Bir uygulama aynı cihazda birden çok kullanıcı için yüklenirse ne olur?

Uri izinleri, diğer tüm çok kullanıcılı platform işlevleri gibi, kullanıcı bazında izole edilir. Yani, iki farklı kullanıcı altında çalışan aynı uygulamanın örtüşen veya paylaşılan Uri izin izinleri yoktur.

İzinler iptal edilebilir mi?

Desteklenen DocumentProvider, bulut tabanlı bir belge silindiğinde olduğu gibi herhangi bir zamanda izni iptal edebilir. Bu iptal edilen izinleri keşfetmenin en yaygın yolu, ContentResolver.getPersistedUriPermissions()yukarıda bahsedilen izinlerin ortadan kalktığı zamandır.

İzin kapsamındaki her iki uygulama için uygulama verileri temizlendiğinde de izinler iptal edilir.

İzin istemek, seçilen bir klasörde yinelemeli olarak çalışır mı?

Evet, ACTION_OPEN_DOCUMENT_TREEamaç size hem mevcut hem de yeni oluşturulan dosyalara ve dizinlere yinelemeli erişim sağlar.

Bu çoklu seçime izin veriyor mu?

Evet, KitKat'tan bu yana çoklu seçim destekleniyor ve amacınızı EXTRA_ALLOW_MULTIPLEbaşlatırken ayarlayarak buna izin verebilirsiniz ACTION_OPEN_DOCUMENT. Seçilebilecek dosya türlerini daraltmak için Intent.setType()veya kullanabilirsiniz EXTRA_MIME_TYPES:

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

Öykünücüde yeni API'yi denemenin bir yolu var mı?

Evet, birincil paylaşılan depolama cihazı, öykünücüde bile seçicide görünmelidir. Uygulamanız sadece paylaşılan depolama erişmek için Depolama Erişim Framework kullanıyorsa, artık ihtiyaç READ/WRITE_EXTERNAL_STORAGEizinleri hiç ve bunları kaldırmak veya kullanabilir android:maxSdkVersionyalnızca daha eski platformu sürümleri bunları istemek için özelliğini.

Kullanıcı SD kartını bir başkasıyla değiştirdiğinde ne olur?

Fiziksel medya söz konusu olduğunda, temeldeki medyanın UUID'si (FAT seri numarası gibi) her zaman döndürülen Uri'ye yazılır. Sistem, kullanıcı medyayı birden çok yuva arasında değiştirse bile, sizi kullanıcının orijinal olarak seçtiği medyaya bağlamak için kullanır.

Kullanıcı ikinci bir kart takarsa, yeni karta erişim sağlamak için istemeniz gerekir. Sistem, UUID bazında verilen hibeleri hatırladığından, kullanıcı daha sonra yeniden takarsa, orijinal karta önceden erişim izni vermiş olmaya devam edeceksiniz.

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


2
Anlıyorum. Bu nedenle karar, bilinen API'ye (Dosyanın) daha fazlasını eklemek yerine, farklı bir API kullanmaktı. TAMAM. Lütfen diğer soruları cevaplayabilir misiniz (ilk yorumda yazılan)? BTW, tüm bunları yanıtladığınız için teşekkür ederiz.
android geliştiricisi

7
@JeffSharkey OPEN_DOCUMENT_TREE'ye bir başlangıç ​​konumu ipucu sağlamanın herhangi bir yolu var mı? Kullanıcılar, uygulamanın erişmesi gereken şeylere gitme konusunda her zaman en iyi kişi değildir.
Justin

2
Last Modified Timestamp'ı ( File sınıfında setLastModified () yöntemi ) değiştirmenin bir yolu var mı ? Arşivleyiciler gibi uygulamalar için gerçekten çok önemlidir.
Quark

1
Diyelim ki saklanmış bir klasör belgeniz Uri var ve daha sonra cihaz yeniden başlatıldıktan sonra bir noktada dosyaları listelemek istiyorsunuz. DocumentFile.fromTreeUri, vermiş olduğunuz Uri (alt Uri bile) ne olursa olsun her zaman kök dosyaları listeler, bu nedenle bir DocumentFile'ı nasıl oluşturursunuz, burada DocumentFile bir kök veya bir SingleDocument'i yeniden sunmadığında listfiles'ı çağırabilirsiniz.
AndersC

1
@JeffSharkey Çıktı dosyası yolu olarak bir dizeyi kabul ettiğinden, bu URI MediaMuxer'da nasıl kullanılabilir? MediaMuxer (java.lang.String, int
Petrakeas

45

Aşağıda bağlantısı verilen Github'daki Android projemde, Android 5'de extSdCard'a yazmaya izin veren çalışma kodunu bulabilirsiniz. Kullanıcının tüm SD Karta erişim verdiğini ve ardından bu kartın her yerine yazmanıza izin verdiğini varsayar. (Yalnızca tek dosyalara erişim sağlamak istiyorsanız işler kolaylaşır.)

Ana Kod parçacıkları

Depolama Erişim Çerçevesini Tetikleme:

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

Depolama Erişim Çerçevesinden gelen yanıtı işleme:

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

Depolama Erişim Çerçevesi aracılığıyla bir dosya için outputStream alma (bunun harici SD kartın kök klasörünün URL'si olduğu varsayılarak depolanmış URL'yi kullanarak)

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

Bu, aşağıdaki yardımcı yöntemleri kullanır:

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

Tam koda referans

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

ve

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


1
Lütfen bunu sadece SD kartı kullanan küçültülmüş bir projeye koyabilir misiniz? Ayrıca, mevcut tüm harici depolama alanlarının da erişilebilir olup olmadığını nasıl kontrol edebilirim, böylece hiçbir şey için izinlerini istemeyeceğim?
android geliştiricisi

1
Keşke buna daha fazla oy verebilseydim. Bu çözümün yarısına gelmiştim ve Belge navigasyonunu o kadar berbat buldum ki yanlış yaptığımı düşündüm. Bu konuda biraz onay almak güzel. Teşekkürler Google ... hiçbir şey için.
Anthony

1
Evet, harici SD'ye yazmak için artık normal Dosya yaklaşımını kullanamazsınız. Öte yandan, eski Android sürümleri ve birincil SD için Dosya hala en verimli yaklaşımdır. Bu nedenle, dosya erişimi için özel bir yardımcı program sınıfı kullanmalısınız.
Jörg Eisfeld

15
@ JörgEisfeld: File254 kez kullanan bir uygulamam var . Bunu tamir etmeyi hayal edebiliyor musun? Android, geriye dönük uyumluluktan tamamen yoksun olmasıyla geliştiriciler için bir kabusa dönüşüyor. Google'ın neden harici depolamayla ilgili tüm bu aptalca kararları aldığını açıkladıkları bir yer bulamadım. Bazıları "güvenlik" iddiasında bulunur, ancak herhangi bir uygulama dahili depolamayı bozabileceğinden elbette saçmadır. Tahminim, bizi bulut hizmetlerini kullanmaya zorlamak. Neyse ki, köklendirme sorunları çözer ... en azından Android <6 için ....
Luis A. Florit

1
TAMAM. Sihirli bir şekilde, telefonumu yeniden başlattıktan sonra çalışıyor :)
sancho21

0

Bu sadece tamamlayıcı bir cevaptır.

Yeni bir dosya oluşturduktan sonra, konumunu veritabanınıza kaydetmeniz ve yarın okumanız gerekebilir. Bu yöntemi kullanarak geri almayı tekrar okuyabilirsiniz:

/**
 * 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();
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.