我们上个月才决定开始进行Android M、N的集中适配,发现很多问题,在此一起进行总结。
首先我们把buildToolsVersion和compileSdkVersion都改为24,相关support的lib也都改为24.*,以此放开了适配,遇上了很多坑。
这里不是一个大而全的适配方案,仅仅是一个小app(好奇心日报)的适配总结。
Android N的适配主要为组内 同事 操刀,所以文内部分内容源于该同事的总结。
ps:此后统一博客文章的路由命名方式,改为文章创见时间命名,如“2016-11-20”,若当天有第二篇则顺序命名为“2016-11-20-1”,以此来统一化,避免未来路由失效问题。
作为一个新闻类app,适配的最主要的部分应该就是权限了。
Android6.0引入了动态权限控制,7.0使用了 私有目录被限制访问
, Strict Mode API 政策
。
因此权限适配包含app权限获取部分和私有目录访问部分。
在这里,我们采用的适配方案是 关键权限预申请
, 次要权限动态获取
的方式。至于为什么要两者结合,你自己去体会原因``。
这里我们学习了支付宝和饿了吗针对权限的处理方式,开启app就申请两个一定要拿到的权限:本地文件读写权限和手机识别标识的权限,如下图所示:
如果权限没有获取成功,或者后来被用户自己关掉,那则弹窗提示用户进行手动权限打开,否则app不允许进入试用,如下图,点击后跳转app的权限设定界面:
权限判断及申请代码如下所示,所有activity的onCreate都判断是否获取了必须权限:
if (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; } }
关键是一下几个api
聊一聊Android 6.0的运行时权限 这篇文章写的已经非常好了,不需要我继续做总结了。
####目录被限制访问
在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之前调用系统相机拍照:
File 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上会抛出异常:
android.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里注册provider
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.jph.takephoto.fileprovider" android:grantUriPermissions="true" android:exported="false"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/> </provider>
exported必须要求为false,为true则会报安全异常。grantUriPermissions为true,表示授予URI临时访问权限。
指定共享目录
为了指定共享的目录我们需要在资源目录下(res)创建一个xml目录,然后创建一个名为“file_paths”(名字可以随便起,只要和在manifest里注册的provider所引用的resource保持一致即可)的资源文件
<?xml version="1.0" encoding="utf-8"?> <resources> <paths> <external-path path="" name="camera_photos" /> </paths> </resources>
* 代表的根目录: Context.getFilesDir() * 代表的根目录: Environment.getExternalStorageDirectory() * 代表的根目录: getCacheDir() _path=""是有意义的,它代表根目录,你可以向其他的应用共享根目录及其子目录下的任何一个文件。若设置path="pictures",它代表着根目录下的pictures目录,那么你想向其他应用共享pictures目录范围之外的文件是不可行的。_
使用FileProvider
上述工作做完之后我们就可以使用FileProvider了,以调用相机为例:
File 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);
上面的代码有两处改变:
添加了intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);来对目标应用临时授权该Uri所代表的文件。
通过FileProvider的getUriForFile(Context context, String authority, File file)静态方法来获取URI,方法中的authority就是manifest里注册provider使用的authority。
getUriForFile方法返回的Uri为:
content://com.jph.takephoto.fileprovider/camera_photos/temp/1474960080319.jpg`
其中 camera_photos 就是file_paths文件中paths的name。
在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都改为wrap content,就变成了之前一模一样的效果
不过这里我还是建议针对item进行一些优化,将原来在ViewHolder地方进行的尺寸计划重新赋予ItemView layoutParent和通过嵌套来实现的“撑开”操作都改为在root view上进行,可以减少代码逻辑和UI层级。
RecyclerView的修改代码如下:
SDK 24 下的NotificationManager.java的notifyAsUser出现了以下的修改,强迫6.0及以上系统下在使用notification时一定要传入small icon。我们app中仅在小米手机中用了这里的api进行打开app清理所有通知的操作,导致了非常隐蔽的crash,我们app差一点点就携带这个致命crash上线了,特此标记。
出错堆栈:
Caused 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的…
NotificationManager.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); } } ... ... ... }