Выходим на рынок Huawei, или Как мы адаптировали приложение для работы с HMS / Хабр

xxtkfmhiat cewgbbfs Новости

Почему стоит обратить внимание на экосистему huawei

Смартфоны Huawei очень популярны: в 2020 году в России они занимали почти 18% рынка (Рис.1), а в мире — 11% (Рис.2), (

). Huawei заявила, что более 490 млн человек в более чем в 170 странах мира пользуются AppGallery (

). Поскольку аудитория у Huawei-устройств огромная, мы не можем это игнорировать и решили поддержать пользователей нашего приложения. Далее поэтапно рассмотрим, что же нужно сделать.

Выходим на рынок Huawei, или Как мы адаптировали приложение для работы с HMS / Хабр
Рис.1Выходим на рынок Huawei, или Как мы адаптировали приложение для работы с HMS / Хабр
Рис.2

24 января 2021

Темы — важнейший элемент настройки смартфона «под себя». Ведь далеко не всегда чувство прекрасного у производителя и у пользователя совпадает. Тогда хочется что-то поменять. Кому-то нравятся темные тона вместо светлого по умолчанию, кого-то раздражают обводки вокруг значков. Рассмотрим, что сегодня предлагают лидеры рынка: Samsung, Huawei и Xiaomi.

Huawei

Набор стоковых тем зависит от аппарата. Так, например, на P10 lite он не поражает воображение.

И онлайн-магазина нет. Возможно, его получится включить, отредактировав системные файлы после получения root, но это явно рецепт не для всех. Зато есть простой механизм установки тем, скачанных из сети. Скачиваем файлы *.hwt и сохраняем их в папку HWThemes.

Polyutil. расшифровка с помощью polyline


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

После расшифровки мы получили список координат курьера.

Samsung

Тема «По умолчанию» Samsung Galaxy S7 мне тоже не понравилась.

Но аппарат содержит магазин тем «из коробки». Можно выбрать бесплатную или платную тему.

Причем, в отличие от Xiaomi, для оплаты темы не надо продираться через иероглифы. Я выбрал темную тему в стиле стандартного Android и поменял обои (также выбираются в стоковом приложении Samsung Themes).

Xiaomi

Тема MIUI по умолчанию как раз и содержит обводки значков:

Но смартфон еще при начальной настройке предлагает выбрать одну из двух (по крайней мере, на Mi Max 2 их именно две) предустановленных тем. Можно это сделать и позднее:

Тема «Безграничнос» (именно так, без «ть» на конце) не рисует обводки для значков:

А после входа в Mi-аккаунт становится доступен полноценный онлайн-каталог тем.

Среди них огромное количество бесплатных, но есть и платные. Правда, чтобы купить тему, придется продраться сквозь перипетии китайского интерфейса.

Можно найти темы на любой вкус. Есть имитация стокового Android или Grace UI от Samsung:

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

Проблема: «карта не работает»

Выходим на рынок Huawei, или Как мы адаптировали приложение для работы с HMS / Хабр

Однажды нам сообщили о баге. Пользователь с устройством Huawei, находившийся в центре Москвы (Рис.3), открыл приложение, нажал на кнопку «Переместиться на своё местоположение», и его перенесло в пустоту (Рис.4). Пользователь не видит, ни улиц, ни зданий, и он решил, что карта не работает.

Мы попробовали воспроизвести у себя эту проблему. И действительно попадали в неопределённое пространство. Когда попробовали чуть-чуть уменьшить масштаб карты, то оказалось, что мы попали в пригород Мариуполя (Рис.5). То есть из московских координат (55.819207, 37.493424) перенеслись в мариупольские (47.187447, 37.593137).

Мы были в полном недоумении. Может быть, где-то у нас с числами что-то не то происходит. Возможно, происходят некие вычитания наших координат. Очень долго искали решение этой проблемы или хотя бы причину. Оказалось, что мы заменили импорты из Google-карт, и поэтому всё перестало работать. В конце концов мы добрались до padding’а.

Давайте быстро вспомним, что такое padding у карты. На (Рис.6) показан экран авторизации, карта занимает всю область экрана, даже под плашкой ручного ввода адреса. В таком случае, если мы не добавим padding карте, её центр будет находиться на месте зелёного треугольника, но мы хотим, чтобы он был в центре рабочей области карты.

Padding сужает рабочую область (Рис.7). Не видимую, а именно рабочую. Карта будет по-прежнему занимать весь экран, но размер её рабочей области изменится. И когда вы будете переходить в новую координату, она будет принимать положение новой рабочей карты. Как оказалось, баг был именно из-за этого.

Первое решение: убрать padding. Как вы понимаете, такой вариант нам не подошёл. Мы хотели, чтобы всё отображалось красиво.

Второе решение проблемы: использовать анимированное перемещение, но с масштабированием.

val zoom = map.cameraPosition.zoom
map.animateCamera(CameraUpdateFactory.newLatLngZoom(position, zoom))

При переходе с изменением масштаба карты всё работало правильно. Здорово! Мы подумали, что это нам подходит. На самом деле нет. У нас ещё есть третий экран, на котором нужно увеличивать карту относительно двух маркеров, чтобы

zoom

сам рассчитывался, поэтому мы не можем задать какое-то константное масштабирование. То есть такой вариант нам тоже не подошёл. Начали думать дальше и нашли новое решение.

Третье решение проблемы: вообще отказаться от анимации. Как оказалось, если вместо animateCamera сделать просто move, то перемещение будет происходить правильно. Так мы и сделали. Надеемся, в скором времени Huawei устранит эту проблему.

Реализация поддержки двух карт

Для поддержки нескольких карт необходимо создать обёртку для самих карт и для объекта.

Добавляем общий интерфейс, например, IMapWidget. Не забываем сделать общий класс для LatLng — список координат курьера. У Google он лежит в пакете com.google.android.gms.maps.model.LatLng, а у Huawei в com.huawei.hms.maps.model.LatLng. Кладём список в PolyLineOptions и задаём ширину и цвет линии маршрута.

interface IMapWidget {

    void animateCamera(...);

    void setListener(OnMapEventListener listener);

    void setMapPadding(...);

    MapMarker addMarker(...);

    ...
}


Добавляем Custom Map View реализующего интерфейс

IMapWidget

Добавляем обёртку, которая позволяет нам указать, где мы хотим отрисовывать карту:

class MapWrapper : FrameLayout() {

    fun setupMap(widget: IMapWidget) {
        removeAllViews()
        addView(widget as View)
    }
}

И в нужном месте вызываем метод добавления карты.

override fun onCreateView(...) {
    ...
    val map: IMapWidget = MapFactory.createMap()
    viewMapWrapper.setupMap(map)
    ...
}

Такие обёртки класса нужно создать для всего: объектов, маркеров,

PolyUtilPolyLine

и т.д.

Этап 1: проверка наличия services


Если у вас в приложении при входе есть проверка наличия Google Services, то придётся от этого отказаться, и проверять наличие соответствующих сервисов только по мере необходимости.

Этап 2: карты

В приложении Delivery Club три основные страницы:

На устройствах Huawei все эти карты не работают. Чтобы это исправить, можно просто заменить зависимости: вместо пакета

com.google.android.gms

использовать

com.huawei.hms

Конечно, есть нюансы, но мы уже сделали большу́ю часть работы. Huawei сделала Maps SDK с контрактами, по большей части соответствующий Google Maps SDK. Однако у Google есть deprecated-методы, если вы их используете, то аналогов у Huawei может и не найтись. Например, для получения местоположения пользователя мы используем:

LocationServices.FusedLocationApi.getLastLocation(googleApiClien)

Такой подход считается deprecated, и если мы просто скопируем код для Huawei Maps и заменим зависимости, то работать не будет. Нужно поменять так:

LocationServices.getFusedLocationProviderClient(…)
.getLastLocation()
.addOnSuccessListener(…)

Этап 3: push-сервис

Идём дальше. На Huawei-устройства не приходят уведомления нашего приложения. Дело в том, что мы не можем получить токен. Давайте его получим. В Google мы получаем задачу и извлекаем токены так:

FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
    if (task.isSuccessful) {
        val token = task.result
    }
}

Наше решение:

class ImplementationHuaweiMessagingService : HmsMessageService() {

    override fun onNewToken(token: String?) {
        val commonApi = getComponentFactory().get(CommonApi::class.java)
        commonApi.settingsManager().setPushToken(token)
    }

    override fun onMessageReceived(message: RemoteMessage?) {
        message?.let {
            val appManagersComponent = getComponentFactory().get(AppManagersApi::class.java)
            appManagersComponent.pushManager().handle(it.dataOfMap)
        }
    }


Выглядит всё так же, как и с реализацией

FirebaseMessagingService()

, даже есть

callbackonNewTokenonMessageReceived

. Однако без нюансов не обойтись. Случается, что на некоторых редких устройствах

onMessageReceived

вызывается в главном потоке, поэтому лучше не использовать здесь долго выполняющиеся задачи.

Получаем токены на Huawei:

val token = HmsInstanceId.getInstance(context)
    .getToken(appId, com.huawei.hms.push.HmsMessaging.DEFAULT_TOKEN_SCOPE)



public static final String DEFAULT_TOKEN_SCOPE = "HCM";

Обратите внимание, что метод выполняется в главном потоке. И для получения токена нужно отдельно реализовать поток. У Google такой подход уже считается устаревшим, возможно, Huawei придёт к тому же.

Мы можем вообще не использовать getToken, а прописать в манифесте автоматическую инициализацию или в коде методом setAutoInitEnabled() и всегда получать token в onNewToken (подробнее). Это решит ещё одну проблему: getToken в версиях EMUI ниже 10 вообще возвращает null.

Этап 4: chrome custom tabs


Наше приложение при запуске регулярно вылетает с ошибкой

ActivityNotFoundException

. Чтобы от этого избавиться, нужно обработать отсутствие Chrome Tabs.

fun Context.openLink(url: String, customTabsSession: CustomTabsSession? = null): Boolean {
    try {
        openLinkInCustomTab(url, customTabsSession)
        return true
    } catch (throwable: Throwable) {
        Timber.tag("Context::openLink").e(throwable, "CustomTabsIntent error on url: $url")
    }

    return openLinkInBrowser(url)
}

@Throws(Throwable::class)
fun Context.openLinkInCustomTab(url: String, customTabsSession: CustomTabsSession? = null) {
    CustomTabsIntent.Builder(customTabsSession)
        .build()
        .launchUrl(this, Uri.parse(url))
}

private fun Context.openLinkInBrowser(url: String): Boolean {
    val intent: Intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
        addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
    }

    if (intent.resolveActivity(packageManager) != null) {
        startActivity(intent)
        return true
    }

    return false
}

Мы просто обернули

openLinkInCustomTab()try catch

и в случае ошибки пытаемся открыть в браузере. Но бывает такого, чтобы на устройстве не было подходящего браузера, способного обработать наш неявный

intent

. Поэтому если метод

openLinkInBrowser()

возвращает

false

, мы открываем страницу в

webview

Этап 5: аналитика


Аналитика у Huawei похожа на Google Analytics. Покажу замену на примере Firebase. Сначала инициализируем:

HiAnalytics.getInstance(context)

. Затем с помощью

HAEventType.STARTCHECKOUT

копируем все наши события из Firebase в отдельный файл

huaweiAnalytics

huaweiAnalytics.onEvent(name, bundle)

Системные параметры:

HAParamType.PRICEHAParamType.CURRNAME

Даже если у вас нет Firebase, добавить аналитику в Huawei очень просто. У них отличная документация, контракт соблюдается. Также у Huawei есть отличные инструменты для исследования аудитории.

Этап 6: crashlytics

Следующий инструмент, который нам тоже стало интересно попробовать, это Crashlytics от Huawei, которая называется

AGConnectCrash

. Она позволяет с минимальными усилиями собирать и анализировать информацию о падении приложения.

Инициализируем crashlytics:

AGConnectCrash.getInstance().enableCrashCollection(true)

Добавляем свои ключи и журналируем нужные события:

Этап 7: покупки в приложении

Если в вашем приложении есть встроенные покупки, вы должны подписать согласие на передачу персональных данных, и отослать письмом в конверте или через курьера в московский офис Huawei. И только через пару дней они вам дадут доступ для реализации этой функциональности.

Всё очень похоже на реализацию Google. При запуске приложения запрашиваем все прошлые покупки пользователя:

fun getOwnedPurchases(
    activity: Activity,
    ownedPurchasesResultOnSuccessListener: OnSuccessListener<OwnedPurchasesResult>,
    failureListener: OnFailureListener
) {
    val ownedPurchasesReq = OwnedPurchasesReq()

    // priceType: 0: consumable; 1: non-consumable; 2: auto-renewable subscription
    ownedPurchasesReq.priceType = IapClient.PriceType.IN_APP_SUBSCRIPTION

    // To get the Activity instance that calls this API.
    val task: Task<OwnedPurchasesResult> = Iap.getIapClient(activity)
        .obtainOwnedPurchases(ownedPurchasesReq)

    task.addOnSuccessListener(ownedPurchasesResultOnSuccessListener)
        .addOnFailureListener(failureListener)
}

Если какой-то товар был куплен, мы разблокируем его функциональность. Потом запрашиваем подробности по товарам, доступным для продажи — цену и описание:

fun loadProduct(
    context: Context,
    productInfoResultOnSuccessListener: OnSuccessListener<ProductInfoResult>,
    onFailureListener: OnFailureListener
) {
    // obtain in-app product details configured in AppGallery Connect, and then show the products
    val iapClient: IapClient = Iap.getIapClient(context)
    val task: Task<ProductInfoResult> = iapClient.obtainProductInfo(createProductInfoReq())
    task.addOnSuccessListener(productInfoResultOnSuccessListener)
        .addOnFailureListener(onFailureListener)
}

private fun createProductInfoReq(): ProductInfoReq {
    val req = ProductInfoReq()
    // 0: consumable ; 1: non-consumable ; 2: auto-renewable subscription
    req.priceType = IapClient.PriceType.IN_APP_SUBSCRIPTION

    val productIds = ArrayList<String>()
    productIds.add("PRODUCT_ID")
    req.productIds = productIds
    return req
}

Когда пользователь кликает на товар, мы открываем страницу с оплатой. Она не такая красивая, как у Google, и не выезжает снизу.

fun gotoPay(activity: Activity, productId: String, type: Int) {
    val client: IapClient = Iap.getIapClient(activity)
    val task: Task<PurchaseIntentResult> = client.createPurchaseIntent(createPurchaseIntentReq(type, productId))
    task.addOnSuccessListener { result ->
        result?.let {
            val status: Status = result.status
            if (status.hasResolution()) {
                try {
                    status.startResolutionForResult(activity, PAY_RESULT_ARG)
                } catch (exception: SendIntentException) {
                    Timber.e(exception)
                }
            } else {
                Timber.d("intent is null")
            }
        }
    }.addOnFailureListener { exception ->
        Timber.e(exception)
    }
}


Так как это Activity, мы передаём ему аргумент, по которому можно отловить

OnActivityResult

и понять, успешно ли прошла оплата и как закончилась транзакция:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (resultCode == PAY_RESULT_ARG) {
        val purchaseResultInfo: PurchaseResultInfo = Iap.getIapClient(this).parsePurchaseResultInfoFromIntent(data)
        when (purchaseResultInfo.returnCode) {
            OrderStatusCode.ORDER_STATE_SUCCESS -> {
                successResult(purchaseResultInfo)
            }

            OrderStatusCode.ORDER_STATE_CANCEL -> {

            }

            OrderStatusCode.ORDER_PRODUCT_OWNED -> {

            }
        }
    }
}

У нас есть специальные статусы:

ORDER_SUCCESSCANCELOWNED

. Первый означает успешную оплату. Второй — пользователь просто закрыл страницу без покупки, тогда мы обрабатываем этот callback и предлагаем скидку, чтобы уговорить на покупку. А третий статус означает, что товар уже куплен пользователем. Если товар разовый или подписочный, то на этом моменте нужно остановиться, в противном случае виртуально доставить покупку.

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

private fun successResult(purchaseResultInfo: PurchaseResultInfo) {
    val inAppPurchaseData = InAppPurchaseData(purchaseResultInfo.inAppPurchaseData)
    val req = ConsumeOwnedPurchaseReq()
    req.purchaseToken = inAppPurchaseData.purchaseToken

    val client: IapClient = Iap.getIapClient(this)
    val task: Task<ConsumeOwnedPurchaseResult> =
        client.consumeOwnedPurchase(req)

    task.addOnSuccessListener {
        // Consume success
    }.addOnFailureListener { exception ->
        Timber.e(exception)
    }
}

Если не сделать доставку, то функциональность товара будет у пользователя заблокирована, а деньги возвращены. В Google Play Billing Library до третьей версии этого делать не нужно было, но потом Google тоже это добавил, и если мы не доставим товар, через 48 часов покупка отменится, а деньги вернутся пользователю. То есть в Huawei покупки реализованы как в третьей версии Google Play Billing.

Выводы

На реализацию поддержки Huawei-устройств не уйдёт много времени. Даже без реальных устройств вы сможете проверить работоспособность вашего приложение: у Huawei есть своя тестовая лаборатория с виртуальными устройствами наподобие

. Количество пользователей быстро растёт, и бизнесу может оказаться выгодным вложиться в доработку продуктов, а отличная документация поможет разработчикам всё сделать быстро. Поддержка HMS активно отвечает на любые вопросы, если вы не сможете в документации что-то найти.

Видеозапись доклада с конференции Mobius 2020.

Заключение

Три компании, занимающие ведущие позиции на рынке (как мировом, так и в России), показывают три различных подхода к использованию тем. Samsung предоставляет магазин тем из коробки. Huawei дает возможность легко устанавливать скачиваемые темы, но онлайн-каталог несколько странен (запускается только при наличии SIM-карты и темы однообразны).

Xiaomi дает максимальное количество возможностей: есть магазин тем, можно установить (пусть даже сложнее, чем в Huawei — с применением сторонних инструментов) скачанную с форума тему; и при этом возможности кастомизации максимальны, например, можно отдельно заменить шрифт.

Оцените статью
Huawei Devices
Добавить комментарий