tarjeta samsung que prime plus moto micro memoria lollipop las imágenes imagenes hacer guarden guardar grand fotos externa cómo comprobando como android file uri android-sdcard documentfile

android - samsung - ¿Cómo obtener acceso a todas las tarjetas SD utilizando la nueva API de Lollipop?



guardar fotos en la tarjeta sd en android 5.0 lollipop (2)

fondo

A partir de Lollipop, las aplicaciones pueden obtener acceso a tarjetas SD reales (una vez que no estaba disponible en Kitkat y que no tenían soporte oficial, pero que funcionaban en versiones anteriores), como he preguntado here .

El problema

Debido a que es bastante raro ver un dispositivo Lollipop que admita tarjetas SD y porque el emulador realmente no tiene la capacidad (¿o sí?) Para emular una compatibilidad con tarjetas 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 ella, usando DocumentFile.

Esto limita el acceso a las rutas normales, ya que no puedo encontrar una manera de convertir los Uris en rutas y viceversa (además, es bastante molesto). También significa que no sé cómo verificar si las tarjetas SD actuales están accesibles, por lo que no sé cuándo pedirle al usuario permiso para leer o escribir en ellas (o en ellas).

Lo que he intentado

Actualmente, así es como obtengo las rutas a 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 a qué Uris puedo llegar:

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

Así es como acceder 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 verificar si las tarjetas SD actuales son accesibles, cuáles no, y de alguna manera pedirle al usuario que obtenga su permiso?

  2. ¿Hay una forma oficial de convertir entre los uris DocumentFile y las rutas 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 muestre solo el diálogo de "¿acepta sí / no?"?

  4. ¿Es posible usar la API de archivo normal en lugar de la API de DocumentFile una vez que se otorgó el permiso?

  5. Dado un archivo / ruta de archivo, ¿es posible simplemente solicitar un permiso para acceder a él (y verificar si se ha otorgado anteriormente), o su ruta raíz?

  6. ¿Es posible hacer que el emulador tenga una tarjeta SD? Actualmente, se menciona "tarjeta SD", pero funciona como el almacenamiento externo principal, y me gustaría probarlo utilizando el almacenamiento externo externo para probar y utilizar la nueva API.

Creo que para algunas de esas preguntas, una ayuda mucho para responder la otra.


¿Es posible verificar si las tarjetas SD actuales son accesibles, cuáles no, y de alguna manera pedirle al usuario que obtenga su permiso?

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

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

Puede que no sean buenos chicos. Y no todos tienen la versión más nueva, más caliente y libre de errores con actualizaciones regulares. Por lo tanto, es posible que existan algunos directorios que no aparecen en la lista (como el almacenamiento USB OTG, etc.) En caso de duda, puede obtener una lista completa de los montajes del SO desde el archivo /proc/self/mounts . Este archivo es parte del kernel de Linux, su formato está documentado here . Puede usar el contenido de /proc/self/mounts como copia de seguridad: analizarlo, encontrar sistemas de archivos utilizables (fat, ext3, ext4, etc.) y eliminar duplicados con la salida de getExternalFilesDirs . Ofrezca las opciones restantes a los usuarios bajo algún nombre no intrusivo, algo así como "directorios misceláneos". Eso te dará todo el almacenamiento externo posible, interno, que sea.

EDITAR : después de dar el consejo anterior, finalmente he tratado de seguirlo yo mismo. Hasta el momento funcionó bien, pero tenga en cuenta que en lugar de /proc/self/mounts está mejor analizando /pros/self/mountinfo (el primero 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 realizar suposiciones basadas en el contenido de la lista de montajes.

Puede verificar ingenuamente , si el directorio es legible / escribible llamando a canRead y canWrite en él. Si esto tiene éxito, no tiene sentido hacer trabajo extra. Si no es así, tienes un permiso Uri persistente o no.

"Obtener permiso" es una parte fea. AFAIK, no hay manera de hacerlo limpiamente dentro de la infraestructura de SAF. Intent.ACTION_PICK suena como algo que puede funcionar (ya que acepta un Uri, desde el cual elegir), pero no lo hace. Tal vez, esto se puede considerar un error y se debe informar al rastreador de errores de Android como tal.

Dado un archivo / ruta de archivo, ¿es posible simplemente solicitar un permiso para acceder a él (y verificar si se ha otorgado anteriormente), o su ruta raíz?

Esto es para lo que ACTION_PICK es. Nuevamente, el selector SAF no admite ACTION_PICK fuera de la caja. Los administradores de archivos de terceros pueden, pero muy pocos de ellos realmente le otorgarán acceso real. También puedes reportar esto como un error, si te apetece.

EDITAR : esta respuesta fue escrita antes de que saliera Android Nougat. A partir de la API 24, todavía es imposible solicitar un acceso detallado al directorio específico, pero al menos puede solicitar acceso dinámico a todo el volumen: determine el volumen , que contiene el archivo, y solicite el acceso usando cualquiera de null argumentos getAccessIntent con argumento null (para volúmenes secundarios) o solicitando el permiso WRITE_EXTERNAL_STORAGE (para el volumen primario).

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

No nunca. ¡Siempre permanecerá sometido a los caprichos de los creadores de Storage Access Framework! risa malvada

En realidad, hay una forma mucho más sencilla: simplemente abra Uri y verifique la ubicación del sistema de archivos del descriptor creado (por simplicidad, versión solo para 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, aún necesita acceso para usarlo con File . Si no devuelve una ubicación, el Uri en cuestión no se refiere al archivo (incluso uno temporal). Puede ser un flujo de red, Unix pipe, lo que sea. Puede obtener la versión del método anterior para versiones anteriores de Android a partir de esta respuesta . Funciona con cualquier Uri, cualquier ContentProvider, no solo SAF, siempre que se pueda abrir con openFileDescriptor (por ejemplo, es de Intent.CATEGORY_OPENABLE ).

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

EDITAR : Android Nougat introdujo varios cambios de seguridad , especialmente los cambios en los permisos de los directorios privados de las aplicaciones. Esto significa que, desde la API 24, el fragmento de código anterior siempre fallará con Excepción cuando Uri haga referencia al archivo dentro de un directorio privado de la aplicación. Esto es como se esperaba: ya no se espera que conozcas 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 utilizando esa ruta. Incluso si la otra aplicación coopera con usted y cambia el permiso de archivo a legible para todo el mundo, 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 usar la API de archivo normal en lugar de la API de DocumentFile una vez que se otorgó el permiso?

No se puede. Al menos no en Nougat. La API de archivo normal va al kernel de Linux para permisos. Según el kernel de Linux, su tarjeta SD externa tiene permisos restrictivos que impiden que su aplicación la use. Los permisos, otorgados por Storage Access Framework, son administrados por SAF (IIRC almacenado en algunos archivos xml ) y el kernel no sabe nada sobre ellos. Debe utilizar 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 Storage Access Framework no lo saben o no quieren usarlo.

Puede obtener cierto grado de acceso (casi cualquier cosa, que se puede hacer con File ) usando un descriptor de archivos, creado a partir de Uri. Le recomiendo que lea esta respuesta , puede tener alguna información útil sobre el uso de descriptores de archivos en general y en relación con Android.


Esto limita el acceso a las rutas normales, ya que no puedo encontrar una manera de convertir los Uris en rutas y viceversa (además, es bastante molesto). También significa que no sé cómo verificar si las tarjetas SD actuales están accesibles, por lo que no sé cuándo pedirle al usuario permiso para leer o escribir en ellas (o en ellas).

A partir de la clase de Android no pública API 19 (KitKat), StorageVolume puede usarse para obtener algunas respuestas a su pregunta por medio de 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 brinda todos los almacenamientos no primarios montados (incluido USB OTG) y asigna su UUID a su ruta que le ayudará a convertir DocumentUri a una 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 nos va a dar una mejor manera de manejar su feo SAF.

EDITAR: Parece que este enfoque es inútil en Android 6.0 ...