注册

Android 14 适配的那些事情

  • 大家好,我叫 Jack Darren,目前主要负责国内游戏发行 Android SDK 开发

简介

  • 距离 Android 14 发布已经有一段时间了,趁着这次机会,了解和熟悉了 Android 14 更新的内容,现在来和大家分享一下,大家喜欢的话可以点个赞多多支持一下,文章的内容按照适配内容的重要程度进行排序。

targetSdk 版本要求

  • 在 Android 14 上面,新增了一个要求,要求新安装的应用的 targetSdkVersion 需要大于等于 23(即 Android 6.0 及以上),如果小于这个值将无法在 Android 14 的设备上面安装,此时大家心里可能有疑惑了,谷歌为什么要求那么做呢?我们来看看谷歌的原话是什么
恶意软件通常会以较旧的 API 级别为目标平台,
以绕过在较新版本 Android 中引入的安全和隐私保护机制。
例如,有些恶意软件应用使用 targetSdkVersion 22
以避免受到 Android 6.0 Marshmallow(API 级别 23)在 2015 年引入的运行时权限模型的约束。
这项 Android 14 变更使恶意软件更难以规避安全和隐私权方面的改进限制。
  • 从上面这段话不难看出来谷歌的用意,其实为了保障用户的手机安全,如果用户安装应用的 targetSdkVersion 版本过低,有一些恶意软件会利用高系统会兼容旧软件这一特性(漏洞),故意绕过系统的安全检查,从而会导致 Android 高版本上面一些安全特性无法生效,没有了系统的管束,这些恶意软件可能就会肆意乱来。
  • 另外你如果想在 Android 14 系统上面,仍然要安装 targetSdkVersion 小于 23 的应用,可以通过以下 adb 命令来安装 apk,这样就能绕过系统的安装限制。
adb install --bypass-low-target-sdk-block xxx.apk

前台服务类型要求

  • 如果你的应用 targetSdkVersion 升级到了 34(即 Android 14),并且在 Service 中调用了 startForeground 方法,那么就需要进行适配了,否则系统会抛出 MissingForegroundServiceTypeException 异常,这是因为在 Android 14 上面,要求应用在开启前台服务的时候,需要注明这个前台服务的用途,谷歌给我们列举了以下几种用途:
用途说明清单文件权限要求运行时要求
摄像头继续在后台访问相机,例如支持多任务的视频聊天应用FOREGROUND_SERVICE_CAMERA请求 CAMERA 运行时权限
连接的设备与需要蓝牙、NFC、IR、USB 或网络连接的外部设备进行互动FOREGROUND_SERVICE_CONNECTED_DEVICE必须至少满足以下其中一个条件:

在清单中至少声明以下其中一项权限:

CHANGE_NETWORK_STATE
CHANGE_WIFI_STATE
CHANGE_WIFI_MULTICAST_STATE
NFC
TRANSMIT_IR
至少请求以下其中一项运行时权限:

BLUETOOTH_CONNECT
BLUETOOTH_ADVERTISE
BLUETOOTH_SCAN
UWB_RANGING
调用 UsbManager.requestPermission()

数据同步数据传输操作,例如:

数据上传或下载
备份和恢复操作
导入或导出操作
获取数据
本地文件处理
通过网络在设备和云端之间传输数据
FOREGROUND_SERVICE_DATA_SYNC
健康为健身类别的应用(例如锻炼追踪器)提供支持的所有长时间运行的用例FOREGROUND_SERVICE_HEALTH必须至少满足以下其中一个条件:

在清单中声明 HIGH_SAMPLING_RATE_SENSORS 权限。

至少请求以下其中一项运行时权限:

BODY_SENSORS
ACTIVITY_RECOGNITION
位置需要位置信息使用权的长时间运行的用例,
例如导航和位置信息分享
FOREGROUND_SERVICE_LOCATION至少请求以下其中一项运行时权限:

ACCESS_COARSE_LOCATION
ACCESS_FINE_LOCATION
媒体在后台继续播放音频或视频。
在 Android TV 上支持数字视频录制 (DVR) 功能。
FOREGROUND_SERVICE_MEDIA_PLAYBACK
媒体投影使用 MediaProjection API 将内容投影到非主要显示屏或外部设备。这些内容不必全都为媒体内容。不包括 Cast SDKFOREGROUND_SERVICE_MEDIA_PROJECTION调用 createScreenCaptureIntent() 方法。 无
麦克风在后台继续捕获麦克风内容,例如录音器或通信应用FOREGROUND_SERVICE_MICROPHONE请求 RECORD_AUDIO 运行时权限
打电话使用 ConnectionService API 继续当前通话FOREGROUND_SERVICE_PHONE_CALL在清单文件中声明 MANAGE_OWN_CALLS 权限。
消息服务将短信从一台设备转移到另一台设备。在用户切换设备时,帮助确保用户消息任务的连续性FOREGROUND_SERVICE_REMOTE_MESSAGING
短期服务快速完成不可中断或推迟的关键工作。

这种类型有一些独特的特征:

只能持续运行一小段时间(大约 3 分钟)。
不支持粘性前台服务。
无法启动其他前台服务。
不需要类型专用权限,不过它仍需要 FOREGROUND_SERVICE 权限。
正在运行的前台服务不能更改为 shortService 类型或从该类型更改为其他类型。
特殊用途涵盖其他前台服务类型未涵盖的所有有效前台服务用例。

除了声明 FOREGROUND_SERVICE_TYPE_SPECIAL_USE 前台服务类型之外,开发者还应在清单中声明用例。为此,他们会在 `` 元素内指定  元素。当您在 Google Play 管理中心内提交应用时,我们会审核这些值和相应的用例。
FOREGROUND_SERVICE_SPECIAL_USE
系统豁免为系统应用和特定系统集成预留,
使其能继续使用前台服务。

如需使用此类型,应用必须至少满足以下条件之一:

设备处于演示模式状态
应用是设备所有者
应用是性能分析器所有者
属于具有 ROLE_EMERGENCY 角色的安全应用
属于设备管理应用
否则,声明此类型会导致系统抛出 ForegroundServiceTypeNotAllowedException
FOREGROUND_SERVICE_SYSTEM_EXEMPTED
  • 介绍完这几种前台服务类型,接下来介绍如何适配它,适配前台服务类型的特性方式具体有两种方式,一种是注册清单属性,另外一种是代码动态注册
<service
android:name=".XxxService"
android:foregroundServiceType="dataSync"
android:exported="false">

service>
startForeground(xxx, xxx, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
  • 另外附上前台服务类型对应的适配属性
用途清单文件属性值Java 常量值
摄像头cameraServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
连接的设备connectedDeviceServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
数据同步dataSyncServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
健康healthServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH
位置locationServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
媒体mediaPlaybackServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
媒体投影mediaProjectionServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
麦克风microphoneServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
打电话phoneCallServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
消息服务remoteMessagingServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
短期服务shortServiceServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
特殊用途specialUseServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
系统豁免systemExemptedServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED

图片和视频的部分访问权限

  • 谷歌在 API 33(Android 13)上面引入了 READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO 这两个权限,目前针对这两个权限在 Android 14 上面有新的变动,具体的变动点就是新增了 READ_MEDIA_VISUAL_USER_SELECTED 权限,那么这个权限的作用是什么呢?我们都知道 READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO 是申请图片和视频权限的,但是这样会有一个问题,当第三方应用申请到权限后,就拥有了手机相册中所有照片和视频的访问权限,这是十分危险的,也是非常不可控的,因为用户也无法知道第三方应用会干什么,所以谷歌在 API 34(Android 14)引入了这个权限,这样用户拥有了更多的选择,可以将相册中所有的图片和视频授予给第三方应用,也可以将部分的图片和视频给第三方应用。
  • 讲完了这个特性的来龙去脉,那么接下来讲讲这个权限如何适配,如果你的应用申请了 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限,并且 targetSdkVersion 大于等于 33(Android 13),那么需要在申请权限时携带上 READ_MEDIA_VISUAL_USER_SELECTED 权限方能正常申请,如果不携带上 READ_MEDIA_VISUAL_USER_SELECTED 权限就申请 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限,会弹出权限询问对话框,但是如果用户是选择全部授予,那么 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限状态是已授予的状态,如果用户是选择部分授予,那么 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限状态是已拒绝的状态,假设此时有携带了 READ_MEDIA_VISUAL_USER_SELECTED 权限的情况下,那么 READ_MEDIA_VISUAL_USER_SELECTED 权限是已授予的状态。
  • 看到这里,脑洞大的同学可能有想法了,那我不申请 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限,我就只申请 READ_MEDIA_VISUAL_USER_SELECTED 权限行不行啊?答案也是不行的,我替大家试验过了,这个权限申请会在不会询问用户的情况下,被系统直接拒绝掉。
  • 另外需要的一点是 READ_MEDIA_VISUAL_USER_SELECTED 属于危险权限,除了在运行时动态申请外,还需要在清单文件中进行注册。

registerReceiver 需要指定导出行为

  • 谷歌在 Android 12 (API 31)新增了四大组件需要指定 android:exported 属性的特性,这次在 Android 13 上面做了一些变动,因为谷歌之前只考虑到静态注册四大组件的情况,但是遗漏了一种情况,BroadcastReceiver 不仅可以静态注册,还可以动态注册,动态注册的广播不需要额外在 AndroidManifest.xml 中再进行静态注册,所以这次谷歌将这个规则漏洞补上了,并且要求开发者在动态注册广播的时候,能够指定 BroadcastReceiver 是否能支持导出,由此来保护应用免受安全漏洞的影响。
  • 到此,大家心中可能有一个疑惑,这里的支持导出是什么意思?有产生什么作用?可以先看一下谷歌官方的原话
为了帮助提高运行时接收器的安全性,Android 13 允许您指定您应用中的特定广播接收器是否应被导出以及是否对设备上的其他应用可见。
如果导出广播接收器,其他应用将可以向您的应用发送不受保护的广播。
此导出配置在以 Android 13 或更高版本为目标平台的应用中可用,有助于防止一个主要的应用漏洞来源。

在以前的 Android 版本中,设备上的任何应用都可以向动态注册的接收器发送不受保护的广播,除非该接收器受签名权限的保护。
  • 谷歌的解释很明了,如果广播支持导出,那么其他应用可以通过发送这个广播触发我们应用的逻辑,这可能会发生程序安全漏洞的问题。
  • 那么该如何适配这一特性呢?谷歌官方提供了一个 registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) API,flags 参数传入 Context.RECEIVER_EXPORTED(支持导出) 或 Context.RECEIVER_NOT_EXPORTED(不支持导出),具体的代码适配代码如下:
String action = "xxxxxx";
IntentFilter filter = new IntentFilter(action);
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
context.registerReceiver(new LocaleChangeReceiver(), filter, Context.RECEIVER_EXPORTED);
} else {
context.registerReceiver(new LocaleChangeReceiver(), filter);
}
  • 还有一种情况,不需要指定 flag 参数,就是当要注册的广播 action 隶属系统的 action 时候,这个时候可以不需要指定导出行为。

更安全的动态代码加载

  • 如果我们应用有动态加载代码的需求,并且此时 targetSdk 升级到了 API 34(即 Android 14),那么需要注意一个点,动态加载的文件(Jar、Dex、Apk 格式)需要设置成可读的,具体案例的代码如下:
File jar = new File("xxxx.jar");
try (FileOutputStream os = new FileOutputStream(jar)) {
jar.setReadOnly();
} catch (IOException e) { ... }
PathClassLoader cl = new PathClassLoader(jar, parentClassLoader);
  • 至于谷歌这样做的原因,我觉得十分简单,是为了程序的安全,防止有人抢先在动态加载之前先把动态文件替换了,那么会导致执行到一些恶意的代码,间接导致应用被入侵或者篡改。
  • 另外需要注意的一个点的是,如果你的应用 targetSdk 大于等于 API 34(即 Android 14),如果不去适配这一特性,那么运行在 Android 14 的手机上面系统会抛出异常。

屏幕截图检测

  • Android 14 新增引入了屏幕截图检测的 API,方便开发者更好地检测到用户的操作,具体的使用案例如下:

    1. 在清单文件中静态注册权限
    <uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
    1. 创建监听器对象
final Activity.ScreenCaptureCallback screenCaptureCallback = new Activity.ScreenCaptureCallback() {

@Override
public void onScreenCaptured() {
// 监听到截图了
}
};
  1. 在合适的时机注册监听
public final class XxxActivity extends Activity {

@Override
protected void onStart() {
super.onStart();
registerScreenCaptureCallback(executor, screenCaptureCallback);
}
}
  1. 在合适的时机取消注册监听
public final class XxxActivity extends Activity {

@Override
protected void onStop() {
super.onStop();
unregisterScreenCaptureCallback(screenCaptureCallback);
}
}
需要注意的是,如果使用的是 adb 进行的截图,并不会触发 onScreenCaptured 监听方法。如果不想你的应用能被系统截图,可以考虑给当前的 Window 窗口加上 WindowManager.LayoutParams.FLAG_SECURE 标记位。最后表达一下我对这个 API 看法,这个 API 设计得不是很好,比如应用想知道用户是否截图了,应用可能需要知道的是,截图文件的存放路径,但是 onScreenCaptured 是一个空参函数,也就意味着没有携带任何参数,如果要实现获取截图文件存放路径的需求,可能还需要沿用之前的老方式,即使用 ContentObserver 监听媒体数据库的变化,然后从几个维度(文件时间维度、文件路径维度、图片尺寸维度)判断新增的图片是否为用户的截图,这种实现的方式相对是比较麻烦的,但是也无发现更好的实现方式。

完结,撒花 ✿✿ヽ(°▽°)ノ✿


作者:37手游移动客户端团队
来源:juejin.cn/post/7308434314777772042

0 个评论

要回复文章请先登录注册