Android 8.0 Android O Api 26
感谢这几位大佬:考拉8.0适配、皮球二二、8.0广播解决方案、8.0后台执行限制、8.0适配指北
8.0适配主要是以下7个方面:
1.自适应启动图标,让app支持圆形图标
参考这里就够了郭霖的专栏
2.动态权限申请
例子:比如你申请了读sd卡权限,在8.0以前,你还可以写sd卡。
但是在8.0之后,你必须重新申请写sd卡。否则会异常。虽然再申请的时候,不会弹出框,但是必须要这么操作
总结:用什么权限就去申请什么权限,否则会fc。如果你只申请了权限组中的某些权限,却用了同组的其他权限,那么你就需要去适配一下了
3.通知适配
通知是本次更新的重头戏:这个人的通知讲的非常好
针对 8.0 的应用,创建通知前需要创建渠道,创建通知时需要传入 channelId,否则通知将不会显示。示例代码如下:
// 创建通知渠道
private void initNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = mContext.getString(R.string.app_name);
NotificationChannel channel = new NotificationChannel(mChannelId, name, NotificationManager.IMPORTANCE_DEFAULT);
mNotificationManager.createNotificationChannel(channel);
}
}
// 创建通知传入channelId
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NotificationBarManager.getInstance().getChannelId());
我们是对8.0单独适配的,所以无论是NotificationChannelGroup还是NotificationChannel都要与其他版本区分使用,因此我们时不时的要加上这个判断
if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT)
意思是要不低于8.0的时候才使用
4.后台执行限制较重要
方案一应用处于后台时,虽然不能通过startService创建后台服务,但仍可以通过下面的方式创建前台服务。
NotificationManager noti = (NotificationManager)getApplicationContext().getSystemService(NOTIFICATION_SERVICE);
noti.startServiceInForeground();
方案二考拉app:
我们无法得知系统如何判断是否允许应用创建后台服务,所以我们目前只能简单 try-catch startService(),保证应用不会 crash,示例代码:
Intent intent = new Intent(getApplicationContext(), InitializeService.class);
intent.setAction(InitializeService.INITIALIZE_ACTION);
intent.putExtra(InitializeService.EXTRA_APP_INITIALIZE, appInitialize);
ServiceUtils.safeStartService(mApplication, intent);
public static void safeStartService(Context context, Intent intent) {
try {
context.startService(intent);
} catch (Throwable th) {
DebugLog.i("service", "start service: " + intent.getComponent() + "error: " + th);
ExceptionUtils.printExceptionTrace(th);
}
}
5.应用内升级(允许安装未知来源的应用)
针对 8.0 的应用需要在 AndroidManifest.xml 中声明 REQUEST_INSTALL_PACKAGES 权限,否则将无法进行应用内升级。
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
6.主题activity设置屏幕方向
针对 8.0 的应用,设置了透明主题的Activity,再设置屏幕方向,代码如下:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowIsTranslucent">true</item>
</style>
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:theme="@style/AppTheme">
</activity>
将会抛出以下异常:
java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
大概意思是:只有不透明的全屏Activity可以自主设置界面方向.
即使满足上述条件,该异常也并非一定会出现,为什么这么说,看下面两种表现:
-
targetSdk=26,满足上述条件,API 26 手机没问题,API 27 手机没问题
-
targetSdk=27,满足上述条件,API 26 手机Crash,API 27 手机没问题
有点摸不清 Google 的套路了……
可知,targetSdk=26 时,API 26 和 27 都没有问题,所以这个坑暂时放在适配 API 27 时再填吧。
7.广播适配
每次发送广播时,应用的接收器都会消耗资源。 如果多个应用注册了接收基于系统事件的广播,这会引发问题;触发广播的系统事件会导致所有应用快速地连续消耗资源,从而降低用户体验。为了缓解这一问题,Android 7.0(API级别25)对广播施加了一些限制,而Android 8.0让这些限制更为严格。
- 针对 Android 8.0的应用无法继续在其清单中为隐式广播注册广播接收器
- 应用可以继续在它们的清单中注册显式广播
- 应用可以在运行时使用Context.registerReceiver()为任意广播(不管是隐式还是显式)注册接收器
- 需要签名权限的广播不受此限制所限,因为这些广播只会发送到使用相同证书签名的应用,而不是发送到设备上的所有应用
解决的办法:
1.使用动态广播替换静态广播
2.保留原来的静态广播,但需完善component参数
8.悬浮窗适配(暂时没有用到)
还记得各种安全卫士在桌面的那个小火箭小娃娃悬浮窗吧,我们最早是把WindowManager.LayoutParams的type设置为TYPE_SYSTEM_ALERT实现。随后大神们尝试用TYPE_PHONE与TYPE_TOAST来绕过系统限制(详见UCToast)。但是怎么说呢,这些毕竟算旁门左道吧,官方早晚会把这个漏洞堵住的,这不Android 8.0又开始各种android.view.WindowManager$BadTokenException: Unable to add window了
我们看看文档描述:
如果应用使用SYSTEM_ALERT_WINDOW权限并且尝试使用以下窗口类型之一来在其他应用和系统窗口上方显示提醒窗口:
TYPE_PHONE、
TYPE_PRIORITY_PHONE、
TYPE_SYSTEM_ALERT、
TYPE_SYSTEM_OVERLAY、
TYPE_SYSTEM_ERROR
...那么,这些窗口将始终显示在使用 TYPE_APPLICATION_OVERLAY 窗口类型的窗口下方
如果应用针对的是Android 8.0,则应用会使用TYPE_APPLICATION_OVERLAY窗口类型来显示提醒窗口
这样就明白了吧,TYPE_APPLICATION_OVERLAY是最上层显示窗口
来看看代码,首先配置一下权限,为了两种方式都满足
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
记得在6.0之后去申请悬浮权限,首先要判断权限是否已经授予
在4.4以前是不用判断悬浮窗权限的直接使用就可以了。在4.4到6.0之前,google没有提供方法让我们用于判断悬浮窗权限,同时也没有跳转到设置界面进行开启的方法,因为此权限是默认开启的,但是有一些产商会修改它,所以在使用之前最好进行判断,以免使用时出现崩溃,判断方法是用反射的方式获取出是否开启了悬浮窗权限。在6.0以及以后的版本中,google为我们提供了判断方法和跳转界面的方法,直接使用Settings.canDrawOverlays(context)
就可以判断是否开启了悬浮窗权限,没有开启可以跳转到设置界面让用户开启
private fun checkFloatPermission() : Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val appOpsMgr = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
val mode = appOpsMgr.checkOpNoThrow("android:system_alert_window", android.os.Process.myUid(), packageName)
return mode == AppOpsManager.MODE_ALLOWED || mode == AppOpsManager.MODE_IGNORED
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return Settings.canDrawOverlays(this)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
var cls = Class.forName("android.content.Context")
val declaredField = cls.getDeclaredField("APP_OPS_SERVICE")
declaredField.isAccessible = true
var obj = declaredField.get(cls) as? String ?: return false
val str2 = obj
obj = cls.getMethod("getSystemService", String::class.java).invoke(this, str2) as String
cls = Class.forName("android.app.AppOpsManager")
val declaredField2 = cls.getDeclaredField("MODE_ALLOWED")
declaredField2.isAccessible = true
val checkOp = cls.getMethod("checkOp", Integer.TYPE, Integer.TYPE, String::class.java)
val result = checkOp.invoke(obj, 24, Binder.getCallingUid(), packageName) as Int
return result == declaredField2.getInt(cls)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { return true
}
return false
}
这里我只判断6.0以后的跳转,其他版本的跳转方法自行查询
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if(!Settings.canDrawOverlays(applicationContext)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName))
startActivity(intent)
return
}
}
准备就绪之后即可通过代码设置type
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}
else {
mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
更完善的第三方Rom悬浮窗权限判断请参考settingscompat