PendingIntent являются важной частью фреймворка Android, но большинство доступных ресурсов для разработчиков сосредоточены на деталях их имплементации — «ссылка на токен, поддерживаемый системой» — а не на их использовании.
Поскольку Android 12 содержит важные нововведения в отношении отложенных намерений, включая определение того, является ли PendingIntent
изменяемым или неизменяемым, я подумал, что будет полезно рассказать о том, что делают отложенные намерения, как система использует их, и почему вам иногда может понадобиться изменяемый PendingIntent
.
Что такое PendingIntent?
Объект PendingIntent
оборачивает функциональность объекта 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
, прежде чем добавить его в наше уведомление.
В данном случае, поскольку у нас есть точное действие, которое необходимо выполнить, то создаем PendingIntent
, не подлежащий изменению приложением, куда мы его передаем, используя флаг FLAG_IMMUTABLE.
После вызова NotificationManagerCompat.notify() все готово. Система отобразит уведомление и, когда пользователь нажмет на него, вызовет PendingIntent.send() для нашего PendingIntent
, запуская наше приложение.
Обновление неизменяемого PendingIntent
Вы можете подумать, что если приложению нужно обновить PendingIntent
, то он должен быть изменяемым, но это не всегда так! Приложение, создающее PendingIntent
, всегда может обновить его, передав флаг FLAG_UPDATE_CURRENT:
val updatedIntent = Intent(applicationContext, MainActivity::class.java).apply {
action = NOTIFICATION_ACTION
data = differentDeepLink
}
// Because we're passing `FLAG_UPDATE_CURRENT`, this updates
// the existing PendingIntent with the changes we made above.
val updatedPendingIntent = PendingIntent.getActivity(
applicationContext,
NOTIFICATION_REQUEST_CODE,
updatedIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
// The PendingIntent has been updated.
Чуть позже поговорим о том, почему может возникнуть желание сделать PendingIntent
изменяемым.
Технология Inter-app APIs
Распространенный случай полезен не только для взаимодействия с системой. Хотя для получения обратного вызова после выполнения действия чаще всего используются startActivityForResult() и onActivityResult(), это не единственный способ.
Представьте себе приложение для онлайн-заказа, которое предоставляет API для интеграции с ним приложений. Оно может принять PendingIntent
как extra к своему собственному Intent
, которое используется для запуска процесса заказа еды. Приложение заказа запускает PendingIntent
только после того, как заказ будет доставлен.
В данном случае приложение заказа использует PendingIntent
, а не отправляет результат действия, потому что доставка заказа может занять значительное время, и нет смысла заставлять пользователя ждать, пока это происходит.
Мы создадим неизменяемый PendingIntent
, потому что не хотим, чтобы приложение онлайн-заказа меняло что-либо в нашем Intent
. Необходимо, чтобы его отправили в том виде, в каком он есть, когда заказ будет доставлен.
Изменяемые PendingIntents
Что если мы будем разработчиками приложения для заказа и захотим добавить функцию, позволяющую пользователю набрать сообщение, отправляемое обратно в вызывающее приложение? Возможно, чтобы вызывающее приложение могло показать что-то вроде: "Сейчас время PIZZA!".
Ответом на этот вопрос является использование изменяемого PendingIntent
.
Поскольку PendingIntent
— это, по сути, обертка вокруг Intent
, можно подумать, что существует метод PendingIntent.getIntent()
, который можно вызвать для получения и обновления обернутого Intent
, но это не так. Так как же это работает?
Помимо метода send()
в PendingIntent
, который не принимает никаких параметров, есть несколько других версий, включая эту, которая принимает Intent
:
fun PendingIntent.send(
context: Context!,
code: Int,
intent: Intent?
)
Этот параметр intent
не заменяет Intent
, содержащийся в PendingIntent
, а скорее используется для заполнения параметров из обернутого Intent
, которые не были предоставлены при создании PendingIntent
.
Давайте рассмотрим пример.
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
и отправить его обратно в качестве дополнительного намерения, как это сделано ниже:
val intentWithExtrasToFill = Intent().apply {
putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)
}
mutablePendingIntent.send(
applicationContext,
PENDING_INTENT_CODE,
intentWithExtrasToFill
)
Тогда вызывающее приложение увидит дополнительное EXTRA_CUSTOMER_MESSAGE
в своем Intent
и сможет отобразить сообщение.
Важные соображения при объявлении изменяемости отложенного намерения (pending intent)
При создании изменяемого
PendingIntent
ВСЕГДА явно задавайте компонент, который будет запущен в этомIntent
. Это можно реализовать так, как мы сделали выше, явно задав точный класс, который будет его получать, либо с помощью вызова Intent.setComponent().В вашем приложении может быть такой случай, когда проще вызвать
Intent.setPackage()
. Будьте очень осторожны с возможностью сопоставления нескольких компонентов, если вы сделаете это. Лучше указать конкретный компонент для полученияIntent
, если это вообще возможно.Если вы попытаетесь переопределить значения в
PendingIntent
, который был создан сFLAG_IMMUTABLE
, произойдет тихий сбой, и исходный обернутыйIntent
будет передан без изменений.
Помните, что приложение всегда может обновить свой собственный PendingIntent
, даже если они неизменяемы. Единственная причина сделать PendingIntent
изменяемым - если другое приложение будет иметь возможность каким-то образом обновить обернутый Intent
.
Подробности о флагах
Мы немного рассказали о нескольких флагах, которые можно использовать при создании PendingIntent
, но есть и другие, заслуживающие внимания.
FLAG_IMMUTABLE: Указывает, что Intent
внутри PendingIntent
не может быть изменен другими приложениями, которые передают Intent
в PendingIntent.send()
. Приложение всегда может использовать FLAG_UPDATE_CURRENT
для изменения своих собственных PendingIntent
.
До Android 12 PendingIntent
, созданный без этого флага, по умолчанию был изменяемым.
В версиях Android до Android 6 (API 23) PendingIntents
всегда изменяемы.
FLAG_MUTABLE: Указывает, что Intent
внутри PendingIntent
должен позволять приложению обновлять его содержимое путем объединения значений из параметра намерения PendingIntent.send()
.
Всегда заполняйте ComponentName обернутого Intent
любого изменяемого PendingIntent
. Невыполнение этого требования может привести к уязвимостям в системе безопасности!
Этот флаг был добавлен в Android 12. До Android 12 любые PendingIntents
, созданные без флага FLAG_IMMUTABLE
, были неявно изменяемыми.
FLAG_UPDATE_CURRENT: Запрашивает, чтобы система обновила существующий PendingIntent
новыми дополнительными данными, а не создавала новый PendingIntent
. Если PendingIntent
не был зарегистрирован, то регистрируется этот.
FLAG_ONE_SHOT: Позволяет отправить PendingIntent
только один раз (через PendingIntent.send()
). Это может быть важно при передаче PendingIntent
другому приложению, если содержащийся в нем Intent
может быть отправлен только один раз. Такое требование обусловлено удобством или необходимостью предотвратить многократное выполнение приложением какого-либо действия.
Использование FLAG_ONE_SHOT предотвращает такие проблемы, как "атаки повторного воспроизведения (replay attacks)".
FLAG_CANCEL_CURRENT: Отменяет текущий PendingIntent
, если он уже существует, перед регистрацией нового. Это может быть важно, если определенный PendingIntent
был отправлен одному приложению, а вы хотите отправить его другому приложению, потенциально обновляя данные. Используя FLAG_CANCEL_CURRENT
, первое приложение больше не сможет вызвать отправку, но второе приложение сможет.
Получение PendingIntents
Иногда система или другие фреймворки предоставляют PendingIntent как ответ на вызов API. Одним из примеров является метод MediaStore.createWriteRequest(), который был добавлен в Android 11.
static fun MediaStore.createWriteRequest(
resolver: ContentResolver,
uris: MutableCollection<Uri>
): PendingIntent
Резюме
Мы говорили о том, что PendingIntent
можно рассматривать как обертку вокруг Intent
, которая позволяет системе или другому приложению запустить Intent
, созданный одним приложением, в качестве этого приложения, в определенное время в будущем.
Мы также говорили о том, что PendingIntents
обычно должен быть неизменяемым и что это не мешает приложению обновлять свои собственные объекты PendingIntent
. Это можно сделать, используя флаг FLAG_UPDATE_CURRENT
в дополнение к FLAG_IMMUTABLE
.
Мы также говорили о мерах предосторожности, которые необходимо предпринять - заполнить ComponentName обернутого Intent
- если PendingIntent
должен быть изменяемым.
Наконец, мы говорили о том, что иногда система или фреймворки могут предоставлять PendingIntent
нашему приложению, чтобы мы могли решить, как и когда их запускать.
Обновления в PendingIntent
были лишь одной из функций в Android 12, направленной на повышение безопасности приложений. Обо всех изменениях в предварительной версии читайте здесь.
Хотите еще больше? Мы призываем вас протестировать свои приложения на новой предварительной версии ОС для разработчиков и поделиться с нами своими впечатлениями!
Перевод материала подготовлен в рамках курса "Android Developer. Basic". Если вам интересно узнать о курсе подробнее, приходите на день открытых дверей онлайн, где преподаватель расскажет о формате и программе обучения.