需求:

最近把APP的TargetSdk从21提高至25后,测试时,

在Android7.0以上的系统上,爆出了一些异常。

在个别小米等机型也存在一些异常。

问题分析:

FileUriExposedException文件URI暴露异常

主要原因:不符合Android7.0安全要求;

谷歌官方的解释:

对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。

小米手机 Unable to load resource 0x00000000 from pkg=com.android.systemui 异常,

主要原因:裁切图片时,直接通过intent返回图片数据导致。

解决方案:

向其他APP传递uri数据 的异常,我们使用FileProvider 把 file:// URI转为content:// URI再传递即可解决。

如果只是在自己APP中单独使用 file:// URI是没有问题的。

小米手机 Unable to load resource 0x00000000 from pkg=com.android.systemui 的异常,

我们裁剪完图片,不直接返回图片数据,而是返回指向裁剪后图片的uri即可。

我们项目中,传递uri的应用场景主要是:设置用户头像(拍照、相册选取、裁切),拍照等功能;

在使用前,应该先将公共的内容,抽取到一个独立的模块中,以便于将来维护和扩展:

代码实现步骤:

1.封装重复的内容:

a. 封装FileProvider类,提供转换 file:// URI 为 content:// URI的功能

b. AndroidManifest.xml中注册FileProvider(ContentProvider的子类,4大组件之一,需要注册)

c. res中新建@xml/file_paths文件(注册FileProvider时用到)

d. 封装拍照、打开相册、裁切等系统程序的调用

2 UI调用封装好的代码

具体实现:

1.封装重复的内容:

a. FileProviderUtils类,封装FileProvider类,提供转换 file:// URI 为 content:// URI的功能

package iwangzhe.paizhaocaiqie.android7.uri;

import android.app.Activity;

import android.content.Context;

import android.content.Intent;

import android.net.Uri;

import android.os.Build;

import android.support.v4.content.FileProvider;

import java.io.File;

/**

* 类:FileProviderUtils

* 从APP向外共享的文件URI时,必须使用该类进行适配,否则在7.0以上系统,会报错:FileUriExposedException(文件Uri暴露异常)

* 作者: qxc

* 日期:2018/2/23.

*/

public class FileProviderUtils {

/**

* 从文件获得URI

* @param activity 上下文

* @param file 文件

* @return 文件对应的URI

*/

public static Uri uriFromFile(Activity activity, File file) {

Uri fileUri;

//7.0以上进行适配

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

String p = activity.getPackageName() + ".FileProvider";

fileUri = FileProvider.getUriForFile(

activity,

p,

file);

} else {

fileUri = Uri.fromFile(file);

}

return fileUri;

}

/**

* 设置Intent的data和类型,并赋予目标程序临时的URI读写权限

* @param activity 上下文

* @param intent 意图

* @param type 类型

* @param file 文件

* @param writeAble 是否赋予可写URI的权限

*/

public static void setIntentDataAndType(Activity activity,

Intent intent,

String type,

File file,

boolean writeAble) {

//7.0以上进行适配

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

intent.setDataAndType(uriFromFile(activity, file), type);

//临时赋予读写Uri的权限

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

if (writeAble) {

intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

}

} else {

intent.setDataAndType(Uri.fromFile(file), type);

}

}

/**

* 设置Intent的data和类型,并赋予目标程序临时的URI读写权限

* @param context 上下文

* @param intent 意图

* @param type 类型

* @param fileUri 文件uri

* @param writeAble 是否赋予可写URI的权限

*/

public static void setIntentDataAndType(Context context,

Intent intent,

String type,

Uri fileUri,

boolean writeAble) {

//7.0以上进行适配

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {

intent.setDataAndType(fileUri, type);

//临时赋予读写Uri的权限

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

if (writeAble) {

intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

}

} else {

intent.setDataAndType(fileUri, type);

}

}

}

b. AndroidManifest.xml中注册FileProvider(ContentProvider的子类,4大组件之一,需要注册)

package="iwangzhe.paizhaocaiqie">

android:allowBackup="true"

android:icon="@mipmap/ic_launcher"

android:label="@string/app_name"

android:supportsRtl="true"

android:theme="@style/AppTheme">

android:name="android.support.v4.content.FileProvider"

android:authorities="${applicationId}.FileProvider"

android:grantUriPermissions="true"

android:exported="false">

android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/file_paths"/>

c. res中新建@xml/file_paths文件(注册FileProvider时用到)

name="root"

path="" />

name="files"

path="" />

name="cache"

path="" />

name="external"

path="" />

name="external_file"

path="" />

name="external_cache"

path="" />

d. SystemProgramUtils:对于拍照、打开相册、裁切等系统程序的调用进行封装

package iwangzhe.paizhaocaiqie.android7.uri;

import android.app.Activity;

import android.content.Intent;

import android.graphics.Bitmap;

import android.net.Uri;

import android.provider.MediaStore;

import java.io.File;

/**

* 类:SystemProgramUtils 系统程序适配

* 1. 拍照

* 2. 相册

* 3. 裁切

* 作者: qxc

* 日期:2018/2/23.

*/

public class SystemProgramUtils {

public static final int REQUEST_CODE_PAIZHAO = 1;

public static final int REQUEST_CODE_ZHAOPIAN = 2;

public static final int REQUEST_CODE_CAIQIE = 3;

public static void paizhao(Activity activity, File outputFile){

Intent intent = new Intent();

intent.setAction("android.media.action.IMAGE_CAPTURE");

intent.addCategory("android.intent.category.DEFAULT");

Uri uri = FileProviderUtils.uriFromFile(activity, outputFile);

intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);

activity.startActivityForResult(intent, REQUEST_CODE_PAIZHAO);

}

public static void zhaopian(Activity activity){

Intent intent = new Intent();

intent.setType("image/*");

intent.setAction("android.intent.action.PICK");

intent.addCategory("android.intent.category.DEFAULT");

activity.startActivityForResult(intent, REQUEST_CODE_ZHAOPIAN);

}

public static void Caiqie(Activity activity, Uri uri, File outputFile) {

Intent intent = new Intent("com.android.camera.action.CROP");

FileProviderUtils.setIntentDataAndType(activity, intent, "image/*", uri, true);

intent.putExtra("crop", "true");

intent.putExtra("aspectX", 1);

intent.putExtra("aspectY", 1);

intent.putExtra("outputX", 300);

intent.putExtra("outputY", 300);

//return-data为true时,直接返回bitmap,可能会很占内存,不建议,小米等个别机型会出异常!!!

//所以适配小米等个别机型,裁切后的图片,不能直接使用data返回,应使用uri指向

//裁切后保存的URI,不属于我们向外共享的,所以可以使用fill://类型的URI

Uri outputUri = Uri.fromFile(outputFile);

intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);

intent.putExtra("return-data", false);

intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());

intent.putExtra("noFaceDetection", true);

activity.startActivityForResult(intent, REQUEST_CODE_CAIQIE);

}

}

UI调用封装好的代码

package iwangzhe.paizhaocaiqie;

import android.Manifest;

import android.content.Intent;

import android.graphics.Bitmap;

import android.graphics.BitmapFactory;

import android.net.Uri;

import android.os.Bundle;

import android.support.annotation.NonNull;

import android.support.v7.app.AppCompatActivity;

import android.view.View;

import android.widget.Button;

import android.widget.ImageView;

import android.widget.Toast;

import java.io.File;

import iwangzhe.paizhaocaiqie.android7.uri.FileProviderUtils;

import iwangzhe.paizhaocaiqie.android7.uri.SystemProgramUtils;

import iwangzhe.paizhaocaiqie.permission.PermissionUtils;

import iwangzhe.paizhaocaiqie.permission.request.IRequestPermissions;

import iwangzhe.paizhaocaiqie.permission.request.RequestPermissions;

import iwangzhe.paizhaocaiqie.permission.requestresult.IRequestPermissionsResult;

import iwangzhe.paizhaocaiqie.permission.requestresult.RequestPermissionsResultSetApp;

public class MainActivity extends AppCompatActivity {

Button btnPaizhao;

Button btnXiangce;

ImageView ivTupian;

IRequestPermissions requestPermissions = RequestPermissions.getInstance();//动态权限请求

IRequestPermissionsResult requestPermissionsResult = RequestPermissionsResultSetApp.getInstance();//动态权限请求结果处理

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

initView();

initEvent();

}

//初始化控件

private void initView(){

btnPaizhao = (Button) findViewById(R.id.paizhao);

btnXiangce = (Button) findViewById(R.id.xiangce);

ivTupian = (ImageView) findViewById(R.id.tupian);

}

//初始化事件

private void initEvent(){

//拍照

btnPaizhao.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View view) {

if(!requestPermissions()){

return;

}

SystemProgramUtils.paizhao(MainActivity.this, new File("/mnt/sdcard/tupian.jpg"));

}

});

//相册

btnXiangce.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View view) {

if(!requestPermissions()){

return;

}

SystemProgramUtils.zhaopian(MainActivity.this);

}

});

}

//请求权限

private boolean requestPermissions(){

//需要请求的权限

String[] permissions = {Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.CAMERA};

//开始请求权限

return requestPermissions.requestPermissions(

this,

permissions,

PermissionUtils.ResultCode1);

}

//用户授权操作结果(可能授权了,也可能未授权)

@Override

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

super.onRequestPermissionsResult(requestCode, permissions, grantResults);

//用户给APP授权的结果

//判断grantResults是否已全部授权,如果是,执行相应操作,如果否,提醒开启权限

if(requestPermissionsResult.doRequestPermissionsResult(this, permissions, grantResults)){

//请求的权限全部授权成功,此处可以做自己想做的事了

//输出授权结果

Toast.makeText(MainActivity.this,"授权成功,请重新点击刚才的操作!",Toast.LENGTH_LONG).show();

}else{

//输出授权结果

Toast.makeText(MainActivity.this,"请给APP授权,否则功能无法正常使用!",Toast.LENGTH_LONG).show();

}

}

//拍照、相册、图片裁切结果回调

@Override

protected void onActivityResult(int requestCode, int resultCode, Intent data) {

super.onActivityResult(requestCode, resultCode, data);

if (resultCode != RESULT_OK) {

return;

}

Uri filtUri;

File outputFile = new File("/mnt/sdcard/tupian_out.jpg");//裁切后输出的图片

switch (requestCode) {

case SystemProgramUtils.REQUEST_CODE_PAIZHAO:

//拍照完成,进行图片裁切

File file = new File("/mnt/sdcard/tupian.jpg");

filtUri = FileProviderUtils.uriFromFile(MainActivity.this, file);

SystemProgramUtils.Caiqie(MainActivity.this, filtUri, outputFile);

break;

case SystemProgramUtils.REQUEST_CODE_ZHAOPIAN:

//相册选择图片完毕,进行图片裁切

if (data == null || data.getData()==null) {

return;

}

filtUri = data.getData();

SystemProgramUtils.Caiqie(MainActivity.this, filtUri, outputFile);

break;

case SystemProgramUtils.REQUEST_CODE_CAIQIE:

//图片裁切完成,显示裁切后的图片

try {

Uri uri = Uri.fromFile(outputFile);

Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));

ivTupian.setImageBitmap(bitmap);

}catch (Exception ex){

ex.printStackTrace();

}

break;

}

}

}

效果图:

测试机型:7.1小米手机

bec4497c2a63?tdsourcetag=s_pcqq_aiomsg

APP启动后的页面.jpg

demo中使用了拍照、相册等功能,使用前需要先去动态授权,动态授权的代码请参考:

https://www.jianshu.com/p/8e37e9cf20a5

bec4497c2a63?tdsourcetag=s_pcqq_aiomsg

拍照、选择相册图片后的 图片裁剪页面 .jpg

bec4497c2a63?tdsourcetag=s_pcqq_aiomsg

显示裁切后的图片.jpg

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐