Android.os.FileUriExposedException: file.jpg 通过ClipData.Item.getUri()超出应用程序范围

46

我试图创建一个按钮来打开相机并拍照。我的代码在这里。

//for imports check on bottom of this code block

public class HomeProfileActivity extends AppCompatActivity {
//Button camera
public static final String TAG = HomeProfileActivity.class.getSimpleName();
public static final int REQUEST_TAKE_PHOTO = 0;
public static final int REQUEST_TAKE_VIDEO = 1;
public static final int REQUEST_PICK_PHOTO = 2;
public static final int REQUEST_PICK_VIDEO = 3;
public static final int MEDIA_TYPE_IMAGE = 4;
public static final int MEDIA_TYPE_VIDEO = 5;

private Uri mMediaUri;
private ImageView photobutton;
private Button buttonUploadImage, buttonTakeImage;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_home_profile);
    ButterKnife.bind(this);
}

@OnClick(R.id.buttonTakeImage)
void takePhoto() {
    mMediaUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE);
    if (mMediaUri == null) {
        Toast.makeText(this,
                "There was a problem accessing your device's external storage.",
                Toast.LENGTH_LONG).show();
    }
    else {
        Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, mMediaUri);
        startActivityForResult(takePhotoIntent, REQUEST_TAKE_PHOTO);
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (resultCode == RESULT_OK){
        if (requestCode == REQUEST_TAKE_PHOTO) {
            Intent intent = new Intent(this, ViewImageActivity.class);
            intent.setData(mMediaUri);
            startActivity(intent);
        }
    }
    else if (resultCode != RESULT_CANCELED){
        Toast.makeText(this, "Sorry, there was an error", Toast.LENGTH_SHORT).show();
    }
}

private Uri getOutputMediaFileUri(int mediaType) {
    // check for external storage
    if (isExternalStorageAvailable()) {
        // get the URI

        // 1. Get the external storage directory
        File mediaStorageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);

        // 2. Create a unique file name
        String fileName = "";
        String fileType = "";
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());

        if (mediaType == MEDIA_TYPE_IMAGE) {
            fileName = "IMG_" + timeStamp;
            fileType = ".jpg";
        } else if (mediaType == MEDIA_TYPE_VIDEO) {
            fileName = "VID_" + timeStamp;
            fileType = ".mp4";
        } else {
            return null;
        }
        // 3. Create the file
        File mediaFile;
        try {
            mediaFile = File.createTempFile(fileName, fileType, mediaStorageDir);
            Log.i(TAG, "File: " + Uri.fromFile(mediaFile));

            // 4. Return the file's URI
            return Uri.fromFile(mediaFile);
        }
        catch (IOException e) {
                Log.e(TAG, "Error creating file: " +
                        mediaStorageDir.getAbsolutePath() + fileName + fileType);
            }
        }

        // something went wrong
        return null;
    }

private boolean isExternalStorageAvailable(){
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)){
        return true;
    }
    else {
        return false;
    }
}

import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomNavigationView;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import butterknife.ButterKnife;
import butterknife.OnClick;

我在onclick方法中使用startActivityForResult出现问题,同时导入的java.text.SimpleDateFormat也在运行时异常中跳过, 我正在使用buildtoolsversion sdk 25。


5
在 Android 7.0 及以上的系统中,如果 targetSdkVersion 的值大于或等于 24,则无法使用 Uri.fromFile() 方法。应该改用 FileProvider 方法。在这个示例应用程序中我有示范。还可以参考这篇博客文章这篇博客文章 - CommonsWare
请查看此问题的答案:https://dev59.com/zVoT5IYBdhLWcg3wvRpk - Spark.Bao
1
这里提供易于理解的解释:[链接](https://inthecheesefactory.com/blog/how-to-share-access-to-file-with-fileprovider-on-android-nougat/en) - Meet Vora
3个回答

167

除了使用FileProvider解决此问题的方法外,还有另一种方法可行。简单来说

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

在Application.onCreate()方法中。通过这种方式,VM将忽略文件URI的暴露。


4
这对我有用,谢谢。有没有可能会出现未来的问题需要我知道的,还是我暂时没问题了? - Thiago Queiroz
4
默认情况下,StrictMode被禁用,用户需要在其Android设备中进入“开发者模式”才能启用它。这使得使用StrictMode的解决方案变得无关紧要。 - tamtom
1
运行得非常顺利! - GeekWithGlasses
我应该将其包装在SDK版本24或更高版本中吗? - Abu Nayem
@AbuNayem,你应该使用FileProvider,这是正确的解决方案。这个答案是一个hack。 - Xam
显示剩余2条评论

48

这些信息来自于: FileUriExposedException

只有针对 Android N 或更高版本的应用程序才会抛出此异常。 针对早期 SDK 版本的应用程序允许共享 file:// Uri,但强烈不建议这样做。

因此,如果您的 app/build.gradle 文件的 compileSdkVersion(构建目标)是 Android N(API 级别 24)或更高版本,则在编写可能被其他应用程序访问的文件时将引发此错误。 最重要的是,以下是您未来应该如何处理:

取自于此处: FileProvider

更改为:

return Uri.fromFile(mediaFile);

成为

return FileProvider.getUriForFile(getApplicationContext(), getPackageName()+".fileprovider", mediaFile);

注意:如果您控制mydomain.com,您还可以将getPackageName() +".fileprovider"替换为"com.mydomain.fileprovider"(对于下面的AndroidManifest.xml也是如此)。

另外,在</application>标签之前,您需要将以下内容添加到AndroidManifest.xml文件中。

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

接下来,您需要在您的app/src/main/res/xml目录中添加一个名为filepaths.xml的文件,并将其内容设置如下:

<paths>
    <external-files-path name="Pictures" path="Pictures" />
</paths>
注意:对于其他人,我们在上面使用了external-files-path,因为Omer在代码中使用了getExternalFilesDir(Environment.DIRECTORY_PICTURES)。 对于任何其他位置,请查看FileProvider下的“指定可用文件”部分,并根据您的文件位置之一将external-files-path更改为以下之一。
files-path
cache-path
external-path
external-files-path
external-cache-path

同时,将上面的Pictures修改为您的文件夹名称。

有一个需要避免的重要反模式,就是不应该使用 if (Build.VERSION.SDK_INT < 24) { 来让您以旧方式进行操作,因为它不是他们的Android版本需要这样做,而是您的构建版本(我第一次编码时错过了这个问题)。

我知道这会增加更多的工作量,但这使得Android只允许您与其共享的其他应用程序对文件进行临时访问,从而更加安全保护用户的隐私。


2
快速编辑一下:path="pictures" 是区分大小写的。在我的环境中使用 Environment.DIRECTORY_PICTURES 时,我不得不将其大写为 path="Pictures",否则它无法找到路径。 - Dave Sanders
@DaveSanders 感谢您注意到这一点,我已经更新了我的答案以反映Environment.DIRECTORY_PICTURES的情况,它评估为“Pictures”(标题大小写)。 - Jared
2
请注意,FileProvider 的 AndroidX 版本的包名为 androidx.core.content.FileProvider - hornet2319

-3
Add the below code where the SharingIntent called:-

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接