Android 14 正式版适配笔记(一)— 针对所有应用的

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秒后打开一个页面,代码如下:

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
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 设备
Screen_recording_202 -original-original.gif Screen_recording_203 -original-original.gif

可以看见Android 14设备App崩溃,Logcat错误日志如下图:

1699716861857.png

如果App需要设置精准闹钟,可以通过如下步骤申请SCHEDULE_EXACT_ALARM权限:

  1. 通过AlarmManager.canScheduleExactAlarms()确认是否拥有权限。
  2. 没有权限时,通过Intent请求用户授权。
  3. 获取用户授权结果并进行对应处理。

代码如下:

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
39
40
41
42
43
44
45
46
47
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设备效果如图:

Screen_recording_204 -middle-original.gif

广播在应用进入缓存队列时暂停

在Android 14的设备上,当App进入缓存状态(一般来说当App处于后台并且设备内存吃紧时进入缓存状态),通过Context注册的广播接收者对应的广播会暂停发送并存放在一个队列中。当App退出缓存状态,例如回到前台时,在队列中的广播会重新发送,某些可以合并的广播可能会合并发送。

AndroidManifest中注册的广播接收者对应的广播不受此变更影响,并且当广播发送时,App即使处于缓存状态也会恢复为正常状态。

终止后台进程API的限制

通过ActivityManagerkillBackgroundProcesses方法,可以终止置于后台的进程,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 设备
11_kill -big-original.gif 14_kill -big-original.gif

并且Android 14设备能在Logcat中看到如下日志:

1699719625217.png

用户体验

选取部分照片或视频的权限

在Android 13中,媒体文件的读写权限进行了细分,拆分为READ_MEDIA_IMAGESREAD_MEDIA_VIDEOREAD_MEDIA_AUDIO。在Android 14,为了进一步保障用户的隐私,又加入了READ_MEDIA_VISUAL_USER_SELECTED权限,此权限允许用户仅授予对选中的媒体文件的访问权限。官方建议开发者适配READ_MEDIA_VISUAL_USER_SELECTED权限,如果没有添加此权限,也会通过兼容模式运行App。

申请READ_MEDIA_IMAGES权限和READ_MEDIA_VIDEO权限,看看在不同版本设备有什么区别,代码如下:

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
39
40
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设备
meida_13.png media_14.png

可以看到就算没有申请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发送一个打开相机页面的全屏通知,代码如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
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设备
full_screen_11 -original-original.gif full_screen_14.gif

看起来目前这个变更暂时还没有生效,Android 14的设备仍然能直接发送全屏通知。

官方提供了使用全屏通知的最佳做法,步骤如下:

  1. 通过NotificationManager.canUseFullScreenIntent()确认是否拥有权限。
  2. 没有权限时,通过Intent请求用户授权。
  3. 获取用户授权结果并进行对应处理。

代码如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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设备效果如图:

full_screen_adapter.gif
可以看到,使用最佳做法并且把AndroidManifest中的USE_FULL_SCREEN_INTENT权限移除后在模拟器上发生了崩溃,日志如下:

1699750434834.png

没有处理Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENTActivity,不知道是不是因为是模拟器的原因,后续会找台真机试试看。

改善不可关闭通知的用户体验

通过NotificationCompat.Builder.setOngoing创建的不可关闭通知,在Android 14的设备上改为可以被用户手动关闭。

通过一个简单的例子演示一下,代码如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
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设备
ongoing_11 -original-original.gif ongoing_14 -original-original.gif

在以下情况中,这些通知不会被清除:

  • 手机锁屏时。
  • 点击清除所有通知时(避免误操作)。

另外,此变更不会影响下列几种不可关闭通知:

  • CallStyle类型的通知。
  • 设备策略控制器 (DPC)和企业支持包的通知。

安全

最低可安装的targetSdk限制

在Android 14的设备上无法新安装targetSdk小于23的App,并且可以看到如下日志:

1
go复制代码INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target at least SDK version 23, but found 22

虽然官方文档是这样说,但尝试在Android 14的设备上安装targetSdk为22的App时,还是可以安装上,效果如图:

targetsdk22.gif
无障碍功能


非线性字体最大缩放提升至200%

在Android 14的设备上,系统支持字体缩放的上限提升至200%,为弱视用户提供匹配Web Content Accessibility Guidelines (WCAG)标准的无障碍功能。

如果App中的文本大小是通过sp配置,那么这个变更可能不会有太大的影响。但是最好还是测试一下缩放至200%时App的视觉效果以及可用性是否正常。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%