Как адаптировать Android-приложение под Huawei
Всем привет! Меня зовут Миша Вассер, я Head of Android в AGIMA. Мы занимаемся разработкой Digital-продуктов для больших и маленьких компаний, в том числе пилим мобильные приложения.
Не так давно — по сравнению со всей историей Android — Huawei выкатил собственную операционную систему и сказал: «Ребята, вот вам новая система, кайфуйте». Многие отнеслись к новой ОС скептически. Остальным пришлось адаптировать под нее свои Android-приложения.
Мы оказались во второй группе. К нам время от времени обращаются с просьбой помочь с адаптацией под Huawei. И мы неплохо в этом вопросе прокачались. Поэтому сейчас расскажу, что надо сделать, чтобы стало хорошо. А покажу всё это на примере крупного ретейлера, с которым мы работаем.
Что сделать, чтобы было хорошо
Зависимости build.gradle.
Чтобы начать работать с библиотеками Huawei, первым делом добавим нужные зависимости в наш Gradle (модуля app):
dependencies {
implementation 'com.huawei.hms:push:6.3.0.304'
implementation 'com.huawei.hms:maps:6.4.1.300'
implementation 'com.huawei.hms:location:6.4.0.300'
implementation 'com.huawei.hms:hianalytics:6.4.1.302'
implementation 'com.huawei.agconnect:agconnect-crash:1.6.5.300'
implementation 'com.huawei.agconnect:agconnect-remoteconfig:1.6.5.300'
}
В самый верх файла добавим:
apply plugin: 'com.huawei.agconnect'
А теперь добавим в Gradle-файл проекта еще такую зависимость:
dependencies {
classpath 'com.huawei.agconnect:agcp:1.6.0.300'
}
Добавление agconnect-services.json.
В Android с Google-сервисами мы используем файл из Firebase, который называется google-services.json. В Huawei нам потребуется такой же конфигурационный файл. Без него не получится использовать фичи, которые предоставляет вендор (например, push-уведомления, аналитика и т. д.).
Чтобы добавить этот файл, заходим в AppGalleryConnect. Затем в «Мои проекты», выбираем нужный проект, идем в «Настройки проекта» в левом боковом меню. Листаем до раздела «Данные приложения».
Теперь скачиваем файл agconnect-services и кладем его в папку App нашего проекта в Android Studio.
Правила Proguard.
Чтобы не заобфусцировать ничего лишнего, попросим Proguard не трогать Huawei-библиотеки. Для этого добавим в proguard-rules.pro строки из документации для разработчиков:
-ignorewarnings
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
-keep class com.huawei.hianalytics.**{*;}
-keep class com.huawei.updatesdk.**{*;}
-keep class com.huawei.hms.**{*;}
Проверим, доступны ли Google-сервисы.
Важная часть работы с Huawei — это проверка, точно ли у юзера на устройстве нет Google-сервисов. Полезно оставлять в коде возможность запустить Android-версию приложения. Причина банальна: APK из AppGallery кто-то может запостить в другое место, и юзеры скачают его на свои нехуавейные смартфоны.
Чтобы проверить, делаем вот такую Extension-функцию для контекста:
fun Context.areGoogleServicesAvailable(): Boolean {
val availability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this)
return availability == com.google.android.gms.common.ConnectionResult.SUCCESS
}
Теперь можно вызывать ее откуда угодно и строить логику платформозависимых функций, опираясь на неё.
Определяем местоположение юзера.
Часто в приложении нужно определять местоположение юзера, чтобы, например, показать ближайший магазин. Для этого мы пользуемся FusedLocationClient. В Huawei больших изменений нет, надо просто импортировать HMS клиента вот так:
import com.huawei.hms.location.FusedLocationProviderClient
Тогда код получения Location клиента у нас получится вот таким:
if (context.areGoogleServicesAvailable()) {
GoogleFusedLocation(
com.google.android.gms.location.LocationServices.getFusedLocationProviderClient(context)
)
} else {
HuaweiFusedLocation(
com.huawei.hms.location.LocationServices.getFusedLocationProviderClient(context)
)
}
В самих классах FusedLocation для обеих платформ все стандартно — локацию юзера получаем, вешая слушатели на Success и Failure:
providerClient.lastLocation
.addOnSuccessListener{ //Получили location }
.addOnFailureListener{ //Что-то упало }
Меняем все Google Play на AppGallery.
Почти на всех проектах пользователей нужно перевести на страницу приложения в Google Play. Так как у Huawei свой стор AppStoreConnect, пользователя лучше вести туда. Для этого меняем URI и Package в интенте.
Вот как мы открываем магазин в Android:
val uri = Uri.parse("market://details?id=" + applicationId)
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = uri
setPackage("com.android.vending")
}
А вот как открыть его в Huawei:
val uri = Uri.parse("appmarket://details?id=" + applicationId)
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = uri
setPackage("com.huawei.appmarket")
}
Дальше, уже используя написанный нами ранее Checker, можем вести пользователя в тот или иной магазин
Меняем number на phone во всех Layout с android: inputType.
Один из хаков, который вам пригодится.
Если в Android у EditText мы проставляем android: inputtype=«number» — например, на экране ввода кода из смс при авторизации, то пользователь Huawei всё равно увидит клавиатуру QWERTY, а не клавиатуру с цифрами, как в Android. Чтобы это пофиксить, просто поменяйте inputType на «phone».
Добавление SHA-256-ключа
Чтобы полноценно использовать SDK Huawei, нужно добавить отпечаток сертификата SHA-256 в консоль AppGalleryConnect. Если вы добавляете его в первый раз, это можно сделать по инструкции.
А я тем временем расскажу, что делать, если приложение пришло на доработку после того, как кто-то его релизил и уже прописывал свои ключи в консоли.
Первым делом нужно узнать ваш SHA-256, который зашит в APK-шках, которые вы собираете у себя на компьютере или CI. Можно сделать несколькими способами, вот один из них:
собираем APK;
копируем путь до вашего APK;
идем в консоль и вводим следующее (не забудьте вставить свой путь):
keytool -printcert -jarfile .../app-debug.apk
Копируем ключ SHA-256. Он выглядит примерно так: 00: F3:61: A7: AD:6B:13:11:27:02:09:8C: F5:12: FF…… Затем идем в AppGalleryConnect.
Теперь нужно зайти в «Мои проекты», выбрать нужный проект, открыть в левом боковом меню «Настройки проекта» и пролистать до раздела «Данные приложения».
4. Тут нажимаем «Добавить» и вводим наш полученный ранее ключ SHA-256. Теперь можем скачать файл agconnect-services.json — и всё.
А если не добавить ваш ключ в консоль? Не будут доходить пуши, не сработают In-App-Purchases, AppLinking и другие фишки AppGalleryConnect. Да и в целом нельзя будет залить приложение в стор. Поэтому не пропускайте и не игнорируйте этот момент.
Universal APK
При загрузке APK на некоторые Huawei-девайсы, в том числе и на эмулятор внутри AppGalleryConnect, важно сделать universal-сборку для всех типов процессоров. Иначе мы можем получить ошибку вроде This app is no longer compatible with your device.
Чтобы решить эту проблему, соберем универсальный APK. Добавим в Gradle (модуля app) следующие строчки:
android {
......
......
splits {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'mips', 'mips64', 'arm64-v8a'
universalApk true
}
}
}
Теперь каждый раз при сборке APK у нас в папке build/outputs будут лежать несколько APK-файлов. Нам нужен тот, что называется app-universal-debug.apk. Смело загружайте его в эмуляторы AppGalleryConnect и отдавайте тестерам.
Push Kit и тестирование пушей
Отправка и удаление Push-токена
Зачастую крупные продукты рассылают пуши через Backend. Если вы не используете разные SDK (OneSignal, например), вам, скорее всего, нужно отдавать на свой бэк пуш-токен пользователя. Рассмотрим, как токен получить и как удалить.
В Android мы получаем пуш-токен Firebase как-то так:
suspend fun getTokenAndSendGoogle() {
val token = FirebaseMessaging.getInstance().token.await()
sendTokenToServer(token)
}
Huawei же тут решил немного намудрить. Когда-то пуш-токен в Huawei можно было получить синхронным запросом, но сейчас такое не прокатывает. В итоге токен надо получать в отдельном потоке. Делается это вот так:
private fun getTokenAndSendHuawei() {
HmsInstanceId.getInstance(context).run {
//Получаем id AppGallery приложения из конфига
val appId = AGConnectServicesConfig.fromContext(context).getString("client/app_id")
object : Thread() {
override fun run() {
try {
//Получаем HMS пуш-токен по id, который достали ранее
val pushToken = getToken(appId, HmsMessaging.DEFAULT_TOKEN_SCOPE)
if (!pushToken.isNullOrBlank()) {
//Отправляем токен на ваш сервак
sendTokenToServer(pushToken)
}
} catch (e: ApiException) {
}
}
}.start()
}
}
Используя проверку из предыдущего раздела, получаем такое условие:
supervisorScope {
launch {
if (getGMSAvailable()) {
getTokenAndSendGoogle()
} else {
getTokenAndSendHuawei()
}
}
}
С удалением токена всё так же просто. Например, при разлогине пользователя из приложения вам надо удалить токен. Получаем токен из HMS, удаляем с вашего сервера. В идеале надо провести еще одну операцию — удалить инстанс не только у вас, а ещё и из Firebase/HMS. На девайсах с гугл-сервисами делаем так:
FirebaseInstallations.getInstance().delete()
В HMS тоже никаких сложностей:
HmsInstanceId.getInstance(context).deleteAAID()
Тестирование пушей внутри эмулятора App Gallery Console
Ребята из Huawei сделали очень крутую тему. Они добавили эмулятор прямо в свою консоль. Объясняю, как протестить пуши с его помощью.
Чтобы попасть в эмулятор, нужно зайти в «Мои проекты», выбрать нужный проект. Затем в левом меню в разделе «Качество» выбрать «Облачная отладка».
Нажимаем и видим страницу с моделями доступных девайсов. Можно навести на каждый и либо начать отладку, если девайс свободен, либо зарезервировать симулятор на какой-то определенный тайм-слот.
Когда мы запустим эмулятор, увидим вот такой экран:
Справа видим Logcat, список доступных APK и прочие штуки, помогающие нам в отладке.
Устанавливаем приложение на симулятор, получаем Push-токен. Можем вывести получение токена из предыдущего раздела в логи и скопировать его.
Теперь идем в раздел Push Kit, он находится во вкладке «Рост» в левом боковом меню. Попав на страницу Push Kit, можем нажать на кнопку «Добавить уведомление».
Заполняем стандартные поля «Имя», «Заголовок», «Тело» и другие. Когда это готово, скроллим ниже, находим блок «Диапазон отправки». Тут нужно выбрать «Указанное устройство». Появится еще одно поле — «Токен устройства». Вставим туда токен, который мы скопировали пару пунктов назад.
Когда вы заполнили все поля, выбрали время отправки и указали пуш-токен, можно нажимать кнопку «Отправить».
Вернувшись в эмулятор, мы увидим пуш — вуаля! Таким образом вы сможете тестировать отображение пушей, диплинки и всё, что вам нужно в пушах.
Карты
Во многих приложениях есть географические карты. Большинство из них нуждаются в минимальном функционале — добавление маркеров, их удаление и нажатие. Если вы используете Яндекс.Карты или другую SDK, которая не гугл-сервис, скорее всего, вам ничего не придется адаптировать.
В этой части статьи постараюсь рассказать, что вам надо поменять, чтобы базовый функционал легко завелся.
Huawei снова сделал всё максимально удобно для нас. С точки зрения разработки, карты очень похожи друг на друга. Убрав лишнюю шелуху с кода, получаем следующее:
Начнем с XML. Добавляем карту в наш Layout.
Google:
Huawei:
Перейдем в наше Activity или Fragment. Затаскиваем импорты:
Google:
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
Huawei:
import com.huawei.hms.maps.CameraUpdateFactory
import com.huawei.hms.maps.HuaweiMap
import com.huawei.hms.maps.model.BitmapDescriptorFactory
import com.huawei.hms.maps.model.CameraPosition
import com.huawei.hms.maps.model.LatLng
import com.huawei.hms.maps.model.MarkerOptions
Получаем карту и проводим махинации с ней. Для упрощения представим, что у нас все еще Kotlin Synthetics:
mapView.onCreate(null)
mapView.getMapAsync {
with(it) {
// Устанавливаем широту и долготу
val location = LatLng(latitude, longitude)
// Указываем тип карты, для Google соответственно GoogleMap.MAP_TYPE_NORMAL
mapType = HuaweiMap.MAP_TYPE_NORMAL
// Инициализируем позицию для камеры
val cameraPosition = CameraUpdateFactory.newCameraPosition(
CameraPosition.fromLatLngZoom(
location,
15f
)
)
// Перемещаем камеру
moveCamera(cameraPosition)
val icon = BitmapDescriptorFactory.fromResource(R.drawable.marker)
val markerOptions = MarkerOptions()
.icon(icon)
.position(location)
// Добавляем маркер
addMarker(markerOptions)
setOnMapClickListener { clickListener.invoke() }
}
}
У Huawei подробная документация. Поэтому кучу фичей, которых можно делать с картами, можно найти тут: https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/android-sdk-marker-0000001061779995.
Applinking
Диплинки в приложениях — очень важный инструмент для маркетинга, и ваш заказчик точно их захочет. В пуше вы можете вставить такой же диплинк, какой используете на Android. Но когда вы напишете диплинк в заметках и нажмете на него, приложение не откроется.
Как исправить эту историю? На помощь приходит AppLinking.
Доверенные форматы URL
Сначала нужно добавить «Доверенные форматы URL» в нашей консоли. Фактически это хуавеевский URL, который будет префиксом перед вашим реальным диплинком. Идем в «Мои проекты», затем выбираем проект и в левом меню находим вкладку «Рост». В ней — «Создание ссылок» или AppLinking. Нажимаем и попадаем на вкладку «Префиксы URL».
Нажмем на кнопку «Создать префикс URL» и введем какое-нибудь доменное имя типа testapp.
3. Теперь добавим интент-фильтр в манифест нашего приложения, чтобы мы могли обработать такой линк. Делаем такое:
Теперь мы на шаг ближе к корректной обработке внешних диплинков.
Белый список URL-адресов
Наш финальный диплинк будет иметь такой вид:
https://testapp.drru.agconnect.link? deeplink=[наш оригинальный диплинк]
Предположим, что [наш оригинальный диплинк], который мы испальзовали в Android, чтобы открыть историю операций, выглядел так: testapp.link://history. Запомним это и пойдем обратно в консоль.
Диплинки, которые мы хотим обрабатывать, нужно добавить в белый список. Снова проходим этот путь:
«Мои проекты» → «Рост» → «Создание ссылок», AppLinking.
Но в этот раз откроем табик «Белый список URL-адресов».
Нажимаем на кнопку «Создать формат URL-адресов белого списка» и в открывшемся окне заполняем с помощью регулярного выражения [наш оригинальный диплинк].
Наш history отлично обработается, если введем такую штуку:»^testapp.link://.*$». Нажимаем «Опубликовать».
Отлично, мы добавили наши оригинальные диплинки в белый список, осталось только их обработать в коде.
Обработка линков в коде Android-приложения
Добавим зависимость в Gradle «implementation 'com.huawei.agconnect: agconnect-applinking:1.8.0.300'».
Теперь туда, где вы обрабатываете обычный Intent (то есть обычный диплинк Android), вставим вот такой обработчик от HMS:
AGConnectAppLinking.getInstance().getAppLinking(this)
.addOnSuccessListener {
handleDeeplink(it.deepLink.toString())
}.addOnFailureListener {
Log.e("Deeplink main","FAIL")
}
Тут все достаточно просто. У нас есть обработчик, который может отлавливать успешный линк и также ловить ошибки. Ошибки могут возникать, например, когда вы не добавили свой оригинальный диплинк в список белых URL.
Теперь наше приложение будет открывать диплинки вида https://testapp.drru.agconnect.link? deeplink=[наш оригинальный диплинк] и обрабатывать их из любого другого приложения или сайта.
На этом адаптация под Huawei не заканчивается. Тут я привел базовые советы. Они точно помогут, если вы ни разу не сталкивались с задачами такого типа. Но внутри этой темы есть еще много особенностей и фишек. Это и продуктовая аналитика (HiAnalyticsInstance), и крашлитика, и встроенные покупки, и Huawei Barcode Detector. Но об этом я расскажу в следующей статье.
Если у вас есть какие-то вопросы, задавайте в комментариях — на все отвечу. Еще задавать вопросы можно в нашем телеграм-чате AGIMA Dev. У нас там активное сообщество, и мы всем рады.
P.S. А еще наши друзья из компании AFFINAGE проводят большое исследование по No- и Low-code. Если у вас есть опыт в этой сфере, пройдите опрос. Они даже бота для этого специального сделали.