Вступление: «Поломанный» плеер на Android 12 и выше
Представьте: вы с гордостью публикуете обновление своего приложения-плеера или подкаст-клиента. Проходит несколько дней, и в разделе отзывов появляются гневные комментарии: «После обновления музыка перестала воспроизводиться!», «Кнопки на заблокированном экране не работают!», «Приложение вылетает при попытке запустить трек!». Звучит знакомо? Скорее всего, вы столкнулись с той самой ошибкой, которая мучает тысячи разработчиков по всему миру:
MediaSessionCompat: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent
Эта проблема — один из самых популярных вопросов на Stack Overflow (десятки тысяч просмотров), и она напрямую связана с изменениями в Android 12 (API level 31). Хорошая новость: исправление простое и займет у вас не больше 10 минут. В этой статье мы разберем причины ошибки, правильное решение на Kotlin и лучшие практики, чтобы ваше приложение снова работало как часы.
Почему возникает ошибка? Разбираемся с PendingIntent
Чтобы понять суть проблемы, нужно вспомнить, как работает MediaSessionCompat. Это компонент, который позволяет вашему приложению взаимодействовать с системными элементами управления медиа: кнопками на гарнитуре, экраном блокировки, панелью уведомлений. Для этого он создает PendingIntent — своего рода «отложенный вызов» для системных сервисов. Система использует этот Intent, чтобы сказать вашему приложению: «Пользователь нажал Play/Pause/Next».
Начиная с Android 12 (API 31), Google ужесточил требования к безопасности PendingIntent. Раньше разработчики могли не указывать флаг FLAG_IMMUTABLE или FLAG_MUTABLE, и система по умолчанию использовала изменяемый (mutable) вариант. Теперь же вы обязаны явно указать, может ли система изменять содержимое вашего Intent после его создания. Если вы этого не сделаете — получите краш приложения. Android 12+ просто не позволит создать такой PendingIntent.
FLAG_IMMUTABLE vs FLAG_MUTABLE: что выбрать?
- FLAG_IMMUTABLE (рекомендуется): Intent нельзя изменить после создания. Это безопаснее, так как злоумышленник не сможет подменить данные. Используйте этот флаг в 90% случаев, в том числе для
MediaSessionCompat. - FLAG_MUTABLE: Intent можно изменить. Нужен только в особых случаях, например, если вы используете
NotificationCompatс действиями, которые требуют динамических данных, или при работе сAlarmManager.
Для MediaSessionCompat и медиа-уведомлений всегда используйте FLAG_IMMUTABLE. Это повышает безопасность и стабильность вашего приложения.
Пошаговое исправление на Kotlin
Давайте посмотрим, как выглядит типичный код, который вызывает ошибку, и как его исправить. Чаще всего проблема возникает в методе onCreate() вашего сервиса или медиа-браузера.
Неправильный код (вызовет краш на Android 12+)
// Старый способ — без указания флага
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
0 // <-- Ошибка! Нет FLAG_IMMUTABLE или FLAG_MUTABLE
)
Правильный код на Kotlin (исправление)
Просто добавьте PendingIntent.FLAG_IMMUTABLE в последний параметр:
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE // <-- Решение!
)
Если ваше приложение поддерживает более старые версии Android (ниже API 31), используйте условную проверку, чтобы избежать краша на старых устройствах:
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
flags
)
Исправление для MediaSessionCompat (полный пример)
В контексте MediaSessionCompat ошибка обычно проявляется при создании MediaButtonReceiver или в коде вашего MediaBrowserService. Вот как должен выглядеть правильный код инициализации:
class MyMediaService : MediaBrowserServiceCompat() {
private lateinit var mediaSession: MediaSessionCompat
override fun onCreate() {
super.onCreate()
mediaSession = MediaSessionCompat(this, "MyMediaSession")
// Создаем PendingIntent для MediaButtonReceiver
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON, null, this,
MediaButtonReceiver::class.java)
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
val mediaButtonPendingIntent = PendingIntent.getBroadcast(
this,
0,
mediaButtonIntent,
flags
)
mediaSession.setMediaButtonReceiver(mediaButtonPendingIntent)
// Также исправляем PendingIntent для Activity (если используете)
val sessionActivityIntent = Intent(this, MainActivity::class.java)
val sessionActivityPendingIntent = PendingIntent.getActivity(
this,
0,
sessionActivityIntent,
flags
)
mediaSession.setSessionActivity(sessionActivityPendingIntent)
sessionToken = mediaSession.sessionToken
}
}
Обратите внимание: мы используем Build.VERSION.SDK_INT для обратной совместимости. На устройствах с Android 11 и ниже флаг FLAG_IMMUTABLE не требуется и может быть проигнорирован, но его передача не вызовет ошибки. Однако для чистоты кода лучше использовать условную логику.
Где еще может возникнуть эта ошибка?
Проблема с PendingIntent не ограничивается только MediaSessionCompat. Ошибка может появиться в любом месте, где вы создаете PendingIntent без указания флага. Вот типичные сценарии:
- Уведомления (Notifications): при создании
NotificationCompat.Action. - Будильники (AlarmManager): при планировании отложенных задач.
- Виджеты (App Widgets): при обработке нажатий на элементы виджета.
- BroadcastReceiver: при регистрации динамических ресиверов.
Проверьте весь код, где используется PendingIntent.getActivity(), PendingIntent.getService(), PendingIntent.getBroadcast() и PendingIntent.getForegroundService(). Если вы пропустили хотя бы один вызов — приложение упадет на Android 12+.
Лучшие практики и «грабли»
Чтобы больше не возвращаться к этой проблеме, запомните несколько правил:
<