Comment accéder à toutes les cartes SD, en utilisant la nouvelle API Lollipop?

arrière-plan

à partir de Lollipop, les applications peuvent avoir accès à de véritables cartes SD (après Qu'elles N'aient pas été accessibles sur Kitkat, et n'aient pas été officiellement prises en charge et ont travaillé sur les versions précédentes), comme je l'ai demandé sur ici .

le problème

parce qu'il est devenu assez rare de voir un appareil Lollipop qui supporte la carte SD et parce que l'émulateur n'a pas vraiment la capacité (ou le fait?) de émuler un support de carte SD, il m'a fallu un certain temps pour le tester.

quoi qu'il en soit, il semble qu'au lieu d'utiliser les classes de fichier normales pour accéder à la carte SD (Une fois que vous avez obtenu une permission pour cela), vous devez utiliser Uris pour cela, en utilisant DocumentFile .

cela limite l'accès aux chemins normaux, comme je ne peux pas trouver un moyen de convertir les Uris en chemins et vice versa (en plus c'est assez ennuyeux). Cela signifie aussi que je ne sais pas comment vérifier si la carte SD/s sont accessibles, donc je ne sais pas quand à demander à l'utilisateur l'autorisation de lecture/écriture (ou de leur).

Ce que j'ai essayé

actuellement, c'est comme ça que j'obtiens les chemins vers toutes les cartes SD:

  /**
   * returns a list of all available sd cards paths, or null if not found.
   *
   * @param includePrimaryExternalStorage set to true if you wish to also include the path of the primary external storage
   */
  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
  public static List<String> getExternalStoragePaths(final Context context,final boolean includePrimaryExternalStorage)
    {
    final File primaryExternalStorageDirectory=Environment.getExternalStorageDirectory();
    final List<String> result=new ArrayList<>();
    final File[] externalCacheDirs=ContextCompat.getExternalCacheDirs(context);
    if(externalCacheDirs==null||externalCacheDirs.length==0)
      return result;
    if(externalCacheDirs.length==1)
      {
      if(externalCacheDirs[0]==null)
        return result;
      final String storageState=EnvironmentCompat.getStorageState(externalCacheDirs[0]);
      if(!Environment.MEDIA_MOUNTED.equals(storageState))
        return result;
      if(!includePrimaryExternalStorage&&VERSION.SDK_INT>=VERSION_CODES.HONEYCOMB&&Environment.isExternalStorageEmulated())
        return result;
      }
    if(includePrimaryExternalStorage||externalCacheDirs.length==1)
      {
      if(primaryExternalStorageDirectory!=null)
        result.add(primaryExternalStorageDirectory.getAbsolutePath());
      else
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[0]));
      }
    for(int i=1;i<externalCacheDirs.length;++i)
      {
      final File file=externalCacheDirs[i];
      if(file==null)
        continue;
      final String storageState=EnvironmentCompat.getStorageState(file);
      if(Environment.MEDIA_MOUNTED.equals(storageState))
        result.add(getRootOfInnerSdCardFolder(externalCacheDirs[i]));
      }
    return result;
    }


private static String getRootOfInnerSdCardFolder(File file)
  {
  if(file==null)
    return null;
  final long totalSpace=file.getTotalSpace();
  while(true)
    {
    final File parentFile=file.getParentFile();
    if(parentFile==null||parentFile.getTotalSpace()!=totalSpace)
      return file.getAbsolutePath();
    file=parentFile;
    }
  }

C'est comme ça que je vérifie quel Uris je peux atteindre:

final List<UriPermission> persistedUriPermissions=getContentResolver().getPersistedUriPermissions();

comment accéder aux cartes SD:

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

public void onActivityResult(int requestCode,int resultCode,Intent resultData)
  {
  if(resultCode!=RESULT_OK)
    return;
  Uri treeUri=resultData.getData();
  DocumentFile pickedDir=DocumentFile.fromTreeUri(this,treeUri);
  grantUriPermission(getPackageName(),treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  getContentResolver().takePersistableUriPermission(treeUri,Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  }

les questions

  1. est-il possible de vérifier si les cartes SD actuelles sont accessibles, ce qui n'est pas le cas , et de demander d'une manière ou d'une autre l'autorisation à l'utilisateur?

  2. Existe-t-il un moyen officiel de convertir entre l'uris du fichier documentaire et les chemins réels? J'ai trouvé cette réponse , mais il s'est écrasé dans mon cas, plus il semble piraté.

  3. est - il possible de demander l'autorisation de l'utilisateur sur un chemin d'accès spécifique? Peut-être même montrer juste le dialogue de "acceptez-vous oui/non ? "?

  4. est-il possible d'utiliser l'API fichiers normale au lieu de L'API DocumentFile une fois la permission accordée?

  5. avec un fichier/filepath, est-il possible de simplement Demander une permission pour y accéder (et vérifier si elle est donnée avant), ou son chemin racine ?

  6. est-il possible de faire de l'émulateur une carte SD? Actuellement, il a" SD-card " étant mentionné, mais il fonctionne comme le stockage externe primaire, et je voudrais le tester en utilisant le stockage externe secondaire, afin d'essayer d'utiliser la nouvelle API.

je pense que pour certaines de ces questions, une aide beaucoup pour répondre à l'autre.

25
demandé sur Community 2015-07-27 22:53:25

2 réponses

est-il possible de vérifier si les cartes SD actuelles sont accessibles, ce qui n'est pas le cas , et de demander d'une manière ou d'une autre l'autorisation à l'utilisateur?

il y a 3 parties à cela: détecter, quelles cartes il y a, vérifier si une carte est montée, et demander l'accès.

si les fabricants d'appareils sont de bons gars, vous pouvez obtenir la liste de stockage "externe" en appelant getExternalFiles avec null argument (voir le javadoc associé).

ce ne sont peut-être pas des types bien. Et tout le monde n'a pas la version la plus récente, la plus chaude, sans bug avec des mises à jour régulières. Donc, il peut y avoir certains répertoires, qui ne sont pas listés là (comme OTG USB storage etc.) En cas de doute, vous pouvez obtenir la liste complète des Monts OS à partir du fichier /proc/self/mounts . Ce fichier fait partie du noyau Linux, son format est documenté ici . Vous pouvez utiliser le contenu de /proc/self/mounts comme backup: analyse, recherche de systèmes de fichiers utilisables (fat, ext3, ext4, etc.) et supprimer les doublons avec la sortie de getExternalFilesDirs . Offrez les options restantes aux utilisateurs sous un nom discret, quelque chose comme"répertoires divers". Cela vous donnera tous les possibles externes, internes, peu importe le stockage jamais.


EDIT : après avoir donné le Conseil ci-dessus, j'ai finalement essayé de le suivre moi-même. Cela a bien fonctionné jusqu'à présent, mais attention que au lieu de /proc/self/mounts vous êtes mieux parsing /pros/self/mountinfo (le premier est encore disponible sous Linux moderne, mais plus tard est un remplacement meilleur, plus robuste). Assurez-vous également de tenir compte de questions d'atomicité lorsque vous faites des hypothèses basées sur le contenu de la liste des Monts.


Vous pouvez naïvement vérifier si le répertoire est accessible en lecture/écriture par l'appel de canRead et canWrite . Si cela réussit, inutile de faire du travail supplémentaire. Si elle ne le fait pas, soit vous avez une permission URI persisté ou ne pas.

"obtenir la permission" est une partie laide. AFAIK, il n'y a aucun moyen de le faire proprement au sein de l'infrastructure des forces armées soudanaises. Intent.ACTION_PICK sonne comme quelque chose que peut travailler (car il accepte un Uri, à partir de laquelle de cueillir), mais il ne le fait pas. Peut-être, cela peut être considéré comme un bug et doit être signalé à Android bug tracker en tant que tel.

avec un chemin fichier/filepath, est-il possible de simplement Demander une permission pour y accéder (et vérifier si c'est déjà donné), ou son chemin racine ?

C'est à ça que sert ACTION_PICK . Encore une fois, le picker SAF ne supporte pas ACTION_PICK hors de la boîte. Les gestionnaires de fichiers tiers peuvent le faire, mais très peu d'entre eux vous accorderont un accès réel. Vous pouvez signaler ce bug aussi, si vous en avez envie.


modifier : cette réponse a été écrite avant Nougat Android est sorti. Depuis API 24, Il est toujours impossible de demander un accès précis au répertoire spécifique, mais au moins vous pouvez demander dynamiquement l'accès à l'ensemble du volume: déterminer le volume , contenant le fichier, et demander l'accès en utilisant soit getAccessIntent avec null argument (pour les volumes secondaires) ou en demandant WRITE_EXTERNAL_STORAGE permission (pour le volume primaire).


Existe-t-il un moyen officiel de conversion entre l'uris du fichier documentaire et les chemins réels? J'ai trouvé cette réponse, mais elle s'est écrasée dans mon cas, et elle a l'air bidon.

Pas de. Jamais. Vous resterez à jamais asservi aux caprices des créateurs de cadres D'accès au stockage! mauvais rire

en fait, il y a un moyen beaucoup plus simple: il suffit d'ouvrir L'Uri et de vérifier l'emplacement du système de fichiers du descripteur créé (pour simplifier, la version de Lollipop-only):

public String getFilesystemPath(Context context, Uri uri) {
  ContentResolver res = context.getContentResolver();

  String resolved;
  try (ParcelFileDescriptor fd = res.openFileDescriptor(someSafUri, "r")) {
    final File procfsFdFile = new File("/proc/self/fd/" + fd.getFd());

    resolved = Os.readlink(procfsFdFile.getAbsolutePath());

    if (TextUtils.isEmpty(resolved)
          || resolved.charAt(0) != '/'
          || resolved.startsWith("/proc/")
          || resolved.startsWith("/fd/"))
    return null;
  } catch (Exception errnoe) {
    return null;
  }
}

si la méthode ci-dessus retourne un emplacement, vous devez quand même y accéder avec File . S'il ne renvoie pas un emplacement, L'Uri en question ne renvoie pas au fichier (même temporaire). C'est peut-être un flux réseau, un tube Unix, peu importe. Vous peut obtenir la version de la méthode ci-dessus pour les anciennes versions Android de cette réponse . Il fonctionne avec n'importe quel Uri, N'importe quel ContentProvider-pas seulement SAF-aussi longtemps que L'Uri peut être ouvert avec openFileDescriptor (par exemple, il est de Intent.CATEGORY_OPENABLE ).

Note, Cette approche ci-dessus peut être considérée comme officielle, si vous considérez n'importe quelle partie de L'API Linux officielle comme telle. Beaucoup de logiciels Linux l'utilisent, et j'ai aussi vu qu'il était utilisé par certains AOSP code (comme dans les tests de Launcher3).


modifier : Android Nougat introduit le nombre de modifications de sécurité , plus particulièrement les modifications aux permissions des répertoires privés d'application. Cela signifie que, depuis API 24, l'extrait ci-dessus sera toujours fail avec Exception lorsque L'Uri se réfère au fichier à l'intérieur d'un répertoire privé d'application. C'est comme prévu: vous n'êtes plus censé connaître ce chemin du tout . Même si vous déterminez le chemin du système de fichiers par d'autres moyens, vous ne pouvez pas accéder aux fichiers en utilisant ce chemin. Même si l'autre application coopère avec vous et change la permission du fichier en World-readable, vous ne pourrez toujours pas y accéder. C'est parce que Linux ne permet pas l'accès aux fichiers, si vous n'avez pas l'accès de recherche à l'un des répertoires dans le chemin . Réception d'un descripteur de fichier de ContentProvider est donc le seul moyen d'y accéder.


est-il possible d'utiliser l'API fichiers normale au lieu de L'API DocumentFile une fois la permission accordée?

vous ne pouvez pas. Pas sur Nougat du moins. L'API de fichier Normal va au noyau Linux pour les permissions. Selon le noyau Linux, votre carte SD externe a des permissions restrictives, qui empêchent votre application de l'utiliser. Le les permissions, données par le cadre D'accès au stockage, sont gérées par SAF (IIRC stocké dans quelques fichiers xml ) et le noyau n'en sait rien. Vous devez utiliser la partie intermédiaire (cadre D'accès au stockage) pour accéder au stockage externe. Notez que le noyau Linux possède son propre mécanisme pour gérer l'accès aux sous-répertoires (il est appelé bind-mounts ), mais les créateurs de cadres D'accès au stockage ne le savent pas ou ne veulent pas l'utiliser.

vous pouvez obtenir un certain degré d'accès (à peu près n'importe quoi, qui peut être fait avec File ) en utilisant le descripteur de fichier, créé à partir D'Uri. Je vous recommande de lire cette réponse , il peut avoir des informations utiles sur l'utilisation des descripteurs de fichiers en général et en relation avec Android.

12
répondu user1643723 2017-05-23 11:47:26

cela limite l'accès aux chemins normaux, comme je ne peux pas trouver un moyen de convertir les Uris en chemins et vice versa (en plus c'est assez ennuyeux). Cela signifie aussi que je ne sais pas comment vérifier si la carte SD/s sont accessibles, donc je ne sais pas quand à demander à l'utilisateur l'autorisation de lecture/écriture (ou de leur).

à partir de API 19 (KitKat) nonpublic Android class StorageVolume peut être utilisé pour obtenir des réponses à votre question au moyen d'une réflexion:

public static Map<String, String> getSecondaryMountedVolumesMap(Context context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Object[] volumes;

    StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
    Method getVolumeListMethod = sm.getClass().getMethod("getVolumeList");
    volumes = (Object[])getVolumeListMethod.invoke(sm);

    Map<String, String> volumesMap = new HashMap<>();
    for (Object volume : volumes) {
        Method getStateMethod = volume.getClass().getMethod("getState");
        String mState = (String) getStateMethod.invoke(volume);

        Method isPrimaryMethod = volume.getClass().getMethod("isPrimary");
        boolean mPrimary = (Boolean) isPrimaryMethod.invoke(volume);

        if (!mPrimary && mState.equals("mounted")) {
            Method getPathMethod = volume.getClass().getMethod("getPath");
            String mPath = (String) getPathMethod.invoke(volume);

            Method getUuidMethod = volume.getClass().getMethod("getUuid");
            String mUuid = (String) getUuidMethod.invoke(volume);

            if (mUuid != null && mPath != null)
                volumesMap.put(mUuid, mPath);
        }
    }
    return volumesMap;
}

cette méthode vous donne tous les stockages Non-primaires montés (y compris USB OTG) et les cartes leur UUID à leur chemin qui vous aidera à convertir DocumentUri à Chemin réel (et vise versa):

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static String convertToPath(Uri treeUri, Context context) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    String documentId = DocumentsContract.getTreeDocumentId(treeUri);
    if (documentId != null){
        String[] split = documentId.split(":");
        String uuid = null;
        if (split.length > 0)
            uuid = split[0];
        String pathToVolume = null;
        Map<String, String> volumesMap = getSecondaryMountedVolumesMap(context);
        if (volumesMap != null && uuid != null)
            pathToVolume = volumesMap.get(uuid);
        if (pathToVolume != null) {
            String pathInsideOfVolume = split.length == 2 ? IFile.SEPARATOR + split[1] : "";
            return pathToVolume + pathInsideOfVolume;
        }
    }
    return null;
}

il semble que Google ne va pas nous donner une meilleure façon de gérer avec leur laid SAF.

EDIT: semble que cette approche est inutile dans Android 6.0...

3
répondu isabsent 2016-01-25 18:07:38