Hallo Habr! Mein Name ist Andrey, ich mache die " Wallet " -Anwendung für Android. Seit mehr als sechs Monaten helfen wir Huawei-Smartphone-Nutzern, Einkäufe mit Bankkarten kontaktlos zu bezahlen - über NFC. Dazu mussten wir Unterstützung für HMS hinzufügen: Push Kit, Map Kit und Safety Detect. Unter dem Schnitt werde ich Ihnen sagen, welche Probleme wir während der Entwicklung lösen mussten, warum genau und was daraus wurde, und ein Testprojekt für ein schnelleres Eintauchen in das Thema teilen.
Um allen Benutzern neuer Huawei-Smartphones die Möglichkeit zu geben , sofort kontaktlos zu bezahlen und die beste Benutzererfahrung in anderen Szenarien zu gewährleisten, haben wir im Januar 2020 begonnen, neue Push-Benachrichtigungen, Karten und Sicherheitsüberprüfungen zu unterstützen. Das Ergebnis sollte das Erscheinen einer Version der Brieftasche in AppGallery mit mobilen Diensten sein, die für Huawei-Telefone typisch sind.
Folgendes haben wir in der Phase der ersten Studie herausgefunden.
- Huawei vertreibt AppGallery und HMS ohne Einschränkungen - Sie können sie herunterladen und auf Geräten anderer Hersteller installieren.
- Nachdem wir AppGallery auf Xiaomi Mi A1 installiert hatten, wurden alle Updates zunächst von der neuen Site abgerufen. Der Eindruck ist, dass AppGallery Zeit hat, Anwendungen schneller als die Konkurrenz zu aktualisieren.
- Huawei ist nun bestrebt, die AppGallery so schnell wie möglich mit Anwendungen zu füllen. Um die Migration zu HMS zu beschleunigen, haben sie beschlossen, Entwicklern eine bereits bekannte (ähnlich wie GMS) API zur Verfügung zu stellen .
- Bis das Huawei-Entwickler-Ökosystem voll funktionsfähig ist, wird der Mangel an Google-Diensten höchstwahrscheinlich das Hauptproblem für Benutzer neuer Huawei-Smartphones sein, und sie werden versuchen, sie mit allen Mitteln zu installieren .
Wir haben beschlossen, eine gemeinsame Version der Anwendung für alle Vertriebsstandorte zu erstellen. Sie muss in der Lage sein, zur Laufzeit den geeigneten Typ eines mobilen Dienstes zu identifizieren und zu verwenden. Diese Option schien langsamer zu implementieren als eine separate Version für jede Art von Dienst, aber wir hofften, in einer anderen zu gewinnen:
- Das Risiko, die für Google Play vorgesehene Version auf Huawei-Geräten zu erhalten und umgekehrt, ist ausgeschlossen.
- Sie können einen beliebigen Algorithmus für die Auswahl mobiler Dienste implementieren, einschließlich der Funktion zum Umschalten.
- Das Testen einer Anwendung ist einfacher als das Testen von zwei.
- Jede Version kann auf alle Distributionsseiten hochgeladen werden.
- Sie müssen während der Entwicklung / Änderung nicht vom Schreiben von Code zum Verwalten des Builds des Projekts wechseln.
Um mit verschiedenen Implementierungen mobiler Dienste in einer Version der Anwendung arbeiten zu können, müssen Sie:
- Verstecken Sie alle Abstraktionsanforderungen und sparen Sie Arbeit mit GMS.
- Fügen Sie eine Implementierung für HMS hinzu.
- Entwickeln Sie einen Mechanismus zur Auswahl der Implementierung von Diensten zur Laufzeit.
Die Methodik zur Implementierung der Push Kit- und Safety Detect-Unterstützung unterscheidet sich erheblich vom Map Kit, daher werden wir sie separat betrachten.
Unterstützung für Push Kit und Safety Detect
Wie es in solchen Fällen sein sollte, begann der Integrationsprozess mit dem Studium der Dokumentation . Die folgenden Punkte wurden im Warnabschnitt gefunden:
- Wenn die EMUI-Version auf einem Huawei-Gerät 10.0 oder höher ist, wird ein Token über die getToken-Methode zurückgegeben. Wenn die getToken-Methode nicht aufgerufen werden kann, speichert HUAWEI Push Kit die Token-Anforderung automatisch zwischen und ruft die Methode erneut auf. Ein Token wird dann über die onNewToken-Methode zurückgegeben.
- Wenn die EMUI-Version auf einem Huawei-Gerät älter als 10.0 ist und kein Token mit der Methode getToken zurückgegeben wird, wird ein Token mit der Methode onNewToken zurückgegeben.
- 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.
Die Hauptsache, die Sie von diesen Einschränkungen entfernen sollten, ist, dass es einen Unterschied gibt, ein Push-Token für verschiedene Versionen von EMUI zu erhalten . Nach dem Aufruf der Methode getToken () kann das echte Token durch Aufrufen der Methode onNewToken () des Dienstes zurückgegeben werden. Unsere Tests an realen Geräten haben gezeigt, dass Telefone mit EMUI <10.0 beim Aufrufen der getToken-Methode null oder eine leere Zeichenfolge zurückgeben. Danach wird die onNewToken () -Methode des Dienstes aufgerufen. Telefone mit EMUI> = 10.0 haben immer ein Push-Token von der Methode getToken () zurückgegeben.
Sie können eine solche Datenquelle implementieren, um die Arbeitslogik auf ein einziges Formular zu bringen:
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)
})
}
}
}
Wichtige Notizen:
- . , , 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.

-
- Wir erstellen für jede Art von Service eine separate Datenquelle.
- Fügen Sie ein Repository für Push-Benachrichtigungen und Sicherheit hinzu, die den Typ der mobilen Dienste als Eingabe akzeptieren, und wählen Sie eine bestimmte Datenquelle aus.
- Eine Entität der Geschäftslogik bestimmt, welche Art von mobilen Diensten (aus den verfügbaren) in einem bestimmten Fall geeignet ist.
Entwicklung eines Mechanismus zur Auswahl der Implementierung von Diensten zur Laufzeit
Wie gehe ich vor, wenn nur eine Art von Diensten auf dem Gerät installiert ist oder überhaupt keine, aber was ist zu tun, wenn sowohl Google- als auch Huawei-Dienste gleichzeitig installiert werden?
Folgendes haben wir gefunden und wo wir angefangen haben:
- Bei der Einführung neuer Technologien muss diese als Priorität verwendet werden, wenn das Gerät des Benutzers alle Anforderungen vollständig erfüllt.
- EMUI >= 10.0 - ;
- Huawei Google- EMUI 10.0 ;
- Huawei Google-, . , Google- ;
- AppGallery Huawei-, , .
Die Entwicklung des Algorithmus erwies sich als vielleicht das anstrengendste Geschäft. Viele technische und geschäftliche Faktoren kamen hier zusammen, aber am Ende konnten wir die beste Lösung für unser Produkt finden . Jetzt ist es sogar ein bisschen seltsam, dass die Beschreibung des am meisten diskutierten Teils des Algorithmus in einen Satz passt, aber ich bin froh, dass sich am Ende einfach herausstellte:
Wenn beide Arten von Diensten auf dem Gerät installiert sind und festgestellt werden konnte, dass die EMUI-Version <10 ist, verwenden wir Google, andernfalls verwenden wir Huawei.
Um den endgültigen Algorithmus zu implementieren, muss ein Weg gefunden werden, um die EMUI-Version auf dem Gerät des Benutzers zu bestimmen.
Eine Möglichkeit, dies zu tun, besteht darin, die Systemeigenschaften zu lesen:
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
}
}
Für die korrekte Durchführung von Sicherheitsüberprüfungen muss außerdem berücksichtigt werden, dass der Status der Dienste nicht aktualisiert werden muss.
Die endgültige Implementierung des Algorithmus unter Berücksichtigung der Art der Operation, für die der Dienst ausgewählt wird, und der Bestimmung der EMUI-Version des Geräts kann folgendermaßen aussehen:
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
}
}
Map Kit-Unterstützung
Nach der Implementierung des Algorithmus zur Auswahl von Diensten zur Laufzeit sieht der Algorithmus zum Hinzufügen von Unterstützung für die Grundfunktionalität von Karten trivial aus:
- Bestimmen Sie die Art der Dienste für die Anzeige von Karten.
- Blasen Sie das entsprechende Layout auf und arbeiten Sie mit einer bestimmten Kartenimplementierung.
Es gibt hier jedoch eine Funktion, über die ich sprechen möchte. Mit Rx des Gehirns können Sie fast überall asynchrone Vorgänge hinzufügen, ohne das Risiko, die gesamte Anwendung neu zu schreiben, aber es gibt auch eigene Einschränkungen. In diesem Fall müssen Sie beispielsweise, um das geeignete Layout zu bestimmen, höchstwahrscheinlich .blockingGet () irgendwo im Hauptthread aufrufen, was überhaupt nicht gut ist. Sie können dieses Problem beispielsweise mithilfe von untergeordneten Fragmenten lösen:
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)
}
}
}
Jetzt können Sie eine separate Implementierung schreiben, um mit der Karte für jedes einzelne Fragment zu arbeiten. Wenn Sie dieselbe Logik implementieren müssen, können Sie dem bekannten Algorithmus folgen: Passen Sie die Arbeit mit jedem Kartentyp unter einer Schnittstelle an und übergeben Sie eine der Implementierungen dieser Schnittstelle an das übergeordnete Fragment, wie in MapFragment.onMapReady () beschrieben.
Was ist daraus geworden?
In den ersten Tagen nach der Veröffentlichung der aktualisierten Version der Anwendung erreichte die Anzahl der Installationen 1 Million. Wir führen dies teilweise auf die Funktion von AppGallery und teilweise auf die Tatsache zurück, dass unsere Veröffentlichung von mehreren Medien und Bloggern hervorgehoben wurde. Und auch mit der Geschwindigkeit, mit der Anwendungen aktualisiert werden - schließlich war die Version mit dem höchsten Versionscode zwei Wochen lang in AppGallery.
Wir erhalten nützliche Rückmeldungen zur Anwendung im Allgemeinen und zur Tokenisierung von Bankkarten im Besonderen von Benutzern in unserem Thread auf w3bsit3-dns.com. Nach der Veröffentlichung der Pay-Funktionalität für Huawei hat die Anzahl der Besucher im Forum zugenommen, ebenso wie die Probleme, mit denen sie konfrontiert sind. Wir arbeiten weiterhin an allen Einsprüchen, stellen jedoch keine massiven Probleme fest.
Im Allgemeinen war die Veröffentlichung der Anwendung in AppGallery erfolgreich und wir können daraus schließen, dass sich unser Ansatz zur Lösung des Problems als erfolgreich erwiesen hat. Dank der gewählten Implementierungsmethode können wir weiterhin alle Anwendungsversionen sowohl auf Google Play als auch auf AppGallery hochladen.
Mit dieser Methode haben wir der Anwendung Analytics Kit , das APM , hinzugefügt, um das Account Kit zu unterstützen, und planen nicht, dort anzuhalten. Umso mehr werden mit jeder neuen Version HMS noch mehr Möglichkeiten verfügbar .
Nachwort
Das Registrieren eines Entwicklerkontos bei AppGallery ist viel komplizierter als bei Google. Zum Beispiel habe ich 9 Tage gebraucht, um meine Identität zu überprüfen. Ich denke nicht, dass dies jedem passiert, aber jede Verzögerung kann den Optimismus verringern. Daher habe ich zusammen mit dem vollständigen Code der gesamten im Artikel beschriebenen Demolösung alle Anwendungsschlüssel für das Repository festgeschrieben, damit Sie nicht nur die Möglichkeit haben, die Lösung als Ganzes zu bewerten, sondern auch jetzt den vorgeschlagenen Ansatz zu testen und zu verbessern.
Über den Ausgang zum öffentlichen Raum möchte ich dem gesamten Wallet-Team und insbesondere dankenumpteenthdev, Artem Kulakov und Egor Aganin für ihren unschätzbaren Beitrag zur Integration von HMS in die Brieftasche!
Nützliche Links
- Vollständiger Demo-Projektcode auf GitHub;
- AppGallery . HMS-Core AppGallery;
- Push Kit codelab;
- Map Kit codelab;
- Safety Detect codelab;
- - Huawei. AppGallery Connect;
- «» 4PDA.