Почему ваше приложение внезапно перестало запускаться на Android 12?
Вы обновили targetSdkVersion до 31 или выше, собрали APK, запускаете — и получаете краш с загадочным сообщением:
android:exported needs to be explicitly specified for <activity>.
Apps targeting Android 12 and higher are required to specify
an explicit value for `android:exported` when the corresponding
component has an intent filter defined.
Знакомо? Если вы заглядывали на Stack Overflow, то видели, что этот вопрос собрал тысячи просмотров. Разработчики в панике, а пользователи — в недоумении. Но не спешите удалять проект. В этой статье мы разберёмся, что за зверь этот android:exported, почему Google ужесточил требования, и как быстро исправить ошибку на Kotlin. Поехали!
Что такое android:exported и зачем он нужен?
android:exported — это атрибут в AndroidManifest.xml, который управляет тем, могут ли другие приложения (или сама система) запускать ваш компонент (Activity, Service, BroadcastReceiver или ContentProvider). Если атрибут установлен в true — компонент доступен извне. Если false — только внутри вашего приложения.
До Android 12 (API 31) этот атрибут был опциональным. Если вы его не указывали, система сама решала, доступен ли компонент:
- Если у Activity есть
<intent-filter>— система считала, чтоexported = true. - Если нет фильтра —
exported = false.
Но такое «автоматическое» поведение — дыра в безопасности. Представьте: вы написали Activity для входа в приложение и случайно добавили intent-filter. Любое стороннее приложение может вызвать вашу Activity и украсть токены. Поэтому Google решил: с Android 12 вы обязаны явно указать android:exported для любого компонента, у которого есть <intent-filter>.
Как выглядит ошибка на практике?
Допустим, у вас есть простой манифест:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Если targetSdkVersion = 31 или выше, вы получите ошибку при компиляции или запуске. Решение — добавить android:exported:
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Для главной Activity, которая запускается с рабочего стола, exported должен быть true — иначе лаунчер не сможет её открыть.
Как правильно исправить: пошаговая инструкция
Шаг 1. Откройте AndroidManifest.xml
Найдите все <activity>, <service>, <receiver> и <provider>, у которых есть <intent-filter>. Для каждого из них добавьте android:exported.
Шаг 2. Определите значение атрибута
Вот простое правило:
- true — если компонент должен быть доступен из других приложений (например, Activity для входа через Google, BroadcastReceiver для системных событий, главная Activity с фильтром LAUNCHER).
- false — если компонент используется только внутри вашего приложения (например, Activity, которая открывается по нажатию кнопки в вашем же коде).
Шаг 3. Примеры для разных сценариев
Сценарий 1: Главная Activity (Launcher)
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Сценарий 2: Activity, которая открывается только из вашего приложения (например, экран настроек)
<activity
android:name=".SettingsActivity"
android:exported="false" />
Обратите внимание: у этой Activity нет <intent-filter>, и она помечена как false. Ошибки не будет, потому что без фильтра атрибут всё ещё опционален, но лучше указывать его везде для единообразия.
Сценарий 3: Deep Link (глубокие ссылки)
Если вы принимаете внешние ссылки, Activity должна быть доступна извне:
<activity
android:name=".DeepLinkActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
Сценарий 4: BroadcastReceiver (например, для Boot Completed)
<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
Как это выглядит в коде на Kotlin?
Хотя android:exported — это атрибут манифеста, в Kotlin вы можете программно проверять, доступен ли компонент. Например, при запуске Activity через Intent:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Проверяем, может ли система обработать Intent
val intent = Intent(this, SettingsActivity::class.java)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} else {
Toast.makeText(this, "Activity не найдена", Toast.LENGTH_SHORT).show()
}
}
}
Этот код не зависит от exported напрямую, но если вы забудете указать атрибут в манифесте, вы даже не дойдёте до этого момента — приложение упадёт при старте.
Что делать с библиотеками и сторонними SDK?
Часто ошибка возникает не в вашем коде, а в библиотеках, которые добавляют свои Activity в манифест. Например, <