Di balik dua layanan seluler: HMS dan GMS dalam satu aplikasi





Halo, Habr! Nama saya Andrey, saya membuat aplikasi " Dompet " untuk Android. Selama lebih dari enam bulan, kami telah membantu pengguna ponsel cerdas Huawei membayar pembelian dengan kartu bank tanpa kontak - melalui NFC. Untuk melakukan ini, kami perlu menambahkan dukungan untuk HMS: Push Kit, Map Kit, dan Safety Detect. Di bawah potongan, saya akan memberi tahu Anda masalah apa yang harus kami selesaikan selama pengembangan, mengapa tepatnya dan apa yang terjadi, dan juga membagikan proyek uji untuk perendaman yang lebih cepat dalam topik.



Untuk memberikan semua pengguna ponsel cerdas Huawei baru kemampuan membayar tanpa kontak langsung dan memberikan pengalaman pengguna yang lebih baik dalam skenario lain, pada Januari 2020, kami mulai bekerja untuk mendukung pemberitahuan push baru, kartu, dan pemeriksaan keamanan. Hasilnya seharusnya adalah tampilan di AppGallery dari versi Wallet dengan layanan seluler asli ponsel Huawei.



Inilah yang kami temukan pada tahap pengembangan awal



  • Huawei mendistribusikan AppGallery dan HMS tanpa batasan - Anda dapat mengunduh dan menginstalnya di perangkat dari produsen lain;
  • Setelah kami menginstal AppGallery di Xiaomi Mi A1, semua pembaruan mulai ditarik pertama-tama dari situs baru. Kesannya adalah AppGallery punya waktu untuk mengupdate aplikasi lebih cepat dari kompetitor;
  • Huawei sekarang berupaya untuk mengisi AppGallery dengan aplikasi secepat mungkin. Untuk mempercepat migrasi ke HMS, mereka memutuskan untuk menyediakan API familiar (mirip dengan GMS) bagi pengembang ;
  • Pada awalnya, hingga ekosistem pengembang Huawei beroperasi penuh, kurangnya layanan Google kemungkinan besar akan menjadi masalah utama bagi pengguna smartphone Huawei baru, dan mereka akan mencoba menginstalnya dengan segala cara .


Kami memutuskan untuk membuat satu versi umum dari aplikasi untuk semua situs distribusi. Dia harus dapat mengidentifikasi dan menggunakan jenis layanan seluler yang sesuai pada waktu proses. Opsi ini tampaknya lebih lambat untuk diterapkan daripada versi terpisah untuk setiap jenis layanan, tetapi kami berharap bisa menang di yang lain:



  • Menghilangkan risiko mendapatkan versi yang ditujukan untuk Google Play di perangkat Huawei dan sebaliknya;
  • Anda dapat menerapkan algoritme apa pun untuk memilih layanan seluler, termasuk menggunakan fitur toggle;
  • Menguji satu aplikasi lebih mudah daripada menguji dua aplikasi;
  • Setiap rilis dapat diunggah ke semua situs distribusi;
  • Anda tidak perlu beralih dari menulis kode ke mengelola build proyek selama pengembangan / modifikasi.


Untuk bekerja dengan implementasi layanan seluler yang berbeda dalam satu versi aplikasi, Anda harus:



  1. Sembunyikan semua permintaan abstraksi, simpan pekerjaan dengan GMS;
  2. Tambahkan implementasi untuk HMS;
  3. Kembangkan mekanisme untuk memilih implementasi layanan saat runtime.


Metodologi untuk menerapkan Push Kit dan dukungan Safety Detect sangat berbeda dengan Map Kit, jadi kami akan mempertimbangkannya secara terpisah.



Dukungan Kit Dorong dan Deteksi Keamanan



Seharusnya dalam kasus seperti itu, proses integrasi dimulai dengan mempelajari dokumentasi . Poin-poin berikut ditemukan di bagian peringatan:

  • Jika versi EMUI adalah 10.0 atau yang lebih baru pada perangkat Huawei, token akan dikembalikan melalui metode getToken. Jika metode getToken gagal dipanggil, HUAWEI Push Kit secara otomatis menyimpan permintaan token ke dalam cache dan memanggil metode itu lagi. Token kemudian akan dikembalikan melalui metode onNewToken.
  • Jika versi EMUI pada perangkat Huawei lebih lama dari 10.0 dan tidak ada token yang dikembalikan menggunakan metode getToken, token akan dikembalikan menggunakan metode 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.


Hal utama yang harus diambil dari peringatan ini adalah bahwa ada perbedaan dalam mendapatkan token push pada versi EMUI yang berbeda . Setelah memanggil metode getToken (), token asli bisa dikembalikan dengan memanggil metode onNewToken () layanan. Pengujian kami pada perangkat nyata menunjukkan bahwa ponsel dengan EMUI <10.0 mengembalikan null atau string kosong saat metode getToken dipanggil, setelah itu metode onNewToken () dari layanan dipanggil. Ponsel dengan EMUI> = 10.0 selalu mengembalikan token push dari metode getToken ().



Anda dapat mengimplementasikan sumber data seperti itu untuk membawa logika pekerjaan ke dalam satu bentuk:



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


Catatan penting:



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


-



  • Kami membuat sumber data terpisah untuk setiap jenis layanan;
  • Tambahkan repositori untuk pemberitahuan push dan keamanan yang menerima jenis layanan seluler sebagai input dan pilih sumber data tertentu;
  • Beberapa entitas logika bisnis menentukan jenis layanan seluler (dari yang tersedia) yang sesuai untuk digunakan dalam kasus tertentu.


Pengembangan mekanisme untuk memilih implementasi layanan saat runtime



Bagaimana cara melanjutkan jika hanya satu jenis layanan yang diinstal di perangkat atau tidak ada sama sekali, tetapi apa yang harus dilakukan jika layanan Google dan Huawei diinstal secara bersamaan?



Inilah yang kami temukan dan tempat kami memulai:



  • Saat memperkenalkan teknologi baru apa pun, itu harus digunakan sebagai prioritas jika perangkat pengguna sepenuhnya memenuhi semua persyaratan;
  • EMUI >= 10.0 - ;
  • Huawei Google- EMUI 10.0 ;
  • Huawei Google-, . , Google- ;
  • AppGallery Huawei-, , .


Pengembangan algoritme mungkin merupakan hal yang paling melelahkan. Banyak faktor teknis dan bisnis berkumpul di sini, tetapi pada akhirnya kami dapat memberikan solusi terbaik untuk produk kami . Sekarang ini bahkan sedikit aneh bahwa deskripsi dari bagian algoritma yang paling banyak didiskusikan cocok dengan satu kalimat, tetapi saya senang bahwa pada akhirnya ternyata:

Jika kedua jenis layanan diinstal pada perangkat dan dimungkinkan untuk menentukan bahwa versi EMUI <10 - kami menggunakan Google, jika tidak kami menggunakan Huawei.


Untuk mengimplementasikan algoritma terakhir, diperlukan cara untuk menentukan versi EMUI pada perangkat pengguna.



Salah satu cara untuk melakukannya adalah dengan membaca properti sistem:



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


Untuk pelaksanaan pemeriksaan keamanan yang benar, perlu diperhatikan juga bahwa status layanan tidak memerlukan pembaruan.



Implementasi akhir dari algoritme, dengan mempertimbangkan jenis operasi yang dipilih layanannya, dan menentukan versi EMUI perangkat, mungkin terlihat seperti ini:




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


Dukungan Map Kit



Setelah implementasi algoritma untuk memilih layanan pada saat runtime, algoritma untuk menambahkan dukungan untuk fungsionalitas dasar peta terlihat sepele:



  1. Tentukan jenis layanan untuk menampilkan peta;
  2. Kembangkan tata letak yang sesuai dan bekerja dengan implementasi peta tertentu.


Namun, ada satu fitur di sini yang ingin saya bicarakan. Rx otak memungkinkan Anda untuk menambahkan operasi asinkron hampir di mana saja tanpa risiko menulis ulang seluruh aplikasi, tetapi juga memberlakukan batasannya sendiri. Misalnya, dalam kasus ini, untuk menentukan tata letak yang sesuai, kemungkinan besar, Anda perlu memanggil .blockingGet () di suatu tempat di utas Utama, yang sama sekali tidak bagus. Anda dapat mengatasi masalah ini, misalnya, menggunakan fragmen turunan:



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


Sekarang Anda dapat menulis implementasi terpisah untuk bekerja dengan peta untuk setiap fragmen individu. Jika Anda perlu mengimplementasikan logika yang sama, Anda dapat mengikuti algoritme yang sudah dikenal - sesuaikan pekerjaan dengan setiap jenis peta dalam satu antarmuka dan teruskan salah satu implementasi antarmuka ini ke fragmen induk, seperti yang dilakukan di MapFragment.onMapReady ()



Apa hasilnya



Pada hari-hari pertama setelah rilis versi aplikasi yang diperbarui, jumlah penginstalan mencapai 1 juta. Kami menghubungkan ini sebagian dengan fitur unggulan dari AppGallery, dan sebagian lagi karena rilis kami disorot oleh beberapa media dan blogger. Dan juga dengan kecepatan memperbarui aplikasi - lagipula, versi dengan versionCode tertinggi ada di AppGallery selama dua minggu.



Kami menerima umpan balik yang berguna tentang pengoperasian aplikasi secara umum dan tokenisasi kartu bank khususnya dari pengguna di utas kami di w3bsit3-dns.com. Setelah rilis fungsionalitas Pay untuk Huawei, forum tersebut mengalami peningkatan jumlah pengunjung, dan begitu pula masalah yang mereka hadapi. Kami terus mengerjakan semua pengajuan banding, tetapi kami tidak melihat adanya masalah besar.



Secara umum, rilis aplikasi di AppGallery berhasil dan kami dapat menyimpulkan bahwa pendekatan kami untuk memecahkan masalah ternyata berhasil. Berkat metode penerapan yang dipilih, kami masih memiliki kemampuan untuk mengunggah semua rilis aplikasi baik di Google Play dan AppGallery.



Menggunakan metode ini, kami telah menambahkan ke aplikasi Analytics Kit , APM , bekerja untuk mendukung Account Kit dan tidak berencana untuk berhenti di situ, terlebih lagi dengan setiap versi baru menjadi tersedia HMS masih lebih banyak peluang .



Kata Penutup



Mendaftarkan akun pengembang dengan AppGallery jauh lebih rumit daripada Google. Bagi saya misalnya, tahap verifikasi verifikasi identitas memakan waktu 9 hari. Saya tidak berpikir ini terjadi pada semua orang, tetapi penundaan apa pun dapat mengurangi optimisme. Oleh karena itu, bersama dengan kode lengkap dari seluruh solusi demo yang dijelaskan dalam artikel, saya telah memasukkan semua kunci aplikasi ke repositori sehingga Anda memiliki kesempatan tidak hanya untuk mengevaluasi solusi secara keseluruhan, tetapi juga sekarang untuk menguji dan meningkatkan pendekatan yang diusulkan.



Menggunakan pintu keluar ke ruang publik, saya ingin berterima kasih kepada seluruh tim Wallet dan khususnyaumpteenthdev, Artem Kulakov dan Egor Aganin atas kontribusinya yang tak ternilai pada integrasi HMS ke dalam Dompet!



tautan berguna






All Articles