Введение

Базовый разбор манифеста — это «паспорт приложения». Глубокий — это понимание, как система запускает компоненты, резолвит интенты и защищает их разрешениями. Именно второй уровень отличает junior от middle.

На собеседовании на middle вопрос про манифест не заканчивается на «там объявлены Activity и разрешения». Интервьюер копает глубже: что будет, если интент не найдёт ни одного компонента; зачем нужна категория DEFAULT; почему intent-filter — не защита; как Android 11+ ограничил видимость приложений.

Эта статья — продолжение базового разбора AndroidManifest.xml. Здесь идём глубже: компоненты и их атрибуты, механизм резолвинга intent-filter, защита компонентов permissions и versioning.

Компоненты: что именно объявляет манифест

Документация сводит манифест к четырём типам компонентов: для каждого подкласса Activity — <activity>, Service — <service>, BroadcastReceiver — <receiver>, ContentProvider — <provider>. Объявить компонент — значит указать его имя и свойства: имя класса в android:name, иконку и label, разрешения, intent-filter.

Детали, которые всплывают на собеседованиях:

  • .MainActivity — сокращение. Имя с точкой разворачивается через namespace из build.gradle модуля: при namespace com.example.app имя .MainActivity превращается в com.example.app.MainActivity.
  • Иконка и label наследуются. Иконка и label на уровне <application> — значения по умолчанию для всех компонентов. Отдельный intent-filter тоже может задавать свою иконку и label: они показываются пользователю, когда компонент предлагается как вариант выполнения интента в диалоге выбора.
  • Обязательны только <manifest> и <application>. Остальные элементы могут повторяться, но без них манифест бесполезен.
  • Все значения — атрибуты. В манифесте нет текстовых значений внутри элементов: всё задаётся атрибутами.

Ещё один нюанс имени: компоненты в подпакетах требуют полного пути. Если класс лежит в com.example.app.purchases, в манифесте пишут .purchases.PayActivity — или полностью квалифицированное имя.

Плюс неочевидный, но частый вопрос — <activity-alias>: альтернативное имя для существующей Activity. Например, чтобы дать компоненту вторую точку входа с другим intent-filter, не создавая новый класс. Класс один — а точек входа у системы несколько.

Ключевая мысль

Компонент, не объявленный в манифесте, система запустить не может — это база. Но на глубоком уровне важно другое: как объявленный компонент связывается с интентами и кто вообще может его запустить.

Intent-filter: как система резолвит интенты

Intent-filter — это объявление «какие имплицитные интенты компонент готов обрабатывать». Когда приложение вызывает startActivity() с имплицитным интентом, система ищет среди установленных приложений компоненты, чьи фильтры подходят. Если кандидат один — запускается он, если несколько — пользователь выбирает, если ни одного — выбрасывается ActivityNotFoundException.

Как именно система проверяет фильтр, описано в Intents and Intent Filters. Три теста:

  • Action test. Действие интента должно совпасть с одним из <action> в фильтре.
  • Category test. Каждая категория интента должна присутствовать в фильтре. Обратное не требуется: фильтр может объявлять больше категорий, чем в интенте. Интент без категорий проходит всегда.
  • Data test. Если в интенте есть URI или MIME-тип, они должны совпасть с <data> фильтра. Если интент несёт данные, а в фильтре <data> нет — тест не пройден.

Из этого следуют два практических вывода, которые любят проверять:

Классическая ловушка: Activity объявлена с intent-filter, но без категории android.intent.category.DEFAULT. Система добавляет CATEGORY_DEFAULT ко всем имплицитным интентам, передаваемым в startActivity() — и фильтр без DEFAULT не пройдёт тест. Имплицитные интенты до такой Activity просто не доходят. Для приёма имплицитных интентов категория DEFAULT обязательна.

И ещё два нюанса про безопасность:

  • Intent-filter — не защита. Фильтр ограничивает, какие имплицитные интенты принимает компонент, но другое приложение может запустить ваш компонент явным интентом по имени класса. Если компонент должен быть доступен только вашему приложению — не объявляйте intent-filter и ставьте android:exported="false".
  • android:exported — явно. Для компонентов с intent-filter атрибут обязан быть объявлен явно, иначе приложение не установится на Android 12+. LAUNCHER-активность — true, внутренние экраны — false.

Пример фильтра, который принимает шаринг текста:

AndroidManifest.xml
<activity
    android:name=".ShareActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
</activity>

Заметьте: DEFAULT на месте, данные — только MIME без URI-схемы. Это ровно тот фильтр, что используют приложения «поделиться текстом».

Два поведения резолвинга, которые стоит знать из практики:

  • Диалог выбора. Если пользователю может понадобиться разное приложение каждый раз (например, шаринг), показывают диалог через Intent.createChooser() — в нём нельзя закрепить приложение по умолчанию. А при открытии ссылки система запоминает выбор по умолчанию — поэтому браузер открывается «сам».
  • Лаунчер — это тоже резолвинг. Home-экран ищет активности с ACTION_MAIN и CATEGORY_LAUNCHER. Именно поэтому комбинация MAIN + LAUNCHER делает Activity точкой входа в приложение: это не магия, а обычный поиск по фильтрам.

Permissions: защита компонентов своими руками

Базовый разбор покрыл <uses-permission> и рантайм-запросы. Глубокий уровень — защита собственных компонентов.

Компонент можно закрыть разрешением через атрибут android:permission: запустить его сможет только приложение, у которого это разрешение есть. В качестве разрешения годится системное из android.Manifest.permission или собственное, объявленное через <permission>.

AndroidManifest.xml
<permission
    android:name="com.example.app.permission.EXPORT_DATA"
    android:protectionLevel="signature" />

<service
    android:name=".ExportService"
    android:permission="com.example.app.permission.EXPORT_DATA"
    android:exported="true" />

Так строят межприложенческие API: сервис экспорта вызывается только приложениями, которым вы выдали разрешение. На собеседовании стоит проговорить, что protectionLevel — это уровень защиты: normal, dangerous, signature и другие. Разрешение с уровнем signature выдаётся только приложениям, подписанным тем же ключом — распространённый способ ограничить доступ своим приложениям.

  • Свои компоненты защищаются android:permission на элементе компонента.
  • Собственные разрешения объявляются через <permission> с protectionLevel.
  • signature — доступ только для приложений с тем же ключом подписи.
  • Объявление разрешения в манифесте обязательно всегда, рантайм-запрос с API 23 — отдельный шаг.

Атрибут android:permission работает на всех четырёх типах компонентов: receiver и provider тоже можно закрыть разрешением. Например, провайдер данных, который отдаёт контент другим приложениям, часто защищают системным разрешением — так доступ к данным контролируется на уровне ОС, а не проверкой в коде.

Важно не путать два направления: <uses-permission> — это «что нужно мне», а android:permission на компоненте — «что нужно тому, кто запускает мой компонент». Путаница этих направлений — одна из самых частых ошибок на собеседовании.

Versioning и видимость приложений

Третья тема глубокого разбора — как манифест связан с версиями. Формально в манифесте есть <uses-sdk> с minSdkVersion, но с тех пор, как проекты собираются через Gradle, эти значения переопределяются свойствами в build.gradle: minSdk, targetSdk, compileSdk. Поэтому современный ответ: версии SDK живут в build.gradle, а не в манифесте.

Отдельная история — версия приложения. versionCode и versionName тоже в build.gradle: versionCode — целое число для внутренних проверок (должно расти с каждым релизом), versionName — читаемая версия «2.1.0» для пользователя.

Совместимость с устройствами тоже описывается в манифесте — через <uses-feature>. Если приложение не может работать без конкретного оборудования, его объявляют обязательным:

AndroidManifest.xml
<uses-feature
    android:name="android.hardware.sensor.compass"
    android:required="true" />

Google Play не даст установить такое приложение на устройство без компаса. А вот <uses-feature> без required — просто информация для Play: установить можно, но приложение должно корректно проверять наличие функции в рантайме.

И самая современная тема — package visibility. Начиная с Android 11 (API level 30) приложение видит другие установленные приложения, только если они объявили подходящий intent-filter или вы явно перечислили их в <queries>. Практический эффект: queryIntentActivities() не найдёт приложение, которое не объявлено в <queries> и не имеет видимого фильтра. На Android 13+ (API level 33) то же ограничение влияет на доставку интентов: приложение, таргетирующее 33+, обработает ваш имплицитный интент только при совпадении с его фильтром — иначе ActivityNotFoundException.

Когда я провожу собеседования, вопросы про манифест — мой любимый фильтр глубины: перечисление тегов заучивается за вечер, а вот резолвинг интентов, защита компонентов и package visibility требуют понимания системы. Кандидаты, которые отвечают на такие вопросы связно, как правило, закрывают и остальные платформенные темы.

Связка тем

Манифест связывает три уровня: компоненты (что система может запустить), permissions (кто может запустить) и versioning (на каких устройствах и с какими ограничениями). Сильный ответ показывает связку, а не перечисление тегов.

Хотите уверенно отвечать на платформенные вопросы?

Манифест, интенты, permissions — на мок-собеседованиях тренируем не заученные ответы, а механику: как система резолвит, защищает и ограничивает.

Итоги

Глубокое понимание манифеста — это три связки: компоненты и их атрибуты, резолвинг интентов через три теста intent-filter, защита и ограничения через permissions и versioning.

Три тезиса, которые стоит унести с собой:

  • Intent-filter резолвится тремя тестами. Action, category (включая обязательный DEFAULT для имплицитных интентов), data. Нет кандидата — ActivityNotFoundException.
  • Фильтр — не защита. Для ограничения доступа — android:exported="false" и отсутствие intent-filter; межприложенческий доступ — через android:permission.
  • Версии живут в Gradle. minSdk, targetSdk, compileSdk и versionCode, versionName — в build.gradle, а с Android 11+ видимость других приложений регулируется <queries>.

Если резолвинг интентов и exported кажутся кашей — вернитесь к базовому разбору AndroidManifest.xml, а потом перечитайте эту статью. Но если и после этого платформенные вопросы сыпятся — не нужно зубрить дальше. Напишите в Telegram, разберём вашу ситуацию и составим план до уверенного middle.