Como usar o FileProvider de suporte para compartilhar conteúdo com outros aplicativos?

142

Estou procurando uma maneira de compartilhar corretamente (não ABRIR) um arquivo interno com um aplicativo externo usando o FileProvider da biblioteca de suporte do Android .

Seguindo o exemplo nos documentos,

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.example.android.supportv4.my_files"
    android:grantUriPermissions="true"
    android:exported="false">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/my_paths" />
</provider>

e usando o ShareCompat para compartilhar um arquivo com outros aplicativos, da seguinte maneira:

ShareCompat.IntentBuilder.from(activity)
.setStream(uri) // uri from FileProvider
.setType("text/html")
.getIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

não funciona, pois o FLAG_GRANT_READ_URI_PERMISSION concede apenas permissão para o Uri especificado na dataintenção, não o valor do EXTRA_STREAMextra (conforme definido por setStream).

Tentei comprometer a segurança definindo android:exportedcomo truepara o provedor, mas FileProviderverifica internamente se ele próprio é exportado; nesse caso, gera uma exceção.

Randy Sugianto 'Yuku'
fonte
7
Com +1, essa parece ser uma pergunta verdadeiramente original - não há nada no Google ou no StackOverflow , até onde eu sei.
23413 Phil
Aqui está uma postagem para obter uma configuração básica do FileProvider usando um projeto de exemplo mínimo do github: stackoverflow.com/a/48103567/2162226 . Ele fornece etapas para as quais os arquivos devem ser copiados (sem fazer alterações) em um projeto autônomo local.
Gene Bo

Respostas:

159

Usando FileProviderda biblioteca de suporte, você precisa conceder e revogar manualmente permissões (em tempo de execução) para outros aplicativos lerem Uri específico. Use Context.grantUriPermission e Context.revokeUriPermission métodos.

Por exemplo:

//grant permision for app with package "packegeName", eg. before starting other app via intent
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);

//revoke permisions
context.revokeUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);

Como último recurso, se você não puder fornecer o nome do pacote, poderá conceder a permissão a todos os aplicativos que possam lidar com intenções específicas:

//grant permisions for all apps that can handle given intent
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
...
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}

Método alternativo de acordo com a documentação :

  • Coloque o URI do conteúdo em uma Intent chamando setData ().
  • Em seguida, chame o método Intent.setFlags () com FLAG_GRANT_READ_URI_PERMISSION ou FLAG_GRANT_WRITE_URI_PERMISSION ou com ambos.
  • Por fim, envie a intenção para outro aplicativo. Na maioria das vezes, você faz isso chamando setResult ().

    As permissões concedidas em uma Intenção permanecem em vigor enquanto a pilha da Atividade receptora está ativa. Quando a pilha termina, as
    permissões são removidas automaticamente. As permissões concedidas a uma
    Atividade em um aplicativo cliente são automaticamente estendidas para outros
    componentes desse aplicativo.

Btw. se necessário, você pode copiar a origem do FileProvider e alterar o attachInfométodo para impedir que o provedor verifique se ele foi exportado.

Leszek
fonte
26
Usando o grantUriPermissionmétodo, precisamos fornecer o nome do pacote do aplicativo ao qual queremos conceder a permissão. No entanto, ao compartilhar, geralmente não sabemos qual aplicativo é o destino de compartilhamento.
Randy Sugianto 'Yuku'
E se não soubermos o nome do pacote e apenas quisermos que outros aplicativos possam abri-lo?
StuStirling
3
Você pode fornecer um código de trabalho para a solução mais recente @Lecho?
31314 StErMi
2
Existe um rastreamento aberto de bug por que o suporte FileProvider não funciona com setFlags, mas funciona com grantUriPermission? Ou isso não é um bug? Nesse caso, como isso não é um bug?
nmr
1
Além disso, descobri que a chamada grantUriPermission não funcionava para uma intenção criada usando o ShareCompat, mas funcionava ao criar manualmente a intenção.
StuStirling
33

Exemplo de código totalmente funcional como compartilhar arquivos da pasta interna do aplicativo. Testado no Android 7 e Android 5.

AndroidManifest.xml

</application>
   ....
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="android.getqardio.com.gmslocationtest"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths"/>
    </provider>
</application>

xml / provider_paths

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path
        name="share"
        path="external_files"/>
</paths>

Código em si

    File imagePath = new File(getFilesDir(), "external_files");
    imagePath.mkdir();
    File imageFile = new File(imagePath.getPath(), "test.jpg");

    // Write data in your file

    Uri uri = FileProvider.getUriForFile(this, getPackageName(), imageFile);

    Intent intent = ShareCompat.IntentBuilder.from(this)
                .setStream(uri) // uri from FileProvider
                .setType("text/html")
                .getIntent()
                .setAction(Intent.ACTION_VIEW) //Change if needed
                .setDataAndType(uri, "image/*")
                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

   startActivity(intent);
Mergulhadores
fonte
1
O uso da ShareCompat.IntentBuilderpermissão de leitura e URI finalmente fez isso por mim.
Cord Rehn
Quebras para outras versões do Android
Oliver Dixon
@OliverDixon: quais? Eu testei no Android 6.0 e 9.0.
Giraffe Violet
1
Esta é a única resposta ao usar o FileProviderque funciona. As outras respostas entendem errado o manuseio da intenção (elas não a usam ShareCompat.IntentBuilder) e essa intenção não pode ser aberta por aplicativos externos de maneira significativa. Passei literalmente a maior parte do dia nessa bobagem até finalmente encontrar sua resposta.
Giraffe Violet
1
@VioletGiraffe Também não estou usando o ShareCompat, mas o meu funciona. Meu problema foi que eu estava concedendo permissões de leitura para a intenção do meu seletor por engano, quando precisei conceder permissões de leitura para a minha intenção ANTES de passar para a intenção do seletor e para o seletor. Espero que faça sentido. Apenas verifique se você está concedendo permissão de leitura para a intenção correta.
Ryan
23

Esta solução funciona para mim desde o OS 4.4. Para fazê-lo funcionar em todos os dispositivos, adicionei uma solução alternativa para dispositivos mais antigos. Isso garante que sempre seja usada a solução mais segura.

Manifest.xml:

    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.package.name.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>

file_paths.xml:

<paths>
    <files-path name="app_directory" path="directory/"/>
</paths>

Java:

public static void sendFile(Context context) {
    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("text/plain");
    String dirpath = context.getFilesDir() + File.separator + "directory";
    File file = new File(dirpath + File.separator + "file.txt");
    Uri uri = FileProvider.getUriForFile(context, "com.package.name.fileprovider", file);
    intent.putExtra(Intent.EXTRA_STREAM, uri);
    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    // Workaround for Android bug.
    // grantUriPermission also needed for KITKAT,
    // see https://code.google.com/p/android/issues/detail?id=76683
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
        List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo resolveInfo : resInfoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            context.grantUriPermission(packageName, attachmentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
        }
    }
    if (intent.resolveActivity(context.getPackageManager()) != null) {
        context.startActivity(intent);
    }
}

public static void revokeFileReadPermission(Context context) {
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
        String dirpath = context.getFilesDir() + File.separator + "directory";
        File file = new File(dirpath + File.separator + "file.txt");
        Uri uri = FileProvider.getUriForFile(context, "com.package.name.fileprovider", file);
        context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
    }
}

A permissão é revogada com revokeFileReadPermission () nos métodos onResume e onDestroy () do Fragmento ou da Atividade.

Oliver Kranz
fonte
1
Essa solução funcionou para mim, mas somente depois que adicionei intent.setDataAndType (uri, "video / *"); A variável de anexo Uri que está faltando é uri
EdgarK:
Você quer dizer que filenão mudará de onResumepara onDestroy?
CoolMind 19/02
16

Como Phil disse em seu comentário sobre a pergunta original, isso é único e não há outras informações sobre SO no google, pensei em compartilhar meus resultados:

No meu aplicativo, o FileProvider funcionou imediatamente para compartilhar arquivos usando a intenção de compartilhar. Não havia nenhuma configuração ou código especial necessário, além disso para configurar o FileProvider. No manifest.xml, coloquei:

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.my.apps.package.files"
        android:exported="false"
        android:grantUriPermissions="true" >
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/my_paths" />
    </provider>

Em my_paths.xml, tenho:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="files" path="." />
</paths>

No meu código eu tenho:

    Intent shareIntent = new Intent();
    shareIntent.setAction(Intent.ACTION_SEND);
    shareIntent.setType("application/xml");

    Uri uri = FileProvider.getUriForFile(this, "com.my.apps.package.files", fileToShare);
    shareIntent.putExtra(Intent.EXTRA_STREAM, uri);

    startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.share_file)));

E posso compartilhar meu armazenamento de arquivos no armazenamento privado de meus aplicativos com aplicativos como o Gmail e o Google Drive sem nenhum problema.

Luke Sleeman
fonte
9
Infelizmente isso não funciona. Essa variável fileToShare funciona apenas se o arquivo existir em um cartão SD ou sistema de arquivos externo. O objetivo de usar o FileProvider é para que você possa compartilhar conteúdo do sistema interno.
Keith Connolly
Isso parece funcionar corretamente com arquivos de armazenamento local (no Android 5+). Mas eu fico questões sobre o Android 4.
Peterdk
15

Tanto quanto posso dizer, isso só funcionará em versões mais recentes do Android, então você provavelmente precisará descobrir uma maneira diferente de fazer isso. Esta solução funciona para mim no 4.4, mas não no 4.0 ou 2.3.3, portanto, não será uma maneira útil de compartilhar conteúdo para um aplicativo que deve ser executado em qualquer dispositivo Android.

No manifest.xml:

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.mydomain.myapp.SharingActivity"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

Tome nota cuidadosa de como você especifica as autoridades. Você deve especificar a atividade a partir da qual você criará o URI e iniciará a intenção de compartilhamento; nesse caso, a atividade será chamada SharingActivity. Este requisito não é óbvio nos documentos do Google!

file_paths.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="just_a_name" path=""/>
</paths>

Cuidado ao especificar o caminho. O padrão acima é a raiz do seu armazenamento interno privado.

Em SharingActivity.java:

Uri contentUri = FileProvider.getUriForFile(getActivity(),
"com.mydomain.myapp.SharingActivity", myFile);
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("image/jpeg");
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(shareIntent, "Share with"));

Neste exemplo, estamos compartilhando uma imagem JPEG.

Finalmente, é provavelmente uma boa idéia garantir que você salvou o arquivo corretamente e que você pode acessá-lo com algo assim:

File myFile = getActivity().getFileStreamPath("mySavedImage.jpeg");
if(myFile != null){
    Log.d(TAG, "File found, file description: "+myFile.toString());
}else{
    Log.w(TAG, "File not found!");
}
Rasmusob
fonte
8

No meu aplicativo, o FileProvider funciona muito bem e posso anexar arquivos internos armazenados no diretório de arquivos a clientes de e-mail como Gmail, Yahoo etc.

No meu manifesto, como mencionado na documentação do Android, eu coloquei:

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.package.name.fileprovider"
        android:grantUriPermissions="true"
        android:exported="false">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepaths" />
    </provider>

E como meus arquivos foram armazenados no diretório de arquivos raiz, os filepaths.xml foram os seguintes:

 <paths>
<files-path path="." name="name" />

Agora no código:

 File file=new File(context.getFilesDir(),"test.txt");

 Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE);

 shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
                                     "Test");

 shareIntent.setType("text/plain");

 shareIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
                                 new String[] {"email-address you want to send the file to"});

   Uri uri = FileProvider.getUriForFile(context,"com.package.name.fileprovider",
                                                   file);

                ArrayList<Uri> uris = new ArrayList<Uri>();
                uris.add(uri);

                shareIntent .putParcelableArrayListExtra(Intent.EXTRA_STREAM,
                                                        uris);


                try {
                   context.startActivity(Intent.createChooser(shareIntent , "Email:").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));                                                      


                }
                catch(ActivityNotFoundException e) {
                    Toast.makeText(context,
                                   "Sorry No email Application was found",
                                   Toast.LENGTH_SHORT).show();
                }
            }

Isso funcionou para mim. Espero que isso ajude :)

Adarsh ​​Chithran
fonte
1
Você é um MVP real para postar caminhos = "." Trabalhou para mim
robsstackoverflow 27/03
Estou recebendo um erro como "Falha ao encontrar a raiz configurada que contém /", usei o mesmo código
Rakshith Kumar 24/17/17
5

Se você obtiver uma imagem da câmera, nenhuma dessas soluções funcionará no Android 4.4. Nesse caso, é melhor verificar as versões.

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getContext().getPackageManager()) != null) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        uri = Uri.fromFile(file);
    } else {
        uri = FileProvider.getUriForFile(getContext(), getContext().getPackageName() + ".provider", file);
    }
    intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
    startActivityForResult(intent, CAMERA_REQUEST);
}
CoolMind
fonte
1

apenas para melhorar a resposta dada acima: se você estiver recebendo o NullPointerEx:

você também pode usar getApplicationContext () sem contexto

                List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
                for (ResolveInfo resolveInfo : resInfoList) {
                    String packageName = resolveInfo.activityInfo.packageName;
                    grantUriPermission(packageName, photoURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
                }
Deepak Singh
fonte
1

Quero compartilhar algo que nos bloqueou por alguns dias: o código do provedor de arquivo DEVE ser inserido entre as tags do aplicativo, não depois dele. Pode ser trivial, mas nunca especificado, e pensei que poderia ter ajudado alguém! (obrigado novamente a piolo94)

Eric Draven
fonte