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

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权限:

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

代码如下:

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的限制

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

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_IMAGESREAD_MEDIA_VIDEOREAD_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的设备仍然能直接发送全屏通知。

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

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

代码如下:

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_INTENTActivity,不知道是不是因为是模拟器的原因,后续会找台真机试试看。

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

通过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的视觉效果以及可用性是否正常。

相关推荐
大白要努力!24 分钟前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟1 小时前
Android音频采集
android·音视频
小白也想学C2 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程3 小时前
初级数据结构——树
android·java·数据结构
闲暇部落5 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX7 小时前
Android 分区相关介绍
android
大白要努力!8 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee8 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood8 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-11 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记