Android 14 快速适配要点

随着 Google I/O 2023 发布的 Android beta2 ,预计 Android 14 将在2023年第三季度发布,目前看整体需要适配的内容已经趋向稳定,那就根据官方文档简单做个适配要点总结吧。

如何做到最优雅的版本适配?那就是尽可能提高 minitSdkVersion ,说服老板相信低版本用户无价值论,低版本用户更多是羊毛党~

针对 Android 14 或更高版本的应用

这部分主要是影响 targetSdkVersion 34 的情况 ,目前 Google Play 已经开始要求 33 了,相信未来 34 也不远了。

前台服务类型

targetSdkVersion 34 的情况下,必须为应用内的每个前台服务(foreground-services) 指定至少一种前台服务类型。

前台服务类型是在 Android 10 引入的,通过 android:foregroundServiceType 可以指定 <service> 的服务类型,可供选择的前台服务类型有:

例如:

<manifest ...>
 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
   <application ...>
     <service
         android:name=".MyMediaPlaybackService"
         android:foregroundServiceType="mediaPlayback"
         android:exported="false">
     </service>
   </application>
</manifest>

如果你 App 中的用例与这些类型中的任何一种都不相关,那么建议还是将服务迁移成 WorkManager jobs

更多详细可见:developer.android.com/about/versi…

安全

对 pending/implicit intent 的限制

对于面向 Android 14 的应用,Android 通过以下方式限制应用向内部应用组件发送隐式 intent:

  • 隐式 intent 仅传递给导出的组件,应用必须使用明确的 intent 来交付给未导出的组件,或者将组件标记为已导出(exported) 。
  • 如果应用创建一个 mutable pending intent ,但 intent 未指定组件或包,系统现在会抛出异常。

这些更改可防止恶意应用拦截只供给用内部组件使用的隐式 intent,例如:

<activity
   android:name=".AppActivity"
   android:exported="false">
   <intent-filter>
       <action android:name="com.example.action.APP_ACTION" />
       <category android:name="android.intent.category.DEFAULT" />
   </intent-filter>
</activity>

如果应用尝试使用隐式 intent 启动该 activity,则会抛出异常:

// Throws an exception when targeting Android 14.
context.startActivity(Intent("com.example.action.APP_ACTION"))

要启动未导出的 Activity,应用应改用显式 Intent:

// This makes the intent explicit.
val explicitIntent =
       Intent("com.example.action.APP_ACTION")
explicitIntent.apply {
   package = context.packageName
}
context.startActivity(explicitIntent)

运行时注册的广播接收器必须指定导出行为

以 Android 14 为目标,并使用 context-registered receiversContextCompat.registerReceiver)应用和服务的需要指定一个标志,以指示接收器是否应导出到设备上的所有其他应用:分别为 RECEIVER_EXPORTEDRECEIVER_NOT_EXPORTED

val filter = IntentFilter(APP_SPECIFIC_BROADCAST)
val listenToBroadcastsFromOtherApps = false
val receiverFlags = if (listenToBroadcastsFromOtherApps) {
   ContextCompat.RECEIVER_EXPORTED
} else {
   ContextCompat.RECEIVER_NOT_EXPORTED
}
ContextCompat.registerReceiver(context, br, filter, receiverFlags)

仅接收系统广播的接收器例外

如果应用仅通过 Context#registerReceiver 方法为系统广播注册接收器时,那么它可以不在注册接收器时指定标志, 例如 android.intent.action.AIRPLANE_MODE

更安全的动态代码加载

如果应用以 Android 14 为目标平台并使用动态代码加载 (DCL),则所有动态加载的文件都必须标记为只读,否则,系统会抛出异常。

我们建议应用尽可能避免动态加载代码,因为这样做会大大增加应用因代码注入或代码篡改而受到危害的风险。

如果必须动态加载代码,请使用以下方法将动态加载的文件(例如 DEX、JAR 或 APK 文件)在文件打开后和写入任何内容之前立即设置为只读:

val jar = File("DYNAMICALLY_LOADED_FILE.jar")
val os = FileOutputStream(jar)
os.use {
   // Set the file to read-only first to prevent race conditions
   jar.setReadOnly()
   // Then write the actual file content
}
val cl = PathClassLoader(jar, parentClassLoader)

处理已存在的动态加载文件

为防止现有动态加载文件抛出异常,我们建议可以尝试在应用中再次动态加载文件之前,删除并重新创建这些文件。

重新创建文件时,请按照前面的指导在写入时将文件标记为只读,或者将现有文件重新标记为只读,但在这种情况下,强烈建议首先验证文件的完整性(例如,通过根据可信值检查文件的签名),以帮助保护应用免受恶意操作。

Zip path traversal

对于针对 Android 14 的应用,Android 通过以下方式防止 Zip 路径遍历漏洞:如果 zip 文件条目名称包含 “..” 或以 “/” 开头,则 ZipFile(String)ZipInputStream.getNextEntry() 会抛出一个 ZipException

应用可以通过调用 dalvik.system.ZipPathValidator.clearCallback() 选择退出验证。

从后台启动活动的附加限制

针对 Android 14 的应用,系统进一步限制了应用在后台启动 Activity 的时间:

这些更改扩展了现有的一组限制 ,通过防止恶意应用滥用 API 从后台启动破坏性活动来保护用户。

具体可见:developer.android.com/guide/compo…

OpenJDK 17

Android 14 会要求的 OpenJDK 17 的支持,这对一些语法上可以会有一定影响,例如:

  • 对正则表达式的更改:现在不允许无效的组引用
  • UUID 处理java.util.UUID.fromString() 方法现在在验证输入参数时会进行更严格的检查
  • ProGuard 问题:在某些情况下,如果使用 ProGuard 缩小、混淆和优化应用,添加 java.lang.ClassValue 会导致出现问题,问题源于 Kotlin 库,库会根据是否 Class.forName("java.lang.ClassValue") 返回类来更改运行时行为。

反正适配 OpenJDK 17 就对了,新版 Android Studio Flamingo 也默认内置 OpenJDK 17 了。

针对所有版本的 Android 14 变更

以下行为主要针对在 Android 14 上运行的所有应用,可以看到从 Android 10 开始, Androd Team 针对这些变动越来越强势。

核心功能

默认情况下拒绝计划精确提醒

精确提醒(Exact alarms)用于用户有目的的通知,或用于需要在精确时间发生的操作情况,从 Android 14 开始,该 SCHEDULE_EXACT_ALARM 权限不再预先授予大多数新安装的针对 Android 13 及更高版本的应用,默认情况下该权限会被拒绝。

如果用户通过备份还原操作将应用数据传输到运行Android 14 的设备上,权限仍然会被拒绝。如果现有的应用已经拥有该权限,它将在设备升级到 Android 14 时预先授予。

而现在需要权限 SCHEDULE_EXACT_ALARM 才能通过以下 API 启动确切的提醒,否则将抛出 SecurityException

注意: 如果确切的 alarms 是使用 OnAlarmListener 对象设置的,例如在 setExact API 中,SCHEDULE_EXACT_ALARM 则不需要权限。

现有的权限最佳实践 SCHEDULE_EXACT_ALARM 仍然适用,包括以下内容:

详细可见:developer.android.com/about/versi…

Context-registered 广播在缓存应用时排队

在 Android 14 上,当应用处于缓存状态时,系统可能会将上下文注册的广播放入队列中。

这类似于 Android 12(API 级别 31)为异步活页夹事务引入的排队行为,清单声明的广播不会排队,并且应用会从缓存状态中删除以进行广播传输。

当应用离开缓存状态时,例如返回前台,系统会传送任何排队中的广播,某些广播的多个实例可以合并为一个广播。

根据其他因素(例如系统运行状况),应用可能会从缓存状态中删除,并且先前排队的所有广播都会被传送。

应用只能杀死自己的后台进程

从 Android 14 开始,应用调用 killBackgroundProcesses() 时,该 API 只能杀死自己应用的后台进程。

如果传入其他应用的包名,该方法对该应用的后台进程没有影响,Logcat中会出现如下信息:

Invalid packageName: com.example.anotherapp

应用不应该使用 killBackgroundProcesses() API ,或以其他方式尝试影响其他应用的进程生命周期,即使是在较旧的操作系统版本上。

Android 旨在将缓存的应用保留在后台,并在系统需要内存时自动终止它们,如果应用出现不必要地杀死其他应用,它会降低系统性能并增加电池消耗,因为稍后需要完全重启这些应用,比恢复现有的缓存应用占用的资源要多得多。

安全

最低可安装目标 API 级别

从 Android 14 开始,无法安装 targetSdkVersion 低于 23 的应用 ,要求应用必须满足这些最低目标 API 级别,这样可以提高用户的安全性和隐私性。

恶意软件通常以较旧的 API 级别为 target,以绕过较新 Android 版本中引入的安全和隐私保护,例如一些恶意软件应用使用 targetSdkVersion 22 的来避免受到 Android 6.0 的运行时权限模型的约束。

Android 14 的这一变化使恶意软件更难避开安全和隐私管理,尝试安装针对较低 API 级别的应用将导致安装失败,并在 Logcat 中显示以下消息:

INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target at least SDK version 23, but found 7

在升级到 Android 14 的设备上,任何低于targetSdkVersion23 的应用都将保持安装状态,如果你需要针对较旧 API 级别的应用进行测试,请使用以下 ADB 命令:

adb install --bypass-low-target-sdk-block FILENAME.apk

用户体验

授予对照片和视频的部分访问权限

注意: 如果你的应用已经使用了系统照片选择器(photopicker),那么无需进行任何改动。

在 Android 14 上,当应用请求 Android 13(API 级别 33)中引入的任何 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 媒体权限时,用户可以授予对其照片和视频的部分访问权限。

新对话框显示以下权限选项:

  • 选择照片和视频: Android 14 中的新功能,用户选择他们想要提供给应用的特定照片和视频。
  • 全部允许:用户授予对设备上所有照片和视频的完整库访问权限。
  • 不允许:用户拒绝所有访问。

要在应用中出现改逻辑,可以声明新的 READ_MEDIA_VISUAL_USER_SELECTED 权限。

详细可见:developer.android.com/about/versi…

安全的全屏 Intent 通知

在 Android 11(API 级别 30)中,任何应用都可以通过 Notification.Builder.setFullScreenIntent 在手机锁定时发送全屏 intent。

一般可以通过在 AndroidManifest 中声明 USE_FULL_SCREEN_INTENT 权限来在应用安装时自动授予该权限 ,全屏 intent 通知专为需要用户立即关注的极高优先级通知而设计,例如来电或用户配置的闹钟设置。

从 Android 14 开始,允许使用该权限的应用仅限于提供通话和闹钟的应用,Google Play 商店会撤销任何不符合该配置文件的应用的默认权限。

在用户更新到 Android 14 之前,该权限对安装在手机上的应用保持启用状态,用户可以打开和关闭该权限。

开发者可以使用新的API NotificationManager.canUseFullScreenIntent 来检查应用是否具有权限;如果没有,应用可以使用新的 ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT 来启动跳转到可以授予权限的设置页面。

改变用户体验不可关闭通知的方式

如果你的应用向用户显示不可关闭的前台通知,Android 14 已更改行为以允许用户关闭此类通知。

此次更改适用于通过 Notification.Builder#setOngoing(true) 或者 NotificationCompat.Builder#setOngoing(true) 来设置 Notification.FLAG_ONGOING_EVENT 从而阻止用户关闭前台通知的应用 的行为。

现在 FLAG_ONGOING_EVENT 的行为已经改变,用户实际上可以关闭此类通知。

在以下情况下,该类通知仍然不可关闭:

  • 当手机被锁定时
  • 如果用户选择清除所有通知操作(这有助于防止意外)

此外,新行为不适用于以下用例中的不可关闭通知:

  • 使用 CallStyle与真实通话相关的通知
  • 使用 MediaStyle 创建的通知
  • 设备策略控制器 (DPC) 和企业支持包

辅助功能

非线性字体缩放至 200%

从 Android 14 开始,系统支持高达 200% 的字体缩放,为弱视用户提供符合 Web 内容无障碍指南 (WCAG) 的额外无障碍选项。

如果已经使用缩放像素 (sp) 单位来定义文本大小,那么此次更改可能不会对应用产生重大影响。