Comment utiliser le support FileProvider pour partager du contenu avec d'autres applications?

142

Je cherche un moyen de partager correctement (et non OPEN) un fichier interne avec une application externe à l'aide de FileProvider de la bibliothèque de support Android .

En suivant l'exemple sur la documentation,

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

et en utilisant ShareCompat pour partager un fichier avec d'autres applications comme suit:

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

ne fonctionne pas, puisque FLAG_GRANT_READ_URI_PERMISSION n'accorde la permission que pour l'URI spécifié sur l'intent data, pas la valeur de l' EXTRA_STREAMextra (comme a été défini par setStream).

J'ai essayé de compromettre la sécurité en définissant android:exportedsur truepour le fournisseur, mais FileProvidervérifie en interne si lui-même est exporté, quand c'est le cas, il lève une exception.

Randy Sugianto 'Yuku'
la source
7
+1 cela semble être une question vraiment originale - il n'y a rien sur Google ou StackOverflow , pour autant que je sache .
Phil
Voici un article pour obtenir une configuration de base de FileProvider à l'aide d'un exemple de projet minimal de github: stackoverflow.com/a/48103567/2162226 . Il fournit les étapes pour les fichiers à copier (sans apporter de modifications) dans un projet autonome local
Gene Bo

Réponses:

159

À l'aide FileProviderde la bibliothèque de support, vous devez accorder et révoquer manuellement les autorisations (au moment de l'exécution) pour que d'autres applications lisent des Uri spécifiques. Utilisez les méthodes Context.grantUriPermission et Context.revokeUriPermission .

Par exemple:

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

En dernier recours, si vous ne pouvez pas fournir de nom de package, vous pouvez accorder l'autorisation à toutes les applications qui peuvent gérer une intention spécifique:

//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éthode alternative selon la documentation :

  • Placez l'URI du contenu dans une intention en appelant setData ().
  • Ensuite, appelez la méthode Intent.setFlags () avec FLAG_GRANT_READ_URI_PERMISSION ou FLAG_GRANT_WRITE_URI_PERMISSION ou les deux.
  • Enfin, envoyez l'intention à une autre application. Le plus souvent, vous faites cela en appelant setResult ().

    Les autorisations accordées dans une intention restent en vigueur tant que la pile de l'activité de réception est active. Lorsque la pile est terminée, les
    autorisations sont automatiquement supprimées. Les autorisations accordées à une
    activité dans une application cliente sont automatiquement étendues à d'autres
    composants de cette application.

Btw. si vous en avez besoin, vous pouvez copier la source de FileProvider et changer de attachInfométhode pour empêcher le fournisseur de vérifier s'il est exporté.

Leszek
la source
26
En utilisant la grantUriPermissionméthode, nous devons fournir le nom du package de l'application à laquelle nous voulons accorder l'autorisation. Cependant, lors du partage, nous ne savons généralement pas quelle application est la destination de partage.
Randy Sugianto 'Yuku'
Et si nous ne connaissons pas le nom du package et que nous voulons simplement que d'autres applications puissent l'ouvrir
StuStirling
3
Pouvez-vous s'il vous plaît fournir un code de travail pour la dernière solution @Lecho?
StErMi
2
Existe-t-il un bug de suivi des raisons pour lesquelles le support FileProvider ne fonctionne pas avec setFlags, mais fonctionne avec grantUriPermission? Ou n'est-ce pas un bug, auquel cas comment n'est-ce pas un bug?
nmr
1
En plus de cela, j'ai trouvé que l'appel grantUriPermission ne fonctionnait pas pour une intention créée à l'aide de ShareCompat, mais c'était le cas lors de la création manuelle de l'intention.
StuStirling
33

Exemple de code entièrement fonctionnel pour partager un fichier à partir du dossier d'application interne. Testé sur Android 7 et 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 / chemin_fournisseur

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

Code lui-même

    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);
Plongeurs
la source
1
L'utilisation de l' ShareCompat.IntentBuilderautorisation de lecture et d'URI l'a finalement fait pour moi.
Cord Rehn
Pauses pour les autres versions d'Android
Oliver Dixon
@OliverDixon: lesquels? Je l'ai testé sur Android 6.0 et 9.0.
Violet Giraffe
1
C'est la seule réponse sur l'utilisation du FileProviderqui fonctionne. Les autres réponses ont une mauvaise gestion de l'intention (elles ne l'utilisent pas ShareCompat.IntentBuilder), et cette intention ne peut pas être ouverte par des applications externes de manière significative. J'ai passé littéralement la majeure partie de la journée sur ce non-sens jusqu'à ce que finalement trouvé votre réponse.
Violet Giraffe
1
@VioletGiraffe Je n'utilise pas non plus le ShareCompat mais le mien fonctionne. Mon problème était que j'accordais par erreur les autorisations de lecture à l'intention de mon sélecteur, alors que je devais accorder des autorisations de lecture à mon intention AVANT qu'il ne soit transmis à l'intention du sélecteur, ainsi qu'au sélecteur. J'espère que cela à du sens. Assurez-vous simplement que vous accordez une autorisation de lecture à l'intention correcte.
Ryan
23

Cette solution fonctionne pour moi depuis OS 4.4. Pour le faire fonctionner sur tous les appareils, j'ai ajouté une solution de contournement pour les appareils plus anciens. Cela garantit que la solution la plus sûre est toujours utilisée.

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

L'autorisation est révoquée avec revokeFileReadPermission () dans les méthodes onResume et onDestroy () du fragment ou de l'activité.

Oliver Kranz
la source
1
Cette solution a fonctionné pour moi, mais seulement après avoir ajouté intent.setDataAndType (uri, "video / *"); BTW manquant la variable attachmentUri est uri
EdgarK
Voulez-vous dire que file cela ne changera pas de onResumeà onDestroy?
CoolMind
16

Comme Phil le dit dans son commentaire sur la question initiale, c'est unique et il n'y a pas d'autre information sur SO sur Google, j'ai pensé que je devrais également partager mes résultats:

Dans mon application, FileProvider a fonctionné dès le départ pour partager des fichiers en utilisant l'intention de partage. Il n'y avait aucune configuration ou code spécial nécessaire, à part cela pour configurer le FileProvider. Dans mon manifest.xml j'ai placé:

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

Dans my_paths.xml, j'ai:

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

Dans mon code j'ai:

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

Et je suis en mesure de partager mon stockage de fichiers dans le stockage privé de mes applications avec des applications telles que Gmail et Google Drive sans aucun problème.

Luke Sleeman
la source
9
Malheureusement, cela ne fonctionne pas. Cette variable fileToShare ne fonctionne que si le fichier existe sur une carte SD ou un système de fichiers externe. Le but de l'utilisation de FileProvider est de pouvoir partager du contenu à partir du système interne.
Keith Connolly
Cela semble fonctionner correctement avec les fichiers de stockage locaux (sur Android 5+). Mais je reçois des problèmes sur Android 4.
Peterdk
15

Pour autant que je sache, cela ne fonctionnera que sur les nouvelles versions d'Android, vous devrez donc probablement trouver une façon différente de le faire. Cette solution fonctionne pour moi sur 4.4, mais pas sur 4.0 ou 2.3.3, donc ce ne sera pas un moyen utile de partager du contenu pour une application destinée à fonctionner sur n'importe quel appareil Android.

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

Prenez bien note de la manière dont vous spécifiez les autorités. Vous devez spécifier l'activité à partir de laquelle vous allez créer l'URI et lancer l'intention de partage. Dans ce cas, l'activité s'appelle SharingActivity. Cette exigence n'est pas évidente dans la documentation de 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>

Faites attention à la manière dont vous spécifiez le chemin. Ce qui précède correspond par défaut à la racine de votre stockage interne privé.

Dans 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"));

Dans cet exemple, nous partageons une image JPEG.

Enfin, c'est probablement une bonne idée de vous assurer que vous avez correctement enregistré le fichier et que vous pouvez y accéder avec quelque chose comme ceci:

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
la source
8

Dans mon application, FileProvider fonctionne très bien, et je suis capable de joindre des fichiers internes stockés dans le répertoire de fichiers à des clients de messagerie tels que Gmail, Yahoo, etc.

Dans mon manifeste comme mentionné dans la documentation Android, j'ai placé:

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

Et comme mes fichiers étaient stockés dans le répertoire racine des fichiers, les chemins de fichiers.xml étaient les suivants:

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

Maintenant dans le code:

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

Cela a fonctionné pour moi, j'espère que cela aide :)

Adarsh ​​Chithran
la source
1
Vous êtes un vrai MVP pour la publication de chemins = "." A travaillé pour moi
robsstackoverflow
Je reçois une erreur comme "Impossible de trouver la racine configurée qui contient /" J'ai utilisé le même code
Rakshith Kumar
5

Si vous obtenez une image de la caméra, aucune de ces solutions ne fonctionne pour Android 4.4. Dans ce cas, il est préférable de vérifier les versions.

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
la source
1

juste pour améliorer la réponse donnée ci-dessus: si vous obtenez NullPointerEx:

vous pouvez également utiliser getApplicationContext () sans contexte

                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
la source
1

Je veux partager quelque chose qui nous a bloqués pendant quelques jours: le code du fournisseur de fichiers DOIT être inséré entre les balises d'application, pas après. C'est peut-être trivial, mais ce n'est jamais spécifié, et je pensais que j'aurais pu aider quelqu'un! (merci encore à piolo94)

Eric Draven
la source