赞
踩
上一篇所说内容是所有 App 安装到 Android14 设备上的影响和需要注意的内容,本篇接下来就要介绍当 targetSdkVersion 升级到 34 时,App 需要注意和修改的地方。
1. 核心功能变更
1.1 前台服务类型
在 targetSdkVersion >= 34 的情况下,必须为应用内的每个前台服务(Foreground Service)指定至少一种前台服务类型。
什么是前台服务?
前台服务(Foreground Service)是一种特殊类型的服务,用于执行与用户当前活动相关的长时间运行的任务,这些服务会在系统状态栏中显示通知,以告知用户应用正在前台执行任务,并且正在使用系统资源。在 Android12(API级别31)及更高版本的设备上,系统对短时间运行的前台服务进行了优化。系统会等待10秒,然后才显示与前台服务相关联的通知,以改善用户体验,减少即时通知的干扰。使用时需要在 Manifest 文件中申请 android.permission.FOREGROUND_SERVICE 权限。
前台服务类型是在 Android10 引入的,通过 android:foregroundServiceType 可以指定 <service> 的服务类型,可供选择的前台服务类型有:
- // code 1
- <service android:name="fooService" android:foregroundServiceType="specialUse">
- <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="foo"/>
- </service>
systemExempted
:预留给系统应用程序和特定的系统集成,以继续使用前台服务。普通 App 开发者不用管。 另外,上述 13 种类型中,做有彩色标记
的是 Android14 上新增的;其他的则是之前就有的。
举个常见的后台播放的例子,先在 Manifest 文件中申明权限,并设置好 foregroundServiceType
:
- // code 2
- <manifest ...>
- <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
- <application ...>
- <service
- android:name=".MusicPlayerService"
- android:foregroundServiceType="mediaPlayback"
- android:exported="false">
- </service>
- </application>
- </manifest>
然后再去实现 MusicPlayerService 类,主要就是在 onStartCommand
回调中打开通知并开始播放音乐:
- // code 3
- class MusicPlayerService : Service() {
- private var mediaPlayer: MediaPlayer? = null
-
- override fun onBind(intent: Intent?): IBinder? {
- return null
- }
-
- @RequiresApi(Build.VERSION_CODES.O)
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- val NOTIFICATION_CHANNEL_ID = "com.example.foregroundservice"
- val notificationManager = NotificationManagerCompat.from(this)
-
- // If the notification supports a direct reply action, use
- // PendingIntent.FLAG_MUTABLE instead.
- val pendingIntent: PendingIntent =
- Intent(this, NotificationFullActivity::class.java).let { notificationIntent ->
- PendingIntent.getActivity(
- this, 0, notificationIntent,
- PendingIntent.FLAG_IMMUTABLE
- )
- }
-
- // 创建通知渠道
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val name: CharSequence = "Notification Channel Name"
- val description = "Description of Notification Channel"
- val importance = NotificationManager.IMPORTANCE_DEFAULT
- val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance)
- channel.description = description
- notificationManager.createNotificationChannel(channel)
- }
-
- // 构建通知
- val builder: NotificationCompat.Builder =
- NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
- .setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
- .setContentTitle("音乐播放中")
- .setContentText("艺术家 - 音乐")
- .setContentIntent(pendingIntent)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
-
- // 启动通知
- val notificationId = 1 // 每个通知的唯一标识符
- if (ActivityCompat.checkSelfPermission(
- this,
- Manifest.permission.POST_NOTIFICATIONS
- ) == PackageManager.PERMISSION_GRANTED
- ) {
- notificationManager.notify(notificationId, builder.build())
- }
- // Notification ID cannot be 0.
- startForeground(notificationId, builder.build())
-
- // 播放音乐
- mediaPlayer = MediaPlayer.create(this, R.raw.music1)
- mediaPlayer?.isLooping = true
- mediaPlayer?.start()
-
- return START_STICKY
- }
- }
最后就是启动这个 Service 了:
requireActivity().startForegroundService(Intent(requireActivity(), MusicPlayerService::class.java))
如果没在 Manifest 文件中写明类型,那么在调用 startForeground() 方法时将会抛出 MissingForegroundServiceTypeException 异常。
对于上面的示例代码需要额外注意的是,在 Android13 及以上的手机上弹出 Notification 通知时,需要动态申请 android.permission.POST_NOTIFICATIONS 权限,当然还需要需要创建一个 NotificationChannel 渠道,这在 Android8 及以上就已经要求了。
再说回前台服务,上述每个前台服务类型所需要的权限是不一样的,并且这些权限都被定义成了普通权限,在默认情况下是已经授予的,用户不能撤销这些权限。例如 Camera 服务类型,需要在 Manifest 文件中声明 FOREGROUND_SERVICE_CAMERA 权限,并在运行时申请 Camera 权限。其他的服务类型都是如此:
Foreground Service Type | Manifest requirements | Runtime requirements |
---|---|---|
Camera | FOREGROUND_SERVICE_CAMERA | CAMERA |
Connected device | FOREGROUND_SERVICE_CONNECTED_DEVICE | 在 Manifest 声明 CHANGE_NETWORK_STATE or CHANGE_WIFI_STATE or CHANGE_WIFI_MULTICAST_STATE or NFC or TRANSMIT_IR 或者请求运行时权限 BLUETOOTH_CONNECT or BLUETOOTH_ADVERTISE or BLUETOOTH_SCAN or UWB_RANGING 或者调用 UsbManager.requestPermission()le/details/132928217 |
Data sync | FOREGROUND_SERVICE_DATA_SYNC | |
Health | FOREGROUND_SERVICE_HEALTH | 在 Manifest 声明 HIGH_SAMPLING_RATE_SENSORS 或申请运行时权限 (BODY_SENSORS or ACTIVITY_RECOGNITION) |
Location | FOREGROUND_SERVICE_LOCATION | ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION |
MediaPlayback | FOREGROUND_SERVICE_MEDIA_PLAYBACK | |
Media projection | FOREGROUND_SERVICE_MEDIA_PROJECTION | 需要在之前调用 createScreenCaptureIntent() |
Microphone | FOREGROUND_SERVICE_MICROPHONE | RECORD_AUDIO |
Phone call | FOREGROUND_SERVICE_PHONE_CALL | 在 Manifest 声明 MANAGE_OWN_CALLS |
Remote messaging | FOREGROUND_SERVICE_REMOTE_MESSAGING | |
Short service | ||
Special use | FOREGROUND_SERVICE_SPECIAL_USE | |
System exempted | FOREGROUND_SERVICE_SYSTEM_EXEMPTED |
当然,肯定有特殊情况。如果自己的前台服务与上面提到的这 13 种都不太相关,那么官方建议将这些服务迁移到 WorkManager 或者 user-initiated data transfer jobs.
user-initiated data transfer jobs 就是由用户发起的数据传输任务。此 API 是 Android14 新增的,适用于需要由用户发起的持续时间较长的数据传输,例如从远程服务器下载文件。这些任务需要在通知栏中显示一个通知,会立即启动,并且可能在系统条件允许的情况下长时间运行。我们可以同时运行多个由用户发起的数据传输作业。
小结:如果目前应用中已用到了前台服务,且 targetSdkVersion 想升到 34,那么就得添加这个前台服务的类型;否则不用管。
1.2 蓝牙连接的权限变更
在 Android14 上,调用 BluetoothAdapter 的 getProfileConnectionState() API 时必须申请 BLUETOOTH_CONNECT 权限,以前不是必须的,现在必须在 Manifest 文件中声明,并且在运行时向用户申请该权限。
很明显 Android 这几年逐渐在回收一些系统权限,对于开发者来说更加麻烦了,但有利于广大的使用者。
1.3 OpenJDK 17 更新
Android14 继续更新 Android 的核心库,使其与最新的 OpenJDK LTS 版本的特性、功能保持一致,包括对库的更新以及对应用和平台开发人员的 Java17 语言的支持。以下的一些变化可能会影响应用的兼容性:
小结:JDK17 虽然会向下兼容,但有空还是升级一下比较好,毕竟有许多新的写法和优化。
Android14 对安全性也有了更高的要求,这也是近几年来 Google 一直在关注的方向。
隐式 Intent(Implicit Intent)是 Android 应用程序组件之间进行通信的一种机制,它不明确指定要启动哪个组件,而是声明要执行的操作。系统会查找能够处理这个操作的组件,并启动它们。隐式 Intent 主要用于在应用程序内或与其他应用程序之间触发各种操作,如启动活动、启动服务、发送广播等。比较常见的例子就是先在 Manifest 文件中设置某个 Activity 中 intent-filter 的 action,然后可以通过设置启动 Intent 中的 action 来匹配这个 Activity 从而启动它.
举个栗子:
- // code 5
- <!-- android:exported 设置为false,隐式 Intent 无法启动 -->
- <activity
- android:name=".AppActivity"
- android:exported="false">
- <intent-filter>
- <action android:name="com.example.action.SEND" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
- // code 6
- // Throws an exception when targeting Android 14. 直接用隐式的 Intent 调用不管用
- context.startActivity(Intent("com.example.action.SEND"))
-
- // This makes the intent explicit. 当设置了 intent 中的 package 参数时就可以了
- val explicitIntent =
- Intent("com.example.action.SEND")
- explicitIntent.apply {
- `package` = context.packageName
- }
- context.startActivity(explicitIntent)
小结:这个变更得重视,特别是 android:exported 设置为 false 的组件,启动这些组件可能会崩溃,需要修改。这个更新还是为了安全,因为这些更改可以防止恶意应用拦截应用内部组件使用的隐式 Intent 。
2.2 动态广播接收器必须指定导出的行为
动态注册的广播接收器必须设置一个标记,用于表明接收器是否被导出到设备上的所有 App。标记位是 RECEIVER_EXPORTED 或 RECEIVER_NOT_EXPORTED。早在 Android13 就引入了这个功能,可以让应用程序指定一个已注册的广播接收器是否应该被导出,并对设备上的其他应用可见。
只不过在 Android14 上变成了“必须设置”。而在以前的 Android 版本中,设备上的任何应用都可以向动态注册的广播接收器发送未受保护的广播,除非该接收器有签名许可。
举个栗子,在 A 应用中注册 AlarmReceiver 并发送广播:
- // code 7
- val filter = IntentFilter("alarmReceiver_custom_action")
- val listenToBroadcastsFromOtherApps = true
- val receiverFlags = if (listenToBroadcastsFromOtherApps) {
- ContextCompat.RECEIVER_EXPORTED // 该接收器对其他应用开放
- } else {
- ContextCompat.RECEIVER_NOT_EXPORTED // 该接收器不对其他应用开放
- }
- // 这里的 registerReceiver 方法必须设置 receiverFlags 参数
- registerReceiver(requireContext(), AlarmReceiver(), filter, receiverFlags)
-
- // 发送广播
- val intent = Intent("alarmReceiver_custom_action") // 方式1
- //val intent = Intent(requireActivity(), AlarmReceiver::class.java) // 方式2
- requireActivity().sendBroadcast(intent)
在其他的应用中只能通过 code7 中的方式1发送广播,如果 A 应用的 listenToBroadcastsFromOtherApps 设置为 true,那么在 A 应用就能收到其他应用通过方式1发送的广播信息了,否则无法收到。
在实践中还发现,如果 A 应用也通过方式1发送自己应用内部的广播,且设置 ContextCompat.RECEIVER_NOT_EXPORTED,那么这个广播是无法收到的,感兴趣的同学可以试试。
如果应用程序只是通过 Context#registerReceiver 方法 (比如 Context#registerReceiver() )为系统广播注册接收器,那么它可以不在注册接收器时指定该标志。
小结:动态广播的注册方法改了,需要设置是否对其他应用可见,这跟 android:exported 的设置是一样的道理。其实本地广播和全局广播的功能和这个一样,只不过在 targetSdkVersion >= 34 上更加重视了。
2.3 更安全的动态代码加载
所有动态加载的文件都必须标记为只读。否则,系统将抛出异常。官方建议应用尽可能避免动态加载代码,因为这样做会大大增加应用被代码注入或代码篡改破坏的风险。
如必须动态加载代码,则需要将动态加载的文件(如 DEX、JAR 或 APK 文件)在文件打开并写入任何内容之前设置为只读:
- // code 8
- 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.absolutePath, parentClassLoader)
此外,为防止系统对现在已有的动态加载文件抛出异常,官方建议先删除并重新创建文件,然后再尝试在应用中重新动态加载这些文件。重新创建文件时,请按照上述指南在写入时将文件标记为只读。或者,可将现有文件重新标记为只读,但在这种情况下,官方建议先验证文件的完整性(例如,对照可信值检查文件的签名)以保护应用免遭恶意操作的影响。
2.4 Zip 路径遍历
针对 Android14 的应用,Android 系统通过以下方式防止 Zip 路径遍历的漏洞:如果 zip 文件条目名称包含 “…” 或以 “/” 开头,则 ZipFile(String) 和 ZipInputStream.getNextEntry() 会抛出一个 ZipException 异常。
如果不想抛出异常且文件名称又不能改,可以通过调用 dalvik.system.ZipPathValidator.clearCallback() 选择退出验证。当然这是不推荐的。
Zip 路径遍历漏洞:指恶意攻击者通过构造含有 “…/” 或以 “/” 开头的文件路径,在解压缩 Zip 文件时可以访问 Zip 文件之外的文件系统上的任意文件或目录,从而对应用程序造成安全风险的漏洞。
2.5 后台启动 Activity 新增限制
在 Android14 上系统进一步限制了 App 从后台启动 Activity 的情况:
这些变化扩展了现有的限制集,通过防止恶意应用程序滥用 API 从后台启动破坏性 Activity 来保护用户。
小结:针对后台启动控制得更严格了,如果项目中有相关逻辑,建议跑一跑看能否后台启动,如有问题再对照上面的内容进行修改,硬啃实在是不知道说的啥意思。。。
3. 有关限制非 SDK 接口的更新
Android14 更新了受限的非 SDK 接口列表(基于与 Android 开发者之间的协作以及最新的内部测试使用的 API 列表)。在限制使用非 SDK 接口之前,官方会尽可能确保有可用的公开替代方案。
如果应用并非以 Android14 为目标平台,其中一些变更可能不会立即对应用产生影响。但只要 App 使用任何非 SDK 方法或字段,终归存在导致应用出问题的显著风险。
一般而言,公共 SDK 接口是在 Android 框架 软件包索引(https://developer.android.google.cn/reference/packages) 中记录的那些接口。非 SDK 接口的处理是 API 抽象出来的实现细节,因此这些接口可能会在不另行通知的情况下随时发生更改。
如果不确定自己的应用是否使用了非 SDK 接口,则可以在 Debug 模式下运行测试 App,如果该应用访问了某些非 SDK 接口,系统就会输出一条日志消息。可以检查应用的日志消息,查找以下详细信息:
1)声明的类、名称和类型(采用 Android 运行时所使用的格式);
2)访问方式:链接、反射或 JNI;
3)所访问的非 SDK 接口属于哪个名单;
还可以使用 adb logcat 来查看这些日志消息,这些消息显示在所运行应用的 PID 下。举例而言,日志中可能包含如下条目:
Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
如果应用依赖于非 SDK 接口,应该开始计划迁移到 SDK 的替代方案。如果无法为应用中的某项功能找到使用非 SDK 接口的替代方案,应向官方请求新的公共 API。
如需查看 Android14 的所有非 SDK 接口的完整列表,可下载查看以下文件:hiddenapi-flags.csv(https://dl.google.com/developers/android/udc/non-sdk/hiddenapi-flags.csv?hl=zh-cn),这个表格文件内容很多,可用于查询。
小结:普通应用开发者一般情况下也不会用到非 SDK 接口,这个可忽略。
以上就是本篇的所有内容,可以看出,现有的 App 如果直接将 targetSdkVersion 升级到 34(Android14)的话还是有些地方需要注意并进行修改测试的。如果还想了解 Android14 新增了哪些功能,欢迎关注我,咱们下篇见!
更多内容,欢迎关注公众号:修之竹
或者查看 修之竹的 Android 专辑
赞人玫瑰,手留余香!欢迎点赞、转发~ 转发请注明出处~
参考文献
Android 14 官方文档 https://developer.android.com/about/versions/14
https://developer.android.google.cn/about/versions/14/behavior-changes-14?hl=zh-cn
Android 14 快速适配要点; 恋猫de小郭; https://juejin.cn/post/7231835495557890106?searchId=202307240025039D8229C74EA62159077B
https://developer.android.google.cn/guide/components/foreground-services
https://developer.android.com/about/versions/14/changes/user-initiated-data-transfers?hl=zh-cn
https://developer.android.google.cn/about/versions/14/changes/fgs-types-required
https://developer.android.google.cn/about/versions/13/features#runtime-receivers
https://developer.android.google.cn/about/versions/14/changes/non-sdk-14?hl=zh-cn
————————————————
原文链接:https://blog.csdn.net/lbs458499563/article/details/132928217
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。