Wie verwende ich die neue SD-Kartenzugriffs-API für Android 5.0 (Lollipop)?

118

Hintergrund

Unter Android 4.4 (KitKat) hat Google den Zugriff auf die SD-Karte stark eingeschränkt.

Ab Android Lollipop (5.0) können Entwickler eine neue API verwenden, die den Benutzer auffordert, den Zugriff auf bestimmte Ordner zu bestätigen, wie in diesem Beitrag von Google-Groups beschrieben .

Das Problem

Der Beitrag weist Sie an, zwei Websites zu besuchen:

Dies sieht aus wie ein inneres Beispiel (das vielleicht später in den API-Demos gezeigt wird), aber es ist ziemlich schwer zu verstehen, was los ist.

Dies ist die offizielle Dokumentation der neuen API, enthält jedoch nicht genügend Details zur Verwendung.

Folgendes sagt es Ihnen:

Wenn Sie wirklich vollen Zugriff auf einen gesamten Teilbaum von Dokumenten benötigen, starten Sie zunächst ACTION_OPEN_DOCUMENT_TREE, damit der Benutzer ein Verzeichnis auswählen kann. Übergeben Sie dann die resultierenden getData () an fromTreeUri (Context, Uri), um mit dem vom Benutzer ausgewählten Baum zu arbeiten.

Wenn Sie im Baum der DocumentFile-Instanzen navigieren, können Sie immer getUri () verwenden, um den Uri abzurufen, der das zugrunde liegende Dokument für dieses Objekt darstellt, und um es mit openInputStream (Uri) usw. zu verwenden.

Um Ihren Code auf Geräten zu vereinfachen, auf denen KITKAT oder früher ausgeführt wird, können Sie fromFile (File) verwenden, das das Verhalten eines DocumentsProviders emuliert.

Die Fragen

Ich habe einige Fragen zur neuen API:

  1. Wie benutzt du es wirklich?
  2. Laut dem Beitrag wird sich das Betriebssystem daran erinnern, dass die App eine Berechtigung zum Zugriff auf die Dateien / Ordner erhalten hat. Wie überprüfen Sie, ob Sie auf die Dateien / Ordner zugreifen können? Gibt es eine Funktion, die mir die Liste der Dateien / Ordner zurückgibt, auf die ich zugreifen kann?
  3. Wie gehen Sie mit diesem Problem bei Kitkat um? Ist es ein Teil der Support-Bibliothek?
  4. Gibt es auf dem Betriebssystem einen Einstellungsbildschirm, auf dem angezeigt wird, welche Apps auf welche Dateien / Ordner zugreifen können?
  5. Was passiert, wenn eine App für mehrere Benutzer auf demselben Gerät installiert ist?
  6. Gibt es eine andere Dokumentation / ein anderes Tutorial zu dieser neuen API?
  7. Können die Berechtigungen widerrufen werden? Wenn ja, gibt es eine Absicht, die an die App gesendet wird?
  8. Würde das Anfordern der Berechtigung für einen ausgewählten Ordner rekursiv funktionieren?
  9. Würde die Verwendung der Berechtigung es dem Benutzer auch ermöglichen, nach Wahl des Benutzers eine Mehrfachauswahl zu treffen? Oder muss die App speziell angeben, welche Dateien / Ordner zugelassen werden sollen?
  10. Gibt es auf dem Emulator eine Möglichkeit, die neue API auszuprobieren? Ich meine, es hat eine SD-Kartenpartition, aber es funktioniert als primärer externer Speicher, sodass der gesamte Zugriff darauf bereits gewährt wird (mit einer einfachen Berechtigung).
  11. Was passiert, wenn der Benutzer die SD-Karte durch eine andere ersetzt?
Android-Entwickler
quelle
FWIW, AndroidPolice hatte heute einen kleinen Artikel darüber.
Fett
@fattire Danke. aber sie zeigen über das, was ich schon gelesen habe. Es sind jedoch gute Nachrichten.
Android-Entwickler
32
Jedes Mal, wenn ein neues Betriebssystem herauskommt, erschweren sie unser Leben ...
Phantômaxx
@Funkystein wahr. Ich wünschte, sie hätten es auf Kitkat gemacht. Jetzt gibt es drei Arten von Verhaltensweisen.
Android-Entwickler
1
@ Funkystein Ich weiß es nicht. Ich habe es vor langer Zeit benutzt. Es ist nicht so schlimm, diese Prüfung durchzuführen, denke ich. Sie müssen sich daran erinnern, dass sie auch Menschen sind, damit sie von Zeit zu Zeit Fehler machen und ihre Meinung ändern können ...
Android-Entwickler

Antworten:

143

Viele gute Fragen, lasst uns reingehen. :)

Wie benutzt man es?

Hier ist ein großartiges Tutorial für die Interaktion mit dem Storage Access Framework in KitKat:

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

Die Interaktion mit den neuen APIs in Lollipop ist sehr ähnlich. Um den Benutzer zur Auswahl eines Verzeichnisbaums aufzufordern, können Sie eine Absicht wie die folgende starten:

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

Anschließend können Sie in Ihrem onActivityResult () die vom Benutzer ausgewählte Uri an die neue DocumentFile-Hilfsklasse übergeben. Hier ist ein kurzes Beispiel, in dem die Dateien im ausgewählten Verzeichnis aufgelistet und anschließend eine neue Datei erstellt werden:

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

Der von zurückgegebene Uri DocumentFile.getUri()ist flexibel genug, um mit möglicherweise verschiedenen Plattform-APIs verwendet zu werden. Zum Beispiel könnten Sie es mit teilen Intent.setData()mit Intent.FLAG_GRANT_READ_URI_PERMISSION.

Wenn Sie über nativen Code auf diesen Uri zugreifen möchten, können Sie eine herkömmliche POSIX-Dateideskriptor-Ganzzahl aufrufen ContentResolver.openFileDescriptor()und dann verwenden ParcelFileDescriptor.getFd()oder abrufen detachFd().

Wie überprüfen Sie, ob Sie auf die Dateien / Ordner zugreifen können?

Standardmäßig werden die über Storage Access Frameworks-Absichten zurückgegebenen Uris bei Neustarts nicht beibehalten. Die Plattform "bietet" die Möglichkeit, die Berechtigung beizubehalten, aber Sie müssen die Berechtigung trotzdem "nehmen", wenn Sie dies wünschen. In unserem obigen Beispiel würden Sie anrufen:

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

Sie können jederzeit über die ContentResolver.getPersistedUriPermissions()API herausfinden, auf welche dauerhaften Berechtigungen Ihre App Zugriff hat . Wenn Sie keinen Zugriff mehr auf einen dauerhaften Uri benötigen, können Sie ihn mit freigeben ContentResolver.releasePersistableUriPermission().

Ist das auf KitKat verfügbar?

Nein, wir können älteren Versionen der Plattform nicht rückwirkend neue Funktionen hinzufügen.

Kann ich sehen, welche Apps Zugriff auf Dateien / Ordner haben?

Derzeit gibt es keine Benutzeroberfläche, die dies anzeigt. Die Details finden Sie jedoch im Abschnitt "Erteilte Uri-Berechtigungen" der adb shell dumpsys activity providersAusgabe.

Was passiert, wenn eine App für mehrere Benutzer auf demselben Gerät installiert ist?

Uri-Berechtigungsgewährungen werden wie alle anderen Funktionen der Mehrbenutzerplattform auf Benutzerbasis isoliert. Das heißt, dieselbe App, die unter zwei verschiedenen Benutzern ausgeführt wird, hat keine überlappenden oder gemeinsam genutzten Uri-Berechtigungen.

Können die Berechtigungen widerrufen werden?

Der unterstützende DocumentProvider kann die Berechtigung jederzeit widerrufen, z. B. wenn ein Cloud-basiertes Dokument gelöscht wird. Der häufigste Weg, diese widerrufenen Berechtigungen zu ermitteln, besteht darin, dass sie aus den ContentResolver.getPersistedUriPermissions()oben genannten verschwinden .

Berechtigungen werden auch widerrufen, wenn App-Daten für eine der an der Gewährung beteiligten Apps gelöscht werden.

Würde das Anfordern der Berechtigung für einen ausgewählten Ordner rekursiv funktionieren?

Ja, die ACTION_OPEN_DOCUMENT_TREEAbsicht gibt Ihnen rekursiven Zugriff auf vorhandene und neu erstellte Dateien und Verzeichnisse.

Ermöglicht dies eine Mehrfachauswahl?

Ja, seit KitKat wird die Mehrfachauswahl unterstützt, und Sie können dies zulassen, indem Sie EXTRA_ALLOW_MULTIPLEbeim Starten Ihrer ACTION_OPEN_DOCUMENTAbsicht eine Einstellung vornehmen. Sie können die Dateitypen verwenden Intent.setType()oder EXTRA_MIME_TYPESeingrenzen, die ausgewählt werden können:

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

Gibt es eine Möglichkeit für den Emulator, die neue API auszuprobieren?

Ja, das primäre gemeinsam genutzte Speichergerät sollte in der Auswahl angezeigt werden, auch auf dem Emulator. Wenn Ihre App nur das Storage Access Framework für den Zugriff auf gemeinsam genutzten Speicher verwendet, benötigen Sie die READ/WRITE_EXTERNAL_STORAGEBerechtigungen überhaupt nicht mehr und können sie entfernen oder die android:maxSdkVersionFunktion verwenden, um sie nur auf älteren Plattformversionen anzufordern.

Was passiert, wenn der Benutzer die SD-Karte durch eine andere ersetzt?

Wenn es sich um physische Medien handelt, wird die UUID (z. B. die FAT-Seriennummer) der zugrunde liegenden Medien immer in den zurückgegebenen Uri gebrannt. Das System verwendet dies, um Sie mit dem Medium zu verbinden, das der Benutzer ursprünglich ausgewählt hat, selbst wenn der Benutzer das Medium zwischen mehreren Steckplätzen austauscht.

Wenn der Benutzer eine zweite Karte eintauscht, müssen Sie aufgefordert werden, auf die neue Karte zuzugreifen. Da sich das System Zuschüsse pro UUID merkt, haben Sie weiterhin Zugriff auf die Originalkarte, wenn der Benutzer sie später erneut einlegt.

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

Jeff Sharkey
quelle
2
Aha. Die Entscheidung war also, anstatt der bekannten API (von File) mehr hinzuzufügen, eine andere zu verwenden. OK. Können Sie bitte die anderen Fragen beantworten (geschrieben im ersten Kommentar)? Übrigens, danke, dass Sie all dies beantwortet haben.
Android-Entwickler
7
@JeffSharkey Gibt es eine Möglichkeit, OPEN_DOCUMENT_TREE einen Hinweis auf den Startort zu geben? Benutzer können nicht immer am besten zu dem navigieren, auf das die Anwendung Zugriff benötigt.
Justin
2
Gibt es eine Möglichkeit, den zuletzt geänderten Zeitstempel ( setLastModified () -Methode in der Dateiklasse) zu ändern ? Es ist wirklich grundlegend für Anwendungen wie Archivierer.
Quark
1
Nehmen wir an, Sie haben ein gespeichertes Ordnerdokument Uri und möchten Dateien später nach dem Neustart des Geräts später auflisten. DocumentFile.fromTreeUri listet immer Stammdateien auf, unabhängig von der Uri, die Sie ihm geben (auch untergeordnete Uri). Wie erstellen Sie also eine DocumentFile, für die Sie Listendateien aufrufen können, bei denen die DocumentFile weder die Root-Datei noch ein SingleDocument darstellt?
AndersC
1
@JeffSharkey Wie kann dieser URI in MediaMuxer verwendet werden, da er eine Zeichenfolge als Ausgabedateipfad akzeptiert? MediaMuxer (java.lang.String, int
Petrakeas
45

In meinem unten verlinkten Android-Projekt in Github finden Sie Arbeitscode, mit dem in Android 5 auf extSdCard geschrieben werden kann. Es wird davon ausgegangen, dass der Benutzer Zugriff auf die gesamte SD-Karte gewährt und Sie dann überall auf diese Karte schreiben können. (Wenn Sie nur auf einzelne Dateien zugreifen möchten, wird es einfacher.)

Hauptcode-Schnipsel

Auslösen des Storage Access Framework:

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

Behandeln der Antwort aus dem 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);
        }
    }
}

Abrufen eines outputStreams für eine Datei über das Storage Access Framework (unter Verwendung der gespeicherten URL, vorausgesetzt, dies ist die URL des Stammordners der externen SD-Karte)

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

Dies verwendet die folgenden Hilfsmethoden:

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

Verweis auf den vollständigen Code

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

und

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

Jörg Eisfeld
quelle
1
Können Sie es bitte in ein minimiertes Projekt einfügen, das nur SD-Karten verarbeitet? Wissen Sie auch, wie ich überprüfen kann, ob alle verfügbaren externen Speicher verfügbar sind, damit ich ihre Erlaubnis nicht umsonst anfordere?
Android-Entwickler
1
Ich wünschte, ich könnte dies mehr unterstützen. Ich war auf halbem Weg zu dieser Lösung und fand die Dokumentennavigation so schrecklich, dass ich dachte, ich mache es falsch. Gut, eine Bestätigung zu haben. Danke Google ... für nichts.
Anthony
1
Ja, zum Schreiben auf einer externen SD-Karte können Sie den normalen Dateiansatz nicht mehr verwenden. Auf der anderen Seite ist File für ältere Android-Versionen und für primäre SD-Dateien immer noch der effizienteste Ansatz. Daher sollten Sie eine benutzerdefinierte Dienstprogrammklasse für den Dateizugriff verwenden.
Jörg Eisfeld
15
@ JörgEisfeld: Ich habe eine App, die File254 mal verwendet. Können Sie sich vorstellen, das zu beheben? Android wird zu einem Albtraum für Entwickler, da es keine Abwärtskompatibilität gibt. Ich habe immer noch keinen Ort gefunden, an dem sie erklären, warum Google all diese dummen Entscheidungen bezüglich des externen Speichers getroffen hat. Einige behaupten "Sicherheit", aber das ist natürlich Unsinn, da jede App den internen Speicher durcheinander bringen kann. Ich vermute, wir müssen versuchen, ihre Cloud-Dienste zu nutzen. Zum Glück löst Rooting die Probleme ... zumindest für Android <6 ...
Luis A. Florit
1
OK.
Magischerweise
0

Es ist nur eine ergänzende Antwort.

Nachdem Sie eine neue Datei erstellt haben, müssen Sie möglicherweise ihren Speicherort in Ihrer Datenbank speichern und morgen lesen. Mit dieser Methode können Sie lesen, wie Sie es erneut abrufen:

/**
 * 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.
quelle