时间过的真快啊,以前还在学习5.0,好不容易把5.0的控件都学会了,发现后来又学会6.0的权限适配,结果7.0又来了,有时候想想未来应该还有8.0、9.0、10.0、11.0吧…
言归正传,有一天我写版本更新的时候,在安装Apk阶段系统报出了FileUriExposedException
错误,一查之后才发现原来是因为Android7.0导致的 ~
无语,写了3个小时,不知道怎么突然没了!!!
版本兼容
Android7.0(N)开发者需要适配的主要有权限更改、后台优化、应用间共享文件等多个方面 ~
权限更改
关于权限更改的适配,太多Blog介绍的千篇一律了,有的直接把特性说出来了,好吧> <
关于系统权限更改的问题,主要是为了提升私有文件的安全性,针对Android7.0(N)或更高版本时限制访问私有目录;这样设置的目的就是防止私有文件数据泄露,比如文件是否存在、文件大小等等;
这个限制并非百利而无一害,所以有着下面这些副作用 ~
-
私有文件的文件权限不应再放权给所有的应用,当使用
MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
进行此类尝试将触发SecurityException
,从而导致App崩溃
处理方式:这项权限的变更将意味着你无法通过File API访问手机存储上的数据了,基于File API的一些文件浏览器等也将受到很大的影响,看到这大家是不是惊呆了呢,不过迄今为止,这种限制尚不能完全执行。 应用仍可能使用原生 API 或 File API 来修改它们的私有目录权限。 但是,Android官方强烈反对放宽私有目录的权限。可以看出收起对私有文件的访问权限是Android将来发展的趋势
-
传递软件包网域外的 file://URI可能给接收器留下无法访问的路径。因此传递file://URI会触发
FileUriExposedException
处理方式:通过使用FileProvide(共享文件)来解决这一问题
-
DownloadManager 不再按文件名分享私人存储的文件。
COLUMN_LOCAL_FILENAME
在Android7.0中被标记为deprecated ,旧版应用在访问COLUMN_LOCAL_FILENAME
时可能出现无法访问的路径。 面向 Android N 或更高版本的应用在尝试访问COLUMN_LOCAL_FILENAME
时会触发SecurityException
处理方式:大家可以通过ContentResolver.openFileDescriptor()来访问由 DownloadManager 公开的文件
后台优化
不知你是否有一些疑问,很多用过苹果手机的用户后来换成了Android手机,发现手机从启动到使用都没有很丝滑的感觉,你可能会将价格不太一个档次,当然这是一个原因,这里想说的是同等价位的场景下,Android7.0之前手机还是会稍微差一点,这差一点的原因主要在于系统默认开启的一些功能被有心人给用咯 ~
Android7.0之前系统会默认隐式注册部分广播,而导致市场上很多app会监听这些隐式广播,从而偷偷开启了自己一些服务,包含部分流氓应用也会借助这部分漏洞,给用户造成了不好的体验 ~
Android7.0后台优化主要体现在删除CONNECTIVITY_ACTION(网络状态)
、ACTION_NEW_PICTURE(拍照)
、 ACTION_NEW_VIDEO(拍摄)
三个隐式广播,优化了设备的内存使用和电量消耗,显著的提升了设备性能和用户体验 ~
Look - here :虽然7.0优化了这三个广播,但并不代表我们无法使用这些常用广播,具体缓解措施如下
- Android7.0应用关于
CONNECTIVITY_ACTION
隐式广播 是无法接收到的!如果你需要监听网络状态,那么你首先需要动态注册CONNECTIVITY_ACTION
广播,然后必须保证app是处于前台运行的状态,最后你会发现在主线程还是可以监听到该广播的(后来,我有记录过关于实时监听网络状态的Blog,欢迎品鉴) - Android 7.0应用关于
ACTION_NEW_PICTURE(拍照)
、ACTION_NEW_VIDEO(拍摄)
正常情况下是无法发送和接受该类型的广播
缓解方案: Android 框架提供多个解决方案来缓解对这些隐式广播的需求。 例如 JobScheduler API 提供了一个稳健可靠的机制来安排满足指定条件(例如连入无限流量网络)时所执行的网络操作。 您甚至可以使用JobScheduler API 来适应内容提供程序变化。
共享文件
关于Android中共享文件的兼容适配是我写此篇的一个重要原因,这里主要围绕应用升级后报错:FileUriExposedException 展开处理
常见的适配场景:版本升级、拍照、图片裁剪等功能
兼容过程
步骤1. AndroidMainfest加入FileProvider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="我们的包名.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
注意
- 如
android.support.v4.content.FileProvider
爆红无法调用,则更换为androidx.core.content.FileProvider
authorities:一般由 包名+fileprovider组成
,用于指定一个或多个 URI 授权方的列表,这些 URI 授权方用于标识内容提供程序提供的数据。列出多个授权方时,用分号将其名称分隔开来;exported:必须为false
,因为如果为true涉及到应用漏洞的问题grantUriPermissions:必须为true
,代表为无访问ContentProvider中的内容权限的用户,提供临时授权meta-data
指定私有文件的路径name
可统一设置为"android.support.FILE_PROVIDER_PATHS"resourc
资源文件的路径名称可自行取名创建(@xml/file_paths 是我们主要的配置文件)
步骤2. res - xml 找到file_path.xml(如无此文件则新建)修改配置,后面后具体介绍
- 示例
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
path="Android/data/我们的包名/"
name="files_root" />
<external-path
path="."
name="external_storage_root" />
</paths>
- 图示
当前项目中file_path配置,涉及多个path,具体解析如下
files-path
对应内部内存卡根目录:Context.getFileDir()cache-path
对应应用默认缓存根目录:Context.getCacheDir()external-path
对应外部内存卡根目录:Environment.getExternalStorageDirectory()external-files-path
对应外部内存卡根目录下的APP公共目录:Context.getExternalFileDir(String)external-cache-path
对应外部内存卡根目录下的APP缓存目录:Context.getExternalCacheDir()root-path
系统目录 必须存在name
就是你给这个访问路径起个名字path
需要临时授权访问的路径(.代表所有路径)
file_path.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--
1、name对应的属性值,开发者可以自由定义;
2、path对应的属性值,当前external-path标签下的相对路径
比如:/storage/emulated/0/92Recycle-release.apk
sdcard路径:/storage/emulated/0(WriteToReadActivity.java:176)
at cn.teachcourse.nougat.WriteToReadActivity.onClick(WriteToReadActivity.java:97)
at android.view.View.performClick(View.java:5610)
at android.view.View$PerformClick.run(View.java:22265)
相对路径:/-->
<!--1、对应内部内存卡根目录:Context.getFileDir()-->
<files-path
name="int_root"
path="/" />
<!--2、对应应用默认缓存根目录:Context.getCacheDir()-->
<cache-path
name="app_cache"
path="/" />
<!--3、对应外部内存卡根目录:Environment.getExternalStorageDirectory()-->
<external-path
name="ext_root"
path="pictures/" />
<!--4、对应外部内存卡根目录下的APP公共目录:Context.getExternalFileDir(String)-->
<external-files-path
name="ext_pub"
path="/" />
<!--5、对应外部内存卡根目录下的APP缓存目录:Context.getExternalCacheDir()-->
<external-cache-path
name="ext_cache"
path="/" />
<!--//系统目录 必须存在-->
<root-path
name="root_path"
path="." />
</paths>
步骤3. 因为我是因为版本更新安装的问题而写此文,故这里使用installApk来检验适配7.0共享文件的问题
/**
* 下载完成,提示用户安装
* file 为File文件 或 fileName 主要看你上一个方法有没有转文件名
*/
private void installApk(File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(this, "我们的包名.fileprovider", file);
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.parse("file://" + file),
"application/vnd.android.package-archive");
}
startActivityForResult(intent, 119);
}
新建一个module
创建一个library的module,在其AndroidManifest.xml中完成FileProvider的注册,代码编写为:
<application>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.android7.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
注意一点,android:authorities不要写死,因为该library最终可能会让多个项目引用,而android:authorities是不可以重复的,如果两个app中定义了相同的,则后者无法安装到手机中(authority conflict)
同样的的编写file_paths~
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path
name="root"
path="" />
<files-path
name="files"
path="" />
<cache-path
name="cache"
path="" />
<external-path
name="external"
path="" />
<external-files-path
name="external_file_path"
path="" />
<external-cache-path
name="external_cache_path"
path="" />
</paths>
最后再编写一个辅助类,例如:
可以根据自己的需求添加方法
public class FileProvider7 {
public static Uri getUriForFile(Context context, File file) {
Uri fileUri = null;
if (Build.VERSION.SDK_INT >= 24) {
fileUri = getUriForFile24(context, file);
} else {
fileUri = Uri.fromFile(file);
}
return fileUri;
}
public static Uri getUriForFile24(Context context, File file) {
Uri fileUri = android.support.v4.content.FileProvider.getUriForFile(context,
context.getPackageName() + ".android7.fileprovider",
file);
return fileUri;
}
public static void setIntentDataAndType(Context context,
Intent intent,
String type,
File file,
boolean writeAble) {
if (Build.VERSION.SDK_INT >= 24) {
intent.setDataAndType(getUriForFile(context, file), type);
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);
}
}
}
好了,这样我们的一个小库就写好了~~
使用
如果哪个项目需要适配7.0,那么只需要这样引用这个库,然后只需要改动一行代码即可完成适配啦,例如:
拍照
public void takePhotoNoCompress(View view) {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
.format(new Date()) + ".png";
File file = new File(Environment.getExternalStorageDirectory(), filename);
mCurrentPhotoPath = file.getAbsolutePath();
Uri fileUri = FileProvider7.getUriForFile(this, file);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
}
}
只需要改动以下方式,即可
Uri fileUri = FileProvider7.getUriForFile(this, file);
安装apk,同样的修改setDataAndType为:
FileProvider7.setIntentDataAndType(this,
intent, "application/vnd.android.package-archive", file, true);