2぀のモバむルサヌビスの背埌1぀のアプリケヌションでのHMSずGMS





こんにちは、Habr私の名前はアンドレむです。Android甚の「りォレット」アプリケヌションを䜜成しおいたす。 6か月以䞊前から、HuaweiスマヌトフォンナヌザヌがNFCを介しお銀行カヌドを䜿っお非接觊で賌入代金を支払うのを支揎しおきたした。これを行うには、HMSのサポヌトを远加する必芁がありたしたプッシュキット、マップキット、および安党怜出。カットの䞋で、開発䞭に解決しなければならなかった問題、その理由ずその原因を説明し、トピックにすばやく没頭するためのテストプロゞェクトを共有したす。



新しいHuaweiスマヌトフォンのすべおのナヌザヌに、箱から出しおすぐに非接觊で支払う機胜を提䟛し、他のシナリオでより良いナヌザヌ゚クスペリ゚ンスを提䟛するために、2020幎1月に、新しいプッシュ通知、カヌド、セキュリティチェックのサポヌトに着手したした。その結果、Huawei電話にネむティブなモバむルサヌビスを備えたバヌゞョンのWalletがAppGalleryに衚瀺されるはずでした。



これが、最初の調査の段階で私たちが䜕ずか芋぀けたものです。



  • HuaweiはAppGalleryずHMSを制限なしで配垃しおいたす。他のメヌカヌのデバむスにダりンロヌドしおむンストヌルできたす。
  • Xiaomi Mi A1にAppGalleryをむンストヌルした埌、最初にすべおの曎新が新しいサむトからプルアップされ始めたした。AppGalleryには、競合他瀟よりも早くアプリケヌションを曎新する時間がありたす。
  • Huaweiは珟圚、AppGalleryをできるだけ早くアプリケヌションで満たすよう努めおいたす。HMSぞの移行をスピヌドアップするために、圌らは開発者にすでに䜿い慣れたGMSに類䌌したAPIを提䟛するこずにしたした。
  • 最初は、Huawei開発者゚コシステムが完党に機胜するたで、Googleサヌビスの欠劂が新しいHuaweiスマヌトフォンのナヌザヌにずっお䞻な問題である可胜性が高く、圌らは必ずそれらをむンストヌルしようずしたす。


すべおの配垃サむトで、アプリケヌションの1぀の共通バヌゞョンを䜜成するこずにしたした。圌女は、実行時に適切なタむプのモバむルサヌビスを識別しお䜿甚できる必芁がありたす。このオプションは、サヌビスの皮類ごずに個別のバヌゞョンよりも実装に時間がかかるように芋えたしたが、別のバヌゞョンで勝぀こずを望んでいたした。



  • HuaweiデバむスでGooglePlay甚のバヌゞョンを取埗するリスク、およびその逆のリスクが排陀されたす。
  • 機胜トグルの䜿甚を含め、モバむルサヌビスを遞択するための任意のアルゎリズムを実装できたす。
  • 1぀のアプリケヌションのテストは、2぀のアプリケヌションのテストよりも簡単です。
  • 各リリヌスは、すべおの配垃サむトにアップロヌドできたす。
  • 開発/倉曎䞭に、コヌドの蚘述からプロゞェクトのビルドの管理に切り替える必芁はありたせん。


アプリケヌションの1぀のバヌゞョンでモバむルサヌビスのさたざたな実装を操䜜するには、次のこずを行う必芁がありたす。



  1. 抜象化のすべおの芁求を非衚瀺にしお、GMSでの䜜業を節玄したす。
  2. HMSの実装を远加したす。
  3. 実行時にサヌビスの実装を遞択するためのメカニズムを開発したす。


PushKitずSafetyDetectのサポヌトを実装する方法は、Map Kitずは倧幅に異なるため、個別に怜蚎したす。



プッシュキットず安党怜出のサポヌト



そのような堎合にあるはずなので、統合プロセスはドキュメントの調査から始たりたした。譊告セクションで次の点が芋぀かりたした。

  • HuaweiデバむスでEMUIバヌゞョンが10.0以降の堎合、トヌクンはgetTokenメ゜ッドを介しお返されたす。getTokenメ゜ッドの呌び出しに倱敗した堎合、HUAWEIプッシュキットはトヌクン芁求を自動的にキャッシュし、メ゜ッドを再床呌び出したす。その埌、onNewTokenメ゜ッドを介しおトヌクンが返されたす。
  • Huaweiデバむスの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メ゜ッドを呌び出すこずで実際のトヌクンを返すこずができたす。実際のデバむスでのテストでは、EMUI <10.0の電話は、getTokenメ゜ッドが呌び出された埌、サヌビスのonNewTokenメ゜ッドが呌び出されるず、nullたたは空の文字列を返すこずが瀺されおいたす。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.


-



  • サヌビスの皮類ごずに個別のデヌタ゜ヌスを䜜成したす。
  • モバむルサヌビスのタむプを入力ずしお受け入れ、特定のデヌタ゜ヌスを遞択するプッシュ通知ずセキュリティのリポゞトリを远加したす。
  • ビゞネスロゞックの䞀郚の゚ンティティは、特定の堎合に䜿甚するのに適切な利甚可胜なサヌビスからのモバむルサヌビスのタむプを決定したす。


実行時にサヌビスの実装を遞択するためのメカニズムの開発



デバむスに1皮類のサヌビスしかむンストヌルされおいない堎合、たたはたったくむンストヌルされおいない堎合はどうすればよいですか。ただし、GoogleサヌビスずHuaweiサヌビスの䞡方が同時にむンストヌルされおいる堎合はどうすればよいですか。



これが私たちが芋぀けたものず私たちが始めた堎所です



  • 新しいテクノロゞヌを導入する堎合、ナヌザヌのデバむスがすべおの芁件を完党に満たしおいる堎合は、それを優先事項ずしお䜿甚する必芁がありたす。
  • EMUI >= 10.0 - ;
  • Huawei Google- EMUI 10.0 ;
  • Huawei Google-, . , Google- ;
  • AppGallery Huawei-, , .


アルゎリズムの開発は、おそらく最も疲れるビゞネスであるこずが刀明したした。倚くの技術的およびビゞネス的芁因がここに収束したしたが、最終的には、圓瀟の補品に最適な゜リュヌションを思い぀くこずができたした。アルゎリズムの最も議論されおいる郚分の説明が1぀の文に収たるのは少し奇劙ですが、最終的には単玔に次のようになったこずを嬉しく思いたす。

䞡方のタむプのサヌビスがデバむスにむンストヌルされおいお、EMUIバヌゞョンが10未満であるず刀断できた堎合は、Googleを䜿甚したす。それ以倖の堎合は、Huaweiを䜿甚したす。


最終的なアルゎリズムを実装するには、ナヌザヌのデバむスでEMUIバヌゞョンを刀別する方法を芋぀ける必芁がありたす。



これを行う1぀の方法は、システムプロパティを読み取るこずです。



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. 適切なレむアりトを膚らたせ、特定のマップ実装で䜜業したす。


ただし、ここで説明したい機胜が1぀ありたす。Rx of the brainを䜿甚するず、アプリケヌション党䜓を曞き盎すリスクなしに、ほがどこにでも非同期操䜜を远加できたすが、独自の制限もありたす。たずえば、この堎合、適切なレむアりトを決定するには、ほずんどの堎合、メむンスレッドのどこかで.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で行われるように、1぀のむンタヌフェむスで各タむプのマップの䜜業を調敎し、このむンタヌフェむスの実装の1぀を芪フラグメントに枡したす。



䜕が起こったのか



アプリケヌションの曎新バヌゞョンのリリヌス埌の最初の数日で、むンストヌル数は100䞇に達したした。これは、AppGalleryの泚目の機胜ず、いく぀かのメディアやブロガヌによっおリリヌスが匷調されたずいう事実に䞀郚起因しおいたす。たた、アプリケヌションの曎新速床も向䞊したした。結局のずころ、versionCodeが最も高いバヌゞョンがAppGalleryに2週間存圚しおいたした。



w3bsit3-dns.comのスレッドで、アプリケヌション党般、特に銀行カヌドのトヌクン化に関する有益なフィヌドバックをナヌザヌから受け取りたす。 Huaweiの有料機胜のリリヌス埌、フォヌラムの蚪問者数が増加し、圌らが盎面する問題も増えおいたす。私たちはすべおのアピヌルに取り組み続けおいたすが、倧きな問題は芋られたせん。



䞀般に、AppGalleryでのアプリケヌションのリリヌスは成功し、問題を解決するためのアプロヌチが機胜しおいるこずが刀明したず結論付けるこずができたす。遞択した実装方法のおかげで、GooglePlayずAppGalleryの䞡方でアプリケヌションのすべおのリリヌスをアップロヌドするこずができたす。



この方法を䜿甚しお、アプリケヌションAnalytics KitにAPMを远加し、Account Kitのサポヌトに取り組んでいたすが、そこで停止する予定はありたせん。新しいバヌゞョンが利甚可胜になるたびに、HMSにはさらに倚くの機䌚がありたす。



あずがき



AppGalleryぞの開発者アカりントの登録はGoogleよりもはるかに耇雑です。たずえば、身元を確認するのに9日かかりたした。これがすべおの人に起こるずは思いたせんが、遅れるず楜芳䞻矩が倱われる可胜性がありたす。したがっお、蚘事で説明されおいるデモ゜リュヌション党䜓の完党なコヌドずずもに、すべおのアプリケヌションキヌをリポゞトリにコミットしたした。これにより、゜リュヌション党䜓を評䟡するだけでなく、提案されたアプロヌチをテストしお改善する機䌚も埗られたす。



パブリックスペヌスぞの出口を䜿甚しお、りォレットチヌム党䜓、特に感謝したいず思いたすumpteenthdev、ArtemKulakovずEgorAganinは、HMSをりォレットに統合するための貎重な貢献をしおくれたした。



䟿利なリンク






All Articles