Comment utiliser la nouvelle API SD card access présentée pour Android 5.0 (Lollipop)?

arrière-plan

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

à partir D'Android Lollipop (5.0), les développeurs peuvent utiliser une nouvelle API qui demande à l'utilisateur de confirmer pour permettre l'accès à des dossiers spécifiques, comme écrit sur le ce poste Google-Groups .

le problème

le post vous dirige vers visitez deux sites web:

cela ressemble à un exemple intérieur (peut-être à montrer sur les démonstrations API plus tard), mais il est assez difficile de comprendre ce qui se passe.

c'est la documentation officielle de la nouvelle API, mais elle ne donne pas assez 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-ensemble de documents, commencez par lancer ACTION_OPEN_DOCUMENT_TREE pour permettre à l'utilisateur de choisir répertoire. Puis passez le GetData () dans fromTreeUri (Context, Uri) pour commencer à travailler avec l'utilisateur sélectionné.

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 cet objet, destiné à être utilisé avec openInputStream(Uri), etc.

pour simplifier votre code sur les appareils exécutant KITKAT ou plus tôt, vous pouvez utilisation fromFile(Fichier) qui émule le comportement d'un DocumentsProvider.

les 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 a été autorisée à accéder aux fichiers/dossiers. Comment vérifier si vous pouvez accéder aux fichiers/dossiers? Est-il une fonction qui me renvoie la liste des fichiers/dossiers que je puisse y accéder?
  3. comment gérer ce problème sur Kitkat? Fait-il partie de la bibliothèque de soutien?
  4. y a-t-il un écran paramètres sur le système D'exploitation qui montre quelles applications ont accès à quels fichiers/dossiers?
  5. Ce qui se passe si une application est installée pour plusieurs utilisateurs sur le même appareil?
  6. y a-t-il d'autres documents/tutoriels sur cette nouvelle API?
  7. les autorisations peuvent-elles être révoquées? Si oui, est-il une intention envoyé à l'application?
  8. est-ce que demander la permission fonctionnerait de façon récursive sur un dossier sélectionné?
  9. en utilisant l'autorisation à donner à l'utilisateur la possibilité de sélection multiple par choix de l'utilisateur? Ou est-ce que l'application doit indiquer 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 la partition SD-card, mais il fonctionne comme le primaire externe stockage, de sorte que tout l'accès à elle est déjà donnée (en utilisant une simple permission).
  11. que se passe-t-il lorsque l'utilisateur remplace la carte SD par une autre?
103
demandé sur Peter Mortensen 2014-11-04 23:54:59

3 réponses

beaucoup de bonnes questions, nous allons creuser. :)

Comment l'utilisez-vous?

voici un excellent tutoriel pour interagir avec le cadre D'accès au stockage dans KitKat:

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

interagissant avec les nouveaux API de Lollipop est très similaire. Pour demander à l'utilisateur de choisir une arborescence de répertoires, vous peut lancer une intention comme celle-ci:

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

puis dans votre onActivityResult(), vous pouvez passer L'Uri choisi par l'utilisateur à la nouvelle classe D'aide DocumentFile. Voici un exemple rapide qui liste 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 retourné par DocumentFile.getUri() est suffisamment flexible pour être utilisé avec des API de plate-forme différentes. Par exemple, vous pouvez le partager en utilisant Intent.setData() avec Intent.FLAG_GRANT_READ_URI_PERMISSION .

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

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

par défaut, les Uris retournés par le biais des cadres D'accès au stockage sont et non ont persisté lors des redémarrages. La plate-forme "offre" la capacité à persister l'autorisation, mais vous avez encore besoin de "prendre" la permission 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 comprendre ce qui persiste subventions votre application a accès à travers L'API ContentResolver.getPersistedUriPermissions() . Si vous n'avez plus besoin d'accéder à un Uri persisté, 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 les 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 sortie adb shell dumpsys activity providers .

Ce qui se passe si une application est installée pour plusieurs utilisateurs sur le même appareil?

Uri autorisation des subventions sont isolés sur une base par utilisateur, tout comme tous les autres multi-utilisateur plate-forme de fonctionnalité. C'est-à-dire que la même application fonctionnant sous deux utilisateurs différents n'a pas de permissions Uri superposées ou partagées.

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

le DocumentProvider de soutien peut révoquer l'autorisation à tout moment, par exemple lorsqu'un document basé sur le cloud est supprimé. La façon la plus commune de découvrir ces permissions révoquées est quand ils disparaissent de ContentResolver.getPersistedUriPermissions() mentionné ci-dessus.

Les Permissions

sont aussi révoqués lorsque les données de l'application est désactivée pour application de l'octroi.

est-ce que demander la permission fonctionnerait de façon récursive sur un dossier sélectionné?

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

est-ce que cela permet la sélection multiple?

Ouais, sélection multiple a été pris en charge depuis KitKat, et vous pouvez vous le permettre en plaçant EXTRA_ALLOW_MULTIPLE au début de votre ACTION_OPEN_DOCUMENT intention. Vous pouvez utiliser Intent.setType() ou EXTRA_MIME_TYPES pour réduire les types de fichiers qui peuvent être choisis:

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?

Yep, le principal dispositif de stockage partagé devrait apparaître dans le picker, même sur le l'émulateur. Si votre application utilise uniquement le cadre D'accès au stockage pour accéder au stockage partagé, vous n'avez plus besoin des READ/WRITE_EXTERNAL_STORAGE autorisations du tout et pouvez les supprimer ou utiliser la fonctionnalité android:maxSdkVersion 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'il s'agit d'un support physique, L'UUID (tel que le numéro de série FAT) du support sous-jacent est toujours brûlé dans L'Uri retourné. Le système utilise ceci pour vous connecter au média que l'Utilisateur a sélectionné à l'origine, même si l'utilisateur change le média entre plusieurs slots.

Si l'utilisateur swaps dans une deuxième carte, vous aurez besoin de invite pour accéder à la nouvelle carte. Puisque le système se souvient des subventions sur une base par UUID, vous continuerez à avoir accordé l'accès à la carte originale si l'utilisateur la réinsère plus tard.

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

130
répondu Jeff Sharkey 2014-11-07 18:11:33

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

code principal snipplets

déclenchement du cadre 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);
}

traitant de la réponse de l'Accès au Stockage-Cadre:

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

obtention d'un outputStream pour un fichier via le cadre D'accès au stockage (en utilisant l'URL stockée, en supposant que ce soit 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'aide 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

40
répondu Jörg Eisfeld 2016-10-19 17:22:31

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

après avoir créé un nouveau fichier, vous aurez peut-être besoin de sauvegarder son emplacement dans votre base de données et de 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
répondu Anggrayudi H 2018-05-11 20:33:41