Geolocalizzazione nei dispositivi mobili: il paradigma di sviluppo
Nella prima parte della serie sono state passate in rassegna le modalità con cui un dispositivo mobile è in grado di determinare la posizione in particolare facendo uso del GPS per ottenere la massima accuratezza possibile. In questa seconda parte entriamo più nel vivo dei paradigmi di sviluppo e delle API disponibili. Concluderemo il nostro discorso nel prossimo articolo, in cui vedremo alcuni “trucchi” operativi di cui occorre essere a conoscenza per ottenere il meglio da questo tipo di applicazioni.
Questione di energia
In un dispositivo mobile l’energia è fondamentale, per cui tutto va pensato allo scopo di risparmiare quanto più possibile le batterie; ma occorre ricordare che il rilevatore GPS è tipicamente tra i sensori che consumano di più in uno smartphone. Questa considerazione è stata alla base della scelta progettuale — sia per la piattaforma Android che per iOS — di definire un particolare paradigma per utilizzare i servizi di localizzazione.
Il paradigma adottato è quello di fornire delle API non tanto per la misura continua della posizione, come ci si potrebbe aspettare, quanto notificare all’applicazione utente, tramite eventi, le variazioni della posizione con una frequenza data e ogni volta che questa supera una certa distanza fissata. Questa modalità di utilizzo del sensore consente ai produttori di smartphone di implementare strategie di ottimizzazione nell’utilizzo fisico del rilevatore GPS.
Per fare un esempio limite, se si desidera eseguire il tracking di un veicolo che corre alla velocità di 130 km/h lungo un tratto di 10 km si deve tenere acceso il GPS per più di 276 secondi. Se però è sufficiente che il tracking sia eseguito per campioni distanti circa 50 m l’uno dall’altro — in pratica si approssima la traiettoria continua con una linea una spezzata di punti equidistanti — basta misurare la posizione di 200 punti.
Come abbiamo visto, i sistemi GPS necessitano di un certo tempo per “entrare a regime”, ossia affinché sia risolta la sincronizzazione temporale con i satelliti. Una volta a regime, per misurare un singolo punto, è necessario tenere acceso il sistema il tempo necessario per osservare le variazioni del segnale pseudo-random in modo da determinare con univocità il momento dell’emissione dalla sequenza nota dal satellite.
Le variazioni della sequenza binaria si succedono a una frequenza di circa 1 Mbps; quindi osservare ad esempio 1000 variazioni del segnale impiegherebbe 1 ms. Per determinare un punto c’è bisogno di eseguire questa misura per almeno 4 satelliti, quindi misurare 200 punti richiede di tenere “acceso”, in modo discontinuo, il rilevatore GPS per 800 ms.
Ovviamente il consumo di batteria è proporzionale al tempo di utilizzo del sensore da cui si evince che, nel primo caso, avremmo un consumo oltre 300 volte superiore con una diretta conseguenza sulla durata delle stesse.
Il ragionamento fatto è puramente teorico: che tipo di strategie utilizzino i produttori non è dato a sapere al possessore del device, a meno che non abbiate installato qualche “mod” open source. Tuttavia, implementando applicazioni che usano il GPS in modo “estremo”, si finirà per incappare in “comportamenti anomali” dovuti proprio a queste ottimizzazioni.
Implementazione secondo API native di Android
In Android il servizio di localizzazione è fornito dalla classe android.location.LocationManager. Come per tutti gli altri servizi nativi, per ottenere l’istanza del manager si passa dal context applicativo:
LocationManager locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
Molte delle funzioni del LocationManager richiedono come parametro il “location provider” che può essere fornito tramite le costanti GPS_PROVIDER e NETWORK_PROVIDER. Il significato è ovviamente quello di indicare quale sensore utilizzare per misurare la posizione: satellitare e di rete.
Il provider GPS utilizzerà il rilevatore GPS per ottenere la posizione e questo tipo di rilevamento ha bisogno che all’applicazione sia concesso il permesso ACCESS_FINE_LOCATION.
Il provider NETWORK invece utilizza le informazioni di localizzazione delle celle telefoniche o reti WiFi e per l’accesso a tali informazioni è necessario che all’applicazione sia concesso il permesso ACCESS_COARSE_LOCATION.
Come indicato in precedenza, non esiste un metodo del LocationManager che consenta di recuperare la posizione corrente, ma solamente l’ultima posizione che è stata determinata. Il metodo è:
Location lastKnownLocation = locationManager.getLastKnownLocation(locationProvider);
Registrare un event listener
Come da paradigma descritto in precedenza, la modalità con cui l’applicazione utente può ottenere la posizione è quello di registrare un proprio event listener indicando l’intervallo minimo di tempo e la distanza minima con cui si intende riceve gli aggiornamenti.
// Si definisce un event listener che riceverà // gli aggiornamenti della posizione LocationListener locationListener = new LocationListener() { public void onLocationChanged(Location location) { //Viene invocata ad ogni aggiornamento della posizione } public void onStatusChanged(String provider, int status, Bundle extras) { //Deprecato. Non sarà mai invocato } public void onProviderEnabled(String provider) { //Viene invocato quando il provider viene abilitato } public void onProviderDisabled(String provider) { //Viene invocato quando il provider viene disabilitato } }; long mT = 0; //Minimo periodo di aggiornamento in millisecondi float mD = 0; //Minima distanza di aggiornamento in metri //Registra il listener al manager locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, mT, mD, locationListener);
Il contratto del metodo è che il listener sarà notificato con una periodicità non inferiore al minimo periodo di aggiornamento specificato e non inferiore alla minima distanza indicata.
Il funzionamento, in pratica
Per capire bene cosa ciò implichi facciamo degli esempi. Se si fissa un periodo di 500 ms e una distanza di 0 m, il sistema garantisce che l’applicazione riceverà delle notifiche ad intervalli di tempo non inferiori a 500 ms ciascuno, indipendentemente dalla variazione della posizione.
Se si fissa una distanza di 50 m e un periodo di 0 ms, il sistema garantisce che l’applicazione riceverà delle notifiche a variazioni di posizioni non inferiori a 50 m, indipendentemente dal tempo trascorso.
Se si fissa un periodo di 500 ms e una distanza di 50 m, il sistema garantisce che l’applicazione riceverà delle notifiche a variazioni di posizioni non inferiori a 50 m purché siano trascorsi anche almeno 500 ms.
Caso limite, come nell’esempio, è se si fissano entrambi i parametri a 0. In questo caso al sistema viene chiesto di inviare all’applicazione le notifiche sulla variazione di posizione appena disponibile.
Ad ogni aggiornamento, il listener riceve un’istanza dell’oggetto Location che contiene sia la latitudine che la longitudine e, a seconda che il provider sia in grado di fornire ulteriori informazioni, l’altezza, la velocità e la direzione ciascuna con un proprio valore di precisione.
È importante rimuovere un event listener tramite il metodo removeUpdates per consentire al LocationManager di liberare le risorse usate, come ad esempio spegnere il GPS se non più utilizzato.
locationManager.removeUpdates(locationListener);
Le Fused Location API di Google
Le Fused Location API sono delle librerie di Google che effettuano il wrapping delle funzionalità native di Android, per fornire servizi a più alto livello e implementare una versione ottimizzata del processo di localizzazione che faccia uso di più sensori contemporaneamente.
Mentre tramite il LocationManager di Android si poteva imporre al più se utilizzare il GPS o le informazioni dalle reti (celle telefoniche o WiFi), le Fused Location API hanno come scopo primario quello di decidere autonomamente l’utilizzo dei provider, sulla base delle necessità dell’applicazione.
Inoltre, per ottimizzare ulteriormente l’utilizzo del GPS, sono integrati anche l’uso di altri sensori a bassissimo consumo e sensibili alle variazioni di moto come l’accelerometro e la bussola.
Se, ad esempio, rispetto all’ultima lettura della posizione si è fermi e l’accelerometro indica che non ci sono variazioni allo stato di moto, ossia che il valore dell’accelerazione è 0, ne deriva implicitamente che la posizione non può variare, per cui non è necessario consumare risorse GPS per eseguire un rilevamento.
Registrare il listener
Come nel caso delle API native di Android, le Fused Location API operano notificando all’applicazione le variazioni di posizione tramite la registrazione di un apposito listener. Una volta connessi a Google Play Services, ottenendo l’handler googleApiClient, la registrazione di un listener può avvenire come da frammento di codice sottostante:
LocationRequest locationRequest = new LocationRequest(); //Resto della parametrizzazione delle API per la registrazione del listener LocationListener locationListener = new LocationListener() { public void onLocationChanged(Location location) { //Viene invocata ad ogni aggiornamento della posizione //TODO } }; LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationRequest, locationListener);
Il listener riceve le notifiche tramite il metodo onLocationChanged esattamente come nel caso delle API native.
La configurazione al processo di acquisizione della posizione è fornita tramite una istanza di LocationRequest. Queste librerie, introducendo molti fattori che concorrono alla gestione del processo di rilevamento, richiedono che il progettista software sappia come definire un’accurata parametrizzazione per ottenere il giusto compromesso tra precisione e consumo della batteria.
La coretta parametrizzazione
Un esempio di parametrizzazione è riportato nel frammento di codice sottostante.
LocationRequest locationRequest = new LocationRequest(); locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); locationRequest.setInterval(500); locationRequest.setFastestInterval(200);
Poiché le Fused Location API sono concepite per determinare autonomamente la migliore strategia di utilizzo dei sensori sulla base della configurazione utente, non è mai richiesto di specificare direttamente il provider nelle parametrizzazioni. Di conseguenza è anche necessario che all’applicazione siano sempre concessi entrambi i permessi ACCESS_COARSE_LOCATION e ACCESS_FINE_LOCATION.
Il valore più importante della parametrizzazione è priority e serve a definire la strategia da adottare. I valori possono essere:
- PRIORITY_HIGH_ACCURACY: cerca sempre di ottenere la posizione con la massima accuratezza e per questo sarà prevalente l’uso del GPS.
- PRIORITY_LOW_POWER: forza l’uso del rilevamento solamente tramite rete per ottenere posizioni approssimate.
- PRIORITY_NO_POWER: richiede di utilizzare una strategia per cui l’applicazione riceve le notifiche solo se un’altra applicazione sta usando i servizi di localizzazione. In pratica utilizza i rilevamenti che un’altra applicazione ottiene in modo da non dover richiedere ulteriore consumo di batteria. Nel caso estremo in cui non ci siano applicazioni che utilizzano i servizi di localizzazione — o in cui anche esse li utilizzano in modalità PRIORITY_NO_POWER — non si riceverà nessuna notifica.
- PRIORITY_BALANCED_POWER_ACCURACY: il nome sembra promettere magie ma in realtà utilizza la strategia LOW_POWER fintantoché l’approssimazione fornita dall’uso delle reti (WiFi e celle telefoniche) è sull’ordine dei 100 m, altrimenti fa uso del GPS.
Anche nelle Fused Location API si può controllare il periodo di tempo con cui l’applicazione può ricevere gli aggiornamenti. Il parametro interval ha pressappoco lo stesso significato del minimo tempo di aggiornamento visto per le API native.
È stato aggiunto il parametro fastestInterval per impostare un valore di tempo massimo entro cui vengono notificate le variazioni di posizione se il sistema disponesse in anticipo un aggiornamento rispetto al periodo interval impostato.
In altre parole con il parametro interval si cerca di regolare il periodo di aggiornamento della posizione desiderato dall’applicazione, mentre, se il sistema per altre ragioni producesse aggiornamenti più frequentemente di quanto impostato, potrebbe notificarli all’applicazione ma limitando il periodo minimo comunque al valore impostato con fastestInterval.
Lo scopo di avere due parametri è sempre legato a regolare il consumo di batteria. Con il primo parametro infatti chiediamo al sistema di eseguire un rilevamento periodicamente e, per contenere il più possibile il consumo, è opportuno che il periodo sia il più alto che l’applicazione si possa permettere senza perdere di funzionalità. Tuttavia, se ad esempio un’altra applicazione sta utilizzando il GPS e quindi produce aggiornamenti della posizione con maggiore frequenza, si può indicare, con il valore fastestInterval, che l’applicazione è disposta a ricevere notifiche, purché si limiti il ritmo degli aggiornamenti a un valore limite; in pratica le posizioni sono come il maiale: non si buttano mai via, l’importante è non strozzarsi.
Non è previsto un controllo così parametrizzato anche per la variazione della distanza: il parametro smallestDisplacement ha pressappoco lo stesso significato della minima distanza in metri visto per le API native.
Il Geofencing tramite le Fused Location API di Google
Una funzionalità molto interessante delle Fusion Location API nell’ambito di applicazioni che usano la geolocalizzazione è il Geofencing. Con questo termine s’intende la possibilità, da parte di una applicazione, di definire un’area chiusa e di ricevere notificazioni quando viene rilevato che la posizione corrente cade all’interno di essa.
La comodità di questa funzionalità sta nel fatto che il sistema può autonomamente decidere la migliore strategia di rilevamento della posizione fintantoché si è molto all’esterno dell’area, utilizzando il GPS o le informazioni dalla rete, per risparmiare in questo modo al massimo l’uso della batteria. Al contempo, potrà utilizzare il provider più idoneo quando si attraversa l’area, in base alle sue dimensioni.
Definire i geofence
Per operare con i geofence bisogna per prima cosa definire l’area chiusa che si intende monitorare. Le Fused Location API consentono al momento di definire solo un’area circolare indicando latitudine e longitudine del centro e il raggio in metri.
Ogni geofence viene identificato tramite un ID fornito dall’applicazione in fase di definizione. Poiché l’elenco dei geofence che le Fused Location API monitora è comune a tutte le applicazioni, è necessario che l’ID sia costruito in modo da essere univoco, ad esempio premettendo al nome l’ID dell’applicazione.
Altri parametri che si possono specificare sono il tempo di validità del geofence in millisecondi: scaduto questo tempo, il sistema provvederà autonomamente a rimuoverlo dalla lista dei geofence attivi.
Infine, si possono specificare gli eventi che l’applicazione intende ricevere. Gli eventi sono ad esempio l’entrata nel geofence (GEOFENCE_TRANSITION_ENTER), l’uscita dal geofence (GEOFENCE_TRANSITION_EXIT) e gli spostamenti rimanendo all’interno del geofence (GEOFENCE_TRANSITION_DWELL).
Per definire un geofence si può utilizzare il pattern builder tramite l’apposita classe Geofence.Builder. Un esempio tipico di creazione di un geofence è riportato nel codice sottostante:
Geofence geofence = new Geofence.Builder() .setRequestId(geofenceId) //Si imposta l’id del geofence .setCircularRegion( //Si definisce l’area circolare del geofence geofenceOriginLatitude, geofenceOriginLongitude, geofenceRadius ) .setExpirationDuration(-1) //Si indica che il geofence non scade mai .setTransitionTypes( //Si vuole essere notificati ENTER e EXIT Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT) .build();
Nel caso si intenda ricevere le notifiche per l’evento GEOFENCE_TRANSITION_DWELL, è necessario specificare nella configurazione del geofence anche il parametro loiteringDelayMs. Questo parametro indica il periodo in ms con cui si intende ricevere gli aggiornamenti della variazione della posizione durante gli spostamenti all’interno del geofence.
Inviare i geofence alle Fused Location API
Una volta definita la lista dei geofence da monitorare è necessario inviarli alle Fused Location API tramite il metodo addGeofences creando un’apposita richiesta. Un tipico esempio di richiesta è riportato nel codice sottostante:
//Si crea un geofencing request per la lista dei geofence //geofenceList con evento iniziale solo ENTER GeofencingRequest geofencingRequest = new GeofencingRequest.Builder() .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) .addGeofences(geofenceList) .build(); PendingResult<Status> result = LocationServices.GeofencingApi.addGeofences(googleApiClient, geofencingRequest, pendingIntent);
L’istanza geofencingRequest ha come scopo quello di raggruppare la lista dei geofence da inviare con una singola chiamata. In questa fase è possibile indicare qual è l’evento iniziale che si intende monitorare per i geofence che si stanno registrando. Se si omette il parametro, gli eventi sono sia ENTER che EXIT; la ragione è evidente: si pensa che sia importante ricevere una notifica se si esce da un geofence anche senza aver ricevuto prima l’evento di ENTER, in pratica perché la posizione al momento iniziale era già interna al geofence. Nel caso indicato dall’esempio, invece, si è interessati solo agli eventi se prima si entra nel geofence.
Intent per gestione degli eventi
Il metodo addGeofences riceve come parametri l’handle di Google Play Services ottenuto inizialmente, la richiesta con l’elenco dei geofence da monitorare e un Intent pendente per la gestione degli eventi di tipo GeofencingEvent.
La ragione di utilizzare un intent e non un listener, come nel caso dei servizi di notifica, è che la gestione dei geofence delle Fused Location API è pensata per essere durevole nel tempo, anche quando l’applicazione utente viene terminata.
Usando un normale riferimento a un listener, le Fused Location API non potrebbero interagire con l’applicazione dopo la chiusura perché il riferimento diventa inconsistente. L’intent diventa quindi la modalità con cui le Fusion Location API possono risvegliare o rieseguire l’applicazione anche dopo la chiusura della stessa.
Chiaramente, dal punto di vista dello sviluppo, la gestione è più complessa perché è necessario dapprima creare un intent, da cui ottenere un pending intent, abbinato agli eventi di tipo GeofencingEvent, e gestirne la ricezione tramite una classe che implementa IntentService.
La creazione del pending intent è riportata nell’esempio sottostante:
Intent intent = new Intent(this, GeofenceTransitionsIntentService.class); PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
La corrispondente classe intent service è ad esempio:
public class GeofenceTransitionsIntentService extends IntentService { protected void onHandleIntent(Intent intent) { GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); //Gestire l’evento... }}
Conclusioni
Nel corso dell’articolo sono state esposte le API che Android mette a disposizione per eseguire il rilevamento della posizione.
Rispetto a quanto ci si poteva aspettare considerando il problema originario della geolocalizzazione, le interfacce a disposizione rendono più complesso l’accesso a tali informazioni facendo ricadere sui progettisti parecchi altri oneri che tipicamente si danno per scontati. Inoltre non essendoci delle linee guida ben definite per la soluzione dei problemi più tipici è necessario che i progettisti si affidino più che altro al loro buon senso.
Tutto ciò è abbastanza frustrante perché, nonostante si disponga di una ottima tecnologia per il rilevamento della posizione, riuscire a sviluppare un’applicazione che svolga con successo un compito che sembra così banale diventa tutt’altro che una impresa semplice.
Pertanto, nel prossimo e ultimo articolo della serie, vedremo un insieme di suggerimenti e trucchi comprovati per raggiungere l’obiettivo senza spendere tantissime risorse andando per “tentativi”.