We need to go deeper: диплинки и кодогенерация

от автора

Привет! Мы написали свою систему диплинков на основе кодогенерации. В этой статье поговорим, как мы упростили работу с диплинками и смогли отловить устаревшие, добавили мониторинг и как собрали все диплинки в одной статье в конфлюенсе.

Диплинк — это uri на конкретный ресурс в приложении. Они нужны бизнесу, чтобы упрощать пользовательский опыт. Так вместо нескольких переходов внутри приложения диплинки позволяют направить пользователя на определённый экран в один клик (польза для пользователя) и снимать статистику (польза для бизнеса). Например: на внешней площадке компания разместила баннер, в котором предлагается заказать виртуальную карту, пользователь может кликнуть по баннеру и сразу попасть на экран заказа карты, а бизнес сможет оценить, какая из площадок более эффективна.  

Самая большая проблема — это проблема безопасности. Объясню на примере активити, которая открывает веб-страницы. Активити открывается по диплинку, в нём указывается URL в качестве параметра. Один из вариантов атаки — когда злоумышленник может заставить пользователя пройти по диплинку с URL на вредоносный сайт и таким образом провести атаку. Ещё одна из возможных проблем — на некоторых экранах нам нужно валидировать параметры, а это иногда занимает значительную часть активити. Было бы хорошо вынести валидацию в отдельное место. 

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

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

Пример объявления диплинка для экрана бонуса.

@DeepLink(    Uri("qiwi", "bonus"),    description = Description("открыть раздел бонус(кэшбек)") ) class BonusHandler : ProxyDeepLinkHandler() {    override val activityClass: Class<*> = BonusShowcaseActivity::class.java } 

С помощью аннотации @Deeplink помечаем обработчик и задаем диплинк, в данном примере диплинк qiwi://bonus откроет BonusShowcaseActivity.

Во втором примере показано, как производим проверки перед открытием диплинка.

@DeepLink(    Uri("qiwi", "bonus"),    description = Description("открыть раздел бонус(кэшбек)") ) class BonusHandler : DeepLinkHandler() {     @Inject    lateinit var bonusShowcaseFeature: BonusShowcaseFeature     override fun deeplinkData(context: Context, intent: Intent): DeepLinkData {        QiwiApplication.get(context)            .appComponent.inject(this)        val clazz = BonusShowcaseActivity::class.java        val startIntent = Intent(context, claszz).copyIntent(intent)        return bonusShowcaseFeature.deepLinkData(DeepLinkData(startIntent, claszz))    } } 

В обработчике проверяем фича флаг BonusShowcaseFeature и открываем экран, если флаг включен. Про наши фича флаги можно почитать в этом посте.

Аннотации

@Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class DeepLink(    vararg val deepLinks: Uri,    val description: Description = Description(),    val commonParams: Array<Parameter> = [],    val addToDoc: Boolean = true )  @Retention(AnnotationRetention.SOURCE) annotation class Uri(    val scheme: String,    val hostPath: String,    val description: Description = Description(),    val parameters: Array<Parameter> = [],    val examples: Array<Example> = [],    val addToDoc: Boolean = true )  @Retention(AnnotationRetention.SOURCE) annotation class Description(    val description: String = "",    val group: String = Groups.OTHER )  @Retention(AnnotationRetention.SOURCE) annotation class Parameter(    val key: String = "",    val value: String = "",    val description: String = "" )  @Retention(AnnotationRetention.SOURCE) annotation class Example(    val description: String,    val parameters: Array<Parameter> = [] ) 

Сейчас в системе у нас пять аннотаций. 

  1. Deeplink — используется для описания одно или нескольких uri, также есть возможность добавить описание, общие параметры для всех uri и можно указать, попадёт ли данный диплинк в документацию.

  2. Uri — позволяет установить диплинк(scheme://hostPath), дополнительно можно оставить описание, параметры, с которыми используется диплинк, и, если необходимо, добавить примеры.

  3. Description— используется для описания. Можно указать группу, в которой содержится диплинк, это необходимо для сортировки в итоговой документации. 

  4. Parameter— аннотация задаёт query-параметры в формате key-value, в value описываем возможные значения. 

  5. Example — используется для примеров.

Аннотация Deeplink указывает, какой обработчик будет обрабатывать диплинк из аннотации Uri. Все обработчики являются наследниками класса DeepLinkHandler

class DeepLinkData(    val startIntent: Intent?,    val activityClazz: Class<*>? )  abstract class DeepLinkHandler {     var analytics: DeepLinkHandlerAnalytics? = null     abstract fun deeplinkData(context: Context, intent: Intent): DeepLinkData     fun process(context: Context, intent: Intent) {        val deepLinkData = deeplinkData(context, intent)        deepLinkData.startIntent?.let { startIntent ->            context.startActivity(startIntent)         }    } } 

Абстрактный класс, который умеет открывать активити по интенту и отправлять аналитику. При наследовании от этого класса нужно реализовать метод deeplinkData и в нём можно проводить обработку диплинка. 

В случае, когда не нужно проводить проверку диплинка, мы используем ProxyDeeplinkHandler.

abstract class ProxyDeepLinkHandler : DeepLinkHandler() {     protected abstract val activityClass: Class<*>     override fun deeplinkData(context: Context, intent: Intent): DeepLinkData {        val startIntent = Intent(context, activityClass).copyIntent(intent)        return DeepLinkData(startIntent, activityClass)    } } 

При наследовании от этого класса в дочернем классе достаточно определить поле activityClass, который должен возвращать ту активити, которую нужно открыть.

Архитектура диплинков

Под капотом всё работает немного сложнее. Точкой входа у нас является SplashActivity. Она с помощью DeepLinkDelegate и остальных компонентов достаёт из реестра нужный обработчик для того диплинка, который пришёл в систему. 

Манифест для SplashActivity выглядит следующим образом.

<activity    android:name=".splashScreen.view.SplashScreenActivity"    android:theme="@style/QiwiSplashScreenTheme">    <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="qiwi" />    </intent-filter>    <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="http" />        <data android:scheme="https" />         <data android:host="qiwi.com" />        <data android:host="qiwi.ru" />         <data android:path="/main.action" />        ...        <data android:pathPattern="/p/.*" />        <data android:path="/feed" />    </intent-filter> </activity> 

В манифесте есть два интент-фильтра для диплинков. Первый для диплинков — со схемой qiwi, что позволяет открывать любые диплинки с этой схемой. Во втором интент-фильтр используется для url-диплинков. Мы редко добавляем url-диплинки, поэтому манифест практически не редактируем.

Кодогенерация

Во время кодогенерации происходит поиск всех классов, помеченных аннотацией Deeplink, потом с помощью Kotlin Poet создаются несколько DeeplinkRegistry и json-файл с описанием диплинков для документации.

DeeplinkRegistry — это интерфейс с методом registry(), возвращающий мапу, где ключ — диплинк, а значение — класс обработчика.

Пример сгенерированного DeeplinkRegistry для диплинков uri:

public class OuterDeepLinkRegistryGenerated : DeepLinkRegistry {    public override fun registry(): Map<String, Class<out Any>> {        val map = mutableMapOf<String, Class<out Any>>()        map["qiwi://bonus"] = BonusHandler::class.java        map["qiwi://main.action"] = MainHandler::class.java        map["qiwi://cards/detail"] = CardDetailActivityHandler::class.java        ...        map["qiwi://help"] = ProfileActivityHandler::class.java        return map    } } 

Json-файл с описанием диплинков выглядит так:

{  "qiwi://bonus": {    "description": "открыть раздел бонус(кэшбек)"  },  "qiwi://qvc/order": {    "group": "QIWI-карты",    "description": "заказ qvc или qvc_mir карты",    "parameters": [      {        "name": "alias",        "value": "qvc | qvc-mir"      }    ]  },  "qiwi://cards/detail": {    "group": "QIWI-карты",    "description": "детали карты",    "parameters": [      {        "name": "id",        "value": "*",        "description": "id карты"      }    ]  } }  

Из этого файла создается документация.

Документация  — это большая таблица, в которой перечислены все диплинки с описанием и параметрами из json-файла.

Таблица всегда актуальная, так как выгрузка json-файла встроена в релизный пайплайн. С помощью таски файл загружается на статик-сервер. А в конфлюенсе настроена таблица, которая подгружает и парсит json-файл.

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

В системе диплинков мы добавили мониторинг и аналитику. И получаем события о необработанных диплинках. Алерты настроены на почту и в телеграм-канал, что позволяет быстро реагировать на инциденты.

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

Итоги

  1. Упростили работу с диплинками, например, в обработчике проверяем фича флаг, проводим валидацию параметров.

  2. Повысили безопасность активити с WebView, завели whitelist, по которому определяется, будет ли открыта веб-страничка или нет, если приходит диплинк с урлом не из белого списка, то кидаем событие в мониторинг.

  3. Добавили мониторинг в систему диплинков.

  4. Улучшили жизнь специалистам из маркетинга, они получили всегда актуальную таблицу с диплинками.

  5. Отловили все устаревшие и кривые диплинки, которые использовались в промо-материалах.


ссылка на оригинал статьи https://habr.com/ru/articles/673850/