android m n 版本,Android M、N适配踩坑

我们上个月才决定开始进行Android M、N的集中适配,发现很多问题,在此一起进行总结。

首先我们把buildToolsVersion和compileSdkVersion都改为24,相关support的lib也都改为24.*,以此放开了适配,遇上了很多坑。

这里不是一个大而全的适配方案,仅仅是一个小app(好奇心日报)的适配总结。

Android N的适配主要为组内同事操刀,所以文内部分内容源于该同事的总结。

ps:此后统一博客文章的路由命名方式,改为文章创见时间命名,如“2016-11-20”,若当天有第二篇则顺序命名为“2016-11-20-1”,以此来统一化,避免未来路由失效问题。

一、权限适配 – Android M

作为一个新闻类app,适配的最主要的部分应该就是权限了。

Android6.0引入了动态权限控制,7.0使用了私有目录被限制访问,Strict Mode API 政策。

因此权限适配包含app权限获取部分和私有目录访问部分。

1、权限申请

在这里,我们采用的适配方案是关键权限预申请,次要权限动态获取的方式。至于为什么要两者结合,你自己去体会原因``。

先说关键权限预申请

这里我们学习了支付宝和饿了吗针对权限的处理方式,开启app就申请两个一定要拿到的权限:本地文件读写权限和手机识别标识的权限,如下图所示:

AAffA0nNPuCLAAAAAElFTkSuQmCC

如果权限没有获取成功,或者后来被用户自己关掉,那则弹窗提示用户进行手动权限打开,否则app不允许进入试用,如下图,点击后跳转app的权限设定界面:

AAffA0nNPuCLAAAAAElFTkSuQmCC

权限判断及申请代码如下所示,所有activity的onCreate都判断是否获取了必须权限:

1

2

3

4

5

6

7

8

9

10

11

12if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

// android 6.0及以上版本

if (checkSelfPermission(Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED

|| checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {

// 有权限没有授予

Intent intent = new Intent();

intent.setClass(this, CheckPermissionActivity.class);

startActivity(intent);

finish();

return;

}

}

动态权限申请

关键是一下几个apiint checkSelfPermission(String permission) 用来检测应用是否已经具有权限

void requestPermissions(String[] permissions, int requestCode) 进行请求单个或多个权限

void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 用户对请求作出响应后的回调

聊一聊Android 6.0的运行时权限这篇文章写的已经非常好了,不需要我继续做总结了。

2、私有目录目录访问 – Android N

####目录被限制访问

在Android中应用可以读写手机存储中任何一个目录和文件,这给系统安全带来了很多问题。

7.0中为了提高私有文件的安全性,面向7.0及更高版本的应用私有目录将被限制访问。

经测试,File api在应用内读取文件存储依然可以继续使用,应用间(主要指调用部分系统应用)进行共享会直接报错。私有文件的文件权限不在放权给所有的应用,在manifest里使用MODE_WORLD_READABLE或MODE_WORL_WRITEABLE进行的操作将触发SecurityException。

给其他应用传递file://URI这种URI类型,可能导致接收者无法访问该路径。因此,在7.0中尝试传递file://URI会触发FileUriExposedException。

###应用间共享文件

在Android7.0版本上,Android系统强制执行了StrictMode API 政策,禁止向你的应用外公开File://URI。如果一项包含文件File://URI类型的Intent离开你的应用,应用失败,并出现FileUriExposedException,比如系统相机拍照,裁剪照片

####在Android 7.0系统调用相机拍照,裁剪照片

在7.0之前调用系统相机拍照:

1File file=new File(Environment.getExternalStorageDirectory(),

"/temp/"+System.currentTimeMillis() + ".jpg");

if (!file.getParentFile().exists()) {

file.getParentFile().mkdirs();

}

Uri imageUri = Uri.fromFile(file);

Intent intent = new Intent();

intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照

intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//将拍取的照片保存到指定URI

startActivityForResult(intent,1006);

在7.0上会抛出异常:

1android.os.FileUriExposedException:file:storage/emulated/0/temp/1474956193735.jpg exposed beyond app through Intent.getData()

at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)

这是因为7.0执行了“StrictMode API 政策”。

应对策略:使用FilrProvider来解决这一个问题

####使用FileProvider在manifest里注册provider1

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

android:authorities="com.jph.takephoto.fileprovider"

android:grantUriPermissions="true"

android:exported="false">

android:name="android.support.FILE_PROVIDER_PATHS"

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

exported必须要求为false,为true则会报安全异常。grantUriPermissions为true,表示授予URI临时访问权限。

指定共享目录

为了指定共享的目录我们需要在资源目录下(res)创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest里注册的provider所引用的resource保持一致即可)的资源文件1<?xml  version="1.0" encoding="utf-8"?>

* 代表的根目录: Context.getFilesDir()

* 代表的根目录: Environment.getExternalStorageDirectory()

* 代表的根目录: getCacheDir()

_path=""是有意义的,它代表根目录,你可以向其他的应用共享根目录及其子目录下的任何一个文件。若设置path="pictures",它代表着根目录下的pictures目录,那么你想向其他应用共享pictures目录范围之外的文件是不可行的。_使用FileProvider

上述工作做完之后我们就可以使用FileProvider了,以调用相机为例:1File file=new File(Environment.getExternalStorageDirectory(),

"/temp/"+System.currentTimeMillis() + ".jpg");

if (!file.getParentFile().exists()) {

file.getParentFile().mkdirs();

}

Uri imageUri = FileProvider.getUriForFile(context,

"com.jph.takephoto.fileprovider", file);//通过FileProvider创建一个content类型的Uri

Intent intent = new Intent();

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件

intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照

intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);//将拍取的照片保存到指定URI

startActivityForResult(intent,1006);

上面的代码有两处改变:将之前Uri的scheme类型为file的Uri改成了有FileProvider创建一个content类型的Uri。

添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件。

通过FileProvider的getUriForFile(Context context, String authority, File file)静态方法来获取URI,方法中的authority就是manifest里注册provider使用的authority。

getUriForFile方法返回的Uri为:1content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg`

其中camera_photos就是file_paths文件中paths的name。

二、support lib 24下RecyclerView的一些适配

在23及以前的RecyclerView,会默认忽略ViewHolder的ItemView的layoutParams,直接用wrap_content进行处理;但在24下,默认layoutParams会对其进行支持,如果在写item的xml时使用了match_parent,会让该item的width(height)等于RecyclerView的对应width(height),写死的dp值也会被优先读取。这当然是一种好的优化,使得过去很多需要嵌套来实现的一些“撑开”item的操作直接写在root view即可,但由于机制的修改,不可避免会出现适配的问题(宽高与想象中严重不一致)。

_无脑的适配方案就是将所有item的xml文件的root的layout_width和layout_height都改为wrapcontent,就变成了之前一模一样的效果

不过这里我还是建议针对item进行一些优化,将原来在ViewHolder地方进行的尺寸计划重新赋予ItemView layoutParent和通过嵌套来实现的“撑开”操作都改为在root view上进行,可以减少代码逻辑和UI层级。

RecyclerView的修改代码如下:

三、support lib 24下Notification的一点适配

SDK 24 下的NotificationManager.java的notifyAsUser出现了以下的修改,强迫6.0及以上系统下在使用notification时一定要传入small icon。我们app中仅在小米手机中用了这里的api进行打开app清理所有通知的操作,导致了非常隐蔽的crash,我们app差一点点就携带这个致命crash上线了,特此标记。

出错堆栈:

1

2

3

4

5Caused by:

java.lang.IllegalArgumentException:Invalid notification (no valid small icon): Notification(pri=0 contentView=com.qdaily.ui/0x1090090 vibrate=null sound=null tick defaults=0x4 flags=0x11 color=0x00000000 vis=PRIVATE)

android.app.NotificationManager.notify(NotificationManager.java:222)

android.app.NotificationManager.notify(NotificationManager.java:194)

...

源代码如下,google在这里直接采用throw new IllegalArgumentException的方式实在太危险了,感觉这种代码都有点无语,应该让app可以设置在DEBUG模式下才throw的…

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18NotificationManager.java

public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)

{

...

...

...

if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {

if (notification.getSmallIcon() == null) {

throw new IllegalArgumentException("Invalid notification (no valid small icon): "

+ notification);

}

}

...

...

...

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值