Zum Hauptinhalt springen

Android Dienst

Angepasst von: Discreet Log #11: Integration von FFI-Prozessen mit Android-Diensten

Zusätzlich zu der Notwendigkeit, einfache Methodenaufrufe in die Cwtch-Bibliothek zu machen, müssen wir auch in der Lage sein, mit langlaufenden Cwtch-Go-Routinen zu kommunizieren (und Ereignisse von ihnen zu empfangen), die den Tor-Prozess im Hintergrund laufen lassen, den Verbindungs- und Gesprächsstatus für alle deine Kontakte verwalten und einige andere Überwachungs- und Wartungsaufgaben erledigen. Auf herkömmlichen Multitasking-Desktop-Betriebssystemen ist dies kein wirkliches Problem, aber auf mobilen Geräten mit Android müssen wir mit kürzeren Sitzungen, häufigen Entladevorgängen sowie Netzwerk- und Energiebeschränkungen zurechtkommen, die sich im Laufe der Zeit ändern können. Da Cwtch metadatenresistent und datenschutzorientiert sein soll, wollen wir auch Benachrichtigungen bereitstellen, ohne den Google-Push-Benachrichtigungsdienst zu nutzen.

Die Lösung für langlaufende Netzwerkanwendungen wie Cwtch besteht darin, unseren FFI-Code in einen Android Foreground Service zu integrieren. (Und nein, es ist mir nicht entgangen, dass der Code für unser Backend in einem sogenannten ForegroundService untergebracht ist) Die WorkManager-API ermöglicht es uns, mit ein wenig Fingerspitzengefühl verschiedene Arten von Diensten zu erstellen und zu verwalten, darunter auch ForegroundServices. Dies erwies sich als eine gute Wahl für uns, da unser gomobile FFI-Handler bereits in Kotlin geschrieben war und WorkManager uns erlaubt, eine Kotlin-Coroutine zu spezifizieren, die als Dienst aufgerufen wird.

Wenn Sie uns folgen wollen, unsere WorkManager-Spezifikationen werden in der handleCwtch()-Methode von MainActivity.kt erstellt, und die Worker selbst sind in FlwtchWorker.kt definiert.

Unsere einfachen Methodenaufrufe an FFI-Routinen werden auch als WorkManager-Arbeitsanforderungen aktualisiert, so dass wir die Rückgabewerte bequem über den Ergebnis-Callback zurückgeben können.

Ein erster Aufruf (passenderweise Start genannt) wird von FlwtchWorker gekapert, um unsere Eventbus-Schleife zu werden. Da es sich bei FlwtchWorker um eine Co-Routine handelt, ist es für sie ein Leichtes, bei Bedarf aufzugeben und fortzufahren, während sie darauf wartet, dass Ereignisse erzeugt werden. Die Goroutinen von Cwtch können dann Ereignisse ausgeben, die von FlwtchWorker aufgegriffen und entsprechend weitergeleitet werden.

Die Eventbus-Schleife von FlwtchWorker ist nicht nur ein langweiliger Forwarder. Es muss auf bestimmte Nachrichtentypen geprüft werden, die den Android-Status beeinflussen. So sollten beispielsweise bei neuen Nachrichtenereignissen normalerweise Benachrichtigungen angezeigt werden, auf die der Benutzer klicken kann, um das entsprechende Konversationsfenster aufzurufen, auch wenn die App nicht im Vordergrund läuft. Wenn es an der Zeit ist, das Ereignis an die Anwendung weiterzuleiten, verwenden wir LocalBroadcastManager, um die Benachrichtigung an MainActivity.onIntent zu erhalten. Von dort aus verwenden wir wiederum Flutter MethodChannels, um die Ereignisdaten von Kotlin an die Flutter-Engine des Frontends weiterzuleiten, wo das Ereignis schließlich von Dart-Code geparst wird, der die Benutzeroberfläche nach Bedarf aktualisiert.

Nachrichten und andere permanente Zustände werden vom Dienst auf der Festplatte gespeichert, so dass das Frontend nicht aktualisiert werden muss, wenn die Anwendung nicht geöffnet ist. Einige Dinge (wie z. B. Daten und ungelesene Nachrichten) können dann jedoch zu Desynchronisationen zwischen Front- und Backend führen, so dass wir dies beim Starten/Fortsetzen der Anwendung überprüfen, um zu sehen, ob wir Cwtch neu initialisieren und/oder den UI-Status neu synchronisieren müssen.

Schließlich haben wir bei der Implementierung dieser Dienste unter Android festgestellt, dass WorkManager sehr gut darin ist, alte Warteschlangen aufrechtzuerhalten, und zwar so gut, dass alte Worker sogar nach einer Neuinstallation der Anwendung wieder aufgenommen wurden! Das Hinzufügen von Aufrufen zu pruneWork() hilft, dies abzumildern, solange die Anwendung ordnungsgemäß heruntergefahren wurde und alte Aufträge ordnungsgemäß abgebrochen wurden. Dies ist unter Android jedoch häufig nicht der Fall, so dass wir es als zusätzliche Abschwächung für sinnvoll erachten, die Arbeit mit dem Namen des nativen Bibliotheksverzeichnisses zu kennzeichnen:

    private fun getNativeLibDir(): String {
val ainfo = this.applicationContext.packageManager.getApplicationInfo(
"im.cwtch.flwtch", // Must be app name
PackageManager.GET_SHARED_LIBRARY_FILES)
return ainfo.nativeLibraryDir
}

... dann brechen wir bei jedem Start der Anwendung alle Aufträge ab, die nicht mit dem richtigen aktuellen Bibliotheksverzeichnis gekennzeichnet sind. Da sich der Name dieses Verzeichnisses bei jeder Installation der Anwendung ändert, verhindert diese Technik, dass wir versehentlich mit einem veralteten Service Worker fortfahren.