两种移动服务的背后:一个应用程序中的HMS和GMS





哈Ha!我的名字叫Andrey,我正在为Android开发Wallet应用程序。半年多以来,我们一直在帮助华为智能手机用户通过NFC非接触式使用银行卡付款。为此,我们需要增加对HMS的支持:推套件,地图套件和安全检测。在摘要中,我将告诉您开发过程中必须解决的问题,确切原因以及产生的原因,并分享一个测试项目以更快地浸入主题。



为了给所有新华为智能手机的用户提供非接触式开箱即用功能,并确保在其他情况下更好的用户体验,我们从2020年1月开始致力于支持新的推送通知,卡和安全检查。结果应该是在AppGallery中出现了带有华为手机固有移动服务的电子钱包版本。



这是我们在初始研究阶段设法找到的。



  • 华为发布AppGallery和HMS不受限制-您可以从其他制造商的设备上下载并安装它们;
  • 在小米Mi A1上安装AppGallery之后,所有更新首先都从新站点中拉起。印象是AppGallery有时间比竞争对手更快地更新应用程序。
  • 华为现在正在努力尽快为AppGallery填充应用程序。为了加快向HMS的迁移,他们决定为开发人员提供熟悉的(类似于GMS)API
  • 首先,在华为开发者生态系统完全运行之前,缺少谷歌服务将很可能是新华为智能手机用户面临的主要问题,他们将尝试以各种方式进行安装


我们决定为所有发行站点制作一个通用的应用程序版本。她必须能够在运行时识别并使用适当类型的移动服务。与每种服务类型的单独版本相比,此选项的实现似乎较慢,但是我们希望能赢得另一种:



  • 消除了在华为设备上获取适用于Google Play的版本的风险,反之亦然;
  • 您可以实现选择移动服务的任何算法,包括使用功能切换。
  • 测试一个应用程序比测试两个应用程序容易。
  • 每个版本都可以上传到所有发行站点;
  • 在开发/修改期间,您不必从编写代码切换为管理项目的构建。


要在一个应用程序版本中使用移动服务的不同实现,您必须:



  1. 隐藏所有抽象请求,以节省使用GMS的工作;
  2. 添加HMS的实现;
  3. 开发一种在运行时选择服务实现的机制。


实施Push Kit和Safety Detect支持的方法与Map Kit显着不同,因此我们将分别考虑它们。



推套件和安全检测支持



在这种情况下,应该从研究文档开始进行集成在警告部分中发现以下几点:

  • 如果华为设备上的EMUI版本为10.0或更高版本,则将通过getToken方法返回令牌。如果无法成功调用getToken方法,HUAWEI Push Kit会自动缓存令牌请求并再次调用该方法。然后,将通过onNewToken方法返回令牌。
  • 如果华为设备上的EMUI版本低于10.0,并且没有使用getToken方法返回令牌,则将使用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.


要避免这些警告最主要的是,在不同版本的EMUI上接收推送令牌有所不同调用getToken()方法后,可以通过调用服务的onNewToken()方法来返回真实令牌。我们在真实设备上的测试表明,调用getToken方法时EMUI <10.0的电话返回null或空字符串,此后调用服务的onNewToken()方法。EMUI> = 10.0的电话始终从getToken()方法返回推入令牌。



您可以实现这样的数据源,以将工作逻辑统一为一种形式:



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


重要笔记:



  • . , , 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.


-



  • 我们为每种服务类型创建一个单独的数据源;
  • 添加用于推送通知和安全性存储库,以接受移动服务的类型作为输入,并选择特定的数据源;
  • 业务逻辑的某些实体确定在特定情况下适合使用哪种类型的移动服务(来自可用的移动服务)。


开发一种在运行时选择服务实现的机制



如果仅在设备上安装了一种服务,或者根本没有安装任何服务,但是如果同时安装了Google和华为服务怎么办?



这是我们找到的内容和开始的地方:



  • 引入任何新技术时,如果用户的设备完全满足所有要求,则必须将其优先考虑。
  • EMUI >= 10.0 - ;
  • Huawei Google- EMUI 10.0 ;
  • Huawei Google-, . , Google- ;
  • AppGallery Huawei-, , .


该算法的开发可能是最费劲的事情。许多技术和商业因素在这里融合在一起,但最终我们能够为我们的产品提供最佳的解决方案现在,将该算法中讨论最多的部分的描述放到一个句子中,这甚至有些奇怪,但是我很高兴最后结果很简单:

如果设备上同时安装了这两种服务,并且可以确定EMUI版本小于10-我们使用Google,否则我们使用华为。


为了实现最终算法,需要找到一种方法来确定用户设备上的EMUI版本。



一种方法是读取系统属性:



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


为了正确执行安全检查,还必须考虑到服务状态不需要更新。



考虑到选择服务的操作类型并确定设备的EMUI版本,该算法的最终实现如下所示:




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


地图套件支持



在实现了用于在运行时选择服务的算法之后,用于添加对地图基本功能的支持的算法看起来很简单:



  1. 确定用于显示地图的服务类型;
  2. 膨胀适当的布局并使用特定的地图实现。


但是,我想谈谈一个功能。大脑的Rx允许您几乎在任何地方添加任何异步操作,而没有重写整个应用程序的风险,但是它也有其自身的局限性。例如,在这种情况下,最有可能要确定适当的布局,您将需要在Main线程的某个位置调用.blockingGet(),这根本不好。您可以解决此问题,例如,使用子片段:



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


现在,您可以编写一个单独的实现,以便为每个单独的片段使用地图。如果需要实现相同的逻辑,则可以遵循熟悉的算法-在一个接口下调整每种地图类型的工作,然后将此接口的实现之一传递给父片段,如MapFragment.onMapReady()一样。



这是怎么回事



在发布该应用程序的更新版本后的头几天,安装数量达到了100万,我们将此部分归因于AppGallery的特色功能,部分归因于我们的发布受到了多家媒体和博客的关注。而且还具有更新应用程序的速度-毕竟,具有最高versionCode的版本已在AppGallery中存放了两个星期。



我们在w3bsit3-dns.com上的线程中从用户那里收到了有关应用程序特别是银行卡令牌化的有用反馈。发布华为支付功能后,论坛的访问者数量有所增加,面临的问题也有所增加。我们将继续处理所有上诉,但不会发现任何大问题。



通常,在AppGallery中发布应用程序是成功的,我们可以得出结论,我们解决问题的方法是行之有效的。由于选择了实施方法,我们仍然能够在Google Play和AppGallery上载所有应用程序版本。



使用这种方法,我们已经添加到应用程序分析工具在APM,努力支持帐户套件,并且不打算就此止步,更何况每个新版本可用HMS还是更多的机会



后记



在AppGallery注册开发者帐户比Google复杂得多。例如,我花了9天的时间来验证我的身份。我认为这并不是每个人都会发生的,但是任何拖延都会削弱乐观情绪。因此,连同本文中描述的整个演示解决方案的完整代码一样,我已将所有应用程序密钥提交给存储库,以便您不仅有机会评估整个解决方案,而且还可以立即测试和改进建议的方法。



我要使用公共空间的出口,感谢整个电子钱包团队,尤其是umpteenthdev,Artem Kulakov和Egor Aganin为将HMS整合到电子钱包中做出了宝贵贡献!



有用的链接






All Articles