Android适配-8.0详解

准备工作

将我们项目中的targetSdkVersion改为 26(8.0) 或者 27(8.1),记住不要超过27,毕竟我还没有告诉你Android P怎么适配(滑稽)。

运行时权限

首先引用官方的原文。

在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。
对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
例如,假设某个应用在其清单中列出READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。

所谓权限组如下:

所以总结下来,如果你之前是用什么权限就去申请什么权限,那么恭喜你,这个变化不会影响到你。如果你只申请了权限组中的某些权限,却用了同组的其他权限,那么你就需要去适配一下了。

那么怎么适配呢,如果你去检查之前每个申请权限的地方,未免太过麻烦。那么你可以根据你项目中的Manifest文件中需要的权限与权限组去对比,整理出你需要申请的各个权限组。比如你需要android.permission.CALL_PHONE(打电话)与android.permission.READ_PHONE_STATE(读取手机状态) 这两个权限。那么你就整理出了类似下面的类。

1
2
3
4
5
6
7
8
9
10
public class PermissionGroup {

//Phone权限
public static String[] PHONE = new String[] {
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.CALL_PHONE
};

...
}

到时在申请权限时就可以直接获取它,一次将它们都申请了。这样就防止了遗漏某个权限,导致的异常。

注意:8.0中PHONE权限组新增两个权限:

``ANSWER_PHONE_CALLS:允许您的应用通过编程方式接听呼入电话。要在您的应用中处理呼入电话,您可以使用acceptRingingCall() `函数。

READ_PHONE_NUMBERS :权限允许您的应用读取设备中存储的电话号码。

通知适配

其实8.0在通知这里变化还挺多的,比如通知渠道通知标志通知超时背景颜色的等,详细的说明可以去看官方的Android 8.0 功能和 API。虽然变化很多,但是国内的机子貌似支持的不多。。。我在小米的文档中了解到,Android 已将通知渠道的逻辑纳入 Android Compatibility Definition Document (CDD) 中,意味着所有 Android 厂商都必须支持。所以我们可以放心的去适配。

通知渠道:Android 8.0 引入了通知渠道,其允许您为要显示的每种通知类型创建用户可自定义的渠道。用户界面将通知渠道称之为通知类别。

我个人很喜欢这个新特性。也就是说,我们可以将我们给用户的通知进行分类,我用高德地图app来举例

小米(MIUI10) 华为(EMUI 8.1.0) 一加(氢OS 5.1)

可以看到高德地图分的很细致,分为四个组共13个类别(华为貌似对组不生效)。这样有个好处,我们可以控制我们想收到的通知,比如我不喜欢运营活动通知,那我就可以把它关闭。这样避免大量的不必要通知,否则使得用户觉得烦,一棒子打死。直接关闭你的允许通知。当然了,大量app都还没有适配,适配的也都分的不是很细致,比如下图的QQ。(没有对比就没有伤害)

小米(MIUI10) 一加(氢OS 5.1)

当然更重要的问题是,如果不去适配,可能通知都不会弹出来。那么适配的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);

//分组(可选)
//groupId要唯一
String groupId = "group_001";
NotificationChannelGroup group = new NotificationChannelGroup(groupId, "广告");

//创建group
notificationManager.createNotificationChannelGroup(group);

//channelId要唯一
String channelId = "channel_001";

NotificationChannel adChannel = new NotificationChannel(channelId,
"推广信息", NotificationManager.IMPORTANCE_DEFAULT);
//补充channel的含义(可选)
adChannel.setDescription("推广信息");
//将渠道添加进组(先创建组才能添加)
adChannel.setGroup(groupId);
//创建channel
notificationManager.createNotificationChannel(adChannel);

//创建通知时,标记你的渠道id
Notification notification = new Notification.Builder(MainActivity.this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentTitle("一条新通知")
.setContentText("这是一条测试消息")
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);

}
}

效果如下:

小米(MIUI10) 华为(EMUI 8.1.0) 一加(氢OS 5.1)

华为手机当只有一个渠道时,不会显示,会当做默认通知处理,除非一个以上。

注意:当Channel已经存在时,后面的createNotificationChannel方法仅能更新其name/description,以及对importance进行降级,其余配置均无法更新。所以如果有必要的修改只能创建新的渠道,删除旧渠道

删除渠道代码如下:

1
2
3
4
5
6
private void deleteNotificationChannel(String channelId){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.deleteNotificationChannel(channelId);
}
}

悬浮窗适配

使用 SYSTEM_ALERT_WINDOW 权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:

  • TYPE_PHONE
  • TYPE_PRIORITY_PHONE
  • TYPE_SYSTEM_ALERT
  • TYPE_SYSTEM_OVERLAY
  • TYPE_SYSTEM_ERROR

相反,应用必须使用名为 TYPE_APPLICATION_OVERLAY 的新窗口类型。

也就是说需要在之前的基础上判断一下:

1
2
3
4
5
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}else {
mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}

当然记得需要有权限

1
2
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />

再进行判断

安装APK

Android 8.0去除了“允许未知来源”选项,所以如果我们的App有安装App的功能(检查更新之类的),那么会无法正常安装。

首先在AndroidManifest文件中添加安装未知来源应用的权限

1
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

这样系统会自动询问用户完成授权。当然你也可以先使用 canRequestPackageInstalls()查询是否有此权限,如果没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES这个action将用户引导至安装未知应用权限界面去授权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static final int REQUEST_CODE_UNKNOWN_APP = 100;

private void installAPK(){

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//安装应用
} else {
//跳转至“安装未知应用”权限界面,引导用户开启权限
Uri selfPackageUri = Uri.parse("package:" + this.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, selfPackageUri);
startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
}
}else {
//安装应用
}

}

//接收“安装未知应用”权限的开启结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_UNKNOWN_APP) {
installAPK();
}
}

对于权限组、悬浮窗还有安装未知来源应用的权限适配,我们也可以使用AndPermission。这样更加便捷。

透明主题的Activity

这个是在targetSdk=27,Android为8.0的手机时,出现的bug(因为官方已经在8.1修复)。问题的探究可以查看这里

只有全屏不透明的activity才可以设置方向。否则报错如下:

1
2
3
4
5
6
7
8
9
10
11
java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2957)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3032)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at adroid.app.ActivityThread$H.handleMessage(ActivityThread.java:1696)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6944)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

容易中枪的比如微信分享、支付的回调页面,我们习惯设为透明。

解决办法:

  1. 要么去掉对应activity中的 screenOrientation 属性,或者对应设置方向的代码。
  2. 要么舍弃透明效果,在它的Theme中添加:
1
<item name="android:windowIsTranslucent">false</item>

集合的处理

现在,AbstractCollection.removeAll(null)AbstractCollection.retainAll(null)始终引发 NullPointerException;之前,当集合为空时不会引发 NullPointerException。所以我们需要做判空处理。

后台执行限制

应用在两个方面受到限制:

  • 后台服务限制:处于空闲状态时,应用可以使用的后台服务存在限制。 这些限制不适用于前台服务,因为前台服务更容易引起用户注意。

  • 广播限制:除了有限的例外情况,应用无法使用清单注册隐式广播。 它们仍然可以在运行时注册这些广播,并且可以使用清单注册专门针对它们的显式广播。

在大多数情况下,应用都可以使用 JobScheduler 克服这些限制。 这种方式让应用安排为在未活跃运行时执行工作,不过仍能够使系统可以在不影响用户体验的情况下安排这些作业。关于的用法可以参考官方例子:android-JobScheduler

后台任务google推荐方案使用 WorkManagerWorkManager可以自动维护后台任务,同时可适应不同的条件,同时满足后台Service和静态广播,内部维护着JobScheduler,而在6.0以下系统版本则可自动切换为AlarmManager!有兴趣的可以了解一下

当然还有后台位置的限制需要去注意。

参考

坚持原创技术分享,您的支持将鼓励我继续创作!