Как разработать Foreground Service и Deep Links для приложений со звонками на Android? — обложка

Рассмотрим реализацию двух удобных функций для приложения со звонками на Android. Сначала настроим работу приложения так, чтобы оно продолжало функционировать после сворачивания или блокировки экрана. Затем разберём, как реализовать прямые ссылки на звонок или конференцию — при нажатии на них пользователи сразу попадут в нужный звонок.

Android Foreground Service

Современные смартфоны и их операционные системы содержат множество встроенных оптимизаций, продлевающих работу от батареи. Разработчикам мобильных приложений важно учитывать возможные действия системы по отношению к их приложению.

Основной пример — освобождение ресурсов и закрытие приложений, с которыми пользователь в данный момент не работает. Система считает «активно используемым» только то приложение, которое сейчас открыто на экране. Все остальные запущенные приложения могут быть закрыты в любой момент, если системе не хватит ресурсов для активного. Благодаря этому можно открыть сколько угодно приложений и не закрывать их вручную — система сама освободит память, закрыв старые, а при возврате в них приложение запустится снова.

В целом механизм удобен и необходим на мобильных устройствах. Но мы хотим обойти это ограничение, чтобы звонок не прерывался из-за внезапного закрытия системы. К счастью, можно «пометить» часть приложения как активно используемую, даже если она уже не отображается на экране. Для этого используем Foreground Service. Отметим, что даже это не даёт полной защиты от системы — но повышает «приоритет» приложения в глазах системы и позволяет сохранить некоторые объекты в памяти даже после закрытия `Activity`.

Добавляем разрешение на запуск таких сервисов:

 
 uses-permission android:name="android.permission.FOREGROUND_SERVICE" /
 

Реализуем наш сервис. В простейшем виде это просто подкласс Service, в котором хранится ссылка на `CallManager` — чтобы сборщик мусора его не удалил:

 
 class OngoingCallService : Service() {
    @Inject
    lateinit var abstractCallManager: AbstractCallManager
    // Реализация абстрактного метода; мы не будем использовать Bind, поэтому просто возвращаем null
    override fun onBind(intent: Intent): IBinder? = null
}
 

Сервис — это компонент приложения, и, как и Activity, его нужно указать в `AndroidManifest.xml`:

 
 service
    // Имя класса нашего сервиса
    android:name=".OngoingCallService"
    android:enabled="true"
    // Этот флаг означает, что другие приложения не могут запускать этот сервис
    android:exported="false"
    // Объявляем тип нашего сервиса
    android:foregroundServiceType="microphone|camera|phoneCall" /
 

Запускается наш Foreground Service немного не так, как обычные сервисы:

 
 private fun startForegroundService() {
    val intent = Intent(this, OngoingCallService::class.java)
    ContextCompat.startForegroundService(this, intent)
}
 

На версиях Android выше 8 Foreground Service обязан вызвать метод startForeground в течение нескольких секунд, иначе система сочтёт приложение зависшим (ANR). В этот метод нужно передать уведомление, поскольку по соображениям безопасности наличие таких сервисов должно быть заметно пользователю (если вы не помните, как создавать уведомления — можно освежить знания в одной из наших предыдущих статей про уведомления о звонках на Android):

 
 val notification = getNotification()startForeground(ONGOING_NOTIFICATION_ID, notification)
 

К этому уведомлению применимо всё, что было сказано в предыдущей статье об уведомлениях — его можно обновлять списком участников звонка, добавлять кнопки или полностью менять дизайн. Единственное отличие — оно по умолчанию будет `ongoing`, и пользователь не сможет его просто «смахнуть».

На Android 13 и выше требуется разрешение на отправку уведомлений — POST_NOTIFICATIONS. Добавьте его в манифест:

 
 uses-permission android:name="android.permission.POST_NOTIFICATIONS" /
 

Также нужно запросить это разрешение во время работы приложения — например, при первом запуске. Подробнее о том, как запрашивать разрешения, можно почитать в документации.

Если пользователь не дал разрешение на показ уведомлений, уведомление, связанное с Foreground Service, будет отображаться в диспетчере задач Foreground Service (FGS).

Если пользователь отклоняет разрешение на уведомления, он видит уведомления, связанные с Foreground Service, в диспетчере задач Foreground Services (FGS), но не видит их в панели уведомлений.

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

 
 private fun stopForegroundService() {
    val intent = Intent(this, OngoingCallService::class.java)
    stopService(intent)
}
 

Запуск и остановка сервиса удобно реализовать, если в CallManager есть реактивное поле для отслеживания состояния звонка, например:

 
 abstractCallManager.isInCall
    .collect { if (it) startForegroundService() else stopForegroundService() }
 

Вот и вся реализация сервиса, который в какой-то степени защитит наше свернутое приложение от закрытия системой.

Android Deep Links

Крайне удобная для пользователей функция, которая помогает быстрее наращивать аудиторию приложения — это ссылки, ведущие прямо в определённое место внутри приложения. Если у пользователя его ещё нет, ссылка открывает страницу в Google Play. В приложениях для звонков особенно удобно использовать такую функцию, чтобы делиться ссылкой на звонок, встречу или комнату. Пользователь хочет пообщаться — отправляет ссылку собеседнику, тот скачивает приложение и сразу попадает в звонок. Что может быть проще и удобнее?

Сами по себе ссылки на определённые места в приложении поддерживаются системой без дополнительных библиотек. Однако, чтобы ссылка продолжала работать после переустановки приложения, понадобится помощь Firebase Dynamic Links.

Сконцентрируемся на реализации обработки ссылок в приложении, а их создание оставим backend-разработчикам.

Для начала добавим библиотеку:

 
 dependencies {
    implementation 'com.google.firebase:firebase-dynamic-links:20.1.1'
}
 

Для пользователя deep-линки — это обычные ссылки, по которым он переходит. Но перед тем как открыть ссылку в браузере, система проверяет реестр приложений и находит те, что зарегистрированы для обработки ссылок с этого домена. Если такое приложение найдено — вместо браузера оно запускается, и ссылка передаётся ему. Если приложений несколько — система покажет окно с выбором, и пользователь сам решит, каким приложением открыть ссылку. Если вы владеете доменом, можно настроить защиту, чтобы ссылки открывались только вашим приложением, пока оно установлено.

Чтобы указать, какие ссылки может обрабатывать наше приложение, нужно в `AndroidManifest.xml` добавить к нашему `Activity` intent-фильтр:

 
 activity ...
    intent-filter
        // Эти action и category сообщают системе, что мы можем "отображать" ссылки
        action android:name="android.intent.action.VIEW"/
        category android:name="android.intent.category.DEFAULT"/
        category android:name="android.intent.category.BROWSABLE"/
        // Описание ссылки, которую мы можем обрабатывать. В данном случае это ссылки, начинающиеся с calls://forasoft.com
        data
            android:host="forasoft.com"
            android:scheme="calls"/
    /intent-filter
/activity
 

Когда пользователь нажмёт на Dynamic Link и установит приложение (или откроет ссылку в уже установленном приложении), запустится Activity, указанная как обработчик этой ссылки. В этой Activity мы можем получить ссылку следующим образом:

 
 Firebase.dynamicLinks
        .getDynamicLink(intent)
        .addOnSuccessListener(this) { data ->
            val deepLink: Uri? = data?.link
        }
 

При использовании обычных deep links данные получаются проще:

 
 val deepLink = intent?.data
 

Вот и всё, теперь остаётся лишь получить нужные параметры из ссылки и выполнить в приложении действия для присоединения к звонку:

 
         val meetindId = deepLink?.getQueryParameter("meetingid")
        if (meetingId != null) abstractCallManager.joinMeeting(meetingId)
 

Итог

В финальной статье нашего цикла «что должно быть в каждом приложении со звонками» мы рассмотрели, как поддерживать работу приложения после сворачивания, и объяснили, почему deeplink — удобный способ приглашений на звонок. Теперь вы знаете все ключевые механизмы, которые помогают улучшить пользовательский опыт не только внутри приложения, но и на уровне всей системы.

  • Разработка