Android 12 Beta 版
Android 12 Beta 版
行为变更:所有应用
Android 12 平台包含一些行为变更,这些变更可能会影响您的应用。以下行为变更将影响在 Android 12 上运行的所有应用,无论采用哪种 targetSdkVersion 都不例外。您应该测试您的应用,然后根据需要进行修改,以适当地支持这些变更。
用户体验
滚动效果
滚动事件的行为在 Android 12 中发生了变化
在搭载 Android 12 及更高版本的设备上,滚动事件的视觉行为发生了变化。
在 Android 11 及更低版本中,滚动事件会使视觉元素发光。在 Android 12 及更高版本中,发生拖动事件时,视觉元素会拉伸和反弹;发生快速滑动事件时,它们会快速滑动和反弹:
新的滚动行为会影响拖动和快速滑动动画。
该行为会应用于使用 EdgeEffect 的所有应用,并且适用于以下类中的所有内容:
- RecyclerView
- ListView
- ScrollView
- NestedScrollView
- HorizontalScrollView
- ViewPager
- ViewPager2
视觉效果对垂直滚动和水平滚动都适用。由于它默认应用于未停用滚动的所有应用,因此可以为用户提供更一致的界面体验。
最佳做法
为了确保新的滚动体验与您的应用完美搭配,请遵循以下最佳做法:
- 适当调整滚动容器的大小,使其子视图能够容纳在边界内。
- 增加滚动拉伸时,对 EdgeEffect.onPull(deltaDistance, displacement) 中的 deltaDistance 使用正值;减少拉伸时,使用负值。
- 向下滚动时捕捉动画。
拉伸 EdgeEffect 的用法
EdgeEffect
添加了两个用于实现拉伸滚动效果的 API。
float getDistance()
float onPullDistance(float deltaDistance, float displacement)
为了利用拉伸滚动提供最佳用户体验,请执行以下操作:
- 当用户在释放动画过程中释放并轻触内容时,将轻触注册为“捕捉”。用户停止动画并再次开始操控拉伸。
- 当用户沿拉伸的相反方向移动手指时,释放拉伸,直到其完全消失,然后开始滚动。
- 当用户在拉伸过程中快速滑动时,快速滑动 EdgeEffect 以增强拉伸效果。
捕捉动画
当用户捕捉活动的拉伸动画时,EdgeEffect.isFinished() 会返回 false。这表示拉伸应由轻触动作操控。在大多数容器中,这在 onInterceptTouchEvent() 中检测:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
...
when (action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN ->
...
isBeingDragged = !edgeEffectBottom.isFinished() ||
!edgeEffectTop.isFinished()
...
}
return isBeingDragged
}
在前面的示例中,当 mIsBeingDragged 为 true 时,onInterceptTouchEvent() 返回 true,因此这对于在子级有机会消耗事件之前消耗事件已经足够了。
释放滚动效果
务必在滚动之前释放拉伸效果,以防止将拉伸应用于滚动内容:
override fun onTouchEvent(ev: MotionEvent): Boolean {
val activePointerIndex = ev.actionIndex
when (ev.getActionMasked()) {
MotionEvent.ACTION_MOVE ->
val x = ev.getX(activePointerIndex)
val y = ev.getY(activePointerIndex)
var deltaY = y - mLastMotionY
val pullDistance = deltaY / height
val displacement = x / width
if (deltaY < 0f && mEdgeEffectTop.distance > 0f) {
deltaY -= height * mEdgeEffectTop
.onPullDistance(pullDistance, displacement);
}
if (deltaY > 0f && mEdgeEffectBottom.distance > 0f) {
deltaY += height * mEdgeEffectBottom
.onPullDistance(-pullDistance, 1 - displacement);
}
...
}
拖动时,在将触摸事件传递给嵌套滚动或拖动滚动内容之前,必须消耗 EdgeEffect 的拉取距离。在前面的代码示例中,当正在显示边缘效果且可以通过动作将其释放时,getDistance() 会返回一个正值。当触摸事件释放拉伸时,它首先由 EdgeEffect 消耗,这样就可以在显示其他效果(如嵌套滚动)之前完全释放。您可以使用 getDistance() 了解释放当前效果所需的拉取距离。
onPullDistance() 与 onPull() 的不同之处在于返回所传递增量的已消耗量。onPull() 以前允许发光效果的总距离为负值。在 Android 12 及更高版本中,如果在 getDistance() 为 0 时向 onPull() 或 onPullDistance() 传递负的 deltaDistance 值,则拉伸不会发生任何变化。
停用
可以在 XML 布局文件中或以编程方式停用滚动:
<!-- Via markup -->
<ScrollView
...
android:overScrollMode="never"
...
或者,在代码中设置:
<!-- Programmatically-->
...
recyclerview.overScrollMode = View.OVER_SCROLL_NEVER
...
行为变更:以 Android 12 为目标平台的应用
与早期版本一样,Android 12 包含一些行为变更,这些变更可能会影响您的应用。以下行为变更仅影响以 Android 12 或更高版本为目标平台的应用。如果您的应用以 Android 12 为目标平台,您应该修改自己的应用以适当地支持这些行为(如果适用)。
用户体验
画中画行为改进
Android 12 针对画中画 (PiP) 模式引入了行为改进。
自定义通知
Android 12 更改了完全自定义通知的外观和行为。以前,自定义通知能够使用整个通知区域并提供自己的布局和样式。由此产生的反模式可能会令用户困惑,或在不同设备上引发布局兼容性问题。
对于以 Android 12 为目标平台的应用,包含自定义内容视图的通知将不再使用完整通知区域;相反,系统会应用标准模板。此模板可确保自定义通知在所有状态下都与其他通知相同,例如,在收起状态下的通知图标和展开功能,以及在展开状态下的通知图标、应用名称和收起功能。此行为与 Notification.DecoratedCustomViewStyle 的行为几乎完全相同。
通过这种方式,Android 12 通过为用户提供可看到且熟悉的通知展开功能,使所有通知保持外观一致且易于浏览。
下图显示了标准模板中的自定义通知:
以下示例展示了在收起状态和展开状态下呈现的自定义通知:
Android 12 中的变更会影响某些定义 Notification.Style 的自定义子类的应用,或使用 Notification.Builder 的方法 setCustomContentView(RemoteViews)、setCustomBigContentView(RemoteViews) 和 setCustomHeadsUpContentView(RemoteViews) 的应用。
如果你的应用使用的是完全自定义的通知,最好尽快使用新模板进行测试
- 启用自定义通知变更:
- 将应用的 targetSdkVersion 变更为 S 以启用新行为。
- 重新编译。
- 在搭载 Android 12 的设备或模拟器上安装您的应用。
- 测试所有使用自定义视图的通知,确保这些通知在通知栏中看起来符合预期。在测试时,请考虑以下注意事项并进行必要的调整:
- 自定义视图的尺寸已更改。一般来说,提供给自定义通知的高度比之前小。在收起状态下,自定义内容的最大高度已从 106dp 减少到 48dp。此外,水平空间也减小了。
- 对于以 Android 12 为目标平台的应用,所有通知都是可展开的。通常,这意味着,如果您使用的是 setCustomContentView,则还需要使用 setBigCustomContentView,以确保收起状态和展开状态保持一致。
- 为了确保“浮动通知”状态看起来符合您的预期,请勿忘记将通知渠道的重要性提升至“高”(在屏幕中弹出)。
Android App Links 验证的变更
对于以 Android 12 为目标平台的应用,系统对 Android App Links 的验证方式进行了一些更改。这些变更可以提升应用链接体验的可靠性,并且能够增强应用开发者和最终用户的控制能力。
如果以 Android 12 为目标平台并且依靠 Android App Links 验证在您的应用中打开网页链接,请更新 Android App Links 声明,以支持更改后的验证流程。您也可以手动调用网域验证来测试声明的可靠性。
隐私设置
大致位置
使用以 Android 12 为目标平台的应用时,用户可以请求应用只能访问大致位置信息。
注意:如果您的应用请求 ACCESS_COARSE_LOCATION 但未请求 ACCESS_FINE_LOCATION,则此变更不会影响您的应用。 |
如果您的应用以 Android 12 为目标平台并且请求 ACCESS_FINE_LOCATION 运行时权限,则您还必须请求 ACCESS_COARSE_LOCATION 权限。您必须在单个运行时请求中包含这两项权限。
当您的应用同时请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 时,系统权限对话框将为用户提供以下新选项,如图 1 所示:
- 确切:提供 ACCESS_FINE_LOCATION 权限提供的位置精确度。
- 大致:提供 ACCESS_COARSE_LOCATION 权限提供的位置精确度。
Android 12 基于最近平台对位置权限模型所做的变更,包括后台位置信息权限和单次授权。当应用以 Android 12 为目标平台时,用户可以请求应用仅检索大致位置信息,即使应用请求 ACCESS_FINE_LOCATION 运行时权限也是如此。
如果您的应用请求 ACCESS_COARSE_LOCATION 但未请求 ACCESS_FINE_LOCATION,则本页介绍的变更不会带来任何影响。
图 1 显示了您的应用以 Android 12 为目标平台且仅请求 ACCESS_COARSE_LOCATION 时显示的面向用户的对话框。
为了更好地尊重用户隐私,建议您仅请求 ACCESS_COARSE_LOCATION。即使您只能访问大致位置信息,也可以满足大多数用例的要求。
如果您的应用以 Android 12 为目标平台并且您请求 ACCESS_FINE_LOCATION 权限,则还必须请求 ACCESS_COARSE_LOCATION 权限。您必须在单个运行时请求中包含这两项权限。如果您尝试仅请求 ACCESS_FINE_LOCATION,则系统会忽略该请求,并在 Logcat 中记录以下错误消息:
ACCESS_FINE_LOCATION must be requested with ACCESS_COARSE_LOCATION。
用户在大致位置与确切位置之间进行选择
当您的应用同时请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 时,系统权限对话框将为用户提供以下新选项:
- 确切:提供 ACCESS_FINE_LOCATION 权限提供的位置信息精确度。
- 大致:提供 ACCESS_COARSE_LOCATION 权限提供的位置信息精确度。
图 2 显示该对话框包含这两个新选项的视觉提示,以帮助用户做出选择。用户确定位置信息精确度后,他们可以点按三个按钮中的一个来选择权限授予的时长。这些按钮与搭载 Android 11(API 级别 30)的设备上的位置权限对话框中显示的按钮相同。
在 Android 12 中,用户可以转到系统设置,以设置任何应用的首选位置信息精确度,而不管该应用的目标 SDK 版本是什么。即使您的应用安装在搭载 Android 11 或更低版本的设备上,然后升级到 Android 12,也是如此。如果用户从权限对话框或在系统设置中将应用的位置信息访问权限从确切位置降级到大致位置,则系统会重启应用的进程。因此,遵循有关请求运行时权限的最佳做法特别重要。
用户的选择会影响权限授予
下表显示了系统根据用户在运行时权限对话框中选择的选项向您的应用授予的权限:
确切 | 大致 | |
---|---|---|
仅在使用该应用时允许 | ACCESS_FINE_LOCATION 和ACCESS_COARSE_LOCATION | ACCESS_COARSE_LOCATION |
仅限这一次 | ACCESS_FINE_LOCATION 和ACCESS_COARSE_LOCATION | ACCESS_COARSE_LOCATION |
拒绝 | 无位置权限 | 无位置权限 |
如需确定系统已向您的应用授予的权限,请查看权限请求的返回值。您可以在类似于下面的代码中使用 Jetpack 库,也可以使用平台库,在这种情况下,您自行管理权限请求代码。
val locationPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
when {
permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> {
// Precise location access granted.
}
permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> {
// Only approximate location access granted.
} else -> {
// No location access granted.
}
}
}
// ...
// Before you perform the actual permission request, check whether your app
// already has the permissions, and whether your app needs to show a permission
// rationale dialog. For more details, see Request permissions.
locationPermissionRequest.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION))
用户的选择还会影响后台位置信息
如果系统向您的应用授予 ACCESS_BACKGROUND_LOCATION 权限,则用户在位置权限对话框中的选择也适用于后台位置信息。
例如,如果用户向您的应用授予 ACCESS_BACKGROUND_LOCATION 权限,但仅授予在前台访问大致位置信息的权限,则您的应用在后台也只有大致位置信息的访问权限。
升级到确切位置
如果您的应用当前依赖于使用 ACCESS_FINE_LOCATION 权限访问确切位置,则大致位置可能会影响您的应用。
在让用户将应用的访问权限升级到确切位置之前,请考虑应用的用例是否确实需要这一级别的精确度。如果您的应用需要通过蓝牙或 Wi-Fi 将某个设备与附近的设备配对,请考虑使用配套设备配对或新的蓝牙权限,而不是请求 ACCESS_FINE_LOCATION 权限。
如需请求用户将应用的位置信息访问权限从大致位置升级到确切位置,请执行以下操作:
- 说明权限请求的原因
- 再次同时请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 权限。由于用户已允许系统向您的应用授予大致位置信息访问权限,因此这次系统对话框有所不同,如图 3 和图 4 所示:
测试您的应用如何处理大致位置信息
如需评估您是否需要更新您的应用以支持用户可配置的位置信息精确度,请完成本部分中所述的测试。
处理对话框中的大致位置信息请求
对于用户要求在新对话框中让您的应用具有大致位置信息访问权限的请求,如需检查您的应用如何处理此类请求,请执行以下操作:
- 同时请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION。
- 在显示的对话框中(图 2),选择顶部附近的大致,以及底部附近的仅在使用该应用时允许或仅限这一次。
- 检查应用的用例是否仍按预期工作,即使您的应用只有大致位置信息访问权限也是如此。
处理系统设置中的大致位置信息降级
对于用户要求在系统设置中将您的应用的位置信息访问权限从确切位置更改为大致位置的请求,如需检查您的应用如何处理此类请求,请执行以下操作:
- 同时请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION。
- 在显示的对话框中(图 2),选择顶部附近的确切,以及底部附近的仅在使用该应用时允许或仅限这一次。
- 转到系统设置中应用的权限屏幕。
- 在位置权限屏幕上,关闭使用确切位置。图 5 中显示了此选项。
与任何权限降级一样,系统会重启应用的进程。 - 检查应用的用例是否仍按预期工作,即使您的应用只有大致位置信息访问权限也是如此。
处理系统设置中的确切位置升级
对于用户要求在系统设置中将您的应用的位置信息访问权限从大致位置更改为确切位置的请求,如需检查您的应用如何处理此类请求,请执行以下操作:
- 同时请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION。
- 在显示的对话框中(图 2),选择顶部附近的大致,以及底部附近的仅在使用该应用时允许或仅限这一次。
- 转到系统设置中应用的权限屏幕。
- 在位置权限屏幕上,开启使用确切位置,如图 5 所示。
由于此权限更改是升级,因此系统不会重启您的应用。 - 检查您的应用是否可以在其基于位置信息的用例中接收更准确的位置数据。
应用休眠
Android 12 在 Android 11(API 级别 30)中引入的自动重置权限行为的基础上进行了扩展。如果您的应用以 Android 12 为目标平台并且用户有几个月未与您的应用互动,则系统会自动重置授予的所有权限并将您的应用置于休眠状态。
注意:如果您的应用是实现 DeviceAdminService 或者在设备所有者或个人资料所有者操作模式下运行的设备政策控制器,则系统绝不会将您的应用置于休眠状态。 |
处于休眠状态的应用具有以下特征:
- 系统会针对存储空间而非性能进行优化。应用缓存中的任何文件都会被移除。
- 应用无法从后台运行作业或提醒。
- 应用无法接收推送通知,包括通过 Firebase Cloud Messaging 发送的高优先级消息。
当用户下次与应用互动时,应用会退出休眠状态,并且可以再次创建作业、提醒和通知。不过,您需要重新调度在应用进入休眠状态之前调度的所有作业、提醒和通知。此工作流与用户从系统设置中手动强行停止应用时的工作流类似。为了更轻松地支持此工作流,请使用 WorkManager。您还可以在 ACTION_BOOT_COMPLETED 广播接收器中添加重新调度逻辑,系统会在您的应用离开休眠状态时以及设备启动后调用它。
请求用户停用休眠
如果您预计应用中的某个用例会受休眠的影响,您可以向用户发送请求,让其准许应用免于休眠和自动重置权限。如果用户希望应用主要在后台运行,即使用户不与应用互动,应用也能正常工作,例如,当应用执行以下一项或多项操作时,这种豁免很有用:
- 通过定期报告家庭成员的位置来保障家庭安全。
- 在设备与应用的服务器之间同步数据。
- 与智能设备(如电视)通信。
- 与配套设备(如手表)配对。
如需请求豁免,请调用包含 Intent.ACTION_APPLICATION_DETAILS_SETTINGS intent 操作的 intent。在显示的屏幕中,用户可以关闭名为撤消权限并释放空间的选项。
注意:在调用 intent 之前,不妨考虑向用户显示一个指导界面,以便用户了解为什么您的应用将他们引导至系统设置。 |
测试休眠行为
如需将应用置于休眠状态以进行测试,请执行以下操作:
- 在设备上启用该行为:
adb shell device_config put app_hibernation app_hibernation_enabled true
- 更改应用的状态,使其进入休眠状态。包含 --global 标志的命令会强制应用进入“完全休眠”状态,以模拟系统已为多用户设备上的所有用户将应用置于休眠状态的情况。
adb shell cmd app_hibernation set-state PACKAGE-NAME true && \
adb shell cmd app_hibernation set-state --global PACKAGE-NAME true
移动传感器有采样率限制
为了保护有关用户的潜在敏感信息,如果您的应用以 Android 12 为目标平台,则系统会对来自某些移动传感器和位置传感器的数据的刷新率施加限制。这些数据包括由设备的加速度计、陀螺仪和地磁场传感器记录的值。
刷新率限制取决于您访问传感器数据的方式:
- 如果您调用 registerListener() 方法,则传感器采样率限制为 200 Hz。对于 registerListener() 方法的所有过载变体都是如此。
- 如果您使用 SensorDirectChannel 类,则传感器采样率限制为 RATE_NORMAL,通常约为 50 Hz。
如果您的应用以 Android 12 为目标平台并且需要以较高的采样率收集移动传感器数据,则您必须声明 HIGH_SAMPLING_RATE_SENSORS 权限。否则,如果您的应用尝试在未声明此权限的情况下以较高的采样率收集移动传感器数据,就会发生 SecurityException。
注意:如果用户使用 Android 12 中的新设备切换开关关闭麦克风访问权限,则移动传感器和位置传感器会有采样率限制。无论您是否声明 HIGH_SAMPLING_RATE_SENSORS 权限,此采样率限制都会生效。 |
数据访问审核
在 Android 11(API 级别 30)中引入的数据访问审核 API 可让您根据应用的用例创建归因标记。这些标记可让您更轻松地确定应用的哪一部分执行特定类型的数据访问。
如果您的应用以 Android 12 为目标平台,您必须使用以下代码段中所示的格式在应用的清单文件中声明这些归因标记。如果您的应用以 Android 12 为目标平台并且您尝试使用未在应用的清单文件中声明的某个归因标记,则系统会为您创建一个 null 标记并在 Logcat 中记录一条消息。
<manifest ...>
<!-- The value of "android:tag" must be a literal string, and the
value of "android:label" must be a resource. The value of
"android:label" should be user-readable. -->
<attribution android:tag="sharePhotos"
android:label="@string/share_photos_attribution_label" />
...
</manifest>
WebView 中的现代 SameSite Cookie
Android 的 WebView 组件基于为 Google 的 Chrome 浏览器提供支持的开源项目 Chromium。在过去一年中,Chromium 变更了对第三方 Cookie 的处理方式,目的是为了更好地保护用户的安全和隐私,并赋予用户更高的透明度和控制权。这些变更已面向很多 Chrome 用户发布,从 Android 12 开始,这些变更将应用于 WebView 中。
Cookie 的 SameSite 属性决定了它是可以与任何请求一起发送,还是只能与同站点请求一起发送。Android 12 中的 WebView 基础版本(版本 89.0.4385.0)包含以下隐私保护方面的变更,旨在改善对第三方 Cookie 的默认处理方式,并帮助防止意外跨站点共享:
- 没有 SameSite 属性的 Cookie 被视为 SameSite=Lax。
- 带有 SameSite=None 的 Cookie 还必须指定 Secure 属性,这意味着它们需要安全的上下文,并应通过 HTTPS 发送。
- 站点的 HTTP 版本和 HTTPS 版本之间的链接现在被视为跨站点请求,因此除非将 Cookie 正确标记为 SameSite=None; Secure,否则 Cookie 不会被发送。
对于开发者而言,一般指导意见是识别关键用户流中的跨站点 Cookie 依赖项,并确保在需要时使用适当的值显式设置 SameSite 属性。您必须显式指定允许在不同网站上运行的 Cookie,或适用于从 HTTP 切换到 HTTPS 进行同站点导航的 Cookie。
ADB 备份限制
为了帮助保护私有应用数据,Android 12 更改了 adb backup 命令的默认行为。对于以 Android 12 为目标平台的应用,当用户运行 adb backup 命令时,从设备导出的其他任何系统数据都不包含应用数据。
如果您的测试或开发工作流程依赖于使用 adb backup 的应用数据,现在您可以选择通过在应用的清单文件中将 android:debuggable 设置为 true 来导出应用数据。
注意:为了帮助保护应用数据,请务必在发布应用前将 android:debuggable 设置为 false。 |
安全
更安全的组件导出
如果您的应用以 Android 12 为目标平台,且包含使用 intent 过滤器的 activity、服务或广播接收器,您必须为这些应用组件显式声明 android:exported 属性。
警告:如果 activity、服务或广播接收器使用 intent 过滤器,并且未显式声明 android:exported 的值,则您的应用将无法在搭载 Android 12 的设备上进行安装。 |
以下代码段显示了一个服务示例,该服务包含 intent 过滤器并针对 Android 12 进行了正确配置:
<service android:name="com.example.app.backgroundService"
android:exported="false">
<intent-filter>
<action android:name="com.example.app.START_BACKGROUND" />
</intent-filter>
</service>
Android Studio 中的消息
如果您的应用包含使用 intent 过滤器的 activity、服务或广播接收器但未声明 android:exported,系统会显示以下警告消息,具体取决于您使用的 Android Studio 版本:
Android Studio 2020.3.1 Canary 11 或更高版本
系统会显示以下消息:
- 清单文件中会显示以下 lint 警告:
When using intent filters, please specify android:exported as well
- 当您尝试编译应用时,系统会显示以下 build 错误消息:
to specify an explicit value for android:exported when the corresponding \
component has an intent filter defined.```
**较低的 Android Studio 版本**
如果您尝试安装应用,Logcat 会显示以下错误消息
```bash
Installation did not succeed.
The application could not be installed: INSTALL_FAILED_VERIFICATION_FAILURE
List of apks:
[0] '.../build/outputs/apk/debug/app-debug.apk'
Installation failed due to: 'null'
待处理 intent 可变性
如果您的应用以 Android 12 为目标平台,您必须为您的应用创建的每个 PendingIntent 对象指定可变性。这项额外的要求可提高应用的安全性。
如需声明特定 PendingIntent 对象是否可变,请分别使用 PendingIntent.FLAG_MUTABLE 或 PendingIntent.FLAG_IMMUTABLE 标志。如果您的应用尝试在不设置任一可变性标志的情况下创建 PendingIntent 对象,系统会抛出 IllegalArgumentException,并在 Logcat 中显示以下消息:
PACKAGE_NAME: Targeting S+ (version 10000 and above) requires that one of \
FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if \
some functionality depends on the PendingIntent being mutable, e.g. if \
it needs to be used with inline replies or bubbles.
尽可能创建不可变的待处理 intent
在大多数情况下,您的应用应创建不可变的 PendingIntent 对象,如以下代码段所示。如果 PendingIntent 对象不可变,则应用无法修改 intent 来调整调用 intent 的结果。
val pendingIntent = PendingIntent.getActivity(applicationContext,
REQUEST_CODE, intent,
/* flags */ PendingIntent.FLAG_IMMUTABLE)
然而,某些应用需要创建可变的 PendingIntent 对象:
- 通知中的直接回复操作需要变更与回复关联的 PendingIntent 对象中的剪辑数据。通常,您可以通过将 FILL_IN_CLIP_DATA 作为标志传递给 fillIn() 的方法请求此变更。
- 如果您的应用使用 PendingIntent 将对话放在气泡中,则 intent 应该可变,以便系统可以应用正确的标志,例如 FLAG_ACTIVITY_MULTIPLE_TASK 和 FLAG_ACTIVITY_NEW_DOCUMENT。
如果您的应用创建了可变的 PendingIntent 对象,强烈建议您使用显式 intent 并填写 ComponentName。如此一来,每当其他应用调用 PendingIntent 并将控制权传回您的应用时,您的应用中的同一组件总是会启动。
测试待处理的 intent 可变性变更
如需确定您的应用是否缺少可变性声明,请在 Android Studio 中查找以下 lint 警告:
Warning: Missing PendingIntent mutability flag [UnspecifiedImmutableFlag]
在开发者预览版计划期间,您可以通过关闭 PENDING_INTENT_EXPLICIT_MUTABILITY_REQUIRED 应用兼容性标志来停用此系统行为以进行测试。
不安全的 intent 启动
为了提高平台安全性,Android 12 提供了一种调试功能,如果您的应用以不安全的方式启动 intent,此功能便会发出警告。例如,您的应用可能会以不安全的方式启动从 URI 重新创建的 intent,或者以不安全的方式启动下一部分定义的嵌套 intent。
嵌套 intent 简介
嵌套 intent 是在其他 intent 中作为 extra 传递的 intent。如果您的应用执行以下两项操作,就会发生 StrictMode 违规行为。
- 您的应用从已传递的 intent 的 extra 中解封嵌套 intent。
- 您的应用立即使用该嵌套 intent 启动应用组件,例如将 intent 传递给 startActivity()、startService() 或 bindService()。
配置应用以检测不安全的 intent 启动
如需检查您的应用中是否有不安全的 intent 启动,请在配置 VmPolicy 时调用 detectUnsafeIntentLaunch(),如以下代码段所示。如果您的应用检测到 StrictMode 违规行为,您可能需要停止应用的执行以保护潜在的敏感信息。
注意:如果您的应用以 Android 12 为目标平台,并在其 VmPolicy 定义中使用 detectAll() 方法,系统将自动调用 detectUnsafeIntentLaunch() 方法。 |
fun onCreate() {
StrictMode.setVmPolicy(VmPolicy.Builder()
// Other StrictMode checks that you've previously added.
// ...
.detectUnsafeIntentLaunch()
.penaltyLog()
// Consider also adding penaltyDeath()
.build())
}
更负责地使用 intent
您的应用可能会启动 intent,以便在应用内的各个组件之间导航,或代表其他应用执行操作。为了在这两种情况下最大限度地降低出现 StrictMode 违规行为的可能性,请执行以下操作:
- 仅复制 intent 中的必要 extra,并执行任何必要的清理和验证。您的应用可能会将 extra 从一个 intent 复制到用于启动新组件的另一个 intent。当您的应用调用 putExtras(Intent) 或 putExtras(Bundle) 时,就会发生这种情况。如果您的应用执行这些操作其中之一,请仅复制接收组件所期望的 extra。如果另一个 intent(它接收副本)启动一个未导出的组件,请先清理和验证 extra,然后再将其复制到启动该组件的 intent。
- 嵌套 intent 的内部启动:确保这些组件不会被导出。
Log()
// Consider also adding penaltyDeath()
.build())
}
更负责地使用 intent
您的应用可能会启动 intent,以便在应用内的各个组件之间导航,或代表其他应用执行操作。为了在这两种情况下最大限度地降低出现 StrictMode 违规行为的可能性,请执行以下操作:
- 仅复制 intent 中的必要 extra,并执行任何必要的清理和验证。您的应用可能会将 extra 从一个 intent 复制到用于启动新组件的另一个 intent。当您的应用调用 putExtras(Intent) 或 putExtras(Bundle) 时,就会发生这种情况。如果您的应用执行这些操作其中之一,请仅复制接收组件所期望的 extra。如果另一个 intent(它接收副本)启动一个未导出的组件,请先清理和验证 extra,然后再将其复制到启动该组件的 intent。
- 嵌套 intent 的内部启动:确保这些组件不会被导出。
- 嵌套 intent 的跨应用启动:使用 PendingIntent 代替嵌套 intent。如此一来,当 PendingIntent 从包含它的 Intent 中解封时,应用组件可以使用调用进程的身份启动 PendingIntent。该配置允许提供程序应用向调用应用的任何组件(包括未导出的组件)发送回调。
性能
前台服务启动限制
以 Android 12 为目标平台的应用再也无法在后台运行时启动前台服务,但一些特殊情况除外。如果应用尝试在后台运行时启动前台服务,则会引发异常(少数特殊情况除外)。当您的应用在后台运行时,请考虑使用 WorkManager 来计划和启动工作。
精确的闹钟权限
为了鼓励应用节省系统资源,Android 12 要求为以 Android 12 为目标平台且设置精确的闹钟的应用配置“闹钟和提醒”特殊应用访问权限。
如需获取这种特殊应用访问权限,请在清单中请求 SCHEDULE_EXACT_ALARM 权限。
精确的闹钟只能用于面向用户的功能,如可接受的用例部分中所述的某种情况。
用户和系统均可撤消“闹钟和提醒”特殊应用访问权限。为您的应用撤消“闹钟和提醒”特殊应用访问权限后,您的应用会停止,并且将来的所有精确的闹钟都会取消。
向您的应用授予“闹钟和提醒”特殊应用访问权限后,系统会向其发送 ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED 广播。您的应用应实现广播接收器,以便执行以下操作:
- 确认您的应用仍具有特殊应用访问权限。为此,请调用 canScheduleExactAlarms()。
- 根据应用的当前状态,重新调度它所需的任何精确的闹钟。此逻辑应与您的应用接收 ACTION_BOOT_COMPLETED 广播时所执行的操作类似。
如果您的应用尝试使用设置精确的闹钟的 API 但未被授予特殊应用访问权限,会发生 SecurityException。
注意:当系统触发应用设置的精确的闹钟时,设备会消耗大量的资源(如电池续航时间),特别是在它必须离开低电耗模式才能触发闹钟时。此外,系统无法轻松地对这些请求进行批处理,以便更高效地使用资源。 |
考虑一下应用的用例是否确实需要精确的闹钟,如可接受的用例部分中所述的某种情况。如需执行时间更长的工作或需要接入网络的工作,请使用 WorkManager 或 JobScheduler。如需在设备处于低电耗模式的同时执行工作,请使用 setAndAllowWhileIdle() 创建不精确的闹钟,并从该闹钟启动作业。
精确的闹钟和不精确的闹钟
您的应用在调用如下方法时设置精确的闹钟:
相比之下,您在调用如下方法时设置不精确的闹钟:
此权限的可接受用例
只有在应用中面向用户的功能需要精确计时的操作时,例如在以下情况下,应用才应使用精确的闹钟,并声明关联的权限和广播接收器:
- 您的应用是闹钟应用或计时器应用。
- 您的应用可让用户调度精确计时的操作,如任务和事件的通知。
Android 12 认为精确的闹钟是对时间敏感的紧急中断。因此,精确的闹钟不受新的前台服务启动限制的影响。
让用户授予应用访问权限
如有必要,您可以将用户引导至系统设置中的闹钟和提醒屏幕,如图 2 所示。为此,请完成以下步骤:
- 在应用的界面中,向用户解释为什么您的应用需要调度精确的闹钟。
- 调用包含 ACTION_REQUEST_SCHEDULE_EXACT_ALARM intent 操作的 intent。
启用行为变更
如需启用行为变更以进行测试,请执行以下某项操作:
- 在开发者选项设置屏幕中,选择应用兼容性变更。在显示的屏幕上,点按应用的名称,然后开启 REQUIRE_EXACT_ALARM_PERMISSION。
- 在开发机器上的终端窗口中,运行以下命令:
adb shell am compat enable REQUIRE_EXACT_ALARM_PERMISSION PACKAGE_NAME
通知 trampoline 限制
当用户与通知互动时,某些应用会启动一个应用组件来响应通知点按操作,该应用组件最终会启动用户最终看到并与之互动的 activity。此应用组件被称为通知 trampoline。
为了改进应用性能和用户体验,以 Android 12 为目标平台的应用无法从用作通知 trampoline 的服务或广播接收器中启动 activity。换言之,当用户点按通知或通知中的操作按钮时,您的应用无法在服务或广播接收器内调用 startActivity()。
当您的应用尝试从充当通知 trampoline 的服务或广播接收器启动 activity 时,系统会阻止该 activity 启动,并在 Logcat 中显示以下消息:
Indirect notification activity start (trampoline) from PACKAGE_NAME, \
this should be avoided for performance reasons.
识别哪些应用组件充当通知 trampoline
测试您的应用时,点按通知后,您可以识别哪个服务或广播接收器在您的应用中充当通知 trampoline。为此,请查看以下终端命令的输出:
adb shell dumpsys activity service \
com.android.systemui/.dump.SystemUIAuxiliaryDumpService
输出的某一部分包含文本“NotifInteractionLog”。此部分包含识别因点按通知而启动的组件所需的信息。
更新应用
如果您的应用从充当通知 trampoline 的服务或广播接收器启动 activity,请完成以下迁移步骤:
- 创建一个与用户点按通知后看到的 activity 关联的 PendingIntent 对象。
- 在构建通知的过程中,使用您在上一步中创建的 PendingIntent 对象。
如需识别 activity 的来源,例如为了执行日志记录,请在发布通知时使用 extra。对于集中式日志记录,请使用 ActivityLifecycleCallbacks 或 Jetpack 生命周期观察器。
切换行为
在开发者预览版计划期间测试您的应用时,您可以使用 NOTIFICATION_TRAMPOLINE_BLOCK 应用兼容性标志启用和停用此限制。
备份和恢复
对于在 Android 12 上运行且以其为目标平台的应用,备份和恢复的工作方式发生了变化。Android 备份和恢复有两种形式:
- 云端备份:用户数据存储在用户的 Google 云端硬盘中,以便之后可以在相应设备或新设备上恢复。
- 设备到设备 (D2D) 传输:用户数据直接从用户的旧设备发送到其新设备,如通过使用数据线。
如需详细了解如何备份和恢复数据,请参阅通过自动备份功能备份用户数据和使用 Android Backup Service 备份键值对。
D2D 传输功能变更
对于在 Android 12 及更高版本上运行且以其为目标平台的应用:
-
指定 android:allowBackup=“false” 会禁止备份到 Google 云端硬盘,但不会停用应用的 D2D 传输这种方式。
-
使用 XML 配置机制指定包含和排除规则不再影响 D2D 传输,不过仍影响 Google 云端硬盘备份。如需指定 D2D 传输的规则,您必须使用下一部分中所述的新配置。
新的包含和排除格式
在 Android 12 及更高版本上运行且以其为目标平台的应用对 XML 配置使用不同的格式。这种格式要求您分别为云端备份和 D2D 传输指定包含和排除规则,从而明确区分 Google 云端硬盘备份和 D2D 传输。
(可选)您还可以使用它来指定备份的规则,在这种情况下,系统会忽略旧配置。
注意:如果您使用新配置格式,您的应用将使用新行为,即使您尚未以 Android 12 为目标平台也是如此。 |
XML 格式变更
下面是 Android 11 及更低版本中用于备份和恢复配置的格式:
<full-backup-content>
<include domain=["file" | "database" | "sharedpref" | "external" |
"root"] path="string"
requireFlags=["clientSideEncryption" | "deviceToDeviceTransfer"] />
<exclude domain=["file" | "database" | "sharedpref" | "external" |
"root"] path="string" />
</full-backup-content>
下面以粗体显示了格式的变更。
<data-extraction-rules>
<cloud-backup [disableIfNoEncryptionCapabilities="true|false"]>
...
<include domain=["file" | "database" | "sharedpref" | "external" |
"root"] path="string"/>
...
<exclude domain=["file" | "database" | "sharedpref" | "external" |
"root"] path="string"/>
...
</cloud-backup>
<device-transfer>
...
<include domain=["file" | "database" | "sharedpref" | "external" |
"root"] path="string"/>
...
<exclude domain=["file" | "database" | "sharedpref" | "external" |
"root"] path="string"/>
...
</device-transfer>
</data-extraction-rules>
配置的每一部分( 和 )包含仅适用于该特定传输类型的规则。例如,这样可让您从 Google 云端硬盘备份中排除某个文件或目录,同时仍在 D2D 传输过程中传输该文件或目录。如果您的文件太大而无法备份到云端,但可以在设备之间毫无问题地传输,这可能会很有用。
如果您没有为某种特定的备份模式指定规则(例如,如果缺少 部分),则系统会为除 no-backup 和 cache 目录之外的所有内容完全启用该模式,如备份的文件中所述。
您的应用可以在 部分中设置 disableIfNoEncryptionCapabilities 标志,以确保只有在可以加密时(例如,当用户具有锁定屏幕时)备份操作才会发生。如果用户的设备无法支持加密,设置此约束条件可阻止将备份内容发送到云端,但由于不会将 D2D 传输内容发送到服务器,因此即使在不支持加密的设备上,D2D 传输也会继续执行。
应用的清单标志
通过在清单文件中使用 android:dataExtractionRules 属性,将您的应用指向新的 XML 配置。当您指向新的 XML 配置时,系统会忽略指向旧配置的 android:fullBackupContent 属性。以下代码示例展示了新的清单文件条目:
<application
...
<!-- The below attribute is ignored. -->
android:fullBackupContent="old_config.xml"
<!-- You can point to your new configuration using the new
dataExtractionRules attribute . -->
android:dataExtractionRules="new_config.xml"
...>
</application>
连接性
并发点对点 + 互联网连接
从 Android 12 开始,支持并发点对点和互联网连接的设备可以保持与对等设备和互联网提供的主要网络的并发 Wi-Fi 连接,从而使用户体验更加顺畅。系统会自动为以 API 级别 31 及更高级别为目标的所有应用启用此功能。以较低 API 级别为目标的应用仍会体验旧行为,即在连接到对等设备之前会断开主要 Wi-Fi 网络。
兼容性
WifiManager.getConnectionInfo() 只能返回一个网络的 WifiInfo。因此,该 API 的行为在 Android 12 中从以下几个方面发生了变化:
- 如果只有一个 Wi-Fi 网络可用,则返回其 WifiInfo。
- 如果有多个 Wi-Fi 网络可用,并且发起调用的应用触发了点对点连接,则返回与对等设备对应的 WifiInfo。
- 如果有多个 Wi-Fi 网络可用,并且发起调用的应用未触发点对点连接,则返回互联网提供的主要连接的 WifiInfo。
为了在支持双并发 Wi-Fi 网络的设备上提供更好的用户体验,我们建议所有应用(特别是触发点对点连接的应用)脱离调用 WifiManager.getConnectionInfo(),而改用 NetworkCallback.onCapabilitiesChanged(),以获取与用于注册 NetworkCallback 的 NetworkRequest 匹配的所有 WifiInfo 对象。从 Android 12 开始,弃用了 getConnectionInfo()。
以下代码示例展示了如何在 NetworkCallback 中获取 WifiInfo:
val networkCallback = object : ConnectivityManager.NetworkCallback() {
...
override fun onCapabilitiesChanged(
network : Network,
networkCapabilities : NetworkCapabilities) {
val transportInfo = networkCapabilities.getTransportInfo()
if (transportInfo !is WifiInfo) return
val wifiInfo : WifiInfo = transportInfo
...
}
}
为 NFC 付款启用屏幕关闭
在以 Android 12 及更高版本为目标平台的应用中,您可以通过将 requireDeviceScreenOn 设置为 false,在设备屏幕未打开的情况下启用 NFC 付款。
供应商库
供应商提供的原生共享库
如果应用以 Android 12 或更高版本为目标平台,默认情况下无法访问由芯片供应商或设备制造商提供的非 NDK 原生共享库。只有在使用 标记明确请求时,才能访问这些库。
如果应用以 Android 11 或更低版本为目标平台,则无需使用 标记。在这种情况下,任何原生共享库均可访问,而不管它是否为 NDK 库。
更新后的非 SDK 限制
Android 12 包含更新后的受限制非 SDK 接口列表(基于与 Android 开发者之间的协作以及最新的内部测试)。在限制使用非 SDK 接口之前,我们会尽可能确保有可用的公开替代方案。
如果您的应用并非以 Android 12 为目标平台,其中一些变更可能不会立即对您产生影响。然而,虽然您目前仍可以使用一些非 SDK 接口(具体取决于应用的目标 API 级别),但只要您使用任何非 SDK 方法或字段,终归存在导致应用出问题的显著风险。
如果您不确定自己的应用是否使用了非 SDK 接口,则可以测试您的应用来进行确认。如果您的应用依赖于非 SDK 接口,您应该开始计划迁移到 SDK 替代方案。然而,我们知道某些应用具有使用非 SDK 接口的有效用例。如果您无法为应用中的某项功能找到使用非 SDK 接口的替代方案,应请求新的公共 API。
废弃
随着每个版本的发布,特定的 Android API 可能会过时或需要进行重构,以提供更好的开发者体验或支持新的平台功能。在这些情况下,Android 将正式废弃过时的 API,并引导开发者改用新的 API。
废弃意味着我们已结束对这些 API 的正式支持,但它们将继续可供开发者使用。本页重点介绍此 Android 版本中废弃的一些 API。如需查看废弃的其他 API,请参阅 API 差异报告。
RenderScript
从 Android 12 开始,废弃了 RenderScript API。它们将继续正常运行,但我们预计设备和组件制造商会逐渐停止提供硬件加速支持。为充分利用 GPU 加速功能,我们建议停止使用 RenderScript。
Android 播放列表
废弃了 Android 播放列表。不再维护该 API,但为了兼容性而保留了当前的功能。
我们建议以 m3u 文件的形式读取和保存播放列表。
废弃了 Display API
Android 设备有许多不同的外形规格,如大屏设备、平板电脑和可折叠设备。为了针对每种设备适当地呈现内容,您的应用需要确定屏幕或显示屏尺寸。随着时间的推移,Android 提供了不同的 API 来检索此信息。在 Android 11 中,我们引入了 WindowMetrics API 并废弃了以下方法:
在 Android 12 中,我们继续建议使用 WindowMetrics,并且正在逐步废弃以下方法:
应用应使用 WindowMetrics API 查询其窗口的边界,或使用 Configuration.densityDpi 查询当前的密度。
请注意,Jetpack WindowManager 库包含一个 WindowMetrics 类,该类支持 Android 4.0.1(API 级别 14)及更高版本。
示例
下面是一些关于如何使用 WindowMetrics 的示例。
首先,确保您的应用可使其 activity 完全可调整大小。
activity 应依赖于来自 activity 上下文的 WindowMetrics 来执行任何与界面相关的工作,特别是 WindowManager.getCurrentWindowMetrics()。
如果您的应用创建了 MediaProjection,则必须正确地调整边界的大小,因为投影会捕捉显示内容。如果应用完全可调整大小,则 activity 上下文会返回正确的边界。
WindowMetrics projectionMetrics = activityContext
.getSystemService(WindowManager.class).getMaximumWindowMetrics();
如果应用并非完全可调整大小,则必须从 WindowContext 实例查询边界,并使用 WindowManager.getMaximumWindowMetrics() 检索应用可用的最大显示区域的 WindowMetrics。
Context windowContext = mContext.createWindowContext(mContext.getDisplay(),
TYPE_APPLICATION, null /* options */);
WindowMetrics projectionMetrics = windowContext.getWindowManager()
.getMaximumWindowMetrics();
注意:使用 MediaProjection 的任何库也应遵循此建议,并查询应用窗口的相应 WindowMetrics。 |