前言

前几天发版时接到了华为那边的提醒,说请尽快将targetSdkVersion提升到26+,2019年5月1号之后将会拒绝所有targetSdkVersion低于26的应用的上架和更新。于是查了一下,发现目前国内的主要应用渠道商都已经签订了电信终端产业协会(TAF)发布《移动应用软件高API等级预置与分发自律公约》。

这个事情似乎没在国内掀起什么舆论,然而这确实是对国内Android用户的重大利好消息,利用android向下兼容,不适配新版本特性进行一些流氓行为的日子即将一去不复返了。
提升targetSdkVersion到26+意味着,必须进行一系列权限适配和兼容,这里也就记录一下我所遇见的兼容问题以及处理方案。

运行时权限

如果你的应用之前的targetSdkVersion < 23,那么升级targetSdkVersion到26+首先要做的就是适配运行时权限。
Android 6.0引入了运行时权限机制,这已经过去2年多了,适配相关的文章、库已经有很多,这里就不再赘述。仅记录一下一些适配经验。

SD卡读写 权限是是运行时权限中最为零碎和麻烦的权限管理,以至于很多应用直接启动时申请如果拒绝就退出。这样一刀切虽然可以保证改动最小化且应用正常运行,但体验很明显是欠妥的。
其实应用中的很多文件操作内容可以放到系统SDK提供的内部和外部路径下

/** 缓存路径,用于临时存放,可以随时清除 **/
// 内部缓存路径
context.getCacheDir();
// 外部缓存路径,使用前最好检查SD卡是否挂载
context.getExternalCacheDir();/** 文件路径,用于存放长期有效的文件 **/
// 内部文件路径
context.getFilesDir();
// 外部文件路径
context.getExternalFilesDir()

编辑这两个路径下的文件是不需要获取SD卡读写权限的,因此我们压缩图片、生成中间文件、存储log日志等操作生成的文件都可以放在这个路径下。需要特别注意的是下载安装的apk最好放在外部存储中,比如应用内更新下载的apk,因为内部缓存路径默认只有本应用才有读写权限,除非调用设置开启,如果将apk放在内部存储中又不做特殊处理,会导致系统的PackageInstaller无法读取apk进而安装失败。

另外需要注意的是并不是只有直接的文件操作才会需要申请SD卡读写权限,使用ContentResolver、读取一些媒体库也需要这类权限,在适配时需要仔细检查。

浮窗

在Android 6.0已经将浮窗划为运行时权限,而且与上面的运行时权限不大相同,属于特殊权限,浮窗权限需要使用Intent唤起应用设置页面让用户开启,然后在onActivityResult()中判断是否拥有权限。WRITE_SETTINGS的申请方法与此相同

// 判断是否拥有浮窗权限
boolean hasPermission = Settings.canDrawOverlays(activity);// 跳转权限申请界面
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, MANAGE_OVERLAY_PERMISSION_REQUEST_CODE);

在Android 8.0中,对于浮窗进行了更严格的区分和限制 官方文档

提醒窗口
使用 SYSTEM_ALERT_WINDOW 权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR
相反,应用必须使用名为 TYPE_APPLICATION_OVERLAY 的新窗口类型。使用 TYPE_APPLICATION_OVERLAY 窗口类型显示应用的提醒窗口时,请记住新窗口类型的以下特性:应用的提醒窗口始终显示在状态栏和输入法等关键系统窗口的下面。
系统可以移动使用 TYPE_APPLICATION_OVERLAY 窗口类型的窗口或调整其大小,以改善屏幕显示效果。
通过打开通知栏,用户可以访问设置来阻止应用显示使用 TYPE_APPLICATION_OVERLAY 窗口类型显示的提醒窗口。

因此需要增加额外的浮窗判断

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}
else {wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

应用安装

在Android 8.0以后,Google对第三方app安装apk进行了严格的限制,新增了android.permission.REQUEST_INSTALL_PACKAGES权限,如果你的应用想要获得安装apk的权限,必须在manifest中声明此权限,并且在需要调用的时候执行类似于浮窗权限申请一样的权限申请流程。 由于Android向下兼容的关系,targetSdkVersion低于26则不用关心这个,但是现在我们需要适配到26+,如果我们有应用内更新或者下载安装其他apk的需求,就必须要关注安装权限了。

// 权限判断
public static boolean hasInstallPackagePermission() {if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {return true;}return context.getPackageManager().canRequestPackageInstalls();
}// 权限申请
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
activity.startActivityForResult(intent, requestCode);

FileUriExposedException

直接将老项目的targetSdkVersion提到24+,在读取媒体库文件、安装apk的时候会出现崩溃,报错FileUriExposedException。其实这也是老项目欠下的适配债了,Google认为通过诸如file://URI这样的URI访问文件是不安全的,特别是访问其它应用的私有目录和文件,因此很早就提供了FileProvider这样的东西用于管理文件访问,只不过由于向下兼容,老方法一直能用,关注寥寥。
在Android 7.0+的系统上,Android SDK的 StrictMode 不再允许在应用外部公开file://URI,如果携带file://URI离开自己的应用(访问PackageInstaller,访问相册 etc.),就会抛出FileUriExposedException

对于这个问题有两种解决方案

  1. 老老实实的编写FileProvider

    第一步、在AndroidManifest.xml中添加如下代码
     
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        ...
        <application
            ...
            <provider
                android:name="android.support.v4.content.FileProvider"
                android:authorities="${applicationId}.provider"
                android:exported="false"
                android:grantUriPermissions="true">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/provider_paths"/>
            </provider>
        </application>
    </manifest>
     
    第二步、在res目录下新建一个xml文件夹,并且新建一个provider_paths的xml文件

    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <external-path name="external_files" path="."/>    <!--有教程上面说,这个path为空就可以,但我这边为空编译错误-->
    </paths>

    第三步、修改代码

    String file = new File(Environment.getExternalStorageDirectory().getPath() + "/" + "itest.jpg";

    Uri photoURI = Uri.fromFile(file);

    变成:

    Uri phot

  2. 重新设置StrictMode,让VM忽略URI检查

    // 在Application onCreate()期间执行
    // 关闭FileUriExposedException检查
    StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
    StrictMode.setVmPolicy(builder.build());
    

    此方法为暂缓之计,如果适配工作量极大可以先行使用,建议逐步适配过渡到FileProvider。

Notification Channel

Android 8.0以后增加了推送渠道(Notification Channel)的概念,用于更精细的划分一个应用的不同推送,比如一个类似知乎这样的以阅读为主的综合类应用,就可以将推送分为推广、私信等channel;用户不想看推广推送,又怕错过私信,就可以将推广关闭,保留私信的channel。
我们知道Android Notification有3个必填属性:icon、title、content,这三个如果缺了任何一个都将会导致Notification不显示,而对于targetSdkVersion=26+的应用,又增加了一个NotificationChannelId,如果构建Notification的时候不设置NotificationChannelId,后果会比不设置前三个还严重,将会在显示notification时崩溃。所以适配26+我们必须对应用的NotificationChannel进行管理。

Notification Channel由开发者创建,构建时的配置与普通Notification相似,可以设定重要等级、是否震动、是否响铃等,

public static NotificationCompat.Builder buildNotification(String channelId, String channelTitle) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {if (TextUtils.isEmpty(channelId) || TextUtils.isEmpty(channelTitle)) {channelId = DEFAULT_CHANNEL_ID;channelTitle = DEFAULT_CHANNEL_NAME;}NotificationManager mNotificationManager = getNotificationManager();NotificationChannel mChannel = mNotificationManager.getNotificationChannel(channelId);if (mChannel == null) {mChannel = new NotificationChannel(channelId, channelTitle, NotificationManager.IMPORTANCE_DEFAULT);mChannel.setSound(null, null);mNotificationManager.createNotificationChannel(mChannel);}return new NotificationCompat.Builder(mContext, mChannel.getId());}return new NotificationCompat.Builder(mContext);}

需要注意的是NotificationChannel一旦创建,则不再能够被应用修改,只有用户可以修改。所以在调试NotificationChannel时,记得卸载重装。

后台Service

Android 8.0对应用启动后台服务进行了严格的限制,对于targetSdkVersion=26+的应用,系统不允许后台应用创建后台服务,必须以通知栏可见的形式告诉用户你正在后台执行任务。也就是使用ContextCompat.startForegroundService()创建前台服务,然后必须在5秒内调用此服务的startForeground()方法,否则系统将立即停止服务,抛出RemoteServiceException异常

android.app.RemoteServiceException:
Context.startForegroundService() did not then call Service.startForeground()
......

在Android 9.0之后,应用想要启动前台服务,必须要拥有android.permission.FOREGROUND_SERVICE权限,否则会抛出异常,这个权限等级并不高,在Manifest中声明即可;如果你的targetSdkVersion=28+,又需要使用前台进程,请记得声明此权限。

非SDK接口访问限制

在Android P中Google正式对使用反射调用Android SDK内部不对外开放的接口来达成一些目的的行为作出限制。对非 SDK 接口的限制 | android developer
这意味着以前很多通过反射实现的小花样即将告一段落,一般我们的开发中很少有这样的需求,但老项目中或多或少会有历史遗留代码,因此也需要注意。
在适配这一块时,需要注意到Google所列出的几个“SDK名单”

用于限制非 SDK 接口的不同名单有哪些,它们在限制性行为方面的对应含义是什么?
下面是名单类型:
白名单:SDK
浅灰名单:仍可以访问的非 SDK 函数/字段。
深灰名单:对于目标 SDK 低于 API 级别 28 的应用,允许使用深灰名单接口。对于目标 SDK 为 API 28 或更高级别的应用:行为与黑名单相同
黑名单:受限,无论目标 SDK 如何。 平台将表现为似乎接口并不存在。 例如,无论应用何时尝试使用接口,平台都会引发 NoSuchMethodError/NoSuchFieldException,即使应用想要了解某个特殊类别的字段/函数名单,平台也不会包含接口。
...

如果你的应用涉及到浅灰名单,可以暂时不用担心,主要的适配工作在深灰名单和黑名单上,至于怎么适配,其实也没啥好说的,无非就是有替代方案的替代,替代不了的改需求呗……

第三方SDK

其实……提升targetSdkVersion最头疼的,不是自己的代码,毕竟自己的代码怎么改都行,而是依赖的第三方library,特别是那些早已不再维护的第三方SDK,头都疼掉……
这一块能做的,只有3点

转载于: https://lx8421bcd.github.io/2018/12/21/%E6%8F%90%E5%8D%87targetSdkVersion%E8%87%B326+%E9%80%82%E9%85%8D%E6%A6%82%E8%A6%81/

  1. 升级到最新版SDK
  2. 在SDK提供的接口中寻找有没有兼容方法
  3. 怼SDK提供方,让他们做新版的适配,如果能怼出来,也算是功德一件了

提升targetSdkVersion至26+适配相关推荐

  1. Android targetSdkVersion从23升级到26适配指南

    根据华为开发者平台AppGallery目标 API等级(targetSdkVersion)重要变更要求的通知,自2018年7月18日,华为应用市场联合国内主流应用预置与分发服务提供者,作为发起单位,共 ...

  2. 为保证交易体验,招行选择快速适配做最新一“派”

    Android 系统仍在不断进化. 随着人们对数码产品更大显示占比需求的高涨,手机全面屏时代已经不可阻挡地到来,面对各式各样的异形屏,应用开发者们似乎多了不少 "额外任务". 在种 ...

  3. android 8.0 调系统拍照_Android通知栏微技巧,8.0系统中通知栏的适配

    为什么要进行通知栏适配? 不得不说,通知栏真是一个让人又爱又恨的东西. 通知栏是Android系统原创的一个功能,虽说乔布斯一直认为Android系统是彻彻底底抄袭iOS的一个产品,但是通知栏确实是A ...

  4. Android 8.0学习(25)---系统的应用图标适配

    Android 8.0系统的应用图标适配 现在已经进入了2018年,Android 8.0系统也逐渐开始普及起来了.三星今年推出的最新旗舰机Galaxy S9已经搭载了Android 8.0系统,紧接 ...

  5. Android 8.0 学习(3)---Android 8.0系统的应用图标适配

    其实在去年Android 8.0系统刚推出的时候,我就仔细翻阅过Google官方的功能变更文档.变更项着实不少,但是真正需要我们去进行功能适配的地方却并不多.总结了一下,最主要需要进行适配的地方有两处 ...

  6. Android Q 适配详细操作

    去年(2018年)我们陆续收到来自各个应用市场平台的API升级通知,下面以阿里应用分发平台通知为例: 亲爱的开发者:为保障用户合法权益,建立健康的移动应用环境,2018年7月18日,国内主流应用预置与 ...

  7. android8.0应用图标适配调整_Android应用图标微技巧,8.0系统中应用图标的适配

    现在已经进入了2018年,Android 8.0系统也逐渐开始普及起来了.三星今年推出的最新旗舰机Galaxy S9已经搭载了Android 8.0系统,紧接着小米.华为.OV等国产手机厂商即将推出的 ...

  8. 安卓8.0桌面图标适配

    一.现状及问题 在安卓8.0之前的版本中,原生安装系统 在应用安装到手机后,显示在桌面上的图标只有一种方式来决定, 即在manifest.xml的Application节点中设置 icon标签的属性, ...

  9. Android通知栏微技巧,8.0系统中通知栏的适配

    转载请注明出处:https://blog.csdn.net/guolin_blog/article/details/79854070 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 ...

  10. Android 实现应用更新适配 Android O (Android 8.0)

    之前写过一篇 Android 实现应用更新(适配Anndroid N),本篇主要讲解Android O(Android 8.0)上应用更新的适配问题,应用更新的完整实现请结合上一篇文章一起,文末也会给 ...

最新文章

  1. oracle安装就是home3,rhel3上安装Oracle(来自Oracle网站)
  2. V7000存储运维使用手册
  3. 【Qt】QModbusRtuSerialMaster类
  4. 什么时候清理物理消息文件?
  5. CodeForces - 1370F2 The Hidden Pair (Hard Version)(交互题+二分)
  6. mysql开方_MySQL数学函数的实际用法
  7. linux上p图工具,linux图片处理工具GraphicsMagick安装使用
  8. ❤️JavaScript系列6部曲:流程控制(万字长文)❤️
  9. 【学习笔记】生成下一个排列(STL库函数next——permutation)
  10. 解读Tilera怪兽级64核处理器(转)
  11. ASP.NET MVC中商品模块小样
  12. ValueError: Cannot feed value of shape (100, 160) for Tensor 'Placeholder:0', which has shape '(?,
  13. 新版MacBookPro风扇狂转的问题
  14. 6.6 PowerBI系列之DAX函数专题 -调节器TREATAS动态建立关系
  15. android蓝牙传文件在哪里找,手机蓝牙传输的文件在哪里_华为手机蓝牙传输记录在哪-系统城...
  16. 编码集---解码和编码
  17. 看完这篇 教你玩转渗透测试靶机vulnhub——DC1
  18. 蚂蚁集团开源大规模视频侵权定位数据集
  19. 爬虫快速入门——Request对象的使用
  20. 几个链接搞定你想要的HEX颜色和RBG

热门文章

  1. 做一款互联网内容平台,到底要懂多少AI?
  2. poj1062(dijkstra+枚举)
  3. 另一种则是用大布巾或大箱子
  4. Vue 的v-if与v-show
  5. 欧姆龙NJ系列使用梯形图和ST语言实现数据记录
  6. ps保存html和图像格式不显示,photoshop另存为jpg和web格式的区别
  7. 弱者易怒如虎,强者平静如水,思怡
  8. git checkout 时出现 error: pathspec '是0106' did not match any file(s) known to git.
  9. MQTT服务器(EMQX)使用方法
  10. 利用AOP实现自定义注解