Comment utiliser la nouvelle API d'accès à la carte SD présentée pour Android 5.0 (Lollipop)?

118

Contexte

Sur Android 4.4 (KitKat), Google a rendu l'accès à la carte SD assez restreint.

À partir d'Android Lollipop (5.0), les développeurs peuvent utiliser une nouvelle API qui demande à l'utilisateur de confirmer pour autoriser l'accès à des dossiers spécifiques, comme indiqué dans cet article Google-Groupes .

Le problème

Le message vous invite à visiter deux sites Web:

Cela ressemble à un exemple interne (peut-être à montrer dans les démos de l'API plus tard), mais il est assez difficile de comprendre ce qui se passe.

Ceci est la documentation officielle de la nouvelle API, mais elle ne donne pas suffisamment de détails sur la façon de l'utiliser.

Voici ce qu'il vous dit:

Si vous avez vraiment besoin d'un accès complet à un sous-arbre entier de documents, commencez par lancer ACTION_OPEN_DOCUMENT_TREE pour permettre à l'utilisateur de choisir un répertoire. Passez ensuite le getData () résultant dans fromTreeUri (Context, Uri) pour commencer à travailler avec l'arborescence sélectionnée par l'utilisateur.

Lorsque vous naviguez dans l'arborescence des instances de DocumentFile, vous pouvez toujours utiliser getUri () pour obtenir l'URI représentant le document sous-jacent pour cet objet, à utiliser avec openInputStream (Uri), etc.

Pour simplifier votre code sur les appareils exécutant KITKAT ou une version antérieure, vous pouvez utiliser fromFile (File) qui émule le comportement d'un DocumentsProvider.

Questions

J'ai quelques questions sur la nouvelle API:

  1. Comment l'utilisez-vous vraiment?
  2. Selon le message, le système d'exploitation se souviendra que l'application a reçu l'autorisation d'accéder aux fichiers / dossiers. Comment vérifier si vous pouvez accéder aux fichiers / dossiers? Existe-t-il une fonction qui me renvoie la liste des fichiers / dossiers auxquels je peux accéder?
  3. Comment gérez-vous ce problème sur Kitkat? Fait-il partie de la bibliothèque de support?
  4. Existe-t-il un écran de paramètres sur le système d'exploitation qui montre quelles applications ont accès à quels fichiers / dossiers?
  5. Que se passe-t-il si une application est installée pour plusieurs utilisateurs sur le même appareil?
  6. Existe-t-il une autre documentation / tutoriel sur cette nouvelle API?
  7. Les autorisations peuvent-elles être révoquées? Si tel est le cas, une intention est-elle envoyée à l'application?
  8. La demande d'autorisation fonctionnerait-elle de manière récursive sur un dossier sélectionné?
  9. L'utilisation de l'autorisation permettrait-elle également de donner à l'utilisateur une chance de sélection multiple au choix de l'utilisateur? Ou l'application doit-elle indiquer spécifiquement à l'intention quels fichiers / dossiers autoriser?
  10. Existe-t-il un moyen sur l'émulateur d'essayer la nouvelle API? Je veux dire, il a une partition de carte SD, mais cela fonctionne comme le stockage externe principal, donc tout accès à celui-ci est déjà donné (en utilisant une simple autorisation).
  11. Que se passe-t-il lorsque l'utilisateur remplace la carte SD par une autre?
développeur android
la source
FWIW, AndroidPolice a publié un petit article à ce sujet aujourd'hui.
fattire
@fattire Merci. mais ils montrent ce que j'ai déjà lu. Mais c'est une bonne nouvelle.
développeur android
32
Chaque fois qu'un nouveau système d'exploitation sort, il nous complique la vie ...
Phantômaxx
@Funkystein vrai. J'aimerais qu'ils l'aient fait sur Kitkat. Il existe maintenant 3 types de comportements à gérer.
développeur android
1
@Funkystein je ne sais pas. Je l'ai utilisé il y a longtemps. Ce n'est pas si mal de faire ce contrôle, je pense. Vous devez vous rappeler qu'ils sont aussi des humains, afin qu'ils puissent faire des erreurs et changer d'avis de temps en temps ...
Développeur Android

Réponses:

143

Beaucoup de bonnes questions, allons-y. :)

Comment l'utilisez-vous?

Voici un excellent didacticiel pour interagir avec le framework d'accès au stockage dans KitKat:

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

L'interaction avec les nouvelles API de Lollipop est très similaire. Pour inviter l'utilisateur à choisir une arborescence de répertoires, vous pouvez lancer une intention comme celle-ci:

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

Ensuite, dans votre onActivityResult (), vous pouvez passer l'URI sélectionné par l'utilisateur à la nouvelle classe d'assistance DocumentFile. Voici un exemple rapide qui répertorie les fichiers dans le répertoire sélectionné, puis crée un nouveau fichier:

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

L'URI renvoyé par DocumentFile.getUri()est suffisamment flexible pour être utilisé avec différentes API de plateforme. Par exemple, vous pouvez le partager en utilisant Intent.setData()avec Intent.FLAG_GRANT_READ_URI_PERMISSION.

Si vous souhaitez accéder à cet Uri à partir du code natif, vous pouvez appeler ContentResolver.openFileDescriptor()puis utiliser ParcelFileDescriptor.getFd()ou detachFd()pour obtenir un entier de descripteur de fichier POSIX traditionnel.

Comment vérifier si vous pouvez accéder aux fichiers / dossiers?

Par défaut, les URL renvoyées via les intentions Storage Access Frameworks ne sont pas conservées lors des redémarrages. La plate-forme "offre" la possibilité de conserver l'autorisation, mais vous devez toujours "prendre" l'autorisation si vous le souhaitez. Dans notre exemple ci-dessus, vous appelleriez:

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

Vous pouvez toujours déterminer les subventions persistantes auxquelles votre application a accès via l' ContentResolver.getPersistedUriPermissions()API. Si vous n'avez plus besoin d'accéder à un Uri persistant, vous pouvez le libérer avec ContentResolver.releasePersistableUriPermission().

Est-ce disponible sur KitKat?

Non, nous ne pouvons pas ajouter rétroactivement de nouvelles fonctionnalités aux anciennes versions de la plate-forme.

Puis-je voir quelles applications ont accès aux fichiers / dossiers?

Il n'y a actuellement aucune interface utilisateur qui montre cela, mais vous pouvez trouver les détails dans la section "Autorisations Uri accordées" de la adb shell dumpsys activity providerssortie.

Que se passe-t-il si une application est installée pour plusieurs utilisateurs sur le même appareil?

Les octrois d'autorisations Uri sont isolés par utilisateur, comme toutes les autres fonctionnalités de la plateforme multi-utilisateurs. Autrement dit, la même application exécutée sous deux utilisateurs différents n'a pas d'octroi d'autorisation Uri qui se chevauchent ou partagé.

Les autorisations peuvent-elles être révoquées?

Le DocumentProvider de support peut révoquer l'autorisation à tout moment, par exemple lorsqu'un document basé sur le cloud est supprimé. Le moyen le plus courant de découvrir ces autorisations révoquées est lorsqu'elles disparaissent de celles ContentResolver.getPersistedUriPermissions()mentionnées ci-dessus.

Les autorisations sont également révoquées chaque fois que les données d'application sont effacées pour l'une ou l'autre des applications impliquées dans l'octroi.

La demande d'autorisation fonctionnerait-elle de manière récursive sur un dossier sélectionné?

Oui, l' ACTION_OPEN_DOCUMENT_TREEintention vous donne un accès récursif aux fichiers et répertoires existants et nouvellement créés.

Cela permet-il une sélection multiple?

Oui, la sélection multiple est prise en charge depuis KitKat et vous pouvez l'autoriser en définissant EXTRA_ALLOW_MULTIPLElors du démarrage de votre ACTION_OPEN_DOCUMENTintention. Vous pouvez utiliser Intent.setType()ou EXTRA_MIME_TYPESpour restreindre les types de fichiers pouvant être sélectionnés:

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

Existe-t-il un moyen sur l'émulateur d'essayer la nouvelle API?

Oui, le périphérique de stockage partagé principal devrait apparaître dans le sélecteur, même sur l'émulateur. Si votre application utilise uniquement Storage Access Framework pour accéder au stockage partagé, vous n'avez plus besoin des READ/WRITE_EXTERNAL_STORAGEautorisations du tout et pouvez les supprimer ou utiliser la android:maxSdkVersionfonctionnalité pour les demander uniquement sur les anciennes versions de plate-forme.

Que se passe-t-il lorsque l'utilisateur remplace la carte SD par une autre?

Lorsqu'un support physique est impliqué, l'UUID (tel que le numéro de série FAT) du support sous-jacent est toujours gravé dans l'URI renvoyé. Le système l'utilise pour vous connecter au support que l'utilisateur a sélectionné à l'origine, même si l'utilisateur échange le support entre plusieurs emplacements.

Si l'utilisateur échange une deuxième carte, vous devrez lui demander d'accéder à la nouvelle carte. Étant donné que le système se souvient des octrois par UUID, vous continuerez à avoir précédemment accordé l'accès à la carte d'origine si l'utilisateur la réinsère plus tard.

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

Jeff Sharkey
la source
2
Je vois. la décision a donc été qu'au lieu d'ajouter plus à l'API connue (de File), d'en utiliser une différente. D'ACCORD. Pouvez-vous s'il vous plaît répondre aux autres questions (écrites dans le premier commentaire)? BTW, merci d'avoir répondu à tout cela.
développeur android
7
@JeffSharkey Un moyen de fournir à OPEN_DOCUMENT_TREE un indice d'emplacement de départ? Les utilisateurs ne sont pas toujours les meilleurs pour accéder à ce à quoi l'application a besoin d'accéder.
Justin
2
Existe-t-il un moyen, comment changer l'horodatage de la dernière modification ( méthode setLastModified () dans la classe File)? C'est vraiment fondamental pour des applications comme les archiveurs.
Quark
1
Disons que vous avez un document de dossier stocké Uri et que vous souhaitez lister les fichiers plus tard à un moment donné après le redémarrage de l'appareil. DocumentFile.fromTreeUri répertorie toujours les fichiers racine, quel que soit l'URI que vous lui donnez (même l'URI enfant), alors comment créer un DocumentFile sur lequel vous pouvez appeler listfiles, où DocumentFile ne représente ni la racine ni un SingleDocument.
AndersC
1
@JeffSharkey Comment cet URI peut-il être utilisé dans MediaMuxer, puisqu'il accepte une chaîne comme chemin du fichier de sortie? MediaMuxer (java.lang.String, int
Petrakeas
45

Dans mon projet Android dans Github, lié ci-dessous, vous pouvez trouver du code de travail qui permet d'écrire sur extSdCard sous Android 5. Il suppose que l'utilisateur donne accès à toute la carte SD et vous permet ensuite d'écrire partout sur cette carte. (Si vous ne souhaitez accéder qu'à des fichiers uniques, les choses deviennent plus faciles.)

Extraits de code principal

Déclenchement de l'infrastructure d'accès au stockage:

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

Gestion de la réponse de 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);
        }
    }
}

Obtenir un outputStream pour un fichier via Storage Access Framework (en utilisant l'URL stockée, en supposant qu'il s'agit de l'URL du dossier racine de la carte SD externe)

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

Cela utilise les méthodes d'assistance suivantes:

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

Référence au code complet

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

et

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

Jörg Eisfeld
la source
1
Pouvez-vous s'il vous plaît le mettre dans un projet réduit, qui ne gère que la carte SD? De plus, savez-vous comment puis-je vérifier si tous les stockages externes disponibles sont également accessibles, de sorte que je ne demande pas leur permission pour rien?
développeur android
1
J'aimerais pouvoir voter davantage. J'étais à mi-chemin de cette solution et j'ai trouvé la navigation dans le document si horrible que j'ai pensé que je l'avais mal fait. C'est bien d'avoir une confirmation à ce sujet. Merci Google ... pour rien.
Anthony
1
Oui, pour écrire sur SD externe, vous ne pouvez plus utiliser l'approche de fichier normale. En revanche, pour les anciennes versions d'Android et pour la SD primaire, File reste l'approche la plus efficace. Par conséquent, vous devez utiliser une classe utilitaire personnalisée pour l'accès aux fichiers.
Jörg Eisfeld
15
@ JörgEisfeld: J'ai une application qui utilise File254 fois. Pouvez-vous imaginer réparer ça ?? Android devient un cauchemar pour les développeurs avec son absence totale de rétrocompatibilité. Je n'ai toujours pas trouvé d'endroit où ils expliquent pourquoi Google a pris toutes ces décisions stupides concernant le stockage externe. Certains prétendent "sécurité", mais bien sûr, c'est absurde car n'importe quelle application peut gâcher le stockage interne. Mon hypothèse est d'essayer de nous forcer à utiliser leurs services cloud. Heureusement, l'enracinement résout les problèmes ... au moins pour Android <6 ....
Luis A. Florit
1
D'ACCORD. Comme par magie, après avoir redémarré mon téléphone, cela fonctionne :)
sancho21
0

C'est juste une réponse complémentaire.

Après avoir créé un nouveau fichier, vous devrez peut-être enregistrer son emplacement dans votre base de données et le lire demain. Vous pouvez lire le récupérer à nouveau en utilisant cette méthode:

/**
 * 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
la source