android.os.FileUriExposedException:file:///storage/emulated/0/test.txt通过Intent.getData()暴露在app之外

问题描述 投票:621回答:22

当我尝试打开文件时,应用程序崩溃了。它可以在Android Nougat下运行,但在Android Nougat上它会崩溃。当我尝试从SD卡打开文件而不是从系统分区打开文件时,它只会崩溃。一些许可问题?

示例代码:

File file = new File("/storage/emulated/0/test.txt");
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "text/*");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent); // Crashes on this line

日志:

android.os.FileUriExposedException:file:///storage/emulated/0/test.txt通过Intent.getData()暴露在app之外

编辑:

在定位Android Nougat时,不再允许使用file:// URI。我们应该使用content:// URI。但是,我的应用程序需要打开根目录中的文件。有任何想法吗?

android android-file android-7.0-nougat
22个回答
1120
投票

如果您的targetSdkVersion >= 24,那么我们必须使用FileProvider类来访问特定文件或文件夹,以使其可供其他应用程序访问。我们创建自己继承FileProvider的类,以确保我们的FileProvider不会与导入的依赖项中声明的FileProviders冲突,如here所述。

file:// URI替换content:// URI的步骤:

  • 添加一个扩展FileProvider的类 public class GenericFileProvider extends FileProvider {}
  • <provider>标签下的AndroidManifest.xml中添加一个FileProvider <application>标签。为android:authorities属性指定唯一权限以避免冲突,导入的依赖项可能指定${applicationId}.provider和其他常用权限。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    <application
        ...
        <provider
            android:name=".GenericFileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
    </application>
</manifest>
  • 然后在provider_paths.xml文件夹中创建一个res/xml文件。如果文件夹不存在,则可能需要创建文件夹。该文件的内容如下所示。它描述了我们希望在名为external_files的根文件夹(path=".")上共享对外部存储的访问。
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>
  • 最后一步是更改下面的代码行 Uri photoURI = Uri.fromFile(createImageFile()); Uri photoURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".my.package.name.provider", createImageFile());
  • 编辑:如果您正在使用意图使系统打开您的文件,您可能需要添加以下代码行: intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

请参考,完整的代码和解决方案已解释here.


18
投票

我的解决方案是将文件路径'Uri.parse'作为字符串,而不是使用Uri.fromFile()。

String storage = Environment.getExternalStorageDirectory().toString() + "/test.txt";
File file = new File(storage);
Uri uri;
if (Build.VERSION.SDK_INT < 24) {
    uri = Uri.fromFile(file);
} else {
    uri = Uri.parse(file.getPath()); // My work-around for new SDKs, causes ActivityNotFoundException in API 10.
}
Intent viewFile = new Intent(Intent.ACTION_VIEW);
viewFile.setDataAndType(uri, "text/plain");
startActivity(viewFile);

似乎fromFile()使用了一个文件指针,我认为当内存地址暴露给所有应用程序时,这可能是不安全的。但是文件路径字符串从不会伤害任何人,因此它可以在不抛出FileUriExposedException的情况下工作。

测试API级别9到27!成功打开文本文件以在另一个应用程序中进行编辑。不需要FileProvider,也不需要Android支持库。


16
投票

只需在活动onCreate()中粘贴以下代码即可。

StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); 
StrictMode.setVmPolicy(builder.build());

它将忽略URI暴露。

快乐的编码:-)


12
投票

我使用了上面给出的Palash的答案,但它有点不完整,我必须提供这样的许可

Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri uri;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        uri = FileProvider.getUriForFile(this, getPackageName() + ".provider", new File(path));

        List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo resolveInfo : resInfoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
        }
    }else {
        uri = Uri.fromFile(new File(path));
    }

    intent.setDataAndType(uri, "application/vnd.android.package-archive");

    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    startActivity(intent);

4
投票

要从服务器下载pdf,请在服务类中添加以下代码。希望这对你有所帮助。

File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName + ".pdf");
    intent = new Intent(Intent.ACTION_VIEW);
    //Log.e("pathOpen", file.getPath());

    Uri contentUri;
    contentUri = Uri.fromFile(file);
    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);

    if (Build.VERSION.SDK_INT >= 24) {

        Uri apkURI = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", file);
        intent.setDataAndType(apkURI, "application/pdf");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

    } else {

        intent.setDataAndType(contentUri, "application/pdf");
    }

是的,不要忘记在清单中添加权限和提供程序。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<application

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths" />
    </provider>

</application>

4
投票

在onCreate中添加这两行

StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
    StrictMode.setVmPolicy(builder.build());

分享方法

File dir = new File(Environment.getExternalStorageDirectory(), "ColorStory");
File imgFile = new File(dir, "0.png");
Intent sendIntent = new Intent(Intent.ACTION_VIEW);
sendIntent.setType("image/*");
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + imgFile));
sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(sendIntent, "Share images..."));

3
投票

我不知道为什么,我做的一切与Pkosta(https://stackoverflow.com/a/38858040)完全一样,但一直都有错误:

java.lang.SecurityException: Permission Denial: opening provider redacted from ProcessRecord{redacted} (redacted) that is not exported from uid redacted

我在这个问题上浪费了几个小时。罪魁祸首?科特林。

val playIntent = Intent(Intent.ACTION_VIEW, uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

intent实际上是设置getIntent().addFlags而不是在我新宣布的playIntent上运行。


1
投票

我把这种方法,所以imageuri路径很容易进入内容。

enter code here
public Uri getImageUri(Context context, Bitmap inImage)
{
    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    inImage.compress(Bitmap.CompressFormat.PNG, 100, bytes);
    String path = MediaStore.Images.Media.insertImage(context.getContentResolver(), 
    inImage, "Title", null);
    return Uri.parse(path);
}

1
投票

我知道这是一个非常古老的问题,但这个答案适合未来的观众。所以我遇到了类似的问题,经过研究,我找到了这种方法的替代品。

您的意图如下:从您在Kotlin的路径中查看您的图像

 val intent = Intent()
 intent.setAction(Intent.ACTION_VIEW)
 val file = File(currentUri)
 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
 val contentURI = getContentUri(context!!, file.absolutePath)
 intent.setDataAndType(contentURI,"image/*")
 startActivity(intent)

主要功能如下

private fun getContentUri(context:Context, absPath:String):Uri? {
        val cursor = context.getContentResolver().query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            arrayOf<String>(MediaStore.Images.Media._ID),
            MediaStore.Images.Media.DATA + "=? ",
            arrayOf<String>(absPath), null)
        if (cursor != null && cursor.moveToFirst())
        {
            val id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID))
            return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, Integer.toString(id))
        }
        else if (!absPath.isEmpty())
        {
            val values = ContentValues()
            values.put(MediaStore.Images.Media.DATA, absPath)
            return context.getContentResolver().insert(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        }
        else
        {
            return null
        }
    }

同样地,您可以使用任何其他文件格式(如pdf)而不是图像,在我的情况下,它工作得很好


0
投票

只需在活动onGreate()中粘贴以下代码即可

StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); StrictMode.setVmPolicy(builder.build());

它将忽略URI暴露


-1
投票

https://stackoverflow.com/a/38858040/395097这个答案是完整的。

这个答案适用于 - 您已经有一个目标低于24的应用程序,现在您正在升级到targetSDKVersion> = 24。

在Android N中,仅更改了暴露给第三方应用程序的文件uri。 (不是我们之前使用它的方式)。因此,只更改与第三方应用程序共享路径的位置(在我的情况下为Camera)

在我们的应用程序中,我们将uri发送到相机应用程序,在该位置我们期望相机应用程序存储捕获的图像。

  1. 对于android N,我们生成新的Content://基于uri的url指向文件。
  2. 我们生成相同的基于File api的路径(使用旧方法)。

现在我们有2个不同的uri用于同一个文件。 #1与相机应用共享。如果摄像头意图成功,我们可以从#2访问图像。

希望这可以帮助。


278
投票

除了使用FileProvider的解决方案,还有另一种解决方法。简单的说

StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());

Application.onCreate()。以这种方式,VM忽略文件URI曝光。

方法

builder.detectFileUriExposure()

启用文件曝光检查,如果我们不设置VmPolicy,这也是默认行为。

我遇到了一个问题,如果我使用content:// URI发送一些东西,一些应用程序根本无法理解它。并且不允许降级target SDK版本。在这种情况下,我的解决方案很有用

更新:

正如评论中所提到的,StrictMode是诊断工具,不应该用于此问题。当我在一年前发布此答案时,许多应用程序只能接收文件uris。当我尝试向他们发送FileProvider uri时,它们就崩溃了。这在大多数应用程序中已得到修复,因此我们应该使用FileProvider解决方案。


-1
投票

XA Marin.Android

注意:无法解析路径xml / provider_paths.xml(.axml),即使在资源下创建了xml文件夹(也许它可以放在像Values这样的现有位置,但没试过),所以我求助于这适用于现在。测试显示每个应用程序运行只需要调用一次(这有意义的是它改变了主机VM的运行状态)。

注意:xml需要大写,所以Resources / Xml / provider_paths.xml

Java.Lang.ClassLoader cl = _this.Context.ClassLoader;
Java.Lang.Class strictMode = cl.LoadClass("android.os.StrictMode");                
System.IntPtr ptrStrictMode = JNIEnv.FindClass("android/os/StrictMode");
var method = JNIEnv.GetStaticMethodID(ptrStrictMode, "disableDeathOnFileUriExposure", "()V");                
JNIEnv.CallStaticVoidMethod(strictMode.Handle, method);

-1
投票

@Pkosta的答案是这样做的一种方式。

除了使用FileProvider之外,您还可以将文件插入MediaStore(特别是图像和视频文件),因为MediaStore中的文件可供每个应用程序访问:

MediaStore主要针对视频,音频和图像MIME类型,但从Android 3.0(API级别11)开始,它还可以存储非媒体类型(有关详细信息,请参阅MediaStore.Files)。可以使用scanFile()将文件插入MediaStore,然后将适合共享的content:// style Uri传递给提供的onScanCompleted()回调。请注意,一旦添加到系统MediaStore,设备上的任何应用程序都可以访问该内容。

例如,您可以将视频文件插入MediaStore,如下所示:

ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DATA, videoFilePath);
Uri contentUri = context.getContentResolver().insert(
      MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);

contentUri就像content://media/external/video/media/183473,可以直接传递给Intent.putExtra

intent.setType("video/*");
intent.putExtra(Intent.EXTRA_STREAM, contentUri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.startActivity(intent);

这对我有用,并且省去了使用FileProvider的麻烦。


-2
投票

在我的情况下,我通过用SetDataAndType替换SetData摆脱了异常。


143
投票

如果您的应用面向API 24+,并且您仍然需要/需要使用file:// intents,则可以使用hacky方式禁用运行时检查:

if(Build.VERSION.SDK_INT>=24){
   try{
      Method m = StrictMode.class.getMethod("disableDeathOnFileUriExposure");
      m.invoke(null);
   }catch(Exception e){
      e.printStackTrace();
   }
}

方法StrictMode.disableDeathOnFileUriExposure被隐藏并记录为:

/**
* Used by lame internal apps that haven't done the hard work to get
* themselves off file:// Uris yet.
*/

问题是我的应用程序不是蹩脚的,而是不希望被使用内容瘫痪://那些许多应用程序无法理解的意图。例如,使用content:// scheme打开mp3文件比在file:// scheme上打开相同的应用程序要少得多。我不想通过限制我的应用程序的功能来支付Google的设计错误。

谷歌希望开发人员使用内容方案,但系统并没有为此做好准备,多年来,应用程序使用文件而不是“内容”,文件可以编辑和保存,而文件服务的内容方案不能(可以他们?)。


131
投票

如果targetSdkVersion高于24,那么FileProvider用于授予访问权限。

创建一个xml文件(Path:res \ xml)provider_paths.xml

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

AndroidManifest.xml中添加提供商

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths"/>
    </provider>

如果您使用的是androidx,则FileProvider路径应为:

 android:name="androidx.core.content.FileProvider"

并替换

Uri uri = Uri.fromFile(fileImagePath);

Uri uri = FileProvider.getUriForFile(MainActivity.this, BuildConfig.APPLICATION_ID + ".provider",fileImagePath);

编辑:当您使用Intent包含URI时,请确保添加以下行:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

你很高兴。希望能帮助到你。


82
投票

如果你的targetSdkVersion是24或更高,you can not use file: Uri values in Intents on Android 7.0+ devices

你的选择是:

  1. 把你的targetSdkVersion降到23或更低,或者
  2. 将您的内容放在内部存储上,然后将use FileProvider选择性地提供给其他应用程序

例如:

Intent i=new Intent(Intent.ACTION_VIEW, FileProvider.getUriForFile(this, AUTHORITY, f));

i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(i);

(来自this sample project


44
投票

首先,您需要为AndroidManifest添加提供程序

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

现在在xml资源文件夹中创建一个文件(如果使用android studio,你可以在突出显示file_paths后选择Alt + Enter并选择创建一个xml资源选项)

接下来在file_paths文件中输入

<?xml version="1.0" encoding="utf-8"?>
<paths>
  <external-path path="Android/data/com.your.package/" name="files_root" />
  <external-path path="." name="external_storage_root" />
</paths>

此示例适用于外部路径,您可以参考here获取更多选项。这将允许您共享该文件夹及其子文件夹中的文件。

现在剩下的就是按如下方式创建意图:

    MimeTypeMap mime = MimeTypeMap.getSingleton();
    String ext = newFile.getName().substring(newFile.getName().lastIndexOf(".") + 1);
    String type = mime.getMimeTypeFromExtension(ext);
    try {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_VIEW);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(getContext(), "com.your.package.fileProvider", newFile);
            intent.setDataAndType(contentUri, type);
        } else {
            intent.setDataAndType(Uri.fromFile(newFile), type);
        }
        startActivityForResult(intent, ACTIVITY_VIEW_ATTACHMENT);
    } catch (ActivityNotFoundException anfe) {
        Toast.makeText(getContext(), "No activity found to open this attachment.", Toast.LENGTH_LONG).show();
    }

编辑:我在file_paths中添加了SD卡的根文件夹。我已经测试了这段代码,但确实有效。


25
投票

@palash k答案是正确的,适用于内部存储文件,但在我的情况下我也想从外部存储打开文件,我的应用程序在从外部存储打开文件时崩溃,如sdcard和usb,但我设法通过修改来解决问题来自接受的答案的provider_paths.xml

像下面一样更改provider_paths.xml

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

<external-path path="Android/data/${applicationId}/" name="files_root" />

<root-path
    name="root"
    path="/" />

</paths>

并在java类中(没有更改,因为接受的答案只是一个小编辑)

Uri uri=FileProvider.getUriForFile(getActivity(), BuildConfig.APPLICATION_ID+".provider", File)

这有助于我修复来自外部存储的文件的崩溃,希望这将帮助一些人有同样的问题,如我的:)


20
投票

只需在活动onGreate()中粘贴以下代码即可

StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); StrictMode.setVmPolicy(builder.build());

它将忽略URI暴露


19
投票

使用fileProvider是可行的方法。但是你可以使用这个简单的解决方法:

警告:它将在下一个Android版本中修复 - https://issuetracker.google.com/issues/37122890#comment4

更换:

startActivity(intent);

通过

startActivity(Intent.createChooser(intent, "Your title"));
© www.soinside.com 2019 - 2024. All rights reserved.