Android 7.0 兼容适配

时间过的真快啊,以前还在学习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_READABLEMODE_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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

远方那座山

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值