
Olá, Habr! Meu nome é Andrey, estou fazendo o aplicativo " Wallet " para Android. Por mais de seis meses, temos ajudado os usuários de smartphones da Huawei a pagar suas compras com cartões bancários sem contato - via NFC. Para fazer isso, precisamos adicionar suporte para HMS: Push Kit, Map Kit e Safety Detect. Abaixo do recorte, vou contar quais problemas tivemos que resolver durante o desenvolvimento, por que exatamente e o que resultou, e também compartilhar um projeto de teste para uma imersão mais rápida no tópico.
Para fornecer a todos os usuários dos novos smartphones Huawei o pagamento sem contato pronto para uso e garantir uma melhor experiência do usuário em outros cenários, em janeiro de 2020, começamos a trabalhar para oferecer suporte a novas notificações push, cartões e verificações de segurança. O resultado deveria ter sido o aparecimento no AppGallery de uma versão da Carteira com serviços móveis nativos para telefones Huawei.
Aqui está o que descobrimos na fase do estudo inicial.
- A Huawei distribui AppGallery e HMS sem restrições - você pode baixá-los e instalá-los em dispositivos de outros fabricantes;
- Depois de instalar o AppGallery no Xiaomi Mi A1, todas as atualizações começaram a ser retiradas primeiro de tudo do novo site. A impressão é que o AppGallery tem tempo para atualizar os aplicativos mais rápido do que os concorrentes;
- A Huawei agora está se esforçando para preencher o AppGallery com aplicativos o mais rápido possível. Para acelerar a migração para o HMS, eles decidiram fornecer aos desenvolvedores uma API já familiar (semelhante ao GMS) ;
- No início, até que o ecossistema de desenvolvedores da Huawei esteja totalmente operacional, a falta de serviços do Google provavelmente será o principal problema para os usuários de novos smartphones Huawei, e eles tentarão instalá-los de todas as formas .
Decidimos fazer uma versão comum do aplicativo para todos os sites de distribuição. Ela deve ser capaz de identificar e usar o tipo apropriado de serviço móvel em tempo de execução. Essa opção parecia mais lenta de implementar do que uma versão separada para cada tipo de serviço, mas esperávamos vencer em outro:
- Elimina o risco de obter a versão destinada ao Google Play em dispositivos Huawei e vice-versa;
- Você pode implementar qualquer algoritmo para escolher serviços móveis, incluindo o uso do alternador de recursos;
- Testar um aplicativo é mais fácil do que testar dois;
- Cada lançamento pode ser carregado em todos os sites de distribuição;
- Você não precisa passar da escrita do código para o gerenciamento da construção do projeto durante o desenvolvimento / modificação.
Para trabalhar com diferentes implementações de serviços móveis em uma versão do aplicativo, você deve:
- Oculte todas as solicitações de abstração, economizando trabalho com GMS;
- Adicione uma implementação para HMS;
- Desenvolva um mecanismo para escolher a implementação de serviços em tempo de execução.
A metodologia para implementar o Push Kit e o suporte à detecção de segurança é significativamente diferente do Map Kit, portanto, vamos considerá-los separadamente.
Suporte para Push Kit e Safety Detect
Como deve ser nesses casos, o processo de integração começou com o estudo da documentação . Os seguintes pontos foram encontrados na seção de aviso:
- Se a versão EMUI for 10.0 ou posterior em um dispositivo Huawei, um token será retornado por meio do método getToken. Se o método getToken não for chamado, o HUAWEI Push Kit automaticamente armazena em cache a solicitação de token e chama o método novamente. Um token será retornado por meio do método onNewToken.
- Se a versão EMUI em um dispositivo Huawei for anterior a 10.0 e nenhum token for retornado usando o método getToken, um token será retornado usando o método onNewToken.
- For an app with the automatic initialization capability, the getToken method does not need to be called explicitly to apply for a token. The HMS Core Push SDK will automatically apply for a token and call the onNewToken method to return the token.
A principal coisa a se tirar dessas advertências é que há uma diferença em obter um token push em diferentes versões do EMUI . Depois de chamar o método getToken (), o token real pode ser retornado chamando o método onNewToken () do serviço. Nossos testes em dispositivos reais mostraram que os telefones com EMUI <10.0 retornam null ou uma string vazia quando o método getToken é chamado, após o qual o método onNewToken () do serviço é chamado. Os telefones com EMUI> = 10.0 sempre retornaram um token push do método getToken ().
Você pode implementar essa fonte de dados para trazer a lógica de trabalho para um único formulário:
class HmsDataSource(
private val hmsInstanceId: HmsInstanceId,
private val agConnectServicesConfig: AGConnectServicesConfig
) {
private val currentPushToken = BehaviorSubject.create<String>()
fun getHmsPushToken(): Single<String> = Maybe
.merge(
getHmsPushTokenFromSingleton(),
currentPushToken.firstElement()
)
.firstOrError()
fun onPushTokenUpdated(token: String): Completable = Completable
.fromCallable { currentPushToken.onNext(token) }
private fun getHmsPushTokenFromSingleton(): Maybe<String> = Maybe
.fromCallable<String> {
val appId = agConnectServicesConfig.getString("client/app_id")
hmsInstanceId.getToken(appId, "HCM").takeIf { it.isNotEmpty() }
}
.onErrorComplete()
}
class AppHmsMessagingService : HmsMessageService() {
val onPushTokenUpdated: OnPushTokenUpdated = Di.onPushTokenUpdated
override fun onMessageReceived(remoteMessage: RemoteMessage?) {
super.onMessageReceived(remoteMessage)
Log.d(LOG_TAG, "onMessageReceived remoteMessage=$remoteMessage")
}
override fun onNewToken(token: String?) {
super.onNewToken(token)
Log.d(LOG_TAG, "onNewToken: token=$token")
if (token?.isNotEmpty() == true) {
onPushTokenUpdated(token, MobileServiceType.Huawei)
.subscribe({},{
Log.e(LOG_TAG, "Error deliver updated token", it)
})
}
}
}
Anotações importantes:
- . , , AppGallery -, . , HmsMessageService.onNewToken() , , , . ;
- , HmsMessageService.onMessageReceived() main , ;
- com.huawei.hms:push, com.huawei.hms.support.api.push.service.HmsMsgService, :pushservice. , , Application. , , , Firebase Performance. -Huawei , AppGallery HMS.
-
- Criamos uma fonte de dados separada para cada tipo de serviço;
- Adicione um repositório para notificações push e segurança que aceite o tipo de serviços móveis como entrada e selecione uma fonte de dados específica;
- Alguma entidade de lógica de negócios determina qual tipo de serviço móvel (entre os disponíveis) é apropriado para usar em um caso particular.
Desenvolvimento de um mecanismo de escolha da implementação de serviços em tempo de execução
Como proceder se apenas um tipo de serviço estiver instalado no dispositivo ou nenhum, mas o que fazer se os serviços do Google e Huawei forem instalados ao mesmo tempo?
Aqui está o que encontramos e por onde começamos:
- Ao introduzir qualquer nova tecnologia, ela deve ser usada como prioridade se o dispositivo do usuário atender totalmente a todos os requisitos;
- EMUI >= 10.0 - ;
- Huawei Google- EMUI 10.0 ;
- Huawei Google-, . , Google- ;
- AppGallery Huawei-, , .
O desenvolvimento do algoritmo acabou sendo, talvez, o negócio mais exaustivo. Muitos fatores técnicos e de negócios convergiram aqui, mas no final fomos capazes de encontrar a melhor solução para o nosso produto . Agora é até um pouco estranho que a descrição da parte mais discutida do algoritmo se encaixe em uma frase, mas estou feliz que no final acabou simplesmente:
Se ambos os tipos de serviços estiverem instalados no dispositivo e for possível determinar que a versão EMUI é <10 - usamos o Google, caso contrário, usamos Huawei.
Para implementar o algoritmo final, é necessário encontrar uma maneira de determinar a versão EMUI no dispositivo do usuário.
Uma maneira de fazer isso é ler as propriedades do sistema:
class EmuiDataSource {
@SuppressLint("PrivateApi")
fun getEmuiApiLevel(): Maybe<Int> = Maybe
.fromCallable<Int> {
val clazz = Class.forName("android.os.SystemProperties")
val get = clazz.getMethod("getInt", String::class.java, Int::class.java)
val currentApiLevel = get.invoke(
clazz,
"ro.build.hw_emui_api_level",
UNKNOWN_API_LEVEL
) as Int
currentApiLevel.takeIf { it != UNKNOWN_API_LEVEL }
}
.onErrorComplete()
private companion object {
const val UNKNOWN_API_LEVEL = -1
}
}
Para a correta execução das verificações de segurança, é ainda necessário levar em consideração que o estado dos serviços não deve exigir atualização.
A implementação final do algoritmo, levando em consideração o tipo de operação para a qual o serviço é selecionado e determinando a versão EMUI do dispositivo, pode ser assim:
sealed class MobileServiceEnvironment(
val mobileServiceType: MobileServiceType
) {
abstract val isUpdateRequired: Boolean
data class GoogleMobileServices(
override val isUpdateRequired: Boolean
) : MobileServiceEnvironment(MobileServiceType.Google)
data class HuaweiMobileServices(
override val isUpdateRequired: Boolean,
val emuiApiLevel: Int?
) : MobileServiceEnvironment(MobileServiceType.Huawei)
}
class SelectMobileServiceType(
private val mobileServicesRepository: MobileServicesRepository
) {
operator fun invoke(
case: Case
): Maybe<MobileServiceType> = mobileServicesRepository
.getAvailableServices()
.map { excludeEnvironmentsByCase(case, it) }
.flatMapMaybe { selectEnvironment(it) }
.map { it.mobileServiceType }
private fun excludeEnvironmentsByCase(
case: Case,
envs: Set<MobileServiceEnvironment>
): Iterable<MobileServiceEnvironment> = when (case) {
Case.Push, Case.Map -> envs
Case.Security -> envs.filter { !it.isUpdateRequired }
}
private fun selectEnvironment(
envs: Iterable<MobileServiceEnvironment>
): Maybe<MobileServiceEnvironment> = Maybe
.fromCallable {
envs.firstOrNull {
it is HuaweiMobileServices
&& (it.emuiApiLevel == null || it.emuiApiLevel >= 21)
}
?: envs.firstOrNull { it is GoogleMobileServices }
?: envs.firstOrNull { it is HuaweiMobileServices }
}
enum class Case {
Push, Map, Security
}
}
Suporte para kit de mapas
Depois de implementar o algoritmo para selecionar serviços em tempo de execução, o algoritmo para adicionar suporte para a funcionalidade básica de mapas parece trivial:
- Determine o tipo de serviços para exibição de mapas;
- Infle o layout apropriado e trabalhe com uma implementação de mapa específica.
No entanto, há um recurso aqui sobre o qual quero falar. O Rx do cérebro permite adicionar qualquer operação assíncrona em quase qualquer lugar sem o risco de reescrever todo o aplicativo, mas também impõe suas próprias limitações. Por exemplo, neste caso, para determinar o layout apropriado, muito provavelmente, você precisará chamar .blockingGet () em algum lugar no thread principal, o que não é nada bom. Você pode resolver esse problema, por exemplo, usando fragmentos filhos:
class MapFragment : Fragment(),
OnGeoMapReadyCallback {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
ViewModelProvider(this)[MapViewModel::class.java].apply {
mobileServiceType.observe(viewLifecycleOwner, Observer { result ->
val fragment = when (result.getOrNull()) {
Google -> GoogleMapFragment.newInstance()
Huawei -> HuaweiMapFragment.newInstance()
else -> NoServicesMapFragment.newInstance()
}
replaceFragment(fragment)
})
}
}
override fun onMapReady(geoMap: GeoMap) {
geoMap.uiSettings.isZoomControlsEnabled = true
}
}
class GoogleMapFragment : Fragment(),
OnMapReadyCallback {
private var callback: OnGeoMapReadyCallback? = null
override fun onAttach(context: Context) {
super.onAttach(context)
callback = parentFragment as? OnGeoMapReadyCallback
}
override fun onDetach() {
super.onDetach()
callback = null
}
override fun onMapReady(googleMap: GoogleMap?) {
if (googleMap != null) {
val geoMap = geoMapFactory.create(googleMap)
callback?.onMapReady(geoMap)
}
}
}
class HuaweiMapFragment : Fragment(),
OnMapReadyCallback {
private var callback: OnGeoMapReadyCallback? = null
override fun onAttach(context: Context) {
super.onAttach(context)
callback = parentFragment as? OnGeoMapReadyCallback
}
override fun onDetach() {
super.onDetach()
callback = null
}
override fun onMapReady(huaweiMap: HuaweiMap?) {
if (huaweiMap != null) {
val geoMap = geoMapFactory.create(huaweiMap)
callback?.onMapReady(geoMap)
}
}
}
Agora você pode escrever uma implementação separada para trabalhar com o mapa para cada fragmento individual. Se você precisar implementar a mesma lógica, pode seguir o algoritmo familiar - ajustar o trabalho com cada tipo de mapa em uma interface e passar uma das implementações dessa interface para o fragmento pai, como é feito em MapFragment.onMapReady ()
O que resultou disso
Nos primeiros dias após o lançamento da versão atualizada do aplicativo, o número de instalações chegou a 1 milhão. Atribuímos isso em parte ao recurso de destaque do AppGallery e em parte ao fato de que nosso lançamento foi destacado por vários meios de comunicação e blogueiros. E também com a velocidade de atualização dos aplicativos - afinal, a versão com o maior versionCode ficou no AppGallery por duas semanas.
Recebemos comentários úteis sobre o funcionamento do aplicativo em geral e sobre a tokenização de cartões bancários, em particular, de usuários em nosso tópico em w3bsit3-dns.com. Após o lançamento da funcionalidade Pay para Huawei, o número de visitantes do fórum aumentou, assim como os problemas que eles enfrentam. Continuamos trabalhando em todos os recursos, mas não observamos nenhum problema massivo.
Em geral, o lançamento do aplicativo no AppGallery foi bem-sucedido e podemos concluir que nossa abordagem para solucionar o problema funcionou. Graças ao método de implementação escolhido, ainda podemos fazer upload de todos os lançamentos de aplicativos no Google Play e no AppGallery.
Usando este método, adicionamos ao aplicativo Analytics Kit , o APM , trabalhando para dar suporte ao Account Kit e não planejamos parar por aí, ainda mais a cada nova versão do HMS torna-se disponível mais oportunidades .
Posfácio
Registrar uma conta de desenvolvedor com AppGallery é muito mais complicado do que o do Google. Para mim, por exemplo, o estágio de verificação da verificação de identidade levou 9 dias. Não acho que isso aconteça com todos, mas qualquer atraso pode diminuir o otimismo. Portanto, junto com o código completo de toda a solução de demonstração descrita no artigo, comprometi todas as chaves do aplicativo no repositório para que você tenha a oportunidade não apenas de avaliar a solução como um todo, mas também agora de testar e melhorar a abordagem proposta.
Usando a saída para o espaço público, gostaria de agradecer a toda a equipe da Wallet e principalmenteumpteenthdev, Artem Kulakov e Egor Aganin por sua contribuição inestimável para a integração do HMS na carteira!
Links Úteis
- Código completo do projeto de demonstração no GitHub;
- AppGallery . HMS-Core AppGallery;
- Push Kit codelab;
- Map Kit codelab;
- Safety Detect codelab;
- - Huawei. AppGallery Connect;
- «» 4PDA.