¿Cómo obtener acceso a todas las tarjetas SD, utilizando la nueva Lollipop API?

fondo

A partir de Lollipop, las aplicaciones pueden tener acceso a tarjetas SD reales (después de que no era accesible en Kitkat, y no estaba oficialmente soportado, pero funcionaba en las versiones anteriores), como he preguntado aquí .

El problema

Debido a que se ha vuelto bastante raro ver un dispositivo Lollipop que admita tarjetas SD y porque el emulador realmente no tiene la capacidad (o no?) De emular un soporte de tarjeta SD, me tomó bastante tiempo probarlo .

De todos modos, parece que en lugar de usar las clases de archivos normales para acceder a la tarjeta SD (una vez que tienes un permiso para ello), necesitas usar Uris para ello, usando DocumentFile.

Esto limita el acceso a los caminos normales, ya que no puedo encontrar una manera de convertir el Uris a caminos y viceversa (además es bastante molesto). También significa que no sé cómo comprobar si la tarjeta SD actual / s son accesibles, por lo que no sé cuándo pedir permiso al usuario para leer / escribir (o para ellos).

Lo que he intentado

En la actualidad, así es como obtengo las rutas de todas las tarjetas 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; } } 

Así es como compruebo lo que Uris puedo alcanzar:

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

Así es como obtener acceso a las tarjetas 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); } 

Las preguntas

  1. ¿Es posible comprobar si las tarjetas SD actuales son accesibles, que no son, y de alguna manera pedir al usuario obtener permiso para ellos?

  2. ¿Existe una forma oficial para convertir entre el DocumentFile uris y caminos reales? He encontrado esta respuesta , pero se estrelló en mi caso, además de que parece hack-y.

  3. ¿Es posible solicitar permiso del usuario sobre una ruta específica? Tal vez incluso mostrar sólo el diálogo de "¿aceptas sí / no?"?

  4. ¿Es posible utilizar la API de archivos normal en lugar de la API DocumentFile una vez que se concedió el permiso?

  5. Dado un file / filepath, ¿es posible solicitar un permiso para acceder a él (y comprobar si se ha dado antes), o su ruta raíz?

  6. ¿Es posible hacer que el emulador tenga una tarjeta SD? Actualmente, se ha mencionado "tarjeta SD", pero funciona como el almacenamiento externo principal, y me gustaría probarlo usando el almacenamiento externo secundario, con el fin de tratar de usar la nueva API.

Creo que para algunas de esas preguntas, uno ayuda mucho a responder al otro.

¿Es posible comprobar si las tarjetas SD actuales son accesibles, que no son, y de alguna manera pedir al usuario obtener permiso para ellos?

Hay 3 partes para esto: detectar, qué tarjetas hay, comprobar si una tarjeta está montada, y pedir acceso.

Si los fabricantes de dispositivos son buenos chicos, puede obtener una lista de almacenamiento "externo" llamando a getExternalFiles con argumento null (consulte el javadoc asociado).

Puede que no sean buenos chicos. Y no todo el mundo tiene más reciente, más caliente, sin errores de la versión del sistema operativo con actualizaciones regulares. Así que puede haber algunos directorios, que no se enumeran allí (tales como almacenamiento OTG USB, etc) Siempre en duda, puede obtener la lista completa de montajes de OS de archivo /proc/self/mounts . Este archivo es parte del kernel de Linux, su formato está documentado aquí . Puede utilizar el contenido de /proc/self/mounts como copia de seguridad: analizarla, buscar sistemas de archivos utilizables (grasa, ext3, ext4, etc.) y eliminar los duplicados con la salida de getExternalFilesDirs . Ofrecer las opciones restantes a los usuarios con un nombre no intrusivo, algo así como "directorios misceláneos". Eso le dará todos los posibles externos, internos, cualquier almacenamiento nunca.


EDIT : después de dar el consejo anterior, finalmente he tratado de seguirlo yo mismo. Funcionó muy bien hasta ahora, pero ten cuidado de que en lugar de /proc/self/mounts es mejor analizar /pros/self/mountinfo (el anterior todavía está disponible en Linux moderno, pero más tarde es un reemplazo mejor y más robusto). También asegúrese de tener en cuenta los problemas de atomicidad al hacer suposiciones basadas en el contenido de la lista de montajes.


Usted puede ingenuamente comprobar, si el directorio es legible / escribible llamando canRead y canWrite en él. Si esto tiene éxito, no tiene sentido hacer un trabajo extra. Si no lo hace, usted tiene un permiso Uri persistente o no.

"Obtener permiso" es una parte fea. AFAIK, no hay manera de hacerlo con claridad dentro de la infraestructura SAF. Intent.ACTION_PICK suena como algo que puede funcionar (ya que acepta un Uri, del cual escoger), pero no lo hace. Tal vez, esto puede ser considerado un error y debe ser reportado a Android bug tracker como tal.

Dado un file / filepath, ¿es posible solicitar un permiso para acceder a él (y comprobar si se ha dado antes), o su ruta raíz?

Esto es para lo que ACTION_PICK . Una vez más, el selector SAF no admite ACTION_PICK fuera de la casilla. Los administradores de archivos de terceros pueden, pero muy pocos de ellos realmente le concederán acceso real. También puede reportar esto como un error, si lo desea.


EDIT : esta respuesta fue escrita antes de Android Nougat salió. A partir de la API 24, todavía es imposible solicitar un acceso de gran precisión al directorio específico, pero al menos puede solicitar dinámicamente el acceso a todo el volumen: determine el volumen , contenga el archivo y solicite el acceso utilizando getAccessIntent con argumento null (Para volúmenes secundarios) o solicitando el permiso WRITE_EXTERNAL_STORAGE (para el volumen primario).


¿Existe una forma oficial para convertir entre el DocumentFile uris y caminos reales? He encontrado esta respuesta, pero se estrelló en mi caso, además de que parece hack-y.

No nunca. ¡Permanecerás siempre subordinado a los caprichos de los creadores de Storage Access Framework! Risa malvada

En realidad, hay una manera mucho más sencilla: simplemente abra el Uri y compruebe la localización del sistema de archivos del descriptor creado (por simplicidad, solo en la versión Lollipop):

 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 el método anterior devuelve una ubicación, todavía necesita acceso para usarla con File . Si no devuelve un lugar, el Uri en cuestión no hace referencia a archivo (ni siquiera temporal). Puede ser un flujo de red, Unix pipe, lo que sea. Puedes obtener la versión del método anterior para versiones anteriores de Android de esta respuesta . Funciona con cualquier Uri, cualquier ContentProvider, no sólo SAF, siempre y cuando el Uri se pueda abrir con openFileDescriptor (por ejemplo, es de Intent.CATEGORY_OPENABLE ).

Tenga en cuenta, que el enfoque anterior puede considerarse oficial, si considera cualquier parte de la API oficial de Linux como tal. Una gran cantidad de software de Linux lo utiliza, y también he visto que sea utilizado por algunos AOSP código (como en Launcher3 pruebas).


EDIT : Android Nougat introdujo el número de cambios de seguridad , sobre todo los cambios a los permisos de los directorios privados de la aplicación. Esto significa que desde el API 24 el fragmento anterior siempre fallará con Exception cuando el Uri se refiere al archivo dentro de un directorio privado de la aplicación. Esto es lo que se pretende: ya no se espera que conozcan ese camino en absoluto . Incluso si de alguna manera determina la ruta del sistema de archivos por otros medios, no puede acceder a los archivos que utilizan esa ruta. Incluso si la otra aplicación coopera con usted y cambia el permiso del archivo a ser legible en todo el mundo, todavía no podrá acceder a él. Esto se debe a que Linux no permite el acceso a los archivos, si no tiene acceso de búsqueda a uno de los directorios en la ruta . Recibir un descriptor de archivo de ContentProvider es la única forma de acceder a ellos.


¿Es posible utilizar la API de archivos normal en lugar de la API DocumentFile una vez que se concedió el permiso?

No puedes. Por lo menos en Nougat. La API normal del archivo va al kernel de Linux para los permisos. Según el kernel de Linux, su tarjeta SD externa tiene permisos restrictivos, que impiden que su aplicación lo utilice. Los permisos dados por Storage Access Framework son gestionados por SAF (IIRC almacenado en algunos archivos xml ) y el kernel no sabe nada sobre ellos. Tienes que usar la parte intermedia (Storage Access Framework) para obtener acceso al almacenamiento externo. Tenga en cuenta que el kernel de Linux tiene su propio mecanismo para administrar el acceso a los subárboles de directorios (se llama bind-mounts ), pero los creadores de Access Access Framework no lo saben o no quieren usarlo.

Usted puede obtener algún grado de acceso (prácticamente cualquier cosa, que se puede hacer con el File ) mediante el descriptor de archivos, creado a partir de Uri. Le recomiendo que lea esta respuesta , puede tener alguna información útil sobre el uso de desriptores de archivos en general y en relación con Android.

Esto limita el acceso a los caminos normales, ya que no puedo encontrar una manera de convertir el Uris a caminos y viceversa (además es bastante molesto). También significa que no sé cómo comprobar si la tarjeta SD actual / s son accesibles, por lo que no sé cuándo pedir permiso al usuario para leer / escribir (o para ellos).

A partir de API 19 (KitKat) no pública Android clase StorageVolume puede ser utilizado para obtener algunas respuestas a su pregunta mediante una reflexión:

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

Este método le proporciona todos los almacenes no primarios montados (incluyendo USB OTG) y asigna su UUID a su ruta de acceso que le ayudará a convertir DocumentUri en ruta real (y viceversa):

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

Parece que Google no va a darnos mejor manera de manejar con su fea SAF.

EDIT: Parece que este enfoque es inútil en Android 6.0 …

  • Recuperación de datos del último evento agregado del calendario android
  • ¿Cómo puedo verificar que la imagen URI es válida en Android?
  • Obtener orientación de la imagen de MediaStore.Images.Media.DATA
  • Obtener datos Exif usando Uri
  • URI personalizado esquemas para el Facebook Messenger
  • Abrir navegador con una url con encabezados adicionales para Android
  • ¿Cómo puedo eliminar archivos mediante programación en Android?
  • Cambiar URI de MediaPlayer
  • Android Wear elimina datos de DataApi con deleteDataItems
  • "La actividad exportada no requiere permiso" al intentar iniciar desde un URI
  • Proveedor de contenido grant uri permission
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.