Únete a ⁠ #Android11: The Beta Launch Show el 3 de junio.

Cómo compartir un archivo

Una vez que hayas configurado tu app para compartir archivos mediante URI de contenido, puedes responder a las solicitudes que otras apps hacen de esos archivos. Una forma de hacerlos es proporcionar una interfaz de selección de archivos desde la app de servidor que otras apps puedan invocar. Este enfoque hace posible que una app cliente permita a los usuarios seleccionar un archivo de la app de servidor y, luego, recibir el URI de contenido del archivo seleccionado.

En esta lección, se muestra cómo crear una Activity de selección de archivos en una app que responda a solicitudes de archivos.

Cómo recibir solicitudes de archivos

Para recibir solicitudes de archivos de apps cliente y responder con un URI de contenido, tu app debe proporcionar una Activity de selección de archivos. Las apps cliente inician esta Activity llamando a startActivityForResult() con un Intent que contiene la acción ACTION_PICK. Cuando la app cliente llama a startActivityForResult(), tu app puede mostrar un resultado a la app cliente, en forma de URI de contenido para el archivo que seleccionó el usuario.

Para obtener información sobre cómo implementar la solicitud de un archivo en una app cliente, consulta la lección Cómo solicitar un archivo compartido.

Cómo crear una actividad de selección de archivos

Para configurar la Activity de selección de archivos, comienza especificando la Activity en tu manifiesto, junto con un filtro de intents que coincida con la acción ACTION_PICK y las categorías CATEGORY_DEFAULT y CATEGORY_OPENABLE. Agrega también filtros de tipo de MIME para los archivos que tu app entrega a otras. En el siguiente fragmento, se muestra cómo especificar el nuevo filtro de Activity y de intents:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        ...
            <application>
            ...
                <activity
                    android:name=".FileSelectActivity"
                    android:label="@File Selector" >
                    <intent-filter>
                        <action
                            android:name="android.intent.action.PICK"/>
                        <category
                            android:name="android.intent.category.DEFAULT"/>
                        <category
                            android:name="android.intent.category.OPENABLE"/>
                        <data android:mimeType="text/plain"/>
                        <data android:mimeType="image/*"/>
                    </intent-filter>
                </activity>

Cómo definir la actividad de selección de archivos en el código

A continuación, define una subclase de Activity que muestre los archivos disponibles del directorio files/images/ de tu app en el almacenamiento interno y que le permita al usuario elegir el archivo deseado. En el siguiente fragmento, se muestra cómo definir esta Activity y responder a la selección del usuario:

Kotlin

    class MainActivity : Activity() {

        // The path to the root of this app's internal storage
        private lateinit var privateRootDir: File
        // The path to the "images" subdirectory
        private lateinit var imagesDir: File
        // Array of files in the images subdirectory
        private lateinit var imageFiles: Array<File>
        // Array of filenames corresponding to imageFiles
        private lateinit var imageFilenames: Array<String>

        // Initialize the Activity
        override fun onCreate(savedInstanceState: Bundle?) {
            ...
            // Set up an Intent to send back to apps that request a file
            resultIntent = Intent("com.example.myapp.ACTION_RETURN_FILE")
            // Get the files/ subdirectory of internal storage
            privateRootDir = filesDir
            // Get the files/images subdirectory;
            imagesDir = File(privateRootDir, "images")
            // Get the files in the images subdirectory
            imageFiles = imagesDir.listFiles()
            // Set the Activity's result to null to begin with
            setResult(Activity.RESULT_CANCELED, null)
            /*
             * Display the file names in the ListView fileListView.
             * Back the ListView with the array imageFilenames, which
             * you can create by iterating through imageFiles and
             * calling File.getAbsolutePath() for each File
             */
            ...
        }
        ...
    }
    

Java

    public class MainActivity extends Activity {
        // The path to the root of this app's internal storage
        private File privateRootDir;
        // The path to the "images" subdirectory
        private File imagesDir;
        // Array of files in the images subdirectory
        File[] imageFiles;
        // Array of filenames corresponding to imageFiles
        String[] imageFilenames;
        // Initialize the Activity
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            ...
            // Set up an Intent to send back to apps that request a file
            resultIntent =
                    new Intent("com.example.myapp.ACTION_RETURN_FILE");
            // Get the files/ subdirectory of internal storage
            privateRootDir = getFilesDir();
            // Get the files/images subdirectory;
            imagesDir = new File(privateRootDir, "images");
            // Get the files in the images subdirectory
            imageFiles = imagesDir.listFiles();
            // Set the Activity's result to null to begin with
            setResult(Activity.RESULT_CANCELED, null);
            /*
             * Display the file names in the ListView fileListView.
             * Back the ListView with the array imageFilenames, which
             * you can create by iterating through imageFiles and
             * calling File.getAbsolutePath() for each File
             */
             ...
        }
        ...
    }
    

Cómo responder a una selección de archivo

Una vez que un usuario selecciona un archivo compartido, tu app debe determinar qué archivo se seleccionó y, luego, generar un URI de contenido correspondiente. Dado que la Activity muestra la lista de archivos disponibles en una ListView, cuando el usuario hace clic en el nombre de un archivo, el sistema llama al método onItemClick(), en el que puedes obtener el archivo seleccionado.

Cuando usas un intent para enviar el URI de un archivo de una app a otra, debes asegurarte de obtener un URI que puedan leer otras apps. En dispositivos que ejecutan Android 6.0 (API nivel 23) y versiones posteriores, se requiere cuidado especial debido a los cambios realizados en el modelo de permisos de esa versión de Android, específicamente en el hecho de que se convirtió READ_EXTERNAL_STORAGE en un permiso riesgoso, que la app receptora podría no tener.

Te recomendamos que tengas eso en cuenta y evites usar Uri.fromFile(), que presenta varios inconvenientes. Este método tiene las siguientes características:

  • No permite compartir archivos entre perfiles.
  • Requiere que tu app tenga el permiso WRITE_EXTERNAL_STORAGE en dispositivos que ejecuten Android 4.4 (API nivel 19) o versiones anteriores.
  • Requiere que las apps receptoras tengan el permiso READ_EXTERNAL_STORAGE, que no se aplicará en objetivos compartidos importantes, como Gmail, que no tengan ese permiso.

En lugar de usar Uri.fromFile(), puedes usar los permisos de URI para permitir que otras apps tengan acceso a URI específicos. Si bien los permisos de URI no funcionan en URI de file:// generados por Uri.fromFile() , sí funcionan en URI asociados con proveedores de contenido. Puedes usar la API de FileProvider como ayuda para crear estos URI. Este enfoque también funciona con archivos que no están en un almacenamiento externo, sino en el almacenamiento local de la app que envía el intent.

En onItemClick(), obtén un objeto File para el nombre del archivo seleccionado y pásalo como argumento a getUriForFile(), junto con la autoridad que especificaste en el elemento <provider> de FileProvider. El URI de contenido resultante contiene la autoridad, un segmento de ruta correspondiente al directorio del archivo (como se especifica en los metadatos XML) y el nombre del archivo, incluida la extensión. En la sección Cómo especificar directorios para compartir, se describe el modo en que FileProvider asigna directorios a segmentos de ruta basados en metadatos XML.

En el siguiente fragmento, se muestra cómo detectar el archivo seleccionado y obtener un URI de contenido para él:

Kotlin

        override fun onCreate(savedInstanceState: Bundle?) {
            ...
            // Define a listener that responds to clicks on a file in the ListView
            fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
                /*
                 * Get a File for the selected file name.
                 * Assume that the file names are in the
                 * imageFilename array.
                 */
                val requestFile = File(imageFilenames[position])
                /*
                 * Most file-related method calls need to be in
                 * try-catch blocks.
                 */
                // Use the FileProvider to get a content URI
                val fileUri: Uri? = try {
                    FileProvider.getUriForFile(
                            this@MainActivity,
                            "com.example.myapp.fileprovider",
                            requestFile)
                } catch (e: IllegalArgumentException) {
                    Log.e("File Selector",
                            "The selected file can't be shared: $requestFile")
                    null
                }
                ...
            }
            ...
        }
    

Java

        protected void onCreate(Bundle savedInstanceState) {
            ...
            // Define a listener that responds to clicks on a file in the ListView
            fileListView.setOnItemClickListener(
                    new AdapterView.OnItemClickListener() {
                @Override
                /*
                 * When a filename in the ListView is clicked, get its
                 * content URI and send it to the requesting app
                 */
                public void onItemClick(AdapterView<?> adapterView,
                        View view,
                        int position,
                        long rowId) {
                    /*
                     * Get a File for the selected file name.
                     * Assume that the file names are in the
                     * imageFilename array.
                     */
                    File requestFile = new File(imageFilename[position]);
                    /*
                     * Most file-related method calls need to be in
                     * try-catch blocks.
                     */
                    // Use the FileProvider to get a content URI
                    try {
                        fileUri = FileProvider.getUriForFile(
                                MainActivity.this,
                                "com.example.myapp.fileprovider",
                                requestFile);
                    } catch (IllegalArgumentException e) {
                        Log.e("File Selector",
                              "The selected file can't be shared: " + requestFile.toString());
                    }
                    ...
                }
            });
            ...
        }
    

Recuerda que solo puedes generar URI de contenido para archivos que se encuentren en un directorio que hayas especificado en el archivo de metadatos que contiene el elemento <paths>, como se describe en la sección Cómo especificar directorios para compartir. Si llamas a getUriForFile() para un File que está en una ruta que no especificaste, recibirás un error IllegalArgumentException.

Cómo otorgar permisos para el archivo

Ahora que tienes un URI de contenido para el archivo que deseas compartir con otra app, debes permitir que la app cliente acceda al archivo. Para permitir el acceso y otorgarle permisos a la app cliente, agrega el URI de contenido a un Intent y, luego, establece marcas de permiso en el Intent. Los permisos que otorgas son temporales y vencerán automáticamente cuando finalice la pila de tareas de la app receptora.

En el siguiente fragmento de código, se muestra cómo configurar el permiso de lectura para el archivo:

Kotlin

        override fun onCreate(savedInstanceState: Bundle?) {
            ...
            // Define a listener that responds to clicks on a file in the ListView
            fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
                ...
                if (fileUri != null) {
                    // Grant temporary read permission to the content URI
                    resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    ...
                }
                ...
            }
            ...
        }
    

Java

        protected void onCreate(Bundle savedInstanceState) {
            ...
            // Define a listener that responds to clicks in the ListView
            fileListView.setOnItemClickListener(
                    new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> adapterView,
                        View view,
                        int position,
                        long rowId) {
                    ...
                    if (fileUri != null) {
                        // Grant temporary read permission to the content URI
                        resultIntent.addFlags(
                            Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    }
                    ...
                 }
                 ...
            });
        ...
        }
    

Precaución: Llamar a setFlags() es la única forma de otorgar acceso a tus archivos de manera segura mediante permisos de acceso temporal. Evita llamar al método Context.grantUriPermission() para el URI de contenido de un archivo, ya que este método otorga acceso que solo puedes revocar llamando a Context.revokeUriPermission().

No uses Uri.fromFile(), ya que obliga a las aplicaciones receptoras a tener el permiso READ_EXTERNAL_STORAGE, no funciona en absoluto si estás intentando compartir entre usuarios y, en versiones de Android inferiores a 4.4 (API nivel 19), requiere que tu app tenga WRITE_EXTERNAL_STORAGE. Por otra parte, los objetivos compartidos realmente importantes, como la app de Gmail, no tienen READ_EXTERNAL_STORAGE, lo que hace que esta llamada falle. En cambio, puedes usar permisos de URI para otorgar a otras apps acceso a URI específicos. Si bien los permisos de URI no funcionan en URI de file:// generados por Uri.fromFile(), sí funcionan en URI asociados con proveedores de contenido. En lugar de implementar el tuyo solo con este objetivo, puedes y deberías usar FileProvider como se explica en Cómo compartir archivos.

Cómo compartir el archivo con la app que lo solicita

Para compartir el archivo con la app que lo solicitó, pasa el Intent que incluye el URI de contenido y los permisos a setResult(). Cuando termine la Activity que acabas de definir, el sistema enviará el Intent que incluye el URI de contenido a la app cliente. En el siguiente fragmento de código, se muestra cómo hacerlo:

Kotlin

        override fun onCreate(savedInstanceState: Bundle?) {
            ...
            // Define a listener that responds to clicks on a file in the ListView
            fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
                ...
                if (fileUri != null) {
                    ...
                    // Put the Uri and MIME type in the result Intent
                    resultIntent.setDataAndType(fileUri, contentResolver.getType(fileUri))
                    // Set the result
                    setResult(Activity.RESULT_OK, resultIntent)
                } else {
                    resultIntent.setDataAndType(null, "")
                    setResult(RESULT_CANCELED, resultIntent)
                }
            }
        }
    

Java

        protected void onCreate(Bundle savedInstanceState) {
            ...
            // Define a listener that responds to clicks on a file in the ListView
            fileListView.setOnItemClickListener(
                    new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> adapterView,
                        View view,
                        int position,
                        long rowId) {
                    ...
                    if (fileUri != null) {
                        ...
                        // Put the Uri and MIME type in the result Intent
                        resultIntent.setDataAndType(
                                fileUri,
                                getContentResolver().getType(fileUri));
                        // Set the result
                        MainActivity.this.setResult(Activity.RESULT_OK,
                                resultIntent);
                        } else {
                            resultIntent.setDataAndType(null, "");
                            MainActivity.this.setResult(RESULT_CANCELED,
                                    resultIntent);
                        }
                    }
            });
    

Proporciona a los usuarios una forma de regresar de inmediato a la app cliente una vez que hayan elegido un archivo. Una forma de hacerlo es proporcionar una marca de verificación o un botón Done. Asocia un método al botón mediante el atributo android:onClick del botón. En el método, llama a finish(). Por ejemplo:

Kotlin

        fun onDoneClick(v: View) {
            // Associate a method with the Done button
            finish()
        }
    

Java

        public void onDoneClick(View v) {
            // Associate a method with the Done button
            finish();
        }
    

Para obtener información adicional relacionada, consulta: