剑客
关注科技互联网

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就申请两个一定要拿到的权限:本地文件读写权限和手机识别标识的权限,如下图所示:

Android M、N适配踩坑

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

Android M、N适配踩坑

权限判断及申请代码如下所示,所有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

  • int 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_READABLEMODE_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

  1. 在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临时访问权限。

  2. 指定共享目录

    为了指定共享的目录我们需要在资源目录下(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目录范围之外的文件是不可行的。_
  1. 使用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);
    

    上面的代码有两处改变:

    1. 将之前Uri的scheme类型为file的Uri改成了有FileProvider创建一个content类型的Uri。
    2. 添加了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。

二、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都改为wrap content,就变成了之前一模一样的效果

不过这里我还是建议针对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上线了,特此标记。

出错堆栈:

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);
            }
        }
        
        ...
        ...
        ...
    }

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址