Привет!

Меня зовут Алексей, я разработчик в компании QIWI.

TL;DR

Как перебросить из мессенджера сразу на платежную форму:

  1. В манифест помещаем пустую Activity c intent-filter вида ACTION_VIEW и ACTION_DIAL со схемой “tel”.
  2. В activity перебрасываем на форму оплаты через существующий deeplink, обогатив его данными из оригинального intent-а “tel:XXXXX”



Профит: по клику на подсвеченный номер телефона в мессенджере человек попадает на форму перевода с заполненным полем получателя перевода.
Бонус: расскажу, как красиво включать эту фичу, не имея возможности изменить список intent-filter в манифесте в рантайме.

Зачем?


Предположим, у человека есть задача перевести деньги за коллективный подарок на день рождения. Принимающая сторона может выбрать удобные варианты перевода: перевод на карту, электронный кошелек, прямой перевод на счёт или что-то другое. Многие компании, предоставляющие услуги переводов, принимают в качестве вторичного или первичного идентификатора пользователя номер телефона. Для того, чтобы не было ошибочных переводов, получатель может донести свой номер телефона через текстовое сообщение: email, мессенджер или SMS. В сообщении также указывают необходимую систему для перевода.

Типичный путь пользователя это скопировать номер телефона из мессенджера и вставить его в приложение для перевода. Такой путь подразумевает переключение между приложениями, возможно, выход на рабочий стол и даже поиск среди установленных приложений поставщика услуги переводов. Можно ли сократить этот путь, уменьшив количество шагов? Есть вариант.

Теория


Многие мессенджеры в смартфонах распознают номера телефонов и подсвечивают их в виде гиперссылки. По умолчанию для обработки такого вида ссылок на любом мобильном девайсе с модулем GSM связи есть приложение. Обычно оно называется “Телефон”. В Android возможность обработки расшаренного номера телефона декларируется в AndroidManifest. Примеры приложений: “Телефон”, Viber, Skype. Если перехватить эти события, можно сразу предоставить пользователю список возможных действий с номером телефона. В нашем случае это будет предложение перевода через QIWI Кошелек.

Нас интересуют события с типами Intent.ACTION_VIEW, Intent.ACTION_DIAL, Intent.ACTION_CALL со схемой “tel”. Событие Intent.ACTION_CALL имеет ограничения по политике безопасности Android и для его инициализации требуется разрешение от пользователя начиная с версии Marshmallow включительно. По нашим исследованиям мессенджеры используют более общие Intent.ACTION_VIEW, Intent.ACTION_DIAL.

Существуют несколько похожих типов специально для телефонов экстренных служб, но это явно не наш случай. Тело интента имеет вид “tel:12345678”. Этот вид интента нельзя обработать android.content.UriMatcher, так как здесь отсутствует host. Самый простой способ обработать этот вид Uri — запросить у него schemeSpecificPart и проверить, является ли он валидным номером телефона.

Список intent-filter-ов нужно указывать в AndroidManifest, поменять его в процессе исполнения приложения нельзя. Как дистанционно отключить эту фичу? Первый вариант — это отбить запрос пользователя на момент открытия Activity — сообщением или просто закрыть приложение. С нашей точки зрения это не лучший UX, так как мы прервем выполнение флоу пользователя на середине. Лучше, если мы не будем давать человеку выбрать наше приложение вообще, если функционал отключен. Проще отключать компонент со списком intent-filter в runtime, это не даст пользователю никаких ложных обещаний в интерфейсе системы. При использовании PackageManager обратите внимание, что проверка на включенность компонента не очень надежна. Предпочтительно выставлять флаг enabled у компонента принудительно, не опираясь на его текущее состояние используя значения COMPONENT_ENABLED_STATE_ENABLED и COMPONENT_ENABLED_STATE_DISABLED. Изменения, сделанные через PackageManager, сохраняются до момента удаления приложения с устройства.

Практика


Для того, чтобы отключить фичу, нам нужен Android-компонент для отключения.
Самый простой способ это использовать пустую Activity, что мы и сделаем. Если вы решите использовать Service с аналогичным набором intent-filter, ознакомьтесь с гайдами Google, где в частности отмечают “do not declare intent filters for your services”. Обратите внимание, что по умолчанию компонент отключен: android:enabled="false"

  <activity
            android:name=".messengerP2P.view.MessengerP2PActivity"
            android:configChanges="orientation"
            android:label="@string/title_activity_messenger_p2_p"
            android:enabled="false"
            android:screenOrientation="portrait">
            <!-- Open shared telephone number as dial application -->
            <intent-filter

                android:label="@string/title_activity_messenger_p2_p">
                <action android:name="android.intent.action.VIEW" />
                <action android:name="android.intent.action.DIAL" />

                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="tel" />
            </intent-filter>
        </activity>

Сама activity просто редиректит на форму оплаты через уже существующие диплинки.

class MessengerP2PActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        var phoneNumberFromDial: String? = intent?.data?.schemeSpecificPart
        phoneNumberFromDial?.let {
            if (ru.mw.utils.Utils
                    .isPhoneNumber(phoneNumberFromDial)) {
                val phoneNumberFromDialLink = PaymentActivity.getUriForProviderId(
                        resources.getInteger(R.integer.providerIdQiwiWallet).toLong(), null, null)
                    .buildUpon()
                    .appendQueryParameter(PaymentActivity.QUERY_PARAM_ACCOUNT, phoneNumberFromDial)
                startActivity(
                    Intent(Intent.ACTION_VIEW, phoneNumberFromDialLink.build()))
            }
        }
        finish()
    }
}

Управлять флагом android:enabled можно этим вспомогательным классом.

import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager

class MessengerP2PUtils {

    companion object {
        private const val componentName = "ru.mw.messengerP2P.view.MessengerP2PActivity"

        private fun switchMessengerP2P(enabled: Boolean = true, packageName: String, packageManager: PackageManager) {
            val compName = ComponentName(packageName, componentName)
            packageManager.setComponentEnabledSetting(
                compName,
                when (enabled) {
                    true -> PackageManager.COMPONENT_ENABLED_STATE_ENABLED
                    else -> PackageManager.COMPONENT_ENABLED_STATE_DISABLED
                },
                PackageManager.DONT_KILL_APP)
        }

        fun enableMessengerP2P(applicationContext: Context) {
            val packageName = applicationContext.packageName
            val packageManager = applicationContext.packageManager
            switchMessengerP2P(enabled = true, packageName = packageName, packageManager = packageManager);
        }

        fun disableMessengerP2P(applicationContext: Context) {
            val packageName = applicationContext.packageName
            val packageManager = applicationContext.packageManager
            switchMessengerP2P(enabled = false, packageName = packageName, packageManager = packageManager);
        }

        fun isMessengerP2PEnabled(packageName: String, packageManager: PackageManager): Boolean {
            val state = packageManager.getComponentEnabledSetting(ComponentName(packageName, componentName))
            return state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED || state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
        }

    }
}

Здесь не используется метод “isMessengerP2PEnabled” для проверки текущего состояния компонента, так как в ходе тестирования он выдавал ненадежные результаты. Если решитесь его использовать, тщательно проверьте его работу в условиях асинхронной бизнес-логики и при загрузке приложения.

Время изменения значения флага enabled не очень важно, мы выбрали момент полной загрузки конфигурации feature-флагов.

Чтобы при нажатии на номер телефона в мессенджерах был системный диалог выбора, нужно снабдить пользователя инструкцией, как скинуть настройки по умолчанию для intent-a “tel:XXXXX”. Например: “Если при нажатии на номер получается только позвонить, зайдите в Настройки смартфона > выберите Телефон в списке приложений > сбросьте настройку «Открывать по умолчанию».”


Если отключить компонент на ходу, когда пользователю показан этот системный диалог, ничего страшного не случится. Из списка мгновенно пропадет соответствующий пункт, в нашем случае “Перевести деньги”.

Интересное поведение начнется, если пользователь будет обрабатывать ссылки через наше приложение по умолчанию. Для этого ему достаточно ткнуть пункт “Запомнить выбор” или “Всегда”. Если отключить компонент, то пользователю система предоставит выбор, какое приложение использовать, причем при первом запуске не будет пункта “Запомнить выбор”. После включения фичи весь флоу восстановится, пользователя будет перебрасывать в приложение напрямую.

Заключение


Идея этой фичи пришла к нам давно, оригинальный тикет был создан в 2017 году. Задумка не утратила своей актуальности даже сейчас, найти банковское приложение со сходным функционалом мне не удалось. Мы зарелизили “переводы из мессенджера” 29 апреля, в первый день ими воспользовались почти 8000 уникальных пользователей. Если бизнес-показатели в ближайшее время нас удовлетворят, мы будем развивать эту фичу дальше.

Как думаете, сколько еще интересных intent-ов Android-а мирно дожидаются своего бизнес-применения?