Detr谩s de dos servicios m贸viles: HMS y GMS en una sola aplicaci贸n





隆Hola, Habr! Mi nombre es Andrey, estoy creando la aplicaci贸n " Wallet " para Android. Durante m谩s de seis meses, hemos estado ayudando a los usuarios de tel茅fonos inteligentes Huawei a pagar sus compras con tarjetas bancarias sin contacto, a trav茅s de NFC. Para hacer esto, necesit谩bamos agregar soporte para HMS: Push Kit, Map Kit y Safety Detect. Debajo del corte, les dir茅 qu茅 problemas tuvimos que resolver durante el desarrollo, por qu茅 exactamente y qu茅 surgi贸, y tambi茅n compartir茅 un proyecto de prueba para una inmersi贸n m谩s r谩pida en el tema.



Para brindar a todos los usuarios de los nuevos tel茅fonos inteligentes de Huawei la capacidad de pagar sin contacto desde el primer momento y garantizar la mejor experiencia de usuario en otros escenarios, en enero de 2020 comenzamos a trabajar para admitir nuevas notificaciones autom谩ticas, tarjetas y controles de seguridad. El resultado deber铆a haber sido la aparici贸n en AppGallery de una versi贸n de Wallet con servicios m贸viles nativos de los tel茅fonos Huawei.



Esto es lo que logramos descubrir en la etapa del estudio inicial.



  • Huawei distribuye AppGallery y HMS sin restricciones: puede descargarlos e instalarlos en dispositivos de otros fabricantes;
  • Despu茅s de instalar AppGallery en Xiaomi Mi A1, todas las actualizaciones comenzaron a extraerse en primer lugar del nuevo sitio. La impresi贸n es que AppGallery tiene tiempo para actualizar las aplicaciones m谩s r谩pido que la competencia;
  • Huawei ahora se esfuerza por llenar la AppGallery con aplicaciones lo m谩s r谩pido posible. Para acelerar la migraci贸n a HMS, decidieron proporcionar a los desarrolladores una API familiar (similar a GMS) ;
  • Al principio, hasta que el ecosistema de desarrolladores de Huawei est茅 completamente operativo, la falta de servicios de Google probablemente ser谩 el principal problema para los usuarios de los nuevos tel茅fonos inteligentes de Huawei, e intentar谩n instalarlos en todos los sentidos .


Decidimos crear una versi贸n com煤n de la aplicaci贸n para todos los sitios de distribuci贸n. Debe poder identificar y utilizar el tipo apropiado de servicio m贸vil en tiempo de ejecuci贸n. Esta opci贸n parec铆a m谩s lenta de implementar que una versi贸n separada para cada tipo de servicio, pero esper谩bamos ganar en otra:



  • Se elimina el riesgo de obtener la versi贸n destinada a Google Play en dispositivos Huawei y viceversa;
  • Puede implementar cualquier algoritmo para elegir servicios m贸viles, incluido el uso de alternancia de funciones;
  • Probar una aplicaci贸n es m谩s f谩cil que probar dos;
  • Cada versi贸n se puede cargar en todos los sitios de distribuci贸n;
  • No tiene que cambiar de escribir c贸digo a administrar la construcci贸n del proyecto durante el desarrollo / modificaci贸n.


Para trabajar con diferentes implementaciones de servicios m贸viles en una versi贸n de la aplicaci贸n, debe:



  1. Ocultar todas las solicitudes de abstracci贸n, ahorrando trabajo con GMS;
  2. Agregue una implementaci贸n para HMS;
  3. Desarrollar un mecanismo para elegir la implementaci贸n de servicios en tiempo de ejecuci贸n.


La metodolog铆a para implementar el soporte Push Kit y Safety Detect es significativamente diferente del Map Kit, por lo que los consideraremos por separado.



Soporte Push Kit y Safety Detect



Como debe ser en tales casos, el proceso de integraci贸n comenz贸 con el estudio de la documentaci贸n . Los siguientes puntos se encontraron en la secci贸n de advertencia:

  • Si la versi贸n EMUI es 10.0 o posterior en un dispositivo Huawei, se devolver谩 un token a trav茅s del m茅todo getToken. Si no se puede llamar al m茅todo getToken, HUAWEI Push Kit autom谩ticamente almacena en cach茅 la solicitud del token y vuelve a llamar al m茅todo. Luego, se devolver谩 un token a trav茅s del m茅todo onNewToken.
  • Si la versi贸n de EMUI en un dispositivo Huawei es anterior a la 10.0 y no se devuelve ning煤n token mediante el m茅todo getToken, se devolver谩 un token mediante el 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.


Lo principal que hay que sacar de estas advertencias es que hay una diferencia en recibir un token de inserci贸n en diferentes versiones de EMUI . Despu茅s de llamar al m茅todo getToken (), el token real se puede devolver llamando al m茅todo onNewToken () del servicio. Nuestras pruebas en dispositivos reales han demostrado que los tel茅fonos con EMUI <10.0 devuelven un valor nulo o una cadena vac铆a cuando se llama al m茅todo getToken, despu茅s de lo cual se llama al m茅todo onNewToken () del servicio. Los tel茅fonos con EMUI> = 10.0 siempre devolv铆an un token de inserci贸n del m茅todo getToken ().



Puede implementar una fuente de datos de este tipo para llevar la l贸gica del trabajo a una sola forma:



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)
               })
       }
   }
}


Notas 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.


-



  • Creamos una fuente de datos separada para cada tipo de servicio;
  • Agregue un repositorio para notificaciones push y seguridad que acepte el tipo de servicios m贸viles como entrada y seleccione una fuente de datos espec铆fica;
  • Alguna entidad de l贸gica empresarial determina qu茅 tipo de servicios m贸viles (de los disponibles) es apropiado para usar en un caso particular.


Desarrollo de un mecanismo para elegir la implementaci贸n de servicios en tiempo de ejecuci贸n



驴C贸mo proceder si solo se instala un tipo de servicios en el dispositivo o ninguno en absoluto, pero qu茅 hacer si los servicios de Google y Huawei se instalan al mismo tiempo?



Esto es lo que encontramos y d贸nde empezamos:



  • Al introducir cualquier nueva tecnolog铆a, se debe utilizar como prioridad si el dispositivo del usuario cumple con todos los requisitos;
  • EMUI >= 10.0 - ;
  • Huawei Google- EMUI 10.0 ;
  • Huawei Google-, . , Google- ;
  • AppGallery Huawei-, , .


El desarrollo del algoritmo result贸 ser, quiz谩s, el asunto m谩s agotador. Aqu铆 convergieron muchos factores t茅cnicos y comerciales, pero al final pudimos encontrar la mejor soluci贸n para nuestro producto . Ahora es incluso un poco extra帽o que la descripci贸n de la parte m谩s discutida del algoritmo encaje en una oraci贸n, pero me alegro de que al final haya resultado simple:

Si ambos tipos de servicios est谩n instalados en el dispositivo y fue posible determinar que la versi贸n de EMUI es <10, usamos Google, de lo contrario usamos Huawei.


Para implementar el algoritmo final, es necesario encontrar una forma de determinar la versi贸n de EMUI en el dispositivo del usuario.



Una forma de hacerlo es leer las propiedades del 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 la correcta ejecuci贸n de los controles de seguridad, adem谩s es necesario tener en cuenta que el estado de los servicios no debe requerir actualizaci贸n.



La implementaci贸n final del algoritmo, teniendo en cuenta el tipo de operaci贸n para la que se selecciona el servicio y determinando la versi贸n EMUI del dispositivo, puede verse as铆:




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
    }
}


Soporte del kit de mapas



Despu茅s de la implementaci贸n del algoritmo para seleccionar servicios en tiempo de ejecuci贸n, el algoritmo para agregar soporte para la funcionalidad b谩sica de los mapas parece trivial:



  1. Determinar el tipo de servicios para mostrar mapas;
  2. Infle el dise帽o apropiado y trabaje con una implementaci贸n de mapa espec铆fica.


Sin embargo, hay una caracter铆stica aqu铆 de la que quiero hablar. Rx of the brain te permite agregar cualquier operaci贸n asincr贸nica casi en cualquier lugar sin el riesgo de reescribir toda la aplicaci贸n, pero tambi茅n impone sus propias limitaciones. Por ejemplo, en este caso, para determinar el dise帽o apropiado, lo m谩s probable es que deba llamar a .blockingGet () en alg煤n lugar del hilo principal, lo cual no es nada bueno. Puede resolver este problema, por ejemplo, utilizando fragmentos secundarios:



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)
       }
   }
}


Ahora puede escribir una implementaci贸n separada para trabajar con el mapa para cada fragmento individual. Si necesita implementar la misma l贸gica, puede seguir el algoritmo familiar: ajustar el trabajo con cada tipo de mapas en una interfaz y pasar una de las implementaciones de esta interfaz al fragmento principal, como se hace en MapFragment.onMapReady ()



Que vino de eso



En los primeros d铆as despu茅s del lanzamiento de la versi贸n actualizada de la aplicaci贸n, el n煤mero de instalaciones alcanz贸 el mill贸n. Lo atribuimos en parte a la funci贸n destacada de AppGallery, y en parte al hecho de que nuestro lanzamiento fue destacado por varios medios y blogueros. Y tambi茅n con la velocidad de actualizaci贸n de las aplicaciones; despu茅s de todo, la versi贸n con el c贸digo de versi贸n m谩s alto estuvo en AppGallery durante dos semanas.



Recibimos comentarios 煤tiles sobre el funcionamiento de la aplicaci贸n en general y sobre la tokenizaci贸n de tarjetas bancarias en particular de los usuarios de nuestro hilo en w3bsit3-dns.com. Despu茅s del lanzamiento de la funcionalidad de pago para Huawei, el foro ha aumentado en n煤mero de visitantes, al igual que los problemas que enfrentan. Seguimos trabajando en todos los llamamientos, pero no observamos ning煤n problema masivo.



En general, el lanzamiento de la aplicaci贸n en AppGallery fue exitoso y podemos concluir que nuestro enfoque para resolver el problema result贸 estar funcionando. Gracias al m茅todo de implementaci贸n elegido, todav铆a tenemos la capacidad de cargar todas las versiones de la aplicaci贸n tanto en Google Play como en AppGallery.



Usando este m茅todo, hemos agregado a la aplicaci贸n Analytics Kit , el APM , trabajando para apoyar el Account Kit y no planeamos detenernos all铆, m谩s a煤n con cada nueva versi贸n que HMS est谩 disponible a煤n m谩s oportunidades .



Ep铆logo



Registrar una cuenta de desarrollador con AppGallery es mucho m谩s complicado que el de Google. Para m铆, por ejemplo, la etapa de verificaci贸n de la verificaci贸n de identidad tom贸 9 d铆as. No creo que esto le pase a todo el mundo, pero cualquier retraso puede disminuir el optimismo. Por lo tanto, junto con el c贸digo completo de la soluci贸n de demostraci贸n completa descrita en el art铆culo, he comprometido todas las claves de la aplicaci贸n en el repositorio para que tenga la oportunidad no solo de evaluar la soluci贸n en su conjunto, sino tambi茅n ahora mismo de probar y mejorar el enfoque propuesto.



Utilizando la salida al espacio p煤blico, quiero agradecer a todo el equipo de Wallet y especialmenteen茅simodev, Artem Kulakov y Egor Aganin por su invaluable contribuci贸n a la integraci贸n de HMS en Wallet.



Enlaces 煤tiles






All Articles