web-dev-qa-db-fra.com

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

Contexte

Sur Android 4.4 (KitKat), Google a limité l'accès à la carte SD.

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

Le problème

Cet article vous invite à visiter deux sites Web:

Cela ressemble à un exemple interne (peut-être à montrer sur 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 assez de détails sur son utilisation.

Voici ce que cela vous dit:

Si vous avez réellement besoin d'un accès complet à une sous-arborescence complète de documents, lancez ACTION_OPEN_DOCUMENT_TREE pour permettre à l'utilisateur de choisir un répertoire. Ensuite, passez le getData () résultant à fromTreeUri (Context, Uri) pour commencer à utiliser 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 périphériques exécutant KitKat ou une version antérieure, vous pouvez utiliser fromFile (File), qui émule le comportement d'un fournisseur de documents.

Questions

J'ai quelques questions sur la nouvelle API:

  1. Comment l'utilisez-vous vraiment?
  2. Selon le post, le système d'exploitation se souviendra que l'application avait reçu l'autorisation d'accéder aux fichiers/dossiers. Comment vérifiez-vous 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? Est-ce une partie de la bibliothèque de support?
  4. Existe-t-il un écran de paramètres sur le système d'exploitation indiquant 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 d'autres documentations/tutoriels sur cette nouvelle API?
  7. Les autorisations peuvent-elles être révoquées? Si oui, y a-t-il une intention qui est envoyée à l'application?
  8. Demander l'autorisation fonctionnerait-il de manière récursive sur un dossier sélectionné?
  9. L'utilisation de l'autorisation permettrait-elle également de donner à l'utilisateur une possibilité de sélection multiple par choix de l'utilisateur? Ou est-ce que l'application doit spécifier spécifiquement l'intention des fichiers/dossiers à autoriser?
  10. Y a-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 stockage externe principal, donc tout accès y 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?
111

Beaucoup de bonnes questions, on va creuser. :)

Comment l'utilisez-vous?

Voici un excellent tutoriel pour interagir avec Storage Access Framework dans KitKat:

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

Interagir 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 transmettre l'URI sélectionné par l'utilisateur à la nouvelle classe d'assistance DocumentFile. Voici un exemple rapide qui répertorie les fichiers du répertoire choisi, 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 souple pour pouvoir être utilisé avec différentes API de plate-forme. 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 de code natif, vous pouvez appeler ContentResolver.openFileDescriptor() puis utiliser ParcelFileDescriptor.getFd() ou detachFd() pour obtenir un entier descripteur de fichier POSIX traditionnel.

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

Par défaut, les uris renvoyés via les intentions de Storage Access Framework sont et non persistants lors des redémarrages. La plateforme "offre" la possibilité de conserver l'autorisation, mais vous devez tout de même "prendre" l'autorisation si vous le souhaitez. Dans notre exemple ci-dessus, vous appelez:

    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'API ContentResolver.getPersistedUriPermissions(). 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 d'Uri accordées" de la sortie adb Shell dumpsys activity providers.

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

Les autorisations accordées par Uri sont isolées utilisateur par utilisateur, comme toutes les autres fonctionnalités de la plate-forme multi-utilisateurs. C'est-à-dire que la même application qui s'exécute sous deux utilisateurs différents n'a pas de permission de superposition ou partagée d'Uri.

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

DocumentProvider qui le sauvegarde peut révoquer une autorisation à tout moment, par exemple lorsqu'un document en nuage est supprimé. Le moyen le plus courant de découvrir ces autorisations révoquées consiste à les supprimer de ContentResolver.getPersistedUriPermissions() mentionné ci-dessus.

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

Demander l'autorisation fonctionnerait-il de manière récursive sur un dossier sélectionné?

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

Est-ce que cela permet 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_MULTIPLE lors du démarrage de votre ACTION_OPEN_DOCUMENT intention. Vous pouvez utiliser Intent.setType() ou EXTRA_MIME_TYPES pour limiter les types de fichiers pouvant être sélectionnés:

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

Y a-t-il un moyen sur l’émulateur d’essayer la nouvelle API?

Oui, le périphérique de stockage partagé principal doit 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 autorisations READ/WRITE_EXTERNAL_STORAGE et vous pouvez les supprimer ou utilisez la fonction Android:maxSdkVersion pour ne les demander que sur les versions antérieures de la 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 initialement sélectionné, même s'il le remplace entre plusieurs emplacements.

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

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

137
Jeff Sharkey

Dans mon projet Android dans Github, lié ci-dessous, vous pouvez trouver un code de travail permettant d'écrire sur une carte extSdCard dans Android 5. Il est supposé que l'utilisateur donne accès à l'intégralité de la carte SD et puis vous permet d'écrire partout sur cette carte. (Si vous voulez avoir accès uniquement à un seul fichier, les choses deviennent plus faciles.)

Extraits de code principal

Déclenchement de la 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);
}

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

Ceci 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

42
Jörg Eisfeld

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