¿Cómo utilizar la nueva API de acceso a la tarjeta SD presentada para Android 5.0(Lollipop)?
android-sdcard android-5.0-lollipop (3)
En mi proyecto de Android en Github, vinculado a continuación, puede encontrar un código de trabajo que permite escribir en extSdCard en Android 5. Supone que el usuario da acceso a toda la tarjeta SD y luego le permite escribir en todas partes en esta tarjeta. (Si desea tener acceso solo a archivos individuales, las cosas se vuelven más fáciles).
Snipplets de código principal
Activación del marco de acceso de almacenamiento:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}
Manejo de la respuesta del Marco de acceso de almacenamiento:
@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);
}
}
}
Obtener un outputStream para un archivo a través del Storage Access Framework (haciendo uso de la URL almacenada, suponiendo que esta sea la URL de la carpeta raíz de la tarjeta SD externa)
DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
getContentResolver().openOutputStream(targetDocument.getUri());
Esto usa los siguientes métodos de ayuda:
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();
}
Referencia al código completo
y
Fondo
En Android 4.4 (KitKat), Google ha restringido bastante el acceso a la tarjeta SD.
A partir de Android Lollipop (5.0), los desarrolladores pueden usar una nueva API que le pide al usuario que confirme para permitir el acceso a carpetas específicas, tal como está escrito en la publicación de este grupo de Google .
El problema
La publicación lo dirige a visitar dos sitios web:
Esto parece un ejemplo interno (tal vez para mostrarse en las demostraciones de la API más adelante), pero es bastante difícil entender lo que está sucediendo.
Esta es la documentación oficial de la nueva API, pero no brinda suficientes detalles sobre cómo usarla.
Esto es lo que te dice:
Si realmente necesita acceso completo a un subárbol completo de documentos, comience por iniciar ACTION_OPEN_DOCUMENT_TREE para permitir que el usuario elija un directorio. Luego pase el getData () resultante en fromTreeUri (Context, Uri) para comenzar a trabajar con el árbol seleccionado por el usuario.
A medida que navega por el árbol de instancias de DocumentFile, siempre puede usar getUri () para obtener el Uri que representa el documento subyacente para ese objeto, para usar con openInputStream (Uri), etc.
Para simplificar su código en dispositivos que ejecutan KITKAT o anterior, puede usar fromFile (File) que emula el comportamiento de un DocumentsProvider.
Las preguntas
Tengo algunas preguntas sobre la nueva API:
- ¿Cómo lo usas realmente?
- Según la publicación, el sistema operativo recordará que a la aplicación se le dio permiso para acceder a los archivos / carpetas. ¿Cómo verifica si puede acceder a los archivos / carpetas? ¿Hay alguna función que me devuelva la lista de archivos / carpetas a los que puedo acceder?
- ¿Cómo manejas este problema en Kitkat? ¿Es parte de la biblioteca de soporte?
- ¿Hay una pantalla de configuración en el sistema operativo que muestra qué aplicaciones tienen acceso a qué archivos / carpetas?
- ¿Qué sucede si una aplicación está instalada para múltiples usuarios en el mismo dispositivo?
- ¿Hay alguna otra documentación / tutorial sobre esta nueva API?
- Pueden los permisos ser revocados? Si es así, ¿hay algún intento de enviarlo a la aplicación?
- ¿Pediría el permiso trabajar recursivamente en una carpeta seleccionada?
- ¿El uso del permiso también permitiría al usuario la oportunidad de selección múltiple a elección del usuario? ¿O la aplicación necesita indicar específicamente la intención de qué archivos / carpetas permitir?
- ¿Hay alguna manera en el emulador de probar la nueva API? Quiero decir, tiene una partición de tarjeta SD, pero funciona como el almacenamiento externo primario, por lo que todo el acceso ya está dado (usando un permiso simple).
- ¿Qué sucede cuando el usuario reemplaza la tarjeta SD con otra?
Es solo una respuesta complementaria.
Después de crear un nuevo archivo, es posible que deba guardar su ubicación en su base de datos y leerlo mañana. Puede leer recuperarlo nuevamente utilizando este método:
/**
* 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();
}
}
Muchas buenas preguntas, profundicemos :)
¿Como lo usas?
Aquí hay un excelente tutorial para interactuar con el Marco de acceso de almacenamiento en KitKat:
https://developer.android.com/guide/topics/providers/document-provider.html#client
La interacción con las nuevas API en Lollipop es muy similar. Para solicitar al usuario que elija un árbol de directorios, puede iniciar una intención como esta:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, 42);
Luego, en su onActivityResult (), puede pasar el Uri elegido por el usuario a la nueva clase de ayuda DocumentFile. Aquí hay un ejemplo rápido que enumera los archivos en el directorio elegido y luego crea un nuevo archivo:
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();
}
}
El Uri devuelto por DocumentFile.getUri()
es lo suficientemente flexible como para ser utilizado con diferentes API de la plataforma. Por ejemplo, puede compartirlo usando Intent.setData()
con Intent.FLAG_GRANT_READ_URI_PERMISSION
.
Si desea acceder a ese Uri desde el código nativo, puede llamar a ContentResolver.openFileDescriptor()
y luego usar ParcelFileDescriptor.getFd()
o detachFd()
para obtener un entero de descriptor de archivo POSIX tradicional.
¿Cómo verifica si puede acceder a los archivos / carpetas?
De forma predeterminada, los Uris devueltos a través de las intenciones de los marcos de acceso de almacenamiento no se conservan durante los reinicios. La plataforma "ofrece" la capacidad de conservar el permiso, pero aún necesita "tomar" el permiso si lo desea. En nuestro ejemplo anterior, llamarías a:
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Siempre puede averiguar a qué subvenciones persistentes tiene acceso su aplicación a través de la API ContentResolver.getPersistedUriPermissions()
. Si ya no necesita acceder a un Uri persistente, puede liberarlo con ContentResolver.releasePersistableUriPermission()
.
¿Está disponible en KitKat?
No, no podemos agregar de manera retroactiva nuevas funcionalidades a versiones anteriores de la plataforma.
¿Puedo ver qué aplicaciones tienen acceso a archivos / carpetas?
Actualmente no hay una IU que muestre esto, pero puede encontrar los detalles en la sección "Permisos de adb shell dumpsys activity providers
concedidos" de la salida de adb shell dumpsys activity providers
de adb shell dumpsys activity providers
.
¿Qué sucede si una aplicación está instalada para múltiples usuarios en el mismo dispositivo?
Las concesiones de permisos URI están aisladas por usuario, al igual que todas las demás funcionalidades de la plataforma multiusuario. Es decir, la misma aplicación que se ejecuta con dos usuarios diferentes no tiene superposición ni otorga permisos de URI compartidos.
Pueden los permisos ser revocados?
El DocumentProvider de respaldo puede revocar el permiso en cualquier momento, como cuando se elimina un documento basado en la nube. La forma más común de descubrir estos permisos revocados es cuando desaparecen de ContentResolver.getPersistedUriPermissions()
mencionado anteriormente.
Los permisos también se revocan cuando los datos de la aplicación se borran para cualquiera de las aplicaciones involucradas en la concesión.
¿Pediría el permiso trabajar recursivamente en una carpeta seleccionada?
Sí, el intento ACTION_OPEN_DOCUMENT_TREE
le brinda acceso recursivo a los archivos y directorios existentes y los creados recientemente.
¿Esto permite una selección múltiple?
Sí, se ha admitido la selección múltiple desde KitKat, y puede permitirla estableciendo EXTRA_ALLOW_MULTIPLE
al iniciar su ACTION_OPEN_DOCUMENT
intento. Puede usar Intent.setType()
o EXTRA_MIME_TYPES
para restringir los tipos de archivos que se pueden seleccionar:
http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT
¿Hay alguna manera en el emulador de probar la nueva API?
Sí, el dispositivo de almacenamiento compartido principal debe aparecer en el selector, incluso en el emulador. Si su aplicación solo utiliza Storage Access Framework para acceder al almacenamiento compartido, ya no necesita los permisos READ/WRITE_EXTERNAL_STORAGE
y puede eliminarlos o usar la función android:maxSdkVersion
para solicitarlos solo en versiones de plataforma más antiguas.
¿Qué sucede cuando el usuario reemplaza la tarjeta SD por otra?
Cuando se trata de medios físicos, el UUID (como el número de serie FAT) del medio subyacente siempre se graba en el Uri devuelto. El sistema usa esto para conectarse con los medios que el usuario seleccionó originalmente, incluso si el usuario intercambia los medios entre múltiples ranuras.
Si el usuario intercambia una segunda tarjeta, deberá solicitar para obtener acceso a la nueva tarjeta. Como el sistema recuerda las concesiones por UUID, continuará teniendo acceso previamente otorgado a la tarjeta original si el usuario la reinicia más tarde.