162

我正在寻找一种使用 Android 支持库的FileProvider与外部应用程序正确共享(而不是打开)内部文件的方法。

按照文档上的示例,

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

并使用 ShareCompat 将文件共享给其他应用程序,如下所示:

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

不起作用,因为 FLAG_GRANT_READ_URI_PERMISSION 仅授予对data意图指定的 Uri 的权限,而不是EXTRA_STREAM额外的值(由 设置setStream)。

我试图通过设置为提供程序来破坏安全性android:exportedtrue但在FileProvider内部检查自身是否被导出,如果是,它会引发异常。

4

10 回答 10

173

使用FileProvider支持库,您必须手动授予和撤销权限(在运行时),其他应用程序才能读取特定的 Uri。使用Context.grantUriPermissionContext.revokeUriPermission方法。

例如:

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

作为最后的手段,如果您不能提供包名称,您可以将权限授予所有可以处理特定意图的应用程序:

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

根据文档的替代方法:

  • 通过调用 setData() 将内容 URI 放入 Intent。
  • 接下来,使用 FLAG_GRANT_READ_URI_PERMISSION 或 FLAG_GRANT_WRITE_URI_PERMISSION 或两者调用方法 Intent.setFlags()。
  • 最后,将 Intent 发送到另一个应用程序。大多数情况下,您通过调用 setResult() 来执行此操作。

    Intent 中授予的权限在接收 Activity 的堆栈处于活动状态时仍然有效。堆栈完成后,
    权限将自动删除。授予
    客户端应用程序中一个 Activity 的权限会自动扩展到
    该应用程序的其他组件。

顺便提一句。如果需要,您可以复制FileProvider 的源并更改attachInfo方法以防止提供程序检查它是否已导出。

于 2013-08-20T09:50:06.143 回答
50

完全工作的代码示例如何从内部应用程序文件夹共享文件。在 Android 7 和 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>

代码本身

    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);
于 2016-09-21T14:30:11.893 回答
24

自 OS 4.4 起,此解决方案对我有效。为了使其适用于所有设备,我为旧设备添加了一种解决方法。这确保始终使用最安全的解决方案。

清单.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>

文件路径.xml:

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

爪哇:

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

使用 Fragment 或 Activity 的 onResume 和 onDestroy() 方法中的 revokeFileReadPermission() 撤销权限。

于 2015-10-05T13:54:31.490 回答
16

因为正如菲尔在他对原始问题的评论中所说,这是独一无二的,并且在谷歌中没有关于 SO 的其他信息,我想我也应该分享我的结果:

在我的应用程序中,FileProvider 使用共享意图开箱即用地共享文件。除了设置 FileProvider 之外,没有必要的特殊配置或代码。在我的 manifest.xml 我放置:

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

在 my_paths.xml 我有:

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

在我的代码中,我有:

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

而且我可以毫无问题地与 Gmail 和谷歌驱动器等应用程序共享我的应用程序私人存储中的文件存储。

于 2013-09-03T04:01:10.223 回答
15

据我所知,这仅适用于较新版本的 Android,因此您可能必须想出一种不同的方法来做到这一点。此解决方案适用于 4.4,但不适用于 4.0 或 2.3.3,因此这不是为要在任何 Android 设备上运行的应用程序共享内容的有用方法。

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

请仔细注意您如何指定权限。您必须指定将创建 URI 并启动共享意图的活动,在这种情况下,该活动称为 SharingActivity。这个要求在谷歌的文档中并不明显!

文件路径.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>

请注意如何指定路径。以上默认为您的私有内部存储的根目录。

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

在此示例中,我们共享 JPEG 图像。

最后,向自己保证您已正确保存文件并且可以通过以下方式访问它可能是一个好主意:

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!");
}
于 2014-01-19T22:37:08.887 回答
9

在我的应用程序中 FileProvider 工作得很好,我可以将存储在文件目录中的内部文件附加到 Gmail、Yahoo 等电子邮件客户端。

在我放置的 Android 文档中提到的清单中:

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

由于我的文件存储在根文件目录中,因此 filepaths.xml 如下:

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

现在在代码中:

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

这对我有用。希望这有帮助:)

于 2014-10-28T13:27:27.023 回答
6

如果您从相机获取图像,这些解决方案都不适用于 Android 4.4。在这种情况下,最好检查版本。

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);
}
于 2018-03-01T16:10:37.733 回答
1

只是为了改进上面给出的答案:如果你得到 NullPointerEx:

您也可以在没有上下文的情况下使用 getApplicationContext()

                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);
                }
于 2017-01-01T09:23:51.623 回答
1

我想分享一些阻止我们几天的东西:文件提供程序代码必须插入应用程序标签之间,而不是在它之后。这可能是微不足道的,但从未指定,我认为我可以帮助某人!(再次感谢 piolo94)

于 2018-08-23T11:52:22.673 回答
1

grantUriPermission(来自 Android 文档)

通常,您应该使用 Intent#FLAG_GRANT_READ_URI_PERMISSION 或 Intent#FLAG_GRANT_WRITE_URI_PERMISSION 来启动活动,而不是直接使用此函数。如果直接使用此函数,则应确保在不再允许目标访问它时调用 revokeUriPermission(Uri, int)。

所以我测试了,我看到了。

  • 如果我们grantUriPermission在开始一项新活动之前使用,我们不需要 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSIONIntent克服SecurityException

  • 如果我们不使用 grantUriPermission. 我们需要使用FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION克服SecurityException

    • 你的意图必须包含UribysetData否则仍然 throw。(我看到一个有趣的:并且不能很好地协同工作,所以如果你需要两者并且你需要。你可以检查内部代码,目前当你,它也会设置 uri=null,当你 setUri 它也会设置 type=null)setDataAndTypeSecurityException setDatasetTypeUritypesetDataAndTypeIntentsetType
于 2021-02-15T07:53:31.767 回答