关于 PendingIntent 您需要知道的那些事

关于 PendingIntent 您需要知道的那些事

PendingIntent 是 Android 框架中非常重要的组成部分,但是目前大多数与该主题相关的开发者资源更关注它的实现细节,即 "PendingIntent 是由系统维护的 token 引用",而忽略了它的用途。

由于 Android 12 对 PendingIntent 进行了 重要更新,包括需要显式确定 PendingIntent 是否是可变的,所以我认为有必要和大家深入聊聊 PendingIntent 有什么作用,系统如何使用它,以及为什么您会需要可变类型的 PendingIntent。

PendingIntent 是什么?

PendingIntent 对象封装了 Intent 对象的功能,同时以您应用的名义指定其他应用允许哪些操作的执行,来响应用户未来会进行的操作。比如,所封装的 Intent 可能会在闹铃关闭后或者用户点击通知时被触发。

PendingIntent 的关键点是其他应用在触发 intent 时是 以您应用的名义。换而言之,其他应用会使用您应用的身份来触发 intent。

为了让 PendingIntent 具备和普通 Intent 一样的功能,系统会使用创建 PendingIntent 时的身份来触发它。在大多数情况下,比如闹铃和通知,其中所用到的身份就是应用本身。

我们来看应用中使用 PendingIntent 的不同方式,以及我们使用这些方式的原因。

常规用法

使用 PendingIntent 最常规最基础的用法是作为关联某个通知所进行的操作。

val intent = Intent(applicationContext, MainActivity::class.java).apply {
    action = NOTIFICATION_ACTION
    data = deepLink
}
val pendingIntent = PendingIntent.getActivity(
    applicationContext,
    NOTIFICATION_REQUEST_CODE,
    intent,
    PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(
        applicationContext,
        NOTIFICATION_CHANNEL
    ).apply {
        // ...
        setContentIntent(pendingIntent)
        // ...
    }.build()
notificationManager.notify(
    NOTIFICATION_TAG,
    NOTIFICATION_ID,
    notification
)

可以看到我们构建了一个标准类型的 Intent 来打开我们的应用,然后,在添加到通知之前简单用 PendingIntent 封装了一下。

在本例中,由于我们明确知道未来需要进行的操作,所以我们使用 FLAG_IMMUTABLE 标记构建了无法被修改的 PendingIntent

调用 NotificationManagerCompat.notify() 之后工作就完成了。当系统显示通知,且用户点击通知时,会在我们的 PendingIntent 上调用 PendingIntent.send(),来启动我们的应用。

更新不可变的 PendingIntent

您也许会认为如果应用需要更新 PendingIntent,那么它需要是可变类型,但其实并不是。应用所创建的 PendingIntent 可通过 FLAG_UPDATE_CURRENT 标记来更新。

val updatedIntent = Intent(applicationContext, MainActivity::class.java).apply {
   action = NOTIFICATION_ACTION
   data = differentDeepLink
}

// 由于我们使用了 FLAG_UPDATE_CURRENT 标记,所以这里可以更新我们在上面创建的 
// PendingIntent
val updatedPendingIntent = PendingIntent.getActivity(
   applicationContext,
   NOTIFICATION_REQUEST_CODE,
   updatedIntent,
   PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// 该 PendingIntent 已被更新

在接下来的内容中我们会解释为什么将 PendingIntent 设置为可变类型。

跨应用 API

通常的用法并不局限于与系统交互。虽然在某些操作后使用 startActivityForResult()onActivityResult()接收回调 是非常常见的用法,但它并不是唯一用法。

想象一下一个线上订购应用提供了 API 使其他应用可以集成。当 Intent 启动了订购食物的流程后,应用可以 Intentextra 的方式访问 PendingIntent。一旦订单完成传递,订购应用仅需启动一次 PendingIntent

在本例中,订购应用使用了 PendingIntent 而没有直接发送 activity 结果,因为订单可能需要更长时间进行提交,而让用户在这个过程中等待是不合理的。

我们希望创建一个不可变的 PendingIntent,因为我们不希望线上订购应用修改我们的 Intent。当订单生效时,我们仅希望其他应用发送它,并保持它本身不变。

可变 PendingIntent

但是如果我们作为订购应用的开发者,希望添加一个特性可以允许用户回送消息至调用订购功能的应用呢?比如可以让调用的应用提示,"现在是披萨时间!"

要实现这样的效果就需要使用可变的 PendingIntent 了。

既然 PendingIntent 本质上是 Intent 的封装,有人可能会想可以通过一个 PendingIntent.getIntent() 方法来获得其中所封装的 Intent。但是答案是不可以的。那么该如何实现呢?

PendingIntent 中除了不含任何参数的 send() 方法之外,还有其他 send 方法的版本,包括这个可以接受 Intent 作为参数的 版本:

fun PendingIntent.send(
   context: Context!,
   code: Int,
   intent: Intent?
)

这里的 Intent 参数并不会替换 PendingIntent 所封装的 Intent,而是通过 PendingIntent 在创建时所封装的 Intent 来填充参数。

我们来看下面的例子。

val orderDeliveredIntent = Intent(applicationContext, OrderDeliveredActivity::class.java).apply {
   action = ACTION_ORDER_DELIVERED
}
val mutablePendingIntent = PendingIntent.getActivity(
   applicationContext,
   NOTIFICATION_REQUEST_CODE,
   orderDeliveredIntent,
   PendingIntent.FLAG_MUTABLE
)

这里的 PendingIntent 会被传递到我们的线上订购应用。当传递完成后,应用可以得到一个 customerMessage,并将其作为 intent 的 extra 回传,如下示例所示:

val intentWithExtrasToFill = Intent().apply {
  putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)
}
mutablePendingIntent.send(
  applicationContext,
  PENDING_INTENT_CODE,
  intentWithExtrasToFill
)

调用端的应用会在它的 Intent 中得到 EXTRA_CUSTOMER_MESSAGE extra,并显示消息。

声明可变的 PendingIntent 时需要特别注意的事

⚠️当创建可变的 PendingIntent 时,始终 显式设置要启动的 Intent 的 component。可以通过我们上面的实现方式操作,即显式设置要接收的准确类名,不过也可以通过 Intent.setComponent() 实现。

您的应用可能会在某些场景下调用 Intent.setPackage() 来实现更方便。但是请特别注意这样的做法有可能会 匹配到多个 component。如果可以的话,最好指定特定的 component。

⚠️如果您尝试覆写使用 FLAG_IMMUTABLE 创建的 PendingIntent 中的值,那么该操作会失败且没有任何提示,并传递原始封装未修改的 Intent

请记住应用总是可以更新自身的 PendingIntent,即使是不可变类型。使 PendingIntent 成为可变类型的唯一原因是其他应用需要通过某种方式更新其中封装的 Intent

关于标记的详情

我们上面介绍了少数几个可用于创建 PendingIntent 的标记,还有一些标记也为大家介绍一下。

FLAG_IMMUTABLE: 表示其他应用通过 PendingIntent.send() 发送到 PendingIntent 中的 Intent 无法被修改。应用总是可以使用 FLAG_UPDATE_CURRENT 标记来修改它自己的 PendingIntent。

在 Android 12 之前的系统中,不带有该标记创建的 PendingIntent 默认是可变类型。

⚠️ Android 6 (API 23) 之前的系统中,PendingIntent 都是可变类型。

🆕FLAG_MUTABLE: 表示由 PendingIntent.send() 传入的 intent 内容可以被应用合并到 PendingIntent 中的 Intent。

⚠️ 对于任何可变类型的 PendingIntent,始终 设置其中所封装的 IntentComponentName。如果未采取该操作的话可能会造成安全隐患。

该标记是在 Android 12 版本中加入。Android 12 之前的版本中,任何未指定 FLAG_IMMUTABLE标记所创建的 PendingIntent 都是隐式可变类型。

FLAG_UPDATE_CURRENT: 向系统发起请求,使用新的 extra 数据更新已有的 PendingIntent,而不是保存新的 PendingIntent。如果 PendingIntent 未注册,则进行注册。

FLAG_ONE_SHOT: 仅允许 PendingIntent (通过 PendingIntent.send()) 被发送一次。对于传递 PendingIntent 时,其内部的 Intent 仅能被发送一次的场景就非常重要了。该机制可能便于操作,或者可以避免应用多次执行某项操作。

🔐 使用 FLAG_ONE_SHOT 来避免类似 "重放攻击" 的问题。

FLAG_CANCEL_CURRENT: 在注册新的 PendingIntent 之前,取消已存在的某个 PendingIntent。该标记用于当某个 PendingIntent 被发送到某应用,然后您希望将它转发到另一个应用,并更新其中的数据。使用 FLAG_CANCEL_CURRENT 之后,之前的应用将无法再调用 send 方法,而之后的应用可以调用。

接收 PendingIntent

有些情况下系统或者其他框架会将 PendingIntent 作为 API 调用的返回值。举一个典型例子是方法 MediaStore.createWriteRequest(),它是在 Android 11 中新增的。

static fun MediaStore.createWriteRequest(
   resolver: ContentResolver,
   uris: MutableCollection<Uri>
): PendingIntent

正如我们应用创建的 PendingIntent 一样,它是以我们应用的身份运行,而系统创建的 PendingIntent,它是以系统的身份运行。具体到这里 API 的使用场景,它允许应用打开 Activity 并赋予我们的应用 Uri 集合的写权限。

总结

我们在本文中介绍了 PendingIntent 如何作为 Intent 的封装使系统或者其他应用能够在未来某一时间以某个应用的身份启动该应用所创建的 Intent。

我们还介绍了 PendingIntent 为何需要设置为不可变,以及这么做并不会影响应用修改自身所创建的 PendingIntent 对象。可以通过 FLAG_UPDATE_CURRENT 标记加上 FLAG_IMMUTABLE 来实现该操作。

我们还介绍了如果 PendingIntent 是可变的,需要做的预防措施 — 保证对封装的 Intent 设置 ComponentName

最后,我们介绍了有时系统或者框架如何向应用提供 PendingIntent,以便我们能够决定如何并且何时运行它们。

Android 12 中提升了应用的安全性,PendingIntent 的这些更新与之相得益彰。更多内容请查阅我们之前的推文《Android 12 首个开发者预览版到来》。

如需了解更多,欢迎 使用 Android 12 开发者预览版 测试您的应用,并 告诉我们 您的使用体验。

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io Funder | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.
🍗