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

5,285 阅读7分钟

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

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.gifScreen_recording_203 -original-original.gif

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

1699716861857.png

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

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

代码如下:

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方法,可以终止置于后台的进程,代码如下:

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.gif14_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权限,看看在不同版本设备有什么区别,代码如下:

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.pngmedia_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发送一个打开相机页面的全屏通知,代码如下:

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.giffull_screen_14.gif

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

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

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

代码如下:

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的设备上改为可以被用户手动关闭。

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

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.gifongoing_14 -original-original.gif

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

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

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

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

安全

最低可安装的targetSdk限制

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

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