美文网首页Android适配
Android 7.0 适配——访问应用私有文件

Android 7.0 适配——访问应用私有文件

作者: 渐渐懂了吧 | 来源:发表于2019-10-08 09:51 被阅读0次

前言

在Android7.0之前,第三方的应用可以访问我们自身应用的私有路径,比如通过Intent分享一张图片到第三方。Android7.0之后,当我们隐式启动其他应用时,使用File文件传递file://这样的URI私有目录,会导致对方应用访问不到这个路径从而触发FileUriExposedException异常,这是官方出于保护用户隐私的考虑,第三方应用是不可以随便访问我们APP的私有路径的。当我们应用打开另一个应用比如系统相机,对该文件的访问权应该是我们的应用程序而不是系统相机的。对文件进行的每个操作都应该通过我们的应用程序完成,而不是由系统相机完成。

官方说明

关于7.0之后的一些变更,具体可以参考地址(需科学上网):https://developer.android.google.cn/about/versions/nougat/android-7.0-changes.html

权限更改.png
权限更改.png

操作

今天就用一个启动系统相机的例子来演示如何避免这个问题。
1.权限

既然是拍照那肯定需要写入权限,照片是要保存到本地的。这里特别说明调用系统相机是不需要拍照权限的。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2.Android7.0之前的调用方式
    private void openCamera() {
        String imageDir = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "images" + File.separator;
        String imageName = System.currentTimeMillis() + ".jpg";
        imagePath = imageDir + imageName;
        File file = new File(imageDir, imageName);
        if (!file.getParentFile().exists()) {
            Log.e("yzt", "创建文件夹成功>>>" + file.getParentFile().mkdirs());
        }
        uri = Uri.fromFile(file);
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        startActivityForResult(intent, REQUEST_CODE_CAMERA);
    }
3.Android7.0之后的调用方式

首先要在res之下新建一个xml文件夹,在其中再建立一个file_path.xml文件,这个文件的作用是为FileProvider提供可以暴露的路径,一旦一个路径在文件中被声明,那么就可以被FileProvider提供。这里的path就是共享的图片路径,name代表使用这个字段去访问真实的文件路径。

<?xml version="1.0" encoding="utf-8"?>
<paths>

    <external-path
        name="Camera"
        path="images/" />

</paths>

<paths>标签下有多种path可供使用,具体可参考地址(需科学上网):
https://developer.android.google.cn/reference/android/support/v4/content/FileProvider.html

然后在AndroidManifest.xml中声明一个provider组件(因为FileProvider是Android四大组件之一的ContentProvider的子类,因此需要在清单文件中声明),其中android:authorities可以随意填写,<meta-data>中的android:resource填写的就是刚才创建的file_path.xml。

        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.yuzhentao.demo.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_path" />
        </provider>

这里与7.0之前最大的不同就在于Uri的获取方式,是通过FileProvider.getUriForFile()这个方法来获取的,其中第二个参数就是上面我们在<provider>中定义的 android:authorities,这里切记要填写相同。这里打印Uri可以看到Uri为content://com.yuzhentao.demo.fileprovider/Camera/XXXXX.jpg

    private void openCamera_N() {
        String imageDir = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "images" + File.separator;
        String imageName = System.currentTimeMillis() + ".jpg";
        imagePath = imageDir + imageName;
        File file = new File(imageDir, imageName);
        if (!file.getParentFile().exists()) {
            Log.e("yzt", "创建文件夹成功>>>" + file.getParentFile().mkdirs());
        }
        uri = FileProvider.getUriForFile(context, "com.yuzhentao.demo.fileprovider", file);
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        startActivityForResult(intent, REQUEST_CODE_CAMERA);
    }
4.使用图片

我们在onActivityResult()中来进行回调,因为ImageView.setImageBitmap()对图片大小有要求,过大的图片使用这个方法不能展示出图片,但是保存到存储中我们又希望是没有压缩的图片,所以这里图片的展示和图片的保存使用了2个Bitmap。这里还使用到了一个ExifInterfaceUtil来获取图片的角度,它是一个照片旋转角度获取工具,主要使用到ExifInterface这个类,通常情况,调用照相机拍照之后产生的图片默认旋转角度为0,此信息可以通过读取图片的EXIF信息来获取到。对于某些手机拍照之后旋转角度被改变了(我也不知道为什么他们要这么做,特别是三星手机),这时我们可以通过android.graphics.Matrix将照片角度在旋转回去即可,然后再进行显示和保存。

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            if (requestCode == REQUEST_CODE_CAMERA) {
                if (uri == null || TextUtils.isEmpty(uri.getPath())) {
                    return;
                }

                Bitmap showBitmap = BitmapUtil.decodeFile(context, imagePath);
                if (BitmapUtil.isUseful(showBitmap) && !TextUtils.isEmpty(imagePath)) {
                    int imageDegree = ExifInterfaceUtil.getDegree(imagePath);
                    if (BitmapUtil.isUseful(showBitmap) && imageDegree > 0) {
                        showBitmap = BitmapUtil.rotateBitmap(showBitmap, imageDegree, false);
                    }
                }
                ((AppCompatImageView) findViewById(R.id.iv)).setImageBitmap(showBitmap);
                Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
                if (BitmapUtil.isUseful(bitmap) && !TextUtils.isEmpty(imagePath)) {
                    int imageDegree = ExifInterfaceUtil.getDegree(imagePath);
                    if (BitmapUtil.isUseful(bitmap) && imageDegree > 0) {
                        bitmap = BitmapUtil.rotateBitmap(bitmap, imageDegree, false);
                    }
                    BitmapUtil.saveImage(context, imagePath, bitmap);
                }
            }
        }
    }

BitmapUtil

public class BitmapUtil {

    public static Bitmap decodeFile(Context context, String path) {
        Bitmap bitmap;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, options);
        options.inSampleSize = calculateInSampleSize(options, DimensionUtil.getWidthInPx(context), DimensionUtil.getHeightInPx(context));
        options.inJustDecodeBounds = false;
        bitmap = BitmapFactory.decodeFile(path, options);
        return bitmap;
    }

    private static int calculateInSampleSize(BitmapFactory.Options options, float reqWidth, float reqHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        float maxPixels = reqWidth * reqHeight * 1.5f;
        float realPixels = width * height;
        if (maxPixels <= 0) {
            return 1;
        }
        try {
            if (realPixels <= maxPixels) {//如果图片尺寸小于最大尺寸,则直接读取
                return 1;
            } else {
                int scale = 2;
                while (realPixels / (scale * scale) > maxPixels) {
                    scale *= 2;
                }
                return scale;
            }
        } catch (Exception e) {
            return 1;
        }
    }

    public static void saveImage(Context context, String imagePath, Bitmap bitmap) {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return;
        }

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(imagePath);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            scanFile(context, imagePath);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.flush();
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    public static Bitmap rotateBitmap(Bitmap bitmap, float degrees, boolean isRecycle) {
        if (degrees == 0 || null == bitmap) {
            return bitmap;
        }

        Matrix matrix = new Matrix();
        matrix.setRotate(degrees, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
        Bitmap bmp = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        if (isRecycle) {
            bitmap.recycle();
        }
        return bmp;
    }

    public static void scanFile(Context context, String filePath) {
        Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        scanIntent.setData(Uri.fromFile(new File(filePath)));
        context.sendBroadcast(scanIntent);
    }

    public static boolean isUseful(Bitmap bitmap) {
        return bitmap != null
                && !bitmap.isRecycled()
                && bitmap.getWidth() > 0
                && bitmap.getHeight() > 0
                && bitmap.getConfig() != null;
    }

}

ExifInterfaceUtil,使用这个工具类先要依赖exifinterface这个库

implementation "com.android.support:exifinterface:28.0.0"
public class ExifInterfaceUtil {

    /**
     * 解决调用系统相机照片会自动旋转的问题
     */
    public static int getDegree(String imagePath) {
        int imageDegree = 0;
        try {
            ExifInterface exif = new ExifInterface(imagePath);
            int ori = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
            switch (ori) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    imageDegree = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    imageDegree = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    imageDegree = 270;
                    break;
                default:
                    imageDegree = 0;
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return imageDegree;
    }

}

结尾

以上大致是解决FileUriExposedException异常的一个流程,也有一些地方可以优化下,比如可以通过判断版本号来生成不同的Uri,这样就能兼容不同的Android版本。

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            uri = FileProvider.getUriForFile(context, "com.yuzhentao.customview.fileprovider", file);
        } else {
            uri = Uri.fromFile(file);
        }

相关文章

网友评论

    本文标题:Android 7.0 适配——访问应用私有文件

    本文链接:https://www.haomeiwen.com/subject/vzuquctx.html