Passa al contenuto principale

Servizio Android

Adattato da: Discreet Log #11: Integrare i processi FFI con i servizi Android

Oltre alla necessità di fare vecchie e cari semplici chiamate a metodi nella libreria Cwtch, dobbiamo anche essere in grado di comunicare con (e ricevere eventi da) goroutine Cwtch di lunga durata che mantengono il processo Tor in esecuzione in background, gestire lo stato della connessione e della conversazione per tutti i contatti di un utente e prenderci cura di alcuni altri compiti di monitoraggio e manutenzione. Questo non è davvero un problema su sistemi operativi desktop che sono in grado di fare multitasking tradizionale, ma su dispositivi mobili dotati di Android dobbiamo fare i conti con sessioni più brevi, scarichi frequenti, e restrizioni di rete e di batteria che possono variare nel tempo. Poiché Cwtch è progettato per essere resistente ai metadati e incentrato sulla privacy, vogliamo anche fornire notifiche senza utilizzare il servizio di notifica push di Google.

La soluzione per applicazioni di rete che girano a lungo come Cwtch è quello di mettere il nostro codice FFI in un servizio Android di primo piano (foreground). (E no, non ci sfugge che il codice per il nostro backend sia inserito in qualcosa chiamato ForegroundService.) Con un po' di astuzia, l'API WorkManager ci consente di creare e gestire vari tipi di servizi, inclusi ForegroundServices. Questa si è rivelata un'ottima scelta per noi, poiché il nostro gestore FFI gomobile era già scritto in Kotlin e WorkManager ci consente di specificare una coroutine Kotlin da invocare come servizio.

Per chi volesse seguire i dettagli, le nostre specifiche di WorkManager vengono create nel metodo handleCwtch() di MainActivity.kt, e i worker stessi sono definiti in FlwtchWorker.kt.

Le nostre care vecchie semplici chiamate di metodo alle routine FFI sono anche aggiornate per essere fatte come richieste di lavoro di WorkManager, che ci permette di restituire comodamente i valori di ritorno tramite il callback del risultato.

Una chiamata iniziale (giustamente chiamata Start) viene dirottata da FlwtchWorker per diventare il nostro circuito eventbus. Dal momento che FlwtchWorker è una coroutine, è facile per essa rendere e riprendere come necessario in attesa di eventi da generare. Le goroutine di Cwtch possono quindi emettere eventi, che saranno raccolti da FlwtchWorker e spediti in modo appropriato.

Il circuito eventbus di FlwtchWorker non è solo un noioso sistema di inoltro. Deve controllare alcuni tipi di messaggi che influenzano lo stato di Android; ad esempio, eventi realtivi a nuovi messaggi dovrebbero tipicamente visualizzare le notifiche su cui l'utente può cliccare per andare alla finestra di conversazione appropriata, anche se l'applicazione non è in esecuzione in primo piano. Quando viene il momento di inoltrare l'evento all'app, utilizziamo LocalBroadcastManager per madare la notifica a MainActivity.onIntent. Da lì, utilizziamo a sua volta i MethodChannels di Flutter per inoltrare i dati dell’evento da Kotlin al motore Flutter del frontend, dove l'evento viene finalmente analizzato dal codice Dart che aggiorna l'interfaccia utente come necessario.

I messaggi e altri stati permanenti sono memorizzati sul disco dal servizio, quindi il frontend non deve essere aggiornato se l'applicazione non è aperta. Tuttavia, alcune cose (come date e messaggi non letti) possono portare alla desincronizzazione tra il frontend e il backend, e quindi facciamo un controllo all'avvio/ripresa dell'app per vedere se abbiamo bisogno di reinizializzare Cwtch e/o risincronizzare lo stato dell'interfaccia utente.

Infine, nel corso dell'implementazione di questi servizi su Android abbiamo osservato che WorkManager è ottimo con la persistenza di vecchio lavoro in coda, al punto che vecchi worker venivano addirittura ripresi dopo le reinstallazioni dell'app! L'aggiunta di chiamate a pruneWork() aiuta a mitigare questo problema, purché l'app sia stata chiusa correttamente e i vecchi lavori siano stati correttamente annullati. Tuttavia, questo spesso non è il caso su Android, quindi come ulteriore mitigazione abbiamo trovato utile taggare il lavoro con il nome della directory della libreria nativa:

    private fun getNativeLibDir(): String {
val ainfo = this.applicationContext.packageManager.getApplicationInfo(
"im.cwtch.flwtch", // deve esse il nome dell'app
PackageManager.GET_SHARED_LIBRARY_FILES)
return ainfo.nativeLibraryDir
}

…quindi, ogni volta che viene avviata l’app, cancelliamo tutti i lavori che non sono taggati con la corretta directory della libreria corrente. Poiché il nome di questa directory cambia tra le installazioni dell'app, questa tecnica ci impedisce di riprendere accidentalmente con un worker obsoleto.