Como compartilhar um arquivo

Depois de configurar o app para compartilhar arquivos usando URIs de conteúdo, você pode responder a solicitações de outros apps para esses arquivos. Uma maneira de responder a essas solicitações é fornecer uma interface de seleção de arquivos do app no servidor que outros apps possam invocar. Com essa abordagem, um app cliente permite que os usuários selecionem um arquivo a partir do app do servidor e recebam o URI de conteúdo do arquivo selecionado.

Esta lição mostra como criar uma Activity de seleção de arquivos no seu app que responda a solicitações de arquivos.

Receber solicitações de arquivos

Para receber solicitações de arquivos a partir de apps cliente e responder com um URI de conteúdo, seu app precisa fornecer uma Activity de seleção de arquivos. Apps cliente iniciam esta Activity chamando startActivityForResult() com uma Intent contendo a ACTION_PICK da ação. Quando o app cliente chama startActivityForResult(), seu app pode retornar um resultado ao app cliente, na forma de um URI de conteúdo para o arquivo que o usuário selecionou.

Para saber como implementar uma solicitação de arquivo em um app cliente, consulte a lição Como solicitar um arquivo compartilhado.

Criar uma atividade de seleção de arquivos

Para configurar a Activity de seleção de arquivos, comece especificando a Activity no manifesto, com um filtro de intent que corresponda à ACTION_PICK da ação e às categorias CATEGORY_DEFAULT e CATEGORY_OPENABLE. Além disso, adicione filtros do tipo MIME para os arquivos que o app exibe para outros apps. O snippet a seguir mostra como especificar a nova Activity e filtro de intent.

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

Definir a atividade de seleção de arquivos no código

Em seguida, defina uma subclasse da Activity que exiba os arquivos disponíveis a partir do diretório files/images/ do app no armazenamento interno e permita que o usuário escolha o arquivo desejado. O snippet a seguir demonstra como definir essa Activity e responder à seleção do usuário.

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
             */
             ...
        }
        ...
    }
    

Responder a uma seleção de arquivos

Depois que um usuário seleciona um arquivo compartilhado, o app precisa determinar qual arquivo foi selecionado e, em seguida, gerar um URI de conteúdo para ele. Como a Activity exibe a lista de arquivos disponíveis em uma ListView, quando o usuário clica em um nome de arquivo, o sistema chama o método onItemClick(), em que você pode receber o arquivo selecionado.

Ao usar uma intent para enviar o URI de um arquivo de um app para outro, tenha o cuidado de ter um URI que outros apps possam ler. Fazer isso em dispositivos que executam o Android 6.0 (API de nível 23) e versões posteriores requer cuidados especiais por causa de mudanças no modelo de permissões naquela versão do Android, particularmente READ_EXTERNAL_STORAGE, que está se tornando uma permissão perigosa, que o app receptor pode não ter.

Com essas considerações em mente, recomendamos que você evite usar Uri.fromFile(), que apresenta várias desvantagens. Esse método:

  • não permite o compartilhamento de arquivos entre perfis;
  • exige que o app tenha permissão para WRITE_EXTERNAL_STORAGE em dispositivos com Android 4.4 (API de nível 19) ou versões anteriores;
  • requer que apps receptores tenham a permissão READ_EXTERNAL_STORAGE, que falhará em segmentos importantes de compartilhamento, como o Gmail, que não têm essa permissão.

Em vez de usar Uri.fromFile(), você pode usar permissões de URI para conceder a outros apps acesso a URIs específicos. Embora as permissões de URI não funcionem em file:// como gerado por Uri.fromFile(), elas funcionam em URIs associados a provedores de conteúdo. A API FileProvider pode ajudar você a criar esses URIs. Essa abordagem também funciona com arquivos que não estão no armazenamento externo, mas no armazenamento local do app que envia a intent.

Em onItemClick(), receba um objeto File para o nome do arquivo selecionado e transmita-o como um argumento para getUriForFile(), com a autoridade que você especificou no elemento <provider> para o FileProvider. O URI de conteúdo resultante contém a autoridade, um segmento de caminho correspondente ao diretório do arquivo (conforme especificado nos metadados XML) e o nome do arquivo que inclui a extensão. A forma como FileProvider mapeia diretórios para segmentos de caminho com base em metadados XML é descrita na seção Especificar diretórios compartilháveis.

O snippet a seguir mostra como detectar o arquivo selecionado e ter um URI de conteúdo para ele:

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());
                    }
                    ...
                }
            });
            ...
        }
    

Lembre-se de que você só pode gerar URIs de conteúdo para arquivos que residem em um diretório que você especificou no arquivo de metadados que contém o elemento <paths> conforme descrito na seção Especificar diretórios compartilháveis. Se você chamar getUriForFile() para um File em um caminho que você não tenha especificado, você receberá uma IllegalArgumentException.

Conceder permissões para o arquivo

Agora que você tem um URI de conteúdo para o arquivo que quer compartilhar com outro app, é necessário permitir que o app cliente acesse o arquivo. Para permitir o acesso, conceda permissões para o app cliente, adicionando o URI de conteúdo a uma Intente, em seguida, definindo sinalizações de permissão na Intent. As permissões concedidas são temporárias e expiram automaticamente quando a pilha de tarefas do app receptor é concluída.

O snippet de código a seguir mostra como configurar a permissão de leitura para o arquivo.

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

Cuidado: chamar setFlags() é a única maneira de fornecer acesso seguro aos seus arquivos usando permissões de acesso temporárias. Evite chamar o método Context.grantUriPermission() para o conteúdo de um arquivo URI, porque esse método concede acesso que você só pode revogar chamando Context.revokeUriPermission().

Não use Uri.fromFile(). Ele obriga o app receptor a ter a permissão READ_EXTERNAL_STORAGE, não funciona se você está tentando o compartilhamento entre usuários e, em versões do Android anteriores a 4.4 (API de nível 19), exigirá que o app tenha WRITE_EXTERNAL_STORAGE. Além disso, segmentos de compartilhamento muito importantes, como o app Gmail, não têm o READ_EXTERNAL_STORAGE, fazendo com que essa chamada falhe. Em vez disso, você pode usar as permissões de URI para conceder a outros apps acesso a URIs específicos. Embora as permissões de URI não funcionem em URIs file:// como gerado por Uri.fromFile(), elas funcionam em URIs associados a provedores de conteúdo. Em vez de implementar um próprio apenas para isso, você pode e precisa usar FileProvider, conforme explicado em Como compartilhar arquivos.

Compartilhar o arquivo com o app solicitante

Para compartilhar o arquivo com o app que o solicitou, transmita a Intent que contém o URI de conteúdo e permissões para setResult(). Quando a Activity que você acabou de definir estiver concluída, o sistema enviará a Intent que contém o URI de conteúdo para o app cliente. O snippet de código a seguir mostra como fazer isso:

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

Forneça aos usuários uma maneira de retornar imediatamente ao app cliente depois de escolher um arquivo. Uma maneira de fazer isso é fornecer uma marca de seleção ou o botão Concluído. Associe um método ao botão usando o atributo android:onClick. No método, chame finish(). Exemplo:

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 ver mais informações relacionadas, consulte: