Android 14(UPSIDE_DOWN_CAKE)在10月份正式发布了,又需要进行新一轮的适配了。
每一个新版本的变更中,适配都分为两种,一种是不论开发时是否将targetSdkVersion更改为为最新版,只要App运行在Android 14的手机上都得适配。另一种是开发时将targetSdkVersion更改为最新版本,才需要适配。本文主要介绍适配针对有所应用的变更。
针对所有应用的变更
核心功能
默认拒绝设置精准闹钟
在Android 14的设备上,当App拥有SCHEDULE_EXACT_ALARM
权限时才能通过以下AlarmManager
的API设置精准闹钟,否则会抛出SecurityException
。
AlarmManager.setExact()
------ 仅在使用PendingIntent
时AlarmManager.setExactAndAllowWhileIdle()
AlarmManager.setAlarmClock()
当AlarmManager.setExact()
传入的参数为OnAlarmListener
时,则不需要SCHEDULE_EXACT_ALARM
权限。
在Android 14的设备上初次安装的targetSdk为33及以上的App,SCHEDULE_EXACT_ALARM
权限默认是拒绝的。通过备份、恢复的方式将App数据传输到Android 14的设备上,即使App本来拥有该权限仍然会被设置为拒绝。当App在安装时设备系统低于Android 14,通过系统升级到Android 14的情况下,若App原来已经拥有该权限,则会继续拥有该权限。
举个例子,通过PendingIntent
在5秒后打开一个页面,代码如下:
kotlin
class TargetSdk14AdapterExampleActivity : AppCompatActivity() {
private lateinit var binding: LayoutTargetSdk14AdapterExampleActivityBinding
private lateinit var alarmManager: AlarmManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutTargetSdk14AdapterExampleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.includeTitle.tvTitle.text = "Adapt Android 14"
alarmManager = getSystemService(AlarmManager::class.java)
binding.btnExactAlarms.setOnClickListener {
openMediaActivityLater()
}
}
private fun openMediaActivityLater() {
// 设置精准闹钟,打开指定页面
val openMedia3ActivityPendingIntent = PendingIntent.getActivity(this, 0, Intent(this, Media3HomeActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
alarmManager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5 * 1000, openMedia3ActivityPendingIntent)
}
}
效果如图:
Android 11 设备 | Android 14 设备 |
---|---|
可以看见Android 14设备App崩溃,Logcat错误日志如下图:
如果App需要设置精准闹钟,可以通过如下步骤申请SCHEDULE_EXACT_ALARM
权限:
- 通过
AlarmManager.canScheduleExactAlarms()
确认是否拥有权限。 - 没有权限时,通过
Intent
请求用户授权。 - 获取用户授权结果并进行对应处理。
代码如下:
kotlin
class TargetSdk14AdapterExampleActivity : AppCompatActivity() {
private lateinit var binding: LayoutTargetSdk14AdapterExampleActivityBinding
private lateinit var alarmManager: AlarmManager
private var requestExactAlarm = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutTargetSdk14AdapterExampleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.includeTitle.tvTitle.text = "Adapt Android 14"
alarmManager = getSystemService(AlarmManager::class.java)
binding.btnExactAlarms.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
requestExactAlarm = true
// 没有权限,申请用户授权
startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM))
} else {
// 获得授权,设置精准闹钟
openMediaActivityLater()
}
}
}
override fun onResume() {
super.onResume()
if (requestExactAlarm) {
requestExactAlarm = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
// 仍然没有授权,考虑使用别的方法
binding.tvTextContent.text = "SCHEDULE_EXACT_ALARM permission still no granted"
} else {
// 获得授权,设置精准闹钟
openMediaActivityLater()
}
}
}
private fun openMediaActivityLater() {
// 设置精准闹钟,打开指定页面
val openMedia3ActivityPendingIntent = PendingIntent.getActivity(this, 0, Intent(this, Media3HomeActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
alarmManager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5 * 1000, openMedia3ActivityPendingIntent)
}
}
修改后Android 14设备效果如图:
广播在应用进入缓存队列时暂停
在Android 14的设备上,当App进入缓存状态(一般来说当App处于后台并且设备内存吃紧时进入缓存状态),通过Context
注册的广播接收者对应的广播会暂停发送并存放在一个队列中。当App退出缓存状态,例如回到前台时,在队列中的广播会重新发送,某些可以合并的广播可能会合并发送。
在AndroidManifest
中注册的广播接收者对应的广播不受此变更影响,并且当广播发送时,App即使处于缓存状态也会恢复为正常状态。
终止后台进程API的限制
通过ActivityManager
的killBackgroundProcesses
方法,可以终止置于后台的进程,代码如下:
kotlin
class TargetSdk14AdapterExampleActivity : AppCompatActivity() {
private lateinit var binding: LayoutTargetSdk14AdapterExampleActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutTargetSdk14AdapterExampleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.includeTitle.tvTitle.text = "Adapt Android 14"
binding.btnKillBackgroundProcess.setOnClickListener {
// 需要在Manifest中配置android.permission.KILL_BACKGROUND_PROCESSES权限
getSystemService(ActivityManager::class.java)?.killBackgroundProcesses("com.chenyihong.exampleadmobdemo")
}
}
}
在Android 14的设备上,killBackgroundProcesses
方法只能终止自己App的后台进程,传入其他App的包名时对其后台进程没有影响。
对比效果如图:
Android 11 设备 | Android 14 设备 |
---|---|
并且Android 14设备能在Logcat中看到如下日志:
用户体验
选取部分照片或视频的权限
在Android 13中,媒体文件的读写权限进行了细分,拆分为READ_MEDIA_IMAGES
、READ_MEDIA_VIDEO
和READ_MEDIA_AUDIO
。在Android 14,为了进一步保障用户的隐私,又加入了READ_MEDIA_VISUAL_USER_SELECTED
权限,此权限允许用户仅授予对选中的媒体文件的访问权限。官方建议开发者适配READ_MEDIA_VISUAL_USER_SELECTED
权限,如果没有添加此权限,也会通过兼容模式运行App。
申请READ_MEDIA_IMAGES
权限和READ_MEDIA_VIDEO
权限,看看在不同版本设备有什么区别,代码如下:
kotlin
class TargetSdk14AdapterExampleActivity : AppCompatActivity() {
private lateinit var binding: LayoutTargetSdk14AdapterExampleActivityBinding
private var requestPermissionNames = arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)
private val requestMultiplePermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions: Map<String, Boolean> ->
val noGrantedPermissions = ArrayList<String>()
permissions.entries.forEach {
if (!it.value) {
noGrantedPermissions.add(it.key)
}
}
if (noGrantedPermissions.isEmpty()) {
// 申请权限通过,可以处理选择照片或视频资源
} else {
//未同意授权
noGrantedPermissions.forEach {
if (!shouldShowRequestPermissionRationale(it)) {
//用户拒绝权限并且系统不再弹出请求权限的弹窗
//这时需要我们自己处理,比如自定义弹窗告知用户为何必须要申请这个权限
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutTargetSdk14AdapterExampleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.includeTitle.tvTitle.text = "Adapt Android 14"
binding.btnRequestMediaPermission.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestMultiplePermissionLauncher.launch(requestPermissionNames)
}
}
}
}
效果如图:
Android 13设备 | Android 14设备 |
---|---|
可以看到就算没有申请READ_MEDIA_VISUAL_USER_SELECTED
权限,在Android 14的设备上仍然提示用户可以仅选择授予选中的照片或视频读写权限。
另外,官方建议使用PhotoPicker来实现媒体文件的读写,可以省略权限的处理。
安全的全屏通知
从Android 11开始,只要App在AndroidManifest
中配置了USE_FULL_SCREEN_INTENT权限,就可以使用Notification.Builder.setFullScreenIntent
发送全屏通知。从Android 14开始,此权限仅提供给呼叫或闹钟应用。预计在2024年,Google Play商店会撤销不符合标准的App的权限。
举个例子,通过Notification.Builder.setFullScreenIntent
发送一个打开相机页面的全屏通知,代码如下:
kotlin
class TargetSdk14AdapterExampleActivity : AppCompatActivity() {
private lateinit var binding: LayoutTargetSdk14AdapterExampleActivityBinding
private lateinit var notificationManager: NotificationManagerCompat
private val exampleNotificationChannel = "example_notification_channel"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutTargetSdk14AdapterExampleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.includeTitle.tvTitle.text = "Adapt Android 14"
notificationManager = NotificationManagerCompat.from(this)
createNotificationChannel()
binding.btnFullScreenNotification.setOnClickListener {
postFullScreenNotification()
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 创建通知渠道
val applicationInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0))
} else {
packageManager.getApplicationInfo(packageName, 0)
}
val exampleChannel = NotificationChannel(exampleNotificationChannel, "${getText(applicationInfo.labelRes)} Notification Channel", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "The description of this notification channel"
}
notificationManager.createNotificationChannel(exampleChannel)
}
}
private fun postFullScreenNotification() {
// 通知渠道的创建在com.chenyihong.exampledemo.tripartite.fcm.ExampleFCMService中
val notification = NotificationCompat.Builder(this, "example_notification_channel")
//设置小图标
.setSmallIcon(R.drawable.notification)
// 设置通知标题
.setContentTitle("full screen notification")
// 设置通知内容
.setContentText("test full screen notification")
// 需要在Manifest中配置USE_FULL_SCREEN_INTENT权限
.setFullScreenIntent(PendingIntent.getActivity(this, this.hashCode(), Intent(this, CameraActivity::class.java), PendingIntent.FLAG_IMMUTABLE),true)
.build()
notificationManager.notify(this.hashCode()+1 , notification)
}
}
效果如图:
Android 11设备 | Android 14设备 |
---|---|
看起来目前这个变更暂时还没有生效,Android 14的设备仍然能直接发送全屏通知。
官方提供了使用全屏通知的最佳做法,步骤如下:
- 通过
NotificationManager.canUseFullScreenIntent()
确认是否拥有权限。 - 没有权限时,通过Intent请求用户授权。
- 获取用户授权结果并进行对应处理。
代码如下:
kotlin
class TargetSdk14AdapterExampleActivity : AppCompatActivity() {
private lateinit var binding: LayoutTargetSdk14AdapterExampleActivityBinding
private lateinit var notificationManager: NotificationManagerCompat
private val exampleNotificationChannel = "example_notification_channel"
private var requestFullScreenIntent = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutTargetSdk14AdapterExampleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.includeTitle.tvTitle.text = "Adapt Android 14"
notificationManager = NotificationManagerCompat.from(this)
createNotificationChannel()
binding.btnFullScreenNotification.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !notificationManager.canUseFullScreenIntent()) {
requestFullScreenIntent = true
// 没有权限,申请用户授权
startActivity(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT))
} else {
// 获得授权,发送全屏通知
postFullScreenNotification()
}
}
}
override fun onResume() {
super.onResume()
if (requestFullScreenIntent) {
requestFullScreenIntent = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !notificationManager.canUseFullScreenIntent()) {
// 仍然没有授权,考虑使用别的方法
binding.tvTextContent.text = "USE_FULL_SCREEN_INTENT permission still no granted"
} else {
// 获得授权,发送全屏通知
postFullScreenNotification()
}
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 创建通知渠道
val applicationInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0))
} else {
packageManager.getApplicationInfo(packageName, 0)
}
val exampleChannel = NotificationChannel(exampleNotificationChannel, "${getText(applicationInfo.labelRes)} Notification Channel", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "The description of this notification channel"
}
notificationManager.createNotificationChannel(exampleChannel)
}
}
private fun postFullScreenNotification() {
val notification = NotificationCompat.Builder(this, "example_notification_channel")
//设置小图标
.setSmallIcon(R.drawable.notification)
// 设置通知标题
.setContentTitle("full screen notification")
// 设置通知内容
.setContentText("test full screen notification")
.setFullScreenIntent(PendingIntent.getActivity(this, this.hashCode(), Intent(this, CameraActivity::class.java), PendingIntent.FLAG_IMMUTABLE),true)
.build()
notificationManager.notify(this.hashCode()+1 , notification)
}
}
Android 14设备效果如图:
可以看到,使用最佳做法并且把AndroidManifest
中的USE_FULL_SCREEN_INTENT
权限移除后在模拟器上发生了崩溃,日志如下:
没有处理Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT
的Activity
,不知道是不是因为是模拟器的原因,后续会找台真机试试看。
改善不可关闭通知的用户体验
通过NotificationCompat.Builder.setOngoing
创建的不可关闭通知,在Android 14的设备上改为可以被用户手动关闭。
通过一个简单的例子演示一下,代码如下:
kotlin
class TargetSdk14AdapterExampleActivity : AppCompatActivity() {
private lateinit var binding: LayoutTargetSdk14AdapterExampleActivityBinding
private lateinit var notificationManager: NotificationManagerCompat
private val exampleNotificationChannel = "example_notification_channel"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutTargetSdk14AdapterExampleActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.includeTitle.tvTitle.text = "Adapt Android 14"
notificationManager = NotificationManagerCompat.from(this)
createNotificationChannel()
binding.btnOngoingNotification.setOnClickListener {
postOngoingNotification()
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 创建通知渠道
val applicationInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0))
} else {
packageManager.getApplicationInfo(packageName, 0)
}
val exampleChannel = NotificationChannel(exampleNotificationChannel, "${getText(applicationInfo.labelRes)} Notification Channel", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "The description of this notification channel"
}
notificationManager.createNotificationChannel(exampleChannel)
}
}
private fun postOngoingNotification() {
val notification = NotificationCompat.Builder(this, "example_notification_channel")
//设置小图标
.setSmallIcon(R.drawable.notification)
// 设置通知标题
.setContentTitle("ongoing notification")
// 设置通知内容
.setContentText("test ongoing notification")
.setContentIntent(PendingIntent.getActivity(this, this.hashCode(), Intent(this, CameraActivity::class.java), PendingIntent.FLAG_IMMUTABLE))
.setOngoing(true)
.build()
notificationManager.notify(this.hashCode() + 2, notification)
}
}
效果如图:
Android 11设备 | Android 14设备 |
---|---|
在以下情况中,这些通知不会被清除:
- 手机锁屏时。
- 点击清除所有通知时(避免误操作)。
另外,此变更不会影响下列几种不可关闭通知:
- CallStyle类型的通知。
- 设备策略控制器 (DPC)和企业支持包的通知。
安全
最低可安装的targetSdk限制
在Android 14的设备上无法新安装targetSdk小于23的App,并且可以看到如下日志:
go
INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target at least SDK version 23, but found 22
虽然官方文档是这样说,但尝试在Android 14的设备上安装targetSdk为22的App时,还是可以安装上,效果如图:
无障碍功能
非线性字体最大缩放提升至200%
在Android 14的设备上,系统支持字体缩放的上限提升至200%,为弱视用户提供匹配Web Content Accessibility Guidelines (WCAG)标准的无障碍功能。
如果App中的文本大小是通过sp配置,那么这个变更可能不会有太大的影响。但是最好还是测试一下缩放至200%时App的视觉效果以及可用性是否正常。