Jak korzystać z nowego interfejsu API dostępu do karty SD dla systemu Android 5.0 (Lollipop)?

118

tło

W systemie Android 4.4 (KitKat) Google ograniczył dostęp do karty SD.

Począwszy od Androida Lollipop (5.0), programiści mogą używać nowego interfejsu API, który prosi użytkownika o potwierdzenie zezwolenia na dostęp do określonych folderów, jak napisano w tym poście w Grupach dyskusyjnych Google .

Problem

Post kieruje do dwóch witryn:

To wygląda jak wewnętrzny przykład (być może zostanie pokazany później w demonstracjach API), ale dość trudno jest zrozumieć, co się dzieje.

To jest oficjalna dokumentacja nowego interfejsu API, ale nie zawiera ona wystarczających informacji o tym, jak z niego korzystać.

Oto, co ci mówi:

Jeśli naprawdę potrzebujesz pełnego dostępu do całego poddrzewa dokumentów, zacznij od uruchomienia ACTION_OPEN_DOCUMENT_TREE, aby umożliwić użytkownikowi wybranie katalogu. Następnie przekaż wynikową metodę getData () do fromTreeUri (Context, Uri), aby rozpocząć pracę z drzewem wybranym przez użytkownika.

Poruszając się po drzewie instancji DocumentFile, zawsze możesz użyć metody getUri (), aby uzyskać Uri reprezentujący dokument bazowy dla tego obiektu, do użycia z openInputStream (Uri) itp.

Aby uprościć kod na urządzeniach z systemem KITKAT lub starszym, możesz użyć fromFile (File), który emuluje zachowanie DocumentsProvider.

Pytania

Mam kilka pytań dotyczących nowego API:

  1. Jak naprawdę tego używasz?
  2. Zgodnie z postem system operacyjny zapamięta, że ​​aplikacja otrzymała pozwolenie na dostęp do plików / folderów. Jak sprawdzić, czy masz dostęp do plików / folderów? Czy jest funkcja, która zwraca mi listę plików / folderów, do których mam dostęp?
  3. Jak radzisz sobie z tym problemem na KitKacie? Czy jest to część biblioteki wsparcia?
  4. Czy w systemie operacyjnym jest ekran ustawień, który pokazuje, które aplikacje mają dostęp do jakich plików / folderów?
  5. Co się stanie, jeśli aplikacja zostanie zainstalowana dla wielu użytkowników na tym samym urządzeniu?
  6. Czy jest jakaś inna dokumentacja / samouczek na temat tego nowego interfejsu API?
  7. Czy uprawnienia można cofnąć? Jeśli tak, czy istnieje zamiar, który jest wysyłany do aplikacji?
  8. Czy pytanie o pozwolenie działałoby rekurencyjnie w wybranym folderze?
  9. Czy użycie pozwolenia pozwoliłoby również użytkownikowi na wielokrotny wybór na podstawie jego wyboru? A może aplikacja musi wyraźnie informować intencję, które pliki / foldery mają zezwolić?
  10. Czy istnieje sposób na emulator, aby wypróbować nowy interfejs API? Mam na myśli, że ma partycję na kartę SD, ale działa jako główna pamięć zewnętrzna, więc cały dostęp do niej jest już udzielony (za pomocą prostego pozwolenia).
  11. Co się stanie, gdy użytkownik wymieni kartę SD na inną?
programista Androida
źródło
FWIW, AndroidPolice opublikowało dzisiaj mały artykuł na ten temat.
fattire
@fattire Dziękuję. ale pokazują o tym, co już przeczytałem. Ale to dobra wiadomość.
programista Androida
32
Za każdym razem, gdy pojawia się nowy system operacyjny, komplikuje nasze życie ...
Phantômaxx
@Funkystein true. Szkoda, że ​​nie zrobili tego na Kitkacie. Teraz mamy do czynienia z trzema typami zachowań.
programista Androida
1
@Funkystein Nie wiem. Użyłem go dawno temu. Myślę, że to nie jest takie złe. Musisz pamiętać, że oni też są ludźmi, więc mogą popełniać błędy i od czasu do czasu zmieniać zdanie ...
programista Androida

Odpowiedzi:

143

Wiele dobrych pytań, zagłębmy się. :)

Jak tego używasz?

Oto świetny samouczek dotyczący interakcji z platformą dostępu do magazynu w KitKat:

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

Interakcja z nowymi interfejsami API w Lollipop jest bardzo podobna. Aby zachęcić użytkownika do wybrania drzewa katalogów, możesz uruchomić następującą intencję:

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

Następnie w swojej onActivityResult () możesz przekazać wybrany przez użytkownika Uri do nowej klasy pomocnika DocumentFile. Oto krótki przykład, który wyświetla listę plików w wybranym katalogu, a następnie tworzy nowy plik:

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

Zwracany identyfikator Uri DocumentFile.getUri()jest wystarczająco elastyczny, aby można go było używać z różnymi interfejsami API platformy. Na przykład możesz udostępnić go za Intent.setData()pomocą Intent.FLAG_GRANT_READ_URI_PERMISSION.

Jeśli chcesz uzyskać dostęp do tego Uri z kodu natywnego, możesz wywołać, ContentResolver.openFileDescriptor()a następnie użyć ParcelFileDescriptor.getFd()lub, detachFd()aby uzyskać tradycyjną liczbę całkowitą deskryptora pliku POSIX.

Jak sprawdzić, czy masz dostęp do plików / folderów?

Domyślnie Uris zwrócone przez intencje Storage Access Framework nie są utrwalane po ponownym uruchomieniu. Platforma „oferuje” możliwość utrwalenia pozwolenia, ale nadal musisz „odebrać” pozwolenie, jeśli chcesz. W naszym przykładzie powyżej zadzwoniłbyś:

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

Zawsze możesz dowiedzieć się, do czego utrzymywana aplikacja ma dostęp za pośrednictwem ContentResolver.getPersistedUriPermissions()interfejsu API. Jeśli nie potrzebujesz już dostępu do utrwalonego Uri, możesz go zwolnić za pomocą ContentResolver.releasePersistableUriPermission().

Czy to jest dostępne na KitKat?

Nie, nie możemy wstecznie dodawać nowych funkcji do starszych wersji platformy.

Czy mogę zobaczyć, które aplikacje mają dostęp do plików / folderów?

Obecnie nie ma interfejsu użytkownika, który to pokazuje, ale szczegóły można znaleźć w sekcji „Udzielone uprawnienia Uri” danych adb shell dumpsys activity providerswyjściowych.

Co się stanie, jeśli aplikacja zostanie zainstalowana dla wielu użytkowników na tym samym urządzeniu?

Przyznania uprawnień Uri są izolowane dla poszczególnych użytkowników, podobnie jak wszystkie inne funkcje platformy dla wielu użytkowników. Oznacza to, że ta sama aplikacja działająca w ramach dwóch różnych użytkowników nie ma nakładających się ani współdzielonych uprawnień Uri.

Czy uprawnienia można cofnąć?

Wspierający DocumentProvider może odwołać uprawnienia w dowolnym momencie, na przykład w przypadku usunięcia dokumentu w chmurze. Najczęstszym sposobem na wykrycie tych odwołanych uprawnień jest ich zniknięcie z ContentResolver.getPersistedUriPermissions()wymienionych powyżej.

Uprawnienia są również odwoływane za każdym razem, gdy dane aplikacji są czyszczone dla dowolnej aplikacji uczestniczącej w przyznaniu.

Czy pytanie o pozwolenie działałoby rekurencyjnie w wybranym folderze?

Tak, ACTION_OPEN_DOCUMENT_TREEintencja zapewnia rekurencyjny dostęp zarówno do istniejących, jak i nowo utworzonych plików i katalogów.

Czy to pozwala na wielokrotny wybór?

Tak, wielokrotny wybór jest obsługiwany od KitKat i możesz na to zezwolić, ustawiając je EXTRA_ALLOW_MULTIPLEpodczas rozpoczynania ACTION_OPEN_DOCUMENTzamiaru. Możesz użyć Intent.setType()lub, EXTRA_MIME_TYPESaby zawęzić typy plików, które można wybrać:

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

Czy istnieje sposób na emulator, aby wypróbować nowy interfejs API?

Tak, podstawowe współdzielone urządzenie magazynujące powinno pojawić się w selektorze, nawet w emulatorze. Jeśli tylko Twoja aplikacja korzysta z dostępu do pamięci ram dostępu do pamięci współdzielonej, które nie są już potrzebne READ/WRITE_EXTERNAL_STORAGEuprawnienia na wszystkich i można je usunąć lub skorzystać z android:maxSdkVersionfunkcji, aby tylko zwrócić je w starszych wersjach platformy.

Co się stanie, gdy użytkownik wymieni kartę SD na inną?

W przypadku nośnika fizycznego identyfikator UUID (taki jak numer seryjny FAT) nośnika podstawowego jest zawsze zapisywany w zwracanym identyfikatorze Uri. System wykorzystuje to, aby połączyć Cię z nośnikiem wybranym przez użytkownika, nawet jeśli użytkownik zamienia nośnik między wieloma gniazdami.

Jeśli użytkownik wymieni drugą kartę, musisz poprosić o dostęp do nowej karty. Ponieważ system zapamiętuje przyznane granty na podstawie identyfikatora UUID, nadal będziesz mieć wcześniej przyznany dostęp do oryginalnej karty, jeśli użytkownik włoży ją ponownie później.

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

Jeff Sharkey
źródło
2
Widzę. więc zdecydowano, że zamiast dodawać więcej do znanego interfejsu API (File), użyć innego. DOBRZE. Czy możesz odpowiedzieć na inne pytania (zapisane w pierwszym komentarzu)? A tak przy okazji, dziękuję za odpowiedź na to wszystko.
programista Androida
7
@JeffSharkey Czy jest jakiś sposób, aby podać OPEN_DOCUMENT_TREE wskazówkę dotyczącą lokalizacji początkowej? Użytkownicy nie zawsze są najlepsi w nawigowaniu do tego, do czego aplikacja potrzebuje dostępu.
Justin,
2
Czy istnieje sposób, jak zmienić Last Modified Timestamp ( metoda setLastModified () w klasie File)? Jest to naprawdę fundamentalne dla aplikacji takich jak archiwa.
Quark
1
Powiedzmy, że masz zapisany dokument folderu Uri i chcesz wyświetlić listę plików później w pewnym momencie po ponownym uruchomieniu urządzenia. DocumentFile.fromTreeUri zawsze wyświetla listę plików głównych, bez względu na Uri, które mu nadasz (nawet podrzędny Uri), więc jak utworzyć plik DocumentFile, na którym możesz wywoływać listfiles, gdzie DocumentFile nie reprezentuje ani katalogu głównego, ani SingleDocument.
AndersC
1
@JeffSharkey W jaki sposób można użyć tego identyfikatora URI w programie MediaMuxer, skoro akceptuje ciąg znaków jako ścieżkę pliku wyjściowego? MediaMuxer (java.lang.String, int
Petrakeas
45

W moim projekcie na Androida na Githubie, do którego link znajdziesz poniżej, możesz znaleźć działający kod, który pozwala pisać na extSdCard w systemie Android 5. Zakłada on, że użytkownik daje dostęp do całej karty SD, a następnie pozwala pisać wszędzie na tej karcie. (Jeśli chcesz mieć dostęp tylko do pojedynczych plików, wszystko staje się łatwiejsze).

Główne fragmenty kodu

Wyzwalanie struktury dostępu do magazynu:

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

Obsługa odpowiedzi z 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);
        }
    }
}

Pobieranie strumienia wyjściowego dla pliku za pośrednictwem Storage Access Framework (przy użyciu przechowywanego adresu URL, zakładając, że jest to adres URL folderu głównego zewnętrznej karty SD)

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

Używa to następujących metod pomocniczych:

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

Odniesienie do pełnego kodu

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

i

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

Jörg Eisfeld
źródło
1
Czy możesz umieścić go w zminimalizowanym projekcie, który obsługuje tylko kartę SD? Czy wiesz również, jak mogę sprawdzić, czy wszystkie dostępne magazyny zewnętrzne są również dostępne, aby nie prosić o ich pozwolenie za nic?
programista Androida
1
Chciałbym móc to bardziej głosować. Byłem w połowie drogi do tego rozwiązania i stwierdziłem, że nawigacja w dokumencie jest tak okropna, że ​​pomyślałem, że robię to źle. Dobrze mieć na to jakieś potwierdzenie. Dzięki Google ... za nic.
Anthony
1
Tak, do pisania na zewnętrznej karcie SD nie można już używać zwykłego podejścia do plików. Z drugiej strony w przypadku starszych wersji Androida i podstawowego SD plik jest nadal najbardziej wydajnym podejściem. Dlatego w celu uzyskania dostępu do plików należy użyć niestandardowej klasy narzędziowej.
Jörg Eisfeld
15
@ JörgEisfeld: Mam aplikację, która używa File254 razy. Czy możesz sobie wyobrazić naprawienie tego? Android staje się koszmarem dla deweloperów z powodu całkowitego braku wstecznej kompatybilności. Nadal nie znalazłem miejsca, w którym wyjaśnią, dlaczego Google podjął te wszystkie głupie decyzje dotyczące pamięci zewnętrznej. Niektórzy twierdzą, że „bezpieczeństwo”, ale oczywiście jest to nonsensowne, ponieważ każda aplikacja może zepsuć pamięć wewnętrzną. Domyślam się, że spróbujemy zmusić nas do korzystania z ich usług w chmurze. Na szczęście rootowanie rozwiązuje problemy ... przynajmniej dla Androida <6 ....
Luis A. Florit,
1
DOBRZE. Magicznie, po zrestartowaniu telefonu działa :)
sancho21 17.04.2016
0

To tylko uzupełniająca odpowiedź.

Po utworzeniu nowego pliku może być konieczne zapisanie jego lokalizacji w bazie danych i przeczytanie go jutro. Możesz przeczytać odzyskać go ponownie za pomocą tej metody:

/**
 * 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
źródło