¿Cómo utilizar el soporte FileProvider para compartir contenido con otras aplicaciones?

142

Estoy buscando una manera de compartir correctamente (no ABRIR) un archivo interno con una aplicación externa utilizando FileProvider de la biblioteca de soporte de Android .

Siguiendo el ejemplo en los 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>

y usar ShareCompat para compartir un archivo con otras aplicaciones de la siguiente manera:

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

no funciona, ya que FLAG_GRANT_READ_URI_PERMISSION solo otorga permiso para el Uri especificado en datala intención, no el valor del EXTRA_STREAMextra (como fue establecido por setStream).

Traté de comprometer la seguridad mediante el establecimiento android:exportedde truepara el proveedor, pero FileProviderinternamente comprueba si misma es exportada, cuando es así, se produce una excepción.

Randy Sugianto 'Yuku'
fuente
77
+1 esta parece ser una pregunta realmente original: no hay nada en Google o StackOverflow , que yo sepa.
Phil
Aquí hay una publicación para obtener una configuración básica de FileProvider utilizando un proyecto de ejemplo de github mínimo: stackoverflow.com/a/48103567/2162226 . Proporciona los pasos para los archivos a copiar (sin hacer cambios) en un proyecto local independiente
Gene Bo

Respuestas:

159

Al usar la FileProviderbiblioteca de soporte, debe otorgar y revocar manualmente permisos (en tiempo de ejecución) para que otras aplicaciones lean Uri específicos. Utilice los métodos Context.grantUriPermission y Context.revokeUriPermission .

Por ejemplo:

//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, si no puede proporcionar el nombre del paquete, puede otorgar el permiso a todas las aplicaciones que puedan manejar una intención específica:

//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 según la documentación :

  • Ponga el URI de contenido en un Intent llamando a setData ().
  • Luego, llame al método Intent.setFlags () con FLAG_GRANT_READ_URI_PERMISSION o FLAG_GRANT_WRITE_URI_PERMISSION o ambos.
  • Finalmente, envíe la intención a otra aplicación. Muy a menudo, haces esto llamando a setResult ().

    Los permisos otorgados en una Intención permanecen vigentes mientras la pila de la Actividad receptora está activa. Cuando finaliza la pila, los
    permisos se eliminan automáticamente. Los permisos otorgados a una
    Actividad en una aplicación cliente se extienden automáticamente a otros
    componentes de esa aplicación.

Por cierto. si es necesario, puede copiar la fuente de FileProvider y cambiar el attachInfométodo para evitar que el proveedor compruebe si se exporta.

Leszek
fuente
26
Usando el grantUriPermissionmétodo, necesitamos proporcionar el nombre del paquete de la aplicación a la que queremos otorgar el permiso. Sin embargo, al compartir, generalmente no sabemos qué aplicación es el destino para compartir.
Randy Sugianto 'Yuku'
¿Qué sucede si no conocemos el nombre del paquete y solo queremos que otras aplicaciones puedan abrirlo
StuStirling
3
¿Puede proporcionar un código de trabajo para la última solución @Lecho?
StErMi
2
¿Hay un seguimiento de error abierto por qué el soporte FileProvider no funciona con setFlags, pero funciona con grantUriPermission? ¿O no es un error, en cuyo caso cómo no es un error?
RMN
1
Además de esto, descubrí que la llamada grantUriPermission no funcionó para un intento creado con ShareCompat, pero sí al crear el intento manualmente.
StuStirling
33

Código de trabajo que muestra cómo compartir archivos desde la carpeta interna de la aplicación. Probado en Android 7 y 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 en sí

    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);
Diversos
fuente
1
El uso del ShareCompat.IntentBuilderpermiso URI de lectura y finalmente lo hizo por mí.
Cord Rehn
Saltos para otras versiones de Android
Oliver Dixon
@OliverDixon: ¿cuáles? Lo probé en Android 6.0 y 9.0.
Violet Giraffe
1
Esta es la única respuesta sobre el uso FileProviderque funciona. Las otras respuestas hacen que el manejo de la intención sea incorrecto (no lo usan ShareCompat.IntentBuilder), y esa aplicación no se puede abrir de ninguna manera significativa. Pasé literalmente la mayor parte del día en estas tonterías hasta que finalmente encontré tu respuesta.
Violet Giraffe
1
@VioletGiraffe Tampoco estoy usando ShareCompat pero el mío funciona. Mi problema fue que estaba otorgando los permisos de lectura a la intención de mi selector por error, cuando necesitaba otorgar permisos de lectura a mi intención ANTES de que se pasara a la intención del selector, así como al selector. Espero que tenga sentido. Solo asegúrese de otorgar permiso de lectura a la intención correcta.
Ryan
23

Esta solución funciona para mí desde OS 4.4. Para que funcione en todos los dispositivos, agregué una solución alternativa para dispositivos más antiguos. Esto asegura que siempre se use la solución más 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);
    }
}

El permiso se revoca con revokeFileReadPermission () en los métodos onResume y onDestroy () del Fragmento o la Actividad.

Oliver Kranz
fuente
1
Esta solución funcionó para mí, pero solo después de agregar intent.setDataAndType (uri, "video / *"); Por cierto, el archivo adjunto
perdidoUri
¿Quieres decir que fileeso no cambiará de onResumea onDestroy?
CoolMind
16

Como, como dice Phil en su comentario sobre la pregunta original, esto es único y no hay otra información sobre SO en Google, pensé que también debería compartir mis resultados:

En mi aplicación, FileProvider funcionó para compartir archivos utilizando la intención de compartir. No hubo una configuración especial o código necesario, más allá de eso para configurar el FileProvider. En mi manifest.xml coloqué:

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

En my_paths.xml tengo:

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

En mi código tengo:

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

Y puedo compartir mi tienda de archivos en el almacenamiento privado de mis aplicaciones con aplicaciones como Gmail y Google Drive sin ningún problema.

Luke Sleeman
fuente
9
Lamentablemente esto no funciona. Esa variable fileToShare solo funciona si el archivo existe en una tarjeta SD o en un sistema de archivos externo. El objetivo de usar FileProvider es para que pueda compartir contenido desde el sistema interno.
Keith Connolly
Esto parece funcionar correctamente con archivos de almacenamiento local (en Android 5+). Pero tengo problemas con Android 4.
Peterdk
15

Por lo que puedo decir, esto solo funcionará en las versiones más nuevas de Android, por lo que probablemente tendrá que encontrar una forma diferente de hacerlo. Esta solución funciona para mí en 4.4, pero no en 4.0 o 2.3.3, por lo que no será una forma útil de compartir contenido para una aplicación que debe ejecutarse en cualquier dispositivo Android.

En 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 cómo especifica las autoridades. Debe especificar la actividad a partir de la cual creará el URI y lanzar la intención de compartir, en este caso la actividad se llama SharingActivity. ¡Este requisito no es obvio en los documentos 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>

Tenga cuidado al especificar la ruta. El valor predeterminado anterior es la raíz de su almacenamiento interno privado.

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

En este ejemplo estamos compartiendo una imagen JPEG.

Finalmente, probablemente sea una buena idea asegurarse de que ha guardado el archivo correctamente y que puede acceder a él con algo como esto:

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

En mi aplicación FileProvider funciona bien, y puedo adjuntar archivos internos almacenados en el directorio de archivos a clientes de correo electrónico como Gmail, Yahoo, etc.

En mi manifiesto como se menciona en la documentación de Android, coloqué:

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

Y como mis archivos se almacenaron en el directorio de archivos raíz, los filepaths.xml fueron los siguientes:

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

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

Esto funcionó para mí. Espero que esto ayude :)

Adarsh ​​Chithran
fuente
1
Usted da MVP real para publicar rutas = "." Trabajó para mí
robsstackoverflow
Recibo un error como "No se pudo encontrar la raíz configurada que contiene /" He usado el mismo código
Rakshith Kumar
5

Si obtiene una imagen de la cámara, ninguna de estas soluciones funciona para Android 4.4. En este caso, es mejor verificar las versiones.

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

solo para mejorar la respuesta dada anteriormente: si obtiene NullPointerEx:

También puede usar getApplicationContext () sin 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
fuente
1

Quiero compartir algo que nos bloqueó durante un par de días: el código del proveedor de archivos DEBE insertarse entre las etiquetas de la aplicación, no después. Puede ser trivial, pero nunca se especifica, ¡y pensé que podría haber ayudado a alguien! (gracias de nuevo a piolo94)

Eric Draven
fuente