Введение

«Как система понимает, какому приложению отдать неявный intent?» — вопрос, после которого собеседование обычно уходит вглубь. И здесь большинство кандидатов заканчивается на фразе «ну, есть intent filter, и оно как-то матчится».

Intent Filter — это объявление в манифесте, которое говорит системе: «мой компонент готов принимать такие-то неявные intent». Без него компонент можно запустить только явно, по имени класса. С ним — вашу Activity может найти и запустить любое приложение на устройстве.

Понимание этой механики важно не только для собеседований: фильтры — это точки входа в ваше приложение извне (deep links, share-меню, открытие файлов), а значит, и поверхность для ошибок и атак. На собеседовании по этой теме проверяют не память, а способность рассуждать о том, что система сделает с вашим intent в конкретной ситуации.

Разберём по шагам: что такое фильтр, из чего он состоит, как система сопоставляет intent с фильтром и где здесь подводные камни.

Что такое Intent Filter

Официальная документация определяет intent filter как выражение в манифесте приложения, которое описывает типы intent, которые компонент хотел бы получать. Объявляется он внутри компонента — Activity, Service или Broadcast Receiver — тегом <intent-filter>.

Ключевое следствие: если у Activity нет ни одного intent-filter, она может быть запущена только явным intent — по имени класса. Никакой другой компонент не сможет её найти для неявного вызова. Это простое правило часто всплывает на собеседовании в формулировке «зачем нужен intent filter, если есть явные intent?» — ответ: чтобы вас могли находить другие приложения.

При этом важно понимать разницу между типами intent:

  • явный intent называет конкретный компонент (ComponentName) — система запускает его без всякого поиска;
  • неявный intent не называет компонент, а описывает действие: action, данные, категорию. Система сравнивает его с intent filters всех приложений на устройстве и запускает подходящие.

Если подходящих компонентов несколько — система показывает пользователю диалог выбора. Если ни одного — бросается ActivityNotFoundException. Если один — запускается он.

Главное

Intent filter — это реклама компонента: «я умею обрабатывать такие-то intent». Явный intent всегда попадает в цель без рекламы; неявный intent находит цель только по фильтрам.

Из чего состоит фильтр: action, category, data

Каждый <intent-filter> может содержать три вида элементов, и все они необязательны: <action>, <category>, <data>. Внутри одного фильтра можно объявить несколько экземпляров каждого элемента — это работает как «ИЛИ»: подойдёт любой из перечисленных.

Action — строка, описывающая общее действие: «посмотреть», «отправить», «редактировать». Стандартные действия — константы класса Intent: ACTION_VIEW, ACTION_SEND, ACTION_EDIT. Свои действия принято называть с префиксом имени пакета приложения, например com.example.action.TIMETRAVEL, чтобы не пересечься с другими приложениями.

Category — дополнительная характеристика компонента. Классика: CATEGORY_LAUNCHER — Activity показывается в лаунчере, CATEGORY_BROWSABLE — Activity может запускаться браузером по ссылке. Большинству intent категории не нужны вовсе.

Data — описание данных, с которыми умеет работать компонент: URI (scheme://host:port/path) и MIME-тип. Например, android:mimeType="text/plain" — компонент умеет работать с текстом, android:scheme="https" — с https-ссылками.

Пример из документации — Activity для приёма share-intent:

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

Обратите внимание на CATEGORY_DEFAULT — без него фильтр не сработает для неявных intent. Почему — в следующем разделе.

Если компоненту нужно принимать разные комбинации action и data — объявляют несколько отдельных <intent-filter>: каждый фильтр — это самостоятельное «И»-условие, а фильтры между собой работают как «ИЛИ».

Как система резолвит неявный intent: три теста

Когда система получает неявный intent, она сравнивает его с каждым фильтром по трём аспектам: action, category, data. Чтобы intent доставили компоненту, он должен пройти все три теста — провал хотя бы одного означает, что компонент не получит intent (но, возможно, пройдёт через другой его фильтр).

Action-тест. Action из intent должен совпасть с одним из <action> в фильтре. Если в фильтре нет ни одного action — проваливаются все intent. Если же action нет в самом intent, тест пройден, лишь бы в фильтре был хотя бы один action.

Category-тест. Каждая категория из intent должна присутствовать в фильтре. Обратное не требуется: фильтр может объявлять больше категорий, чем intent, — это нормально. Intent без категорий проходит тест всегда.

Здесь и прячется правило про CATEGORY_DEFAULT: startActivity() автоматически добавляет категорию CATEGORY_DEFAULT ко всем неявным intent. Поэтому Activity, которая хочет принимать неявные intent, обязана объявить <category android:name="android.intent.category.DEFAULT"/> в фильтре — иначе её категорийный тест не пройдёт никогда.

Data-тест. Сравниваются URI и MIME-тип. URI сравнивается только по тем частям, которые указаны в фильтре, — с линейной зависимостью: если не указан scheme, host игнорируется; если не указан host, игнорируется port; если нет scheme и host, игнорируется path. Фильтр с одним android:scheme="https" подойдёт всем https-ссылкам с любым хостом. В path можно использовать wildcard *.

Отдельное правило, которое часто проверяют на собеседованиях: если intent содержит URI или MIME-тип, а в фильтре нет ни одного <data> — data-тест провален.

MainActivity.kt
val sendIntent = Intent().apply {
    action = Intent.ACTION_SEND
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "Привет с androidbooster.ru!")
}

if (sendIntent.resolveActivity(packageManager) != null) {
    startActivity(sendIntent)
}

Итого по резолвингу: один подходящий компонент — запускается; несколько — диалог выбора; ноль — исключение, поэтому перед запуском неявного intent стоит проверять resolveActivity(). Если вы хотите показывать диалог выбора всегда — используйте Intent.createChooser().

Ошибки и безопасность

Теперь о том, где фильтры ломаются в реальных проектах и почему это тема для собеседования.

  • Забывают CATEGORY_DEFAULT — самая частая ошибка: фильтр объявлен, а неявный intent не доходит. Диагностируется долго и больно.
  • Путают «И» и «ИЛИ» — элементы внутри одного фильтра требуют совпадения по всем трём тестам; разные комбинации action/data нужно раскладывать по отдельным фильтрам.
  • Считают фильтр защитой — это не так: другой компонент может запустить вашу Activity явным intent, если узнает имя класса. Для ограничения доступа есть android:exported="false".
  • Объявляют фильтры у Service — начиная с Android 5.0 (API 21) bindService() с неявным intent бросает исключение. Сервисы нужно запускать только явными intent и фильтры им не объявлять.
  • Не проверяют resolveActivity() — и получают ActivityNotFoundException на устройствах, где нет ни одного обработчика.

Безопасность — отдельная тема. Каждый intent-filter делает компонент видимым для других приложений, а значит, расширяет поверхность атаки: вашу Activity могут запускать со странными данными, и она должна это переживать. На Android 12+ для компонентов с фильтрами атрибут android:exported обязателен — и это хороший повод задуматься, кому вы реально открываете вход.

Ещё один практический совет: intent-фильтры для Activity — это точки входа вроде deep links и share-меню, поэтому логику внутри принимающей Activity стоит писать как для «недоверенного» входа: валидировать данные из intent, не полагаться на то, что экстра точно есть, и не выводить пользовательские данные без обработки.

Меня зовут Рустем Бикбулатов, я senior Android-разработчик, провожу технические собеседования и менторю в Яндекс Практикуме. Более 100 учеников и разборов: вопросы про intent и фильтры я задаю почти каждому кандидату на middle — по ответу видно, собирал ли человек приложение вручную или только по гайдам.

Хотите, чтобы я посмотрел ваш код?

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

Итоги

Intent filter — это объявление в манифесте, которое решает, какие неявные intent получит ваш компонент. Главное, что нужно вынести:

  • Фильтр состоит из <action>, <category> и <data>, элементы внутри одного фильтра работают как «И», несколько фильтров — как «ИЛИ». Без фильтра компонент запускается только явным intent.
  • Система резолвит intent тремя тестами: action, category, data. Все три должны пройти. startActivity() добавляет к intent CATEGORY_DEFAULT, поэтому фильтр без CATEGORY_DEFAULT не получит ни одного неявного intent.
  • Резолвинг — не гарантия запуска: подходящих компонентов может быть несколько (диалог выбора) или ноль (ActivityNotFoundException). Проверка resolveActivity() перед startActivity() — обязательная привычка.

Перед собеседованием

Отрепетируйте рассказ на примере: фильтр с ACTION_SEND + CATEGORY_DEFAULT + text/plain и intent, который его запускает. Если вы можете объяснить, почему неявный intent доходит именно до этого фильтра, — тема закрыта.

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