مرحبا هبر! اسمي أندري ، أنا أقوم بإنشاء تطبيق " Wallet " لنظام Android. منذ أكثر من نصف عام حتى الآن ، نساعد مستخدمي الهواتف الذكية من Huawei على دفع ثمن المشتريات بالبطاقات المصرفية بدون تلامس - عبر NFC. للقيام بذلك ، احتجنا إلى إضافة دعم لـ HMS: Push Kit و Map Kit و Safety Detect. تحت الخفض ، سأخبرك بالمشكلات التي كان علينا حلها أثناء التطوير ، ولماذا بالضبط وماذا نتج عنها ، وأيضًا مشاركة مشروع اختباري من أجل الانغماس السريع في الموضوع.
من أجل تزويد جميع مستخدمي هواتف Huawei الذكية الجديدة بالدفع بدون تلامس خارج الصندوق ولضمان تجربة مستخدم أفضل في السيناريوهات الأخرى ، بدأنا في يناير 2020 العمل لدعم إشعارات الدفع الجديدة والبطاقات وفحوصات الأمان. كان ينبغي أن تكون النتيجة ظهور إصدار من Wallet في AppGallery مع خدمات الهاتف المحمول الأصلية لهواتف Huawei.
هذا ما اكتشفناه في مرحلة التطوير الأولي
- توزع Huawei AppGallery و HMS دون قيود - يمكنك تنزيلهما وتثبيتهما على الأجهزة من الشركات المصنعة الأخرى ؛
- بعد أن قمنا بتثبيت AppGallery على Xiaomi Mi A1 ، بدأ سحب جميع التحديثات أولاً وقبل كل شيء من الموقع الجديد. الانطباع هو أن AppGallery لديها الوقت لتحديث التطبيقات بشكل أسرع من المنافسين ؛
- تسعى Huawei الآن جاهدة لملء AppGallery بالتطبيقات في أسرع وقت ممكن. لتسريع الترحيل إلى HMS ، قرروا تزويد المطورين بواجهة برمجة تطبيقات مألوفة (تشبه GMS) ؛
- في البداية ، حتى يتم تشغيل النظام البيئي لمطوري Huawei بالكامل ، من المرجح أن يكون نقص خدمات Google هو المشكلة الرئيسية لمستخدمي هواتف Huawei الذكية الجديدة ، وسيحاولون تثبيتها بكل الطرق .
قررنا إنشاء إصدار مشترك واحد من التطبيق لجميع مواقع التوزيع. يجب أن تكون قادرة على تحديد النوع المناسب من خدمة الهاتف المحمول واستخدامها في وقت التشغيل. بدا هذا الخيار أبطأ في التنفيذ من إصدار منفصل لكل نوع من أنواع الخدمة ، لكننا كنا نأمل في الفوز بأخرى:
- يزيل مخاطر الحصول على الإصدار المخصص لـ Google Play على أجهزة Huawei والعكس صحيح ؛
- يمكنك تنفيذ أي خوارزمية لاختيار خدمات الجوال ، بما في ذلك استخدام ميزة تبديل ؛
- اختبار تطبيق واحد أسهل من اختبار تطبيقين ؛
- يمكن تحميل كل إصدار على جميع مواقع التوزيع ؛
- لست مضطرًا للتبديل من كتابة التعليمات البرمجية إلى إدارة بناء المشروع أثناء التطوير / التعديل.
للعمل مع تطبيقات مختلفة لخدمات الهاتف المحمول في إصدار واحد من التطبيق ، يجب عليك:
- إخفاء جميع طلبات التجريد وحفظ العمل باستخدام GMS ؛
- إضافة تطبيق HMS ؛
- وضع آلية لاختيار تنفيذ الخدمات في وقت التشغيل.
تختلف منهجية تنفيذ أدوات Push Kit ودعم اكتشاف السلامة اختلافًا كبيرًا عن Map Kit ، لذلك سننظر فيها بشكل منفصل.
دعم أدوات الدفع وكشف السلامة
كما يجب أن يكون في مثل هذه الحالات ، بدأت عملية الدمج بدراسة الوثائق . تم العثور على النقاط التالية في قسم التحذير:
- إذا كان إصدار EMUI هو 10.0 أو أحدث على جهاز Huawei ، فسيتم إرجاع رمز مميز من خلال طريقة getToken. إذا فشل استدعاء طريقة getToken ، فإن HUAWEI Push Kit تخزن طلب الرمز تلقائيًا مؤقتًا وتستدعي الطريقة مرة أخرى. سيتم بعد ذلك إرجاع الرمز المميز من خلال طريقة onNewToken.
- إذا كان إصدار EMUI على جهاز Huawei أقدم من 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 () للخدمة. الهواتف التي تحتوي على 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 و Huawei في نفس الوقت؟
هذا ما وجدناه وأين بدأنا:
- عند إدخال أي تقنية جديدة ، يجب استخدامها كأولوية إذا كان جهاز المستخدم يفي تمامًا بجميع المتطلبات ؛
- EMUI >= 10.0 - ;
- Huawei Google- EMUI 10.0 ;
- Huawei Google-, . , Google- ;
- AppGallery Huawei-, , .
كان تطوير الخوارزمية ، ربما ، العمل الأكثر إرهاقًا. تقاربت العديد من العوامل التقنية والتجارية هنا ، ولكن في النهاية تمكنا من التوصل إلى أفضل حل لمنتجنا . من الغريب الآن أن وصف الجزء الأكثر مناقشة من الخوارزمية يتناسب مع جملة واحدة ، لكنني سعيد لأنه اتضح في النهاية ببساطة:
إذا تم تثبيت كلا النوعين من الخدمات على الجهاز وكان من الممكن تحديد أن إصدار EMUI هو <10 - فإننا نستخدم Google ، وإلا فإننا نستخدم Huawei.
لتنفيذ الخوارزمية النهائية ، يلزم إيجاد طريقة لتحديد إصدار 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
}
}
دعم مجموعة الخرائط
بعد تنفيذ الخوارزمية لاختيار الخدمات في وقت التشغيل ، تبدو خوارزمية إضافة دعم للوظائف الأساسية للخرائط تافهة:
- تحديد نوع الخدمات لعرض الخرائط ؛
- تضخيم التخطيط المناسب والعمل مع تنفيذ خريطة محددة.
ومع ذلك ، هناك ميزة واحدة هنا أريد التحدث عنها. يسمح لك 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 ()
ما جاء منه
في الأيام الأولى بعد إصدار الإصدار المحدث من التطبيق ، وصل عدد عمليات التثبيت إلى مليون. ونعزى هذا جزئيًا إلى الميزة المميزة من AppGallery ، وجزئيًا إلى حقيقة أن إصدارنا تم تسليط الضوء عليه من قبل العديد من الوسائط والمدونين. وأيضًا مع سرعة تحديث التطبيقات - بعد كل شيء ، كان الإصدار الذي يحتوي على أعلى إصدار من CodeCode موجودًا في AppGallery لمدة أسبوعين.
نتلقى تعليقات مفيدة حول تشغيل التطبيق بشكل عام وحول ترميز البطاقات المصرفية بشكل خاص من المستخدمين في موضوعنا على w3bsit3-dns.com. بعد إصدار وظيفة الدفع لهواوي ، زاد المنتدى في عدد الزوار ، وبالتالي يواجهون المشاكل التي يواجهونها. نواصل العمل على جميع النداءات ، لكننا لا نلاحظ أي مشاكل كبيرة.
بشكل عام ، كان إصدار التطبيق في AppGallery ناجحًا ويمكننا أن نستنتج أن نهجنا في حل المشكلة قد نجح. بفضل طريقة التنفيذ المختارة ، لا يزال لدينا القدرة على تحميل جميع إصدارات التطبيق على Google Play و AppGallery.
باستخدام هذه الطريقة ، أضفنا إلى تطبيق Analytics Kit ، APM ، نعمل على دعم Account Kit ولا نخطط للتوقف عند هذا الحد ، وكلما زاد كل إصدار جديد أصبح متاحًا HMS المزيد من الفرص .
خاتمة
يعد تسجيل حساب مطور في AppGallery أكثر تعقيدًا من حساب Google. على سبيل المثال ، استغرق الأمر 9 أيام للتحقق من هويتي. لا أعتقد أن هذا يحدث للجميع ، لكن أي تأخير يمكن أن يقلل من التفاؤل. لذلك ، جنبًا إلى جنب مع الكود الكامل للحل التجريبي بالكامل الموضح في المقالة ، فقد ألزمت جميع مفاتيح التطبيق بالمستودع حتى تتاح لك الفرصة ليس فقط لتقييم الحل ككل ، ولكن أيضًا في الوقت الحالي لاختبار النهج المقترح وتحسينه.
باستخدام الخروج إلى الفضاء العام ، أود أن أشكر فريق Wallet بأكمله وخاصةًاومبتينثديف، Artem Kulakov و Egor Aganin لمساهمتهم التي لا تقدر بثمن في دمج HMS في Wallet!
روابط مفيدة
- كود مشروع تجريبي كامل على جيثب ؛
- AppGallery . HMS-Core AppGallery;
- Push Kit codelab;
- Map Kit codelab;
- Safety Detect codelab;
- - Huawei. AppGallery Connect;
- «» 4PDA.