Android-разработчики и продакты всей галактики ломают голову над одним важным вопросом — “Нужно ли делать интеграцию HMS?”. В это статьей мы расскажем, как у нас получилось затащить поддержку Huawei Mobile Services в регулярных релизах, пусть и со второй попытки.
Здесь вы найдете актуальные цифры, архитектурно-инфраструктурные решения и подробное описание наши ошибок. У статьи есть видеоверсия, и ее можно посмотреть и послушать, если читать совсем не хочется.
Почему на картинке XXX? Все просто, ребят мы расскажем про интеграцию Хуавей и Хэдхантер
Почему год назад мы решили интегрировать HMS
Для начала стоит разобраться, зачем вообще год назад нам потребовалось идти в эту историю. Не будем вдаваться в подробности, но после некоторых политических перипетий Huawei уже не мог устанавливать Google-сервисы на новой модели устройств. Проще говоря, на этих устройствах приложения hh.ru уже попросту не запустятся.
Мы, конечно же, кинулись в аналитику и обнаружили, что порядка 30% наших пользователей ходят с продуктами от Huawei. Может показаться, что единственный путь к решению проблемы — всё бросить и как можно быстрее затаскивать поддержку Huawei. На самом деле, не всё так просто. Этот риск мог закрыться и мы видели несколько потенциальных вариантов развития событий, вот некоторые из них:
Google может снять санкции с Huawei
Huawei просто не выдержит этой борьбы, и процент будет потихоньку снисходить с 30 до нуля
Продавцы в сетях типа «Связного», «Евросети» будут просто за небольшую денежку устанавливать Google Play на устройства
Эти варианты становились всё весомее, а в дополнение к ним появились новые предпосылки идти в историю с HMS:
Нам очень хотелось подружиться с вендором! И дело даже не в том, что Huawei предлагали нам различные бонусы, продвижение промо в AppGallery, размещение иконки на баннерах и так далее. Нам было интересно взаимодействие с технической командой. Бесценно, когда ты можешь просто зарепортить какую-то ошибку и знать, что над ней будут работать или получить моментальный ответ на любой свой вопрос
Ну и, конечно же, это была очень интересная техническая задача. И мне, как инженеру, просто хотелось углубиться в эту историю
Мы собрали все «за» и «против» и решили — интегрироваться надо. Но мы хотели сделать это максимально дешево и невероятно быстро.
Первый пошёл!
Для максимальной скорости и минимизации рисков, на тот случай, если всё-таки мобильные сервисы от Huawei не выстрелят, мы приняли решение:
Ведем всю разработку в отдельной ветке feature-hms, и в ней же и сделаем релизную сборку
Ветка feature-hms после релиза не будет вмерджена в develop
В ветку feature-hms не подмердживаем актуальную версию develop;
Если в App Gallery со временем наберется достаточно пользователей, то тогда мы уже произведем полноценную миграцию на Huawei-сервисы, а в случае неудачи просто забьем
Заменяем часть фич из GMS на HMS и делаем это без какой-либо архитектуры. Просто в модулях с GMS заменяем сервисы на их аналоги из HMS.
HMS-фичи, которые мы представили в нашей пилотной версии – это карты, пуши, гео-локации, аналитика крашей, которую, как потом оказалось, можно было не делать, потому что на HMS-девайсах нормально работает Firebase Crashlytics. Ну и всякое по мелочи.
Остальные фичи Google мы просто отключили без HMS-аналога. У нас получилась довольно урезанная версия приложения, да еще и ревизия отставала от актуальной, на тот момент, версии нашего приложения в Google Play и не имела перспектив ближайших обновлений. При таком подходе, естественно, что-то пошло не так.
Проблемы первой итерации: устройства и эмуляторы
Наш первый челлендж был связан с девайсами. HMS и AppGallery можно поставить на любой девайс, но это покрывает не все случаи для отладки. Например, при работе с гео-локацией и push-уведомлениями вам необходим телефон с EMUI, то есть с Huawei надстройкой над Android. Мы попробовали облачные девайсы через только что появившийся на тот момент Huawei Cloud Debugging, но, к сожалению, устройства там были достаточно нестабильными и периодически отключались. Стабильно отлаживать на них было тяжело. Я надеюсь, сейчас ситуация с ними улучшилась, так как появилась возможность подключаться к девайсам из фермы напрямую из Android Studio по ADB. Если вы недавно пробовали Huawei Cloud Debugging, то, пожалуйста, расскажите об этом в комментариях.
Кроме того, отсутствие Huawei эмуляторов исключает возможность запуска End-to-end тестов, что неплохо ломает флоу релизов.
Однако стоит отметить, что ребята из Huawei прислали нам несколько реальных устройств. Одно из них получил разработчик, который тогда занимался интеграцией HMS, другое — наш тестировщик, а третий мы решили пошарить сразу на всех разработчиков, поместив его в ферму устройств Open STF.
Лайфхак по фермам
Open STF – технология, которая легко позволяет через веб-интерфейс просматривать подключенные девайсы, узнавать их статус и получать их адресат для подключения по ADB.
Совместив это с утилитой scrcpy, которая стримит экран девайса через ADB на компьютер разработчика и позволяет с ним взаимодействовать, можно достаточно гладко пользоваться живым устройством откуда угодно. То есть получился такой in-house cloud debugging.
Проблемы первой итерации: tooling
Отдельно следует сказать про HMS Tooling для разработки. На момент разработки нашей пилотной версии он был далек от идеала, и мы столкнулись с рядом проблем.
Одна из них – это аномальное увеличение времени синк проекта, когда ты добавляешь всё больше и больше HMS-зависимостей к своим Gradle-модулям.
Другая проблема заключалась в конфликте между версиями некоторых библиотек при подключении к одному и тому же модулю.
Уже во второй итерации у нас была проблема с тем, что сборки падали из-за Gradle-плагина AG Connect.
Но со всеми проблемами нам очень оперативно помогали ребята из Huawei, которым мы всё репортили. На данный момент все эти проблемы уже пофикшены. Здесь прям отдельный респект комрадам из Huawei, которые сопровождают платформу. У вас очень здорово получается.
Проблемы первой итерации: подпись и релиз
Кроме проблем с разработками, на самом деле, еще были вопросы и с тем, как релизить. Самое первое, что нам нужно было решить — это какой вообще подписью подписывать. Было два варианта — сгенерить новую подпись и использовать ту которой у нас подписывались приложения в Google Play Store.
Если вы подписали одной и той же подписью приложения с GMS и HMS, то можно ставить эти сборки поверх друг друга. Если же мы подписывали разными, то приложение поверх уже устанавливать нельзя. Такое поведение вы можете наблюдать, когда пытаетесь поверх своей release сборки установить debug.
Таким образом, если вы выбрали одинаковую подпись, то:
Есть возможность заменить все на одну общую сборку
Но придется всегда проверять миграции между GMS и HMS версиями
Мы решили выбрать этот вариант, как более гибкий. Также мы хотели для пользователей, у которых установлены оба типа мобильных сервисов, отдавать именно GMS-сборку из App Gallery, поскольку у таких пользователей GMS-сборка в нашем случае была более полноценной. Для этого мы использовали фичу App Gallery для совместных релизов нескольких APK.
Здесь нас поджидала проблема конфликтов version code. Дело в том, что App Gallery не позволяет загружать артефакты с меньшей версией, чем были загружены. Это вызывало неудобство при перевыпуске —мы несколько раз пересобирали APK только ради того, чтобы поднять version code до нужного. Более того, от version code еще зависет и приоритет выдачи GMS и HMS артефакта пользователям, и мы пытались добиться от App Gallery необходимой нам логики поставок.
Проблемы первой итерации: белый экран смерти
После множества манипуляций над version code, часть наших пользователей с Huawei, на девайсах которых есть и GMS, и HMS, получили пилотную Huawei-сборку, но поверх стоящей более новой GMS-сборки из Play Store. То есть у них произошел, по сути, даунгрейд приложения. В этом не было бы ничего страшного, если бы мы не поймали тогда огромное количество жалоб пользователей на то, что у них не запускается приложение или висит на белом экране при старте.
Оказалось, что одна из наших зависимостей (привет команде AppMetrica), которая запускается при инициализации, не предусматривала миграции на даунгрейдах. В итоге она получала ошибку и намертво вешала нам запуск приложения после прихода обновления из AppGallery на девайсах, где ранее уже была установлена более поздняя версия из Google Play. Из-за этого было очень много негативных отзывов.
Как известно, беда не приходит одна, и баг прошел через все наши заслоны. Дело в том, что проблема не была крашем, это был именно ANR, и мы его не увидели в Crashlytics. Также у нас получилось, что коммуникация с нашей техподдержкой именно в этот момент почему-то сломалась. Как итог, случилось самое страшное: мы узнали о баге из отзывов на Play Store, а потом еще и не отреагировали оперативно.
Это как раз можно видеть по тому тренду, что у нас средняя дневная оценка прямо сильно провалилась. Обычно нам пользователи ставят пятюню, а тут опускалось прямо до четверки. И причина как раз в шквале единичек, которые оставили пострадавшие пользователи Huawei.
Динамика доли HMS-only устройств
С горем пополам мы победили все трудности и отправили эту сборку в свободное плавание. Мы периодически проверяли, сколько же там пользователей HMS-only устройств. На самом деле, здесь мы совершили небольшую ошибку. Она не такая уж страшная, но довольно неприятная: мы не добавили аналитику конфигурации сервисов на устройстве — только HMS или GMS + HMS.
Таким образом, мы анализировали тренд HMS-only устройств по нашим подпискам на пуши. Мы брали слепок базы и смотрели, какой процент подписан на Huawei push. К сожалению, этот слепок базы долго не хранится, поэтому те данные я показать не могу. Однако мне очень хотелось показать вам тренд, поэтому я вам продемонстрирую другой график: сколько было пользователей у той самой версии, которую мы отправили в свободное плавание.
Можно заметить, что пользователей потихоньку становится всё больше и больше:
Почему эта статистика не до конца честная? Потому что пользователям ничего не мешало поставить сборку на любом устройстве из Huawei магазина. Но на самом деле, этот показатель очень близкий к реальному, так как обычно пользователи предпочитают более новую версию из Google Play. В таком режиме мы прожили примерно год, и, когда число пользователей на HMS only устройствах стало достигать 3%, мы поняли, что пришло время второго захода.
Моя попытка номер 2
За прошедший год над приложением для соискателей у нас работало три с половиной команды разработки. Получилось, что приложение ушло вперед уже очень далеко: у него заметно улучшился пользовательский опыт и существенно подросли целевые метрики. Это являлось достаточным обоснованием для бизнеса для запуска второй кампании по интеграции HMS.
На этот раз пришло время интегрировать HMS основательно: с архитектурой, правильно и на века. Вспоминая проблему белого экрана, мы приняли безоговорочное решение создавать единую сборку на оба магазина приложений, не делать никаких изолированных веток, не заниматься ерундой и делать сразу через архитектурное разруливание двух мобильных сервисов в runtime. Также поддерживаемых сервисов стало еще меньше чем в пилотке — мы отпилили еще и работу с картой.
Почему мы вообще решили сделать единую сборку? Во-первых, это проще в инфраструктурном плане. Не нужно отдельно собирать и тестировать сразу две сборки, и одну сборку проще поставлять в Store. Во-вторых, появляется возможность смешанно предоставлять фичи приложения для пользователей, у которых доступны как Huawei-сервисы, так и Google-сервисы. То есть, если какая-то из фичей работает лучше/стоит дешевле на каких-то сервисах, то на телефоне, где она уже есть, за ней будет приоритет использования.
Архитектура решения и ключевые моменты в коде
Мы выделили отдельный модуль platform-services:common, где мы описали абстракции над платформенными сервисами. И эти абстракции не зависят ни от GMS, ни от HMS напрямую.
В отдельном модуле platform-services: integration мы описали логику, как должны сопоставляться интерфейсы из common-модуля с их конкретными платформенными реализациями. Сами реализации под каждую платформу мы поместили тоже в отдельные модули. Получились два платформозависимых модуля GMS и HMS.
Еще есть модуль fallback с заглушечными реализациями сервисов. Он нужен на тот случай, когда на девайсе пользователя вообще, например, нет никаких мобильных сервисов. Или когда у пользователя HMS-девайс, и он в приложении встречает фичу, которая пока что доступна только для GMS-девайсов.
Рассмотрим, как это работает на примере пушей. Вот так выглядит платформонезависимый интерфейс для получения PushToken, который описан в нашем platform-services:common модуле.
interface PushTokenSource {
fun getPushTokenSingle(): Single<String>
}
Его платформенная реализация через Huawei Mobile Services.
@InjectConstructor
class HuaweiPushTokenSource(
private val context: Context,
private val schedulersProvider: SchedulersProvider,
) : PushTokenSource {
companion object {
private const val TOKEN_SCOPE = "HCM"
}
override fun getPushTokenSingle(): Single<String> {
return Single.fromCallable {
val options = AGConnectInstance.getInstance().options.getString("client/app_id")
val instanceId = HmsInstanceId.getInstance(context)
val token = instanceId.getToken(appId, TOKEN_SCOPE)
if (token.isNullOrEmpty()) throw PushTokenException("Push token is null")
token
}.subscribeOn(schedulersProvider.backgroundScheduler)
}
}
В integration-модуле живет код, который определяет доступные пользователю сервисы.
@InjectConstructor
class HuaweiPlatformServicesDetector(
private val context: Context,
) : PlatformServicesDetector {
override fun isAvailable(): Boolean {
val huaweiApiAvailability = HuaweiApiAvailability.getInstance()
val result = huaweiApiAvailability.isHuaweiMobileServicesAvailable(context)
return result == ConnectionResult.SUCCESS
}
}
Для каждой фичи есть отдельный провайдер, который имеет доступ к информации об имеющихся на девайсе сервисах, и мы можем прописать здесь любые условия для получения той или иной реализации сервисов.
@InjectConstructor
class PushTokenSourceProvider(
private val firebasePushTokenSource: FirebasePushTokenSource,
private val huaweiPushTokenSource: HuaweiPushTokenSource,
private val fallbackPushTokenSource: FallbackPushTokenSource,
private val platformServices: PlatformServices,
) : Provider<PushTokenSource> {
override fun get(): PushTokenSource {
return when (platformServices.getPreferredPlatformServices()) {
Type.GOOGLE -> firebasePushTokenSource
Type.HUAWEI -> huaweiPushTokenSource
null -> fallbackPushTokenSource
}
}
}
Ну и по месту использования мы просто инжектим общий платформонезависимый интерфейс из модуля common.
@InjectConstructor
internal class PushTokenManagerImpl(
private val pushTokenSource: PushTokenSource,
/*...*/
) : PushTokenManager {
/*...*/
private fun getTokenSingle(): Single<String> {
return pushTokenSource.getPushTokenSingle()
.doOnError {
clearPushState()
Timber.tag(LOG_TAG).e(it.asNonFatal())
}
.doOnSuccess { token ->
saveToken(token)
Timber.tag(LOG_TAG).d("Push token is $token")
}
}
}
Если мы по какой-то причине в будущем решим отказаться от единой для всех платформ сборки, то мы не будем рефакторить большую часть приложения, а лишь изменим детали реализации integration-модуля, который сопоставляет интерфейс с их реализациями и переделаем связывание в нем из runtime в compile time.
Еще надо отметить, что если у Huawei какие-то сервисы будут работать лучше, или они будут дешевле. Или будет такая ситуация, когда у Huawei эта реализация есть, а у Google ее еще нет (или вообще нет), то можем использовать для Huawei пользователей именно Huawei-реализацию. Не только для HMS only пользователей, а вообще для всех. То есть фактически такая архитектура нам открывает делать что-то уникальное для Huawei пользователей, что тоже само по себе интересно.
А что там по трудозатратам?
Разработка первой версии у нас заняла около 3,5 недель. Во второй раз мы потратили немного больше времени — около пяти недель. Вы спросите: «Почему так долго?». Хоть за первый раз мы и победили львиную долю проблем, во второй раз они тоже все равно возникали (долго пытались пофиксить сборку, воевали с Huawei-плагином).
Также мы попутно отрефачили всю работу с push-уведомлении в приложениях и интегрировали поддержку HMS в приложение для работодателей. Аналитику мы в этот раз сделали правильно и теперь можем предоставить честные результаты.
Честная динамика доли HMS-only устройств
С момента нашего релиза уже прошло больше двух месяцев и можно посмотреть статистику по установкам/обновлениям на эту версию и выше.
От месяца к месяцу наблюдается рост доли HMS-only устройств на фоне падения доли HMS+GMS устройств и сохранения доли GMS-only девайсов. Побочное интересное наблюдение — это доля HMS-only устройств Huawei: она достаточно бодро растёт.
Итоги
Считаем ли мы, что интеграция в два этапа была ошибкой? В умных книжках пишут, что такой подход называется прототип и решение воспользоваться именно им было абсолютно правильным. Если бы мы навернули на первом этапе нормальную архитектуру, то потратили бы сильно больше времени и создали слой лишних абстракций, от которых нам возможно пришлось бы потом избавляться. В то же время, мы поддержали HMS-only устройства и довольно большое число пользователей у нас не страдало от web-версии. Статистика капала, и в нужный момент мы всегда могли вернуться и закончить начатое.
Окупится ли разработка и интеграция с Huawei сейчас? На этот вопрос вам нужно ответить самим. Я постарался дать максимально полные данные, чтобы на основании их вы могли принять решение. Я показал вам текущую статистику и динамику за последний год. Тренд достаточно ровный — можете пофантазировать, что будет через месяцок-другой. Если у вас схожая аудитория (мы хорошо представлены по трудоспособному населению России), то вам достаточно поставить цифры в вашу бизнес-модель и рассчитать окупится ли человекомесяц разработки.
Стоит ли сразу делать хорошую архитектуру, если вы пойдете в интеграцию Huawei сейчас? На мой взгляд, да. Вот сейчас как раз тот момент, когда уже не стоит клепать что-то на коленке, потому что лучше сразу сделать правильно и потом уже с этим жить. Потому что проблемы, которые у вас возникнут дальше с интеграцией на коленке в отдельной ветке, они реально не стоят того, сейчас вы проиграете, потому что HMS сервисы стали реальностью.
Делать реализацию с нуля или есть готовое решение? Как видите, реализация довольно простая, но если хотите библиотеку, то рекомендую посмотреть на UniversalMobileServices от @kirich1409.
Надеюсь, наша история поможет вам понять, нужна ли интеграция HMS прямо сейчас, и подобрать правильную реализацию, избежав наших ошибок.
Маленький опрос для большого исследования
Мы проводим ежегодное исследование технобренда крупнейших IT-компаний России. Нам очень нужно знать, что вы думаете про популярных (и не очень) работодателей. Опрос займет всего 10 минут, а результаты мы по традиции опубликуем на хабре!
Расскажи, что ты думаешь здесь.
Полезные ссылки
Видеоверсия этого доклада — если предпочитаете все же смотреть видео
Наш telegram-канал — в нем новости о наших статьях, видео и конференциях
Чат с разработчиками — можно задать вопрос про модули и прочее нашим техническим специалистам
YouTube-канал hh_tech — тут все о хэхэ и вообще охэхэнно
OrlandoFurioso
Отличный пост, но позволю себе несколько замечаний.
У такого подхода, когда у вас одна сборка для обеих экосистем, есть несколько минусов, но главный из них состоит в том, что время от времени ревью-команда гугла отклоняет сборки, содержащие хуавеевские зависимости, ссылаясь на "уязвимости", "вредоносный код" и так далее. Конечно, в таких случая HMS библиотеки оперативно обновляются чтобы соответствовать представлениям гугл о безопасном коде, но некоторое количество нервов и времени это может скушать.
Поэтому я обычно советую не тащить в сборку те зависимости, которые там не нужны а пилить на флейворы. Единственная существенная издержка это то что надо тестировать и на GMS и на HMS устройствах (а как показал ваш опыт, то и на GMS+HMS тоже). Но на самом деле по-хорошему и ваш подход тоже необходимо тестировать и там и там, я даже не особо верю, что вы этого не делаете.
Ну и про карты. Вы не стали вдаваться в детали, почему не стали в итоге интегрировать хуавеевские в финальной версии, жаль, было бы интересно узнать почему. Но даже если возникли какие-то непреодолимые сложности есть ещё один подход. Можно интегрировать те карты, которое работают и на GMS и на HMS аппаратах, те же яндексовские. Аналогичный подход можно использовать и для других библиотек.
И, кстати, спасибо за тёплые слова)
Xanderblinov Автор
Спасибо, отвечу инлайн!
Все-таки хочется один артефакт — так удобнее работать, ну и можно, как и говорил в статье/видео, для пользователей HMS+GMS приоритезировать HMS для каких-либо фичей
А какие еще видите минусы / риски?
У нас только смоук делается (и то не всегда), основные проверки автоматизированы. Да, для HMS-фичей они не гоняются, так как эмуляторы Huawei не может предоставить
Я, кстати не рекомендую использовать флейворы для разных вариантов кода! При включении варианта сборки остальные убираются из индекса IDE. А еще сборки идут строго друг за другом (баг тулзов наверное). Это крайне затрудняет разработку, лучше уж сделать на модулях + два различных приложения.
Это больше продуктовая история. Пока что сделали MVP технической командой. Если продакт посчитает необходимым добавить карты в HMS сборку, то да, будем рассматривать в том числе и кросссервисные (слово то какое придумал????)варианты
Вы крутые, так держать!