Всем привет, меня зовут Ленар Садыков, и я вместе с командой развиваю и поддерживаю приложение для клиентов Lamoda на базе Android. Сегодня расскажу, как мы добавили поддержку Huawei Mobile Services и Huawei App Gallery.
В мае 2019 года Департамент торговли США внес Huawei в черный список. Вследствие Google отказался от сотрудничества с Huawei, а Huawei, в свою очередь, перестал распространять устройства с сервисами Google. В ответ на это китайский гигант представил миру Huawei Mobile Services, в том числе — магазин приложений Huawei App Gallery.
Чтобы не заставлять наших клиентов искать обходные пути с установкой приложения, мы приняли решение заявиться на новую площадку.
Причины
Причина проста — это наши пользователи. На диаграмме представлена статистика за август–сентябрь 2020 года: 27,8% пользователей используют устройства Huawei.
План
На сегодняшний день в мире существуют два типа устройств от Huawei. Первый тип — устройства, которые были выпущены до конфликта с набором сервисов Google и Huawei. Второй тип — устройства только с сервисами Huawei, которые были выпущены после конфликта.
Так как App Gallery позволяет публиковать сборку для каждого из типов, мы решили интегрировать сервисы в два этапа:
- Внести минимальные изменения для возможности опубликоваться в App Gallery.
- Поддержать все необходимые нам сервисы от Huawei.
На первом этапе мы планировали поддержать только те устройства, которые были выпущены до конфликта и имеют на борту сервисы Google, на втором этапе — все существующие устройства Huawei на базе Android.
Этап I: Минимальные изменения
Наше приложение имеет прямую связь с Google Play Store из-за двух сервисов: Dynamic Delivery и In-App Updates. Dynamic Delivery помогает докачать часть приложения, в нашем случае это виртуальная примерочная для кроссовок. In-App Updates позволяет приложению обновляться в фоновом режиме.
Следовательно, приложение, установленное из App Gallery, должно использовать аналогичные сервисы из Huawei Mobile Services. Аналог Dynamic Delivery для Huawei — Dynamic Ability. Но у Huawei нет конкретного сервиса, который мог бы заменить In-App Updates, однако есть похожая функциональность.
Так как пользователь может установить наше приложение на одно устройство с разных сторов, нужно было научиться определять, откуда же оно было скачано. На первом этапе мы решили использовать package name стора. В Android его можно вытащить через метод getInstallerPackageName у PackageManager. Для Google Play это будет значение com.android.vending, для App Gallery — com.huawei.appmarket.
Dynamic Ability
Мы добавили сервис Dynamic Ability в приложение в соответствии с документацией (также есть кодлаба). Реализация очень похожа на гугловский Dynamic Delivery. Процесс скачивания динамического модуля построен на основе статусов — в данном случае отображается диалоговое окно прогресса скачивания и сообщение о том, что оно завершено.
Но отладка обернулась настоящей болью: аналога internal testing нет, поэтому пришлось тестировать через open testing, у которого есть свое ревью, хоть и ускоренное. Из-за этого сборка может появиться в сторе через час или через сутки.
Из сложностей реализации можно выделить тот момент, что динамический модуль никак не хотел скачиваться. Проблема была в его манифесте. Для Google Dynamic Delivery у нас было прописано так:
<dist:module>
<!-- ... -->
<dist:delivery>
<dist:on-demand/>
</dist:delivery>
<!-- ... -->
</dist:module>
Huawei Dynamic Ability заработал после того, как написали такой код:
<dist:module dist: onDemand=”true”>
<!-- ... -->
</dist:module>
После того как мы починили скачивание, у нас нашлась новая поломка: модуль скачивался, но прогресс скачивания не отображался. Оказалось, проблема была в статусах. В нашем случае порядок статусов Google Dynamic Delivery такой:
- PENDING,
- DOWNLOADING,
- INSTALLED.
Статусы для Huawei Dynamic Ability:
- DOWNLOADING,
- DOWNLOADED,
- INSTALLED.
Альтернатива Google In-App Updates
Реализация In-App Updates от Huawei выглядит очень просто: есть возможность открыть только один вид диалогового окна. Процесс скачивания не реализован — происходит обычный редирект на страницу приложения в App Gallery. Так как диалоговое окно можно всегда закрыть, данную функциональность без доработок можно использовать только для soft update. Для force update мы добавили дополнительный блокирующий экран.
Soft update от Google и Альтернатива от Huawei
Adjust
Также на первый этап мы вынесли обновление Adjust — это система аналитики, которая отслеживает действия пользователя перед установкой приложения. Например, вы видите рекламу нашего приложения в Facebook. Кликаете на нее, переходите в Google Play, скачиваете приложение. В этом случае с помощью Adjust мы поймем, что пользователь установил приложение из Google Play, перейдя из рекламы в Facebook.
Хоть Adjust и отчитался о поддержке App Gallery, этого оказалось недостаточно: мы всегда получали данные, что приложение установлено из Google Play. Чтобы разделять данные от Adjust по сторам, мы добавили в versionName постфикс -huawei. Например, если пользователь скачал наше приложение с Google Play, он видит версию 3.77.0, а если из App Gallery, то 3.77.0-huawei. Благодаря разным versionName, наши аналитики могут разделять установки по сторам. При этом versionCode остался одинаковым, что позволяет корректно обновиться из любого стора.
Этап II: Полная интеграция
Перед началом второго этапа мы решили пересмотреть логику определения, из какого стора было скачано наше приложение. Мы видели два существенных минуса:
- Ветвление в коде.
- Зависимости, которые никогда не будут использоваться. Для Google Play — это зависимости от Huawei-сервисов, а для App Gallery — зависимости от Google-сервисов.
Решение — productFlavor. Первая проблема решается определением флейворов: Android Studio сам производит ветвление. А вторая — с помощью метода подключения зависимостей. Например, Google-сервисы подключаем с помощью googleImplementation, а Huawei-сервисы — с помощью huaweiImplementation.
Но у флейворов есть и минус: индексируется только тот код, который связан с флейвором, включенным в данный момент. Простой пример: у нас есть метод с одинаковым названием в обоих флейворах, но при переименовании метода в одном из них, в другом он останется со старым названием. В будущем это приведет к ошибкам сборки на других флейворах.
Мы решили сгладить эту проблему с помощью отдельного модуля mobile-services. В большинстве случаев разработчику не так важно, какие сервисы он использует — ему достаточно обращаться к сущностям из mobile-services.
Например, есть основной модуль приложения app и есть модуль с мобильными сервисами mobile-services. В нашем случае, в модуле app мог бы быть такой код:
class MapFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...
val map = Map()
map.draw()
//..
}
}
Абстракция над картами Map находилась бы в модуле mobile-services без привязки к флейворам:
class Map() {
fun draw() {
drawFlavorMap()
}
}
А вот сама реализация отрисовки карт находилась бы в модуле mobile-services под соответствующим флейвором.
Для флейвора google:
fun drawFlavorMap() {
val map = GoogleMap()
map.drawMap()
}
Для флейвора huawei:
fun drawFlavorMap() {
val map = HuaweiMap()
map.drawMap()
}
Интеграция оставшихся сервисов
После внедрения флейворов и отдельного модуля для сервисов мы интегрировали остальные необходимые сервисы Huawei: Map Kit, Analytics Kit, Push Kit, Location Kit, Crash Service. В основном, все предсказуемо и делается по документации. Выделю пару важных моментов.
Карты от Huawei
Карты от Huawei вполне подходят в качестве замены карт от Google. Однако у них есть два недостатка:
- Нет возможности указать цвет кластера (точки, которые объединяют в себе несколько других точек). Поэтому на карте отображаются кластеры разных цветов.
- Иногда неправильно указывается количество точек в кластере.
Мы спрашивали, как можно решить эти проблемы у разработчиков Huawei, но не добились какой-то конкретики. В целом, карты справляются со своей задачей, пусть и не настолько идеально, как привычные Google-карты.
Работа Google-карт и Huawei-карт
Пуши от Huawei
На дебажной сборке у нас не заработали пуши, потому что в файле agconnect-services.json был указан продовый package name нашего приложения. Проблема решилась после того, как мы создали в App Gallery Connect новое приложение с нужным package name.
После этого agconnect-services.json стал содержать такие строки:
"appInfos":[
{
"package_name":"com.lamoda.lite.test",
"app_id":"123456789"
},
{
"package_name":"com.lamoda.lite",
"app_id":"9876"
}
]
Как мы живем с двумя сторами
CI/CD
Мы используем Bamboo и gradle-таски. Небольшая сложность возникла только в тот момент, когда мы переходили на флейворы — тогда из-за изменения названий gradle-тасок сломался наш CI. Например, bundleRelease разделился на bundleHuaweiRelease и bundleGoogleRelease.
В остальном все хорошо: некоторые Bamboo-планы продублировались из-за флейворов, некоторые так же, как и раньше, работают только с Google-сборкой. Кстати, для публикации в App Gallery мы используем вот этот плагин.
Разработка
Мы редко переключаемся на Huawei-флейвор, благодаря нашему модулю mobileservices и тонким абстракциям внутри него. Crash rate сборки из App Gallery также стабилен — за полгода мы еще не сталкивались с крашем, который был бы связан только с Huawei Mobile Services.
QA
У тестировщиков прибавилось работы: теперь на релизе они тестируют дополнительную сборку, предназначенную для App Gallery.
Итог
Все работает так, как и предполагалось: за полгода зарегистрировано больше 2 миллионов установок из App Gallery. Также мы видим, что пользователи Huawei из Google Play Store «перетекают» в Huawei App Gallery, а это значит, что интеграция прошла успешно, и мы смогли подстроиться под наших пользователей.