Derrière deux services mobiles: HMS et GMS dans une seule application





Bonjour, Habr! Je m'appelle Andrey, je crée l' application " Wallet " pour Android. Depuis plus de six mois maintenant, nous aidons les utilisateurs de smartphones Huawei à régler leurs achats par carte bancaire sans contact - via NFC. Pour ce faire, nous devions ajouter la prise en charge de HMS: Push Kit, Map Kit et Safety Detect. Sous la coupe, je vais vous dire quels problèmes nous avons dû résoudre pendant le développement, pourquoi exactement et ce qui en est issu, et également partager un projet de test pour une immersion plus rapide dans le sujet.



Afin de fournir à tous les utilisateurs de nouveaux smartphones Huawei la possibilité de payer sans contact directement et de fournir une meilleure expérience utilisateur dans d'autres scénarios, en janvier 2020, nous avons commencé à travailler pour prendre en charge les nouvelles notifications push, cartes et contrôles de sécurité. Le résultat aurait dû être l'apparition dans AppGallery d'une version du Wallet avec des services mobiles natifs des téléphones Huawei.



Voici ce que nous avons réussi à découvrir au stade de l'étude initiale.



  • Huawei distribue AppGallery et HMS sans restrictions - vous pouvez les télécharger et les installer sur des appareils d'autres fabricants;
  • Après avoir installé AppGallery sur Xiaomi Mi A1, toutes les mises à jour ont commencé à être extraites en premier lieu du nouveau site. L'impression est qu'AppGallery a le temps de mettre à jour les applications plus rapidement que ses concurrents;
  • Huawei s'efforce désormais de remplir AppGallery d'applications le plus rapidement possible. Pour accélérer la migration vers HMS, ils ont décidé de fournir aux développeurs une API déjà familière (similaire à GMS) ;
  • Dans un premier temps, jusqu'à ce que l'écosystème de développeurs Huawei soit pleinement opérationnel, le manque de services Google sera probablement le principal problème pour les utilisateurs de nouveaux smartphones Huawei, et ils essaieront de les installer par tous les moyens .


Nous avons décidé de créer une version commune de l'application pour tous les sites de distribution. Elle doit être en mesure d'identifier et d'utiliser le type de service mobile approprié au moment de l'exécution. Cette option semblait plus lente à mettre en œuvre qu'une version séparée pour chaque type de service, mais nous espérions gagner dans une autre:



  • Le risque d'obtenir la version destinée à Google Play sur les appareils Huawei et vice versa est éliminé;
  • Vous pouvez implémenter n'importe quel algorithme pour choisir les services mobiles, y compris en utilisant la bascule de fonctionnalité;
  • Tester une application est plus facile que d'en tester deux;
  • Chaque version peut être téléchargée sur tous les sites de distribution;
  • Vous n'êtes pas obligé de passer de l'écriture de code à la gestion de la construction du projet pendant le développement / la modification.


Pour travailler avec différentes implémentations de services mobiles dans une seule version de l'application, vous devez:



  1. Masquer toutes les demandes d'abstraction, en économisant le travail avec GMS;
  2. Ajouter une implémentation pour HMS;
  3. Développer un mécanisme pour choisir l'implémentation des services lors de l'exécution.


La méthodologie de mise en œuvre du support Push Kit et Safety Detect est très différente de celle du Map Kit, nous allons donc les considérer séparément.



Prise en charge de Push Kit et Safety Detect



Comme il se doit dans de tels cas, le processus d'intégration a commencé par l'étude de la documentation . Les points suivants ont été trouvés dans la section d'avertissement:

  • Si la version EMUI est 10.0 ou ultérieure sur un appareil Huawei, un jeton sera renvoyé via la méthode getToken. Si la méthode getToken ne parvient pas à être appelée, HUAWEI Push Kit met automatiquement en cache la demande de jeton et appelle à nouveau la méthode. Un jeton sera ensuite retourné via la méthode onNewToken.
  • Si la version EMUI sur un appareil Huawei est antérieure à 10.0 et qu'aucun jeton n'est retourné à l'aide de la méthode getToken, un jeton sera renvoyé à l'aide de la méthode 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.


La principale chose à retenir de ces mises en garde est qu'il existe une différence entre l'obtention d'un jeton push sur différentes versions d' EMUI . Après avoir appelé la méthode getToken (), le jeton réel peut être retourné en appelant la méthode onNewToken () du service. Nos tests sur des appareils réels ont montré que les téléphones avec EMUI <10.0 renvoient null ou une chaîne vide lorsque la méthode getToken est appelée, après quoi la méthode onNewToken () du service est appelée. Les téléphones avec EMUI> = 10.0 renvoyaient toujours un jeton push de la méthode getToken ().



Vous pouvez implémenter une telle source de données pour amener la logique de travail sous une forme unique:



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


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


-



  • Nous créons une source de données distincte pour chaque type de service;
  • Ajoutez un référentiel pour les notifications push et la sécurité qui acceptent le type de services mobiles comme entrée et sélectionnez une source de données spécifique;
  • Une certaine entité de la logique métier détermine quel type de services mobiles (parmi ceux disponibles) est approprié à utiliser dans un cas particulier.


Développement d'un mécanisme de choix de l'implémentation des services à l'exécution



Comment procéder si un seul type de services est installé sur l'appareil ou s'il n'y en a pas du tout, mais que faire si les services Google et Huawei sont installés en même temps?



Voici ce que nous avons trouvé et par où nous avons commencé:



  • Lors de l'introduction de toute nouvelle technologie, elle doit être utilisée en priorité si l'appareil de l'utilisateur répond pleinement à toutes les exigences;
  • EMUI >= 10.0 - ;
  • Huawei Google- EMUI 10.0 ;
  • Huawei Google-, . , Google- ;
  • AppGallery Huawei-, , .


Le développement de l'algorithme s'est avéré être, peut-être, l'activité la plus épuisant. De nombreux facteurs techniques et commerciaux ont convergé ici, mais nous avons finalement pu trouver la meilleure solution pour notre produit . Maintenant, il est même un peu étrange que la description de la partie la plus discutée de l'algorithme tienne dans une seule phrase, mais je suis heureux qu'au final, cela se soit révélé simplement:

Si les deux types de services sont installés sur l'appareil et qu'il était possible de déterminer que la version EMUI est <10 - nous utilisons Google, sinon nous utilisons Huawei.


Pour implémenter l'algorithme final, il est nécessaire de trouver un moyen de déterminer la version EMUI sur l'appareil de l'utilisateur.



Une façon de procéder consiste à lire les propriétés système:



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


Pour une exécution correcte des contrôles de sécurité, il est en outre nécessaire de prendre en compte que l'état des services ne doit pas nécessiter de mise à jour.



L'implémentation finale de l'algorithme, en tenant compte du type d'opération pour laquelle le service est sélectionné et en déterminant la version EMUI de l'appareil, peut ressembler à ceci:




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


Prise en charge de Map Kit



Après avoir implémenté l'algorithme de sélection des services lors de l'exécution, l'algorithme d'ajout de la prise en charge des fonctionnalités de base des cartes semble trivial:



  1. Déterminer le type de services d'affichage des cartes;
  2. Gonflez la mise en page appropriée et travaillez avec une implémentation de carte spécifique.


Cependant, il y a une caractéristique ici dont je veux parler. Rx of the brain vous permet d'ajouter n'importe quelle opération asynchrone quasiment n'importe où sans risquer de réécrire l'ensemble de l'application, mais il impose également ses propres limites. Par exemple, dans ce cas, pour déterminer la disposition appropriée, vous devrez probablement appeler .blockingGet () quelque part sur le thread principal, ce qui n'est pas du tout bon. Vous pouvez résoudre ce problème, par exemple, en utilisant des fragments enfants:



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


Vous pouvez maintenant écrire une implémentation distincte pour travailler avec la carte pour chaque fragment individuel. Si vous devez implémenter la même logique, vous pouvez suivre l'algorithme familier - ajustez le travail avec chaque type de carte sous une interface et passez l'une des implémentations de cette interface au fragment parent, comme cela est fait dans MapFragment.onMapReady ()



Qu'est-il arrivé



Dans les premiers jours qui ont suivi la sortie de la version mise à jour de l'application, le nombre d'installations a atteint 1 million. Nous attribuons cela en partie à la fonctionnalité proposée par AppGallery, et en partie au fait que notre version a été mise en avant par plusieurs médias et blogueurs. Et aussi avec la vitesse de mise à jour des applications - après tout, la version avec le plus haut versionCode était dans AppGallery pendant deux semaines.



Nous recevons des retours utiles sur le fonctionnement de l'application en général et sur la tokenisation des cartes bancaires en particulier de la part des utilisateurs dans notre fil de discussion sur w3bsit3-dns.com. Après la sortie de la fonctionnalité Pay pour Huawei, le forum a augmenté le nombre de visiteurs, tout comme les problèmes auxquels ils sont confrontés. Nous continuons à travailler sur tous les appels, mais nous n'observons aucun problème majeur.



En général, la publication de l'application dans AppGallery a été un succès et nous pouvons conclure que notre approche pour résoudre le problème s'est avérée efficace. Grâce à la méthode de mise en œuvre choisie, nous avons toujours la possibilité de télécharger toutes les versions de l'application à la fois dans Google Play et dans AppGallery.



En utilisant cette méthode, nous avons ajouté à l'application Analytics Kit , l'APM , travaillant pour soutenir Account Kit et ne prévoyons pas de s'arrêter là, d'autant plus qu'à chaque nouvelle version devient disponible HMS encore plus d'opportunités .



Épilogue



Créer un compte développeur avec AppGallery est beaucoup plus compliqué que Google. Par exemple, il m'a fallu 9 jours pour vérifier mon identité. Je ne pense pas que cela arrive à tout le monde, mais tout retard peut diminuer l’optimisme. Par conséquent, avec le code complet de toute la solution de démonstration décrite dans l'article, j'ai engagé toutes les clés d'application dans le référentiel afin que vous puissiez non seulement évaluer la solution dans son ensemble, mais également tester et améliorer dès maintenant l'approche proposée.



En utilisant la sortie vers l'espace public, je tiens à remercier toute l'équipe Wallet et en particulierénième dev, Artem Kulakov et Egor Aganin pour leur précieuse contribution à l'intégration de HMS dans le Wallet!



Liens utiles






All Articles