Pull to refresh
750.3
OTUS
Цифровые навыки от ведущих экспертов

Всё о PendingIntents

Reading time7 min
Views23K
Original author: Nicole Borrelli

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". Если вам интересно узнать о курсе подробнее, приходите на день открытых дверей онлайн, где преподаватель расскажет о формате и программе обучения.

Tags:
Hubs:
+7
Comments0

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS