Cómo compartir un archivo

Una vez que hayas configurado tu app para compartir archivos mediante URI de contenido, podrás responder las solicitudes de otras apps solicitudes para esos archivos. Una forma de responder a estas solicitudes es proporcionar una selección de archivos interfaz de la app del servidor que otras aplicaciones pueden invocar. Este enfoque le permite al cliente app para permitir que los usuarios seleccionen un archivo de la app de servidor y, luego, reciban el archivo URI de contenido.

En esta lección, se muestra cómo crear una selección de archivos Activity en tu app que responde a las 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 proporciona una selección de archivos Activity. Las apps cliente inician esto. Activity llamando a startActivityForResult() con un Intent que contiene la acción ACTION_PICK Cuando la app cliente llama startActivityForResult(), tu app puede hacer lo siguiente: mostrar un resultado a la app cliente, en forma de un 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 selección de archivos Activity, comienza por especificar el Activity en tu manifiesto, junto con un filtro de intents que coincida con la acción ACTION_PICK y categorías CATEGORY_DEFAULT y CATEGORY_OPENABLE Agregar también filtros de tipo de MIME por los archivos que tu app envía a otras apps. En el siguiente fragmento, se muestra cómo especificar la Activity y filtro de intents nuevos:

<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 Activity que muestre los archivos disponibles en el directorio files/images/ de tu app en el almacenamiento interno y permite que el usuario elija el archivo deseado. En el siguiente fragmento, se muestra cómo definir esta Activity y responde 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 aplicación debe determinar qué archivo se seleccionó y luego, genera un URI de contenido para el archivo. Dado que Activity muestra el Lista de archivos disponibles en un ListView, cuando el usuario hace clic en un nombre de 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 tener cuidado para obtener un URI que otros pueden leer las apps. Hacerlo en dispositivos con Android 6.0 (nivel de API 23) y versiones posteriores requiere atención cuidado debido a los cambios en el modelo de permisos de esa versión de Android, en particular, De READ_EXTERNAL_STORAGE convertirse en permiso peligroso, que la app receptora podría no tener.

Con estas consideraciones en mente, recomendamos que evites usar Uri.fromFile(), que presenta varias desventajas. El método tiene las siguientes características:

  • No permite compartir archivos entre perfiles.
  • Requiere que tu app tenga WRITE_EXTERNAL_STORAGE en dispositivos con Android 4.4 (nivel de API 19) o versiones anteriores.
  • Requiere que las apps receptoras tengan las READ_EXTERNAL_STORAGE, que fallarán en objetivos de uso compartido importantes, como Gmail, que no tengan ese permiso.

En lugar de usar Uri.fromFile(), puedes usar permisos del URI para otorgar a otras apps acceso a URI específicos. Aunque los permisos de URI no funcionan en URI de file:// generada por Uri.fromFile(), sí funcionan en URI asociados con proveedores de contenido. El La API de FileProvider puede ayudarte a crear esos URIs. Este enfoque también funciona con archivos que no son pero en el local de la app que envía el intent.

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

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 especificaste en el archivo de metadatos que contiene el elemento <paths>, se describe en la sección Cómo especificar directorios para compartir. Si llamas getUriForFile() para un File en una ruta de acceso que no especificaste, recibirás un IllegalArgumentException

Cómo otorgar permisos para el archivo

Ahora que tienes un URI de contenido para el archivo que quieres compartir con otra app, debes hacer lo siguiente: permitir que la app cliente acceda al archivo. Para permitir el acceso, otorga permisos a la app cliente. Para ello, haz lo siguiente: agregar el URI de contenido a un Intent y, luego, establecer marcas de permiso en Intent Los permisos que otorgas son temporales y vencen 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 más sencilla de otorgar acceso a tus archivos de forma segura mediante permisos de acceso temporal. Evitar llamadas Método Context.grantUriPermission() para un el URI de contenido del archivo, ya que este método otorga acceso que solo puedes revocar llamando a Context.revokeUriPermission().

No uses Uri.fromFile(). Fuerza la recepción de apps para tener el permiso READ_EXTERNAL_STORAGE no funcionará en absoluto si intentas compartir contenido entre usuarios y en de las versiones de Android anteriores a la 4.4 (nivel de API 19), requeriría para que la app tenga WRITE_EXTERNAL_STORAGE. Y los objetivos de uso compartido muy importantes, como la app de Gmail, no tienen el READ_EXTERNAL_STORAGE, lo que provoca esta llamada falle. En su lugar, puedes usar permisos de URI para otorgar a otras apps acceso a URI específicos. Si bien los permisos de URI no funcionan en los URI file:// como los genera Uri.fromFile(), sí funcionan en URI asociados con proveedores de contenido. En lugar de implementar la tuya solo para esto, puedes y debes usar FileProvider como se explica en Uso compartido de 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 para setResult(). Cuando finaliza el Activity que acabas de definir, el el sistema envía el Intent que tiene 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 con mediante el botón atributo android:onClick. En el método, llama 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: