Como compartilhar um arquivo

Depois de configurar seu app para compartilhar arquivos usando URIs de conteúdo, é possível 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 do servidor que outros aplicativos possam invocar. Essa abordagem permite que um aplicativo cliente permita que os usuários selecionem um arquivo 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 de apps clientes e responder com um URI de conteúdo, seu app precisa fornecer um Activity de seleção de arquivos. Apps clientes iniciam essa Activity chamando startActivityForResult() com um Intent contendo a ação ACTION_PICK. 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 o Activity no manifesto, com um filtro de intent que corresponda à ação ACTION_PICK e às categorias CATEGORY_DEFAULT e CATEGORY_OPENABLE. Adicione também filtros do tipo MIME para os arquivos que o app exibe a outros apps. O snippet a seguir mostra como especificar o novo 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 Activity que exiba os arquivos disponíveis no 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 esse 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, seu aplicativo precisa determinar qual arquivo foi selecionado e gerar um URI de conteúdo para ele. Como o Activity exibe a lista de arquivos disponíveis em uma ListView, quando o usuário clica no nome de um arquivo, o sistema chama o método onItemClick(), em que você pode acessar o arquivo selecionado.

Ao usar uma intent para enviar o URI de um arquivo de um app para outro, é preciso ter o cuidado de ter um URI que outros apps possam ler. Fazer isso em dispositivos com o Android 6.0 (nível 23 da API) e versões mais recentes exige cuidados especiais por causa de mudanças no modelo de permissões nessa versão do Android, principalmente a READ_EXTERNAL_STORAGE's se tornando uma permissão perigosa, que o app receptor pode não ter.

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

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

Em vez de usar Uri.fromFile(), use 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:// gerados por Uri.fromFile(), elas funcionam em URIs associados a provedores de conteúdo. A API FileProvider pode ajudar 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 especificada no elemento <provider> para a 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, incluindo 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 especificado 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 não foi especificado, 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 ao app cliente adicionando o URI de conteúdo a uma Intent e 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 conceder acesso com segurança aos seus arquivos usando permissões de acesso temporárias. Evite chamar o método Context.grantUriPermission() para o URI de conteúdo de um arquivo, já que ele concede acesso que só pode ser revogado chamando Context.revokeUriPermission().

Não use Uri.fromFile(). Ele força o recebimento de apps a ter a permissão READ_EXTERNAL_STORAGE, não funciona se você estiver tentando compartilhar entre usuários e, em versões do Android anteriores à 4.4 (API de nível 19), exigiria que o app tivesse WRITE_EXTERNAL_STORAGE. Além disso, destinos 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 com provedores de conteúdo. Em vez de implementar um próprio apenas para isso, você pode e precisa usar FileProvider, conforme explicado em Compartilhamento de arquivos.

Compartilhar o arquivo com o app solicitante

Para compartilhar o arquivo com o app que o solicitou, transmita o Intent que contém o URI de conteúdo e as permissões para setResult(). Quando a Activity que você acabou de definir for concluída, o sistema vai 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(). Por 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 outras informações relacionadas, consulte: