Sei sulla pagina 1di 761

SVILUPPARE

APPLICAZIONI ANDROID CON GOOGLE PLAY SERVICES


Massimo Carli

Apogeo - IF - Idee editoriali Feltrinelli s.r.l.


Socio Unico Giangiacomo Feltrinelli Editore s.r.l.

ISBN: 9788850317332

Google, Google Play e Android sono marchi registrati da Google Inc. Android
robot riprodotto o modificato dallopera creata e rilasciata da Google e utilizzato
secondo i termini descritti nella licenza Creative Commons BY 3.0.
Il presente file pu essere usato esclusivamente per finalit di carattere personale.
Tutti i contenuti sono protetti dalla Legge sul diritto dautore.
Nomi e marchi citati nel testo sono generalmente depositati o registrati dalle
rispettive case produttrici.
Ledizione cartacea in vendita nelle migliori librerie.
~
Sito web: www.apogeonline.com
Scopri le novit di Apogeo su Facebook
Seguici su Twitter @apogeonline
Rimani aggiornato iscrivendoti alla nostra newsletter

A mio padre

Prefazione

Sono passati ormai diversi anni dal 23 settembre del 2008, giorno in cui stata
rilasciata la prima versione di Android. Da allora si sono susseguite diverse release
che si sono differenziate per funzionalit e prestazioni, aiutate anche dal fatto di
utilizzare hardware sempre pi potenti. In questi anni uno dei punti di forza di
Android, ovvero la portabilit, si rivelato spesso un problema. Spesso i costruttori
di dispositivi rilasciavano nuovi prodotti che si appoggiavano a versioni della
piattaforma poi superate, senza preoccuparsi di aggiornare i dispositivi ormai in
commercio. Molti utenti si ritrovavano con dispositivi relativamente nuovi, ma con
sistemi operativi ormai obsoleti. Il problema stato in parte risolto attraverso quelli
che si chiamano Google Play services.
Si tratta di API che non dipendono pesantemente dallhardware dei dispositivi e
che quindi possono essere aggiornate automaticamente come fossero una qualunque
applicazione. Molte delle funzionalit pi importanti della piattaforma come la
gestione della location, delle mappe e molte altre sono state spostate in questa
libreria, permettendo di avere sempre lultimo aggiornamento senza la necessit di
attendere update del firmware da parte del costruttore. Tutto questo viene ora gestito
da Google, che, in questo modo, si ripresa parte del controllo sulla piattaforma
perso in precedenza. I Google Play services stanno quindi assumendo sempre
maggiore importanza e sono divenuti fondamentali nella realizzazione di una
qualunque applicazione pi o meno professionale.
Lidea di questo libro nasce da qui: creare un riferimento per lo sviluppo di
applicazioni Android fortemente orientate allo sfruttamento delle API messe a
disposizione da Google e dedicate a dispositivi smartphone, tablet ma anche
wearable e TV. Abbiamo infatti trattato gli aspetti fondamentali dellutilizzo di
tecnologie anche diverse tra loro, ma accomunate dal fatto di essere raccolte
allinterno dei Google Play services. Per fare questo abbiamo utilizzato un approccio
pratico, attraverso la scrittura di moltissimo codice abbondantemente spiegato nel
testo. Il consiglio quindi quello di leggere il libro con il relativo progetto aperto
allinterno di Android Studio.
NOTA
Tutto il codice di esempio pu essere scaricato allindirizzo http://bit.ly/apo-gps.

A chi rivolto il testo


Questo testo rivolto a chi, partendo da conoscenze di base della piattaforma
Android e buone conoscenze del linguaggio di programmazione Java, voglia
apprendere e approfondire lutilizzo delle librerie dei Google Play services.

Organizzazione del testo


Il Capitolo 1 presenta le motivazioni che hanno portato alla creazione da parte di
Google di una libreria esterna. Faremo quindi una panoramica di tutte le API e
soprattutto descriveremo come eseguire il setup di tali librerie allinterno dei nostri
progetti.
Il Capitolo 2 invece dedicato alla gestione della Location. Vedremo come
ottenere la Location corrente, come eseguire le operazioni di georeferenziazione
attraverso un oggetto di nome Geocoder e come tracciare le posizioni dellutente e
soprattutto renderle persistenti.
Il Capitolo 3 dedicato alla gestione delle Google Maps. Descriveremo sia come
eseguire il setup, sia come personalizzare le mappe attraverso lutilizzo dei Marker.
Vedremo poi come disegnare dei percorsi e soprattutto come gestire quelli che si
chiamano Geofence, ovvero delle zone per le quali poter ricevere delle notifiche al
momento dellingresso o delluscita. In questo capitolo vedremo anche come
utilizzare le API per la gestione delle Street View.
Il Capitolo 4 dedicato alla gestione delle API per Google Drive. Con il pretesto
di rendere persistenti le informazioni relative ai percorsi tracciati nei capitoli
precedenti, vedremo come salvare e ripristinare dei file nel cloud.
Il Capitolo 5 dedicato agli aspetti social e quindi a come sfruttare le API per
linterazione con Google+.
Il Capitolo 6 descrive nel dettaglio le API Google Cloud Messaging. Vedremo
come funzionano e soprattutto come creare e utilizzare un server per linvio delle
informazioni in push alla nostra applicazione.
Il Capitolo 7 dedicato alla programmazione dei dispositivi weareable, che
rappresentano la vera novit descritta durante il Google I/O del 2014. Vedremo i
nuovi pattern di interazione con questo tipo di dispositivi e come gestire e
personalizzare le varie Notification. La seconda parte del capitolo dedicata invece
alla realizzazione delle applicazioni weareable vere e proprie, perfettamente integrate
con lecosistema Android.
Nel Capitolo 8 cambiamo completamente argomento e ci dedichiamo allutilizzo
di uno strumento molto interessante che si chiama Chromecast e che permette di
proiettare contenuti multimediali in un dispositivo esterno, come pu essere una TV.
Nella prima parte di questo capitolo descriviamo anche il funzionamento del
MediaRouter su cui si basa linterazione con Chromecast.

Il Capitolo 9 dedicato a unaltra novit presentata al Google I/O del 2014,


ovvero Google Fit. Si tratta di API che permettono di raccogliere in un unico
repository e quindi condividere con diverse applicazioni, le informazioni relative
allattivit fisica degli utenti.
Il Capitolo 10 descrive il funzionamento delle Game API di Google. Sar
lopportunit di scoprire diversi meccanismi mirati alla gestione dei giochi, degli
obiettivi, delle leaderboard, ma anche quelli che permettono a pi utenti di giocare in
modalit realtime oppure a turni.
Il Capitolo 11 dedicato invece a una funzionalit molto delicata, ovvero quella
che permette allutente di eseguire degli acquisti attraverso unapplicazione Android.
Vedremo sia la parte di amministrazione, attraverso la console di Google, sia quella
legata alla vera e propria scrittura del codice. In questo capitolo molto importante
anche la descrizione delle modalit di test.
Il Capitolo 12 dedicato a un aspetto fondamentale nella realizzazione di
unapplicazione Android, ovvero la gestione del tracking. Vedremo come utilizzare
gli strumenti di Google Analytics per poter capire il comportamento dellutente e
fornirgli la migliore esperienza possibile.
Nel Capitolo 13 descriviamo, infine, le API che permettono la visualizzazione dei
banner allinterno dellapplicazione, in modo da poter in qualche modo monetizzare
il nostro lavoro. Si tratta di un argomento molto semplice, che pu comunque dare
diverse soddisfazioni.

Tool e versioni
I progetti realizzati utilizzano come tool di riferimento Android Studio nella
versione 1.x mentre i Google Play services sono gli ultimi disponibili a inizio 2015,
ovvero la versione 6.5.
Negli ultimi mesi Google ha cambiato molte cose, sia per quello che riguarda il
tool di sviluppo, sia per quello che riguarda le API. Per questo motivo abbiamo
cercato di descrivere non solo il codice in quelle che sono le istruzioni vere e proprie,
ma soprattutto nei meccanismi di utilizzo, che sono rimasti pressoch gli stessi nelle
diverse versioni. Potrebbe quindi capitare che alcune delle immagini relative alle
schermate di Android Studio o ai risultati nel dispositivo siano leggermente diverse
da quelle testate dal lettore.

Capitolo 1

Introduzione ai Google Play services

Per iniziare, vedremo cosa sono i Google Play services ed esamineremo, ad alto
livello, le funzionalit di questa libreria, che andremo a utilizzare e descrivere nel
dettaglio attraverso vari esempi pratici. Nella seconda parte ci dedicheremo invece
alle procedure da seguire per preparare una qualunque applicazione che utilizzi
queste librerie. La versione di riferimento quella disponibile al momento, ovvero la
6.5, la quale ha portato, come vedremo, ad alcune novit per quello che riguarda la
procedura di configurazione nelle applicazioni. Prima fra tutte la possibilit di
importare solamente le API che interessano e non lintera libreria, con conseguente
risparmio di spazio. Fatto questo, vedremo come preparare lo scheletro di
unapplicazione con Android Studio, descrivendone le parti fondamentali. Dove
possibile faremo in modo di restare il pi possibile indipendenti dallIDE, anche se
Android Studio si sta affermando come lo strumento al momento migliore per lo
sviluppo di applicazioni con la piattaforma di Google. La versione da noi utilizzata
la 1.0, resa disponibile verso la fine del 2014 come evoluzione di quella annunciata
allultimo Google I/O del 2014.

Che cosa sono i Google Play services?


Prima di addentrarci nelle diverse librerie messe a disposizione dai Google Play
services importante capire quali siano state le motivazioni che hanno spinto Google
a realizzarle.
NOTA
Spesso si fa riferimento ai Google Play services attraverso lacronimo GPs, che, ovviamente, non
ha nulla a che fare con il Global Positioning System. Faremo attenzione nei casi di ambiguit.

Fin dalle prime versioni della piattaforma di Android, Google ha dovuto affrontare
il problema della frammentazione. Quello che doveva essere un punto a favore di
Android, ovvero la possibilit di essere installato e personalizzato su diversi tipi di
dispositivi, era infatti diventato un problema, causa di un certo malcontento tra gli
utenti. In corrispondenza di ogni nuova release del sistema operativo, era necessario
informare i vari produttori di dispositivi, i quali fornivano la propria
implementazione. Questo processo non solo richiedeva moltissimo tempo, ma spesso
era accompagnato dalla scelta dei produttori di abbandonare alcuni modelli a favore
di altri. Molti utenti si ritrovavano quindi con un dispositivo relativamente nuovo, ma
non pi compatibile con lultima versione di Android e questo per una pura scelta di
marketing o di costi dellazienda produttrice. Si trattava di un evidente problema per
la diffusione di questo ormai non pi nuovo sistema operativo per dispositivi mobili.
Per alcuni maliziosi lo stesso Google vedeva in un certo senso perdere il controllo
sulla propria creatura. Serviva quindi una soluzione che riducesse il pi possibile la
dipendenza di Google e dei servizi da essa offerti dai produttori di dispositivi. Si
fatto un ragionamento molto semplice: ci si chiesti quali fossero gli strumenti legati
allhardware e quali potessero invece essere gestiti a livello software. Per i primi la
dipendenza dai produttori inevitabile, mentre per i secondi possibile scegliere un
approccio diverso, ovvero creare unapplicazione in grado di aggiornarsi
automaticamente e di fornire una serie di servizi che, anche alla luce della filosofia di
Android (all applications are created equals), possono essere utilizzati dalle altre
applicazioni. Ecco il motivo della nascita di unapplicazione chiamata, appunto,
Google Play services, che contiene tutte le API che descriveremo in questo testo
attraverso la realizzazione di alcune applicazioni reali. In questo modo tutti i
dispositivi potranno avere sempre lultima versione dei GPs ed essere aggiornati con
gli ultimi servizi messi a disposizione da Google.
NOTA
Questo un grosso passo in avanti verso una maggiore semplificazione nella gestione degli
aggiornamenti, ma, ovviamente, non potr mai sostituire completamente la fase di
aggiornamento legata agli aspetti puramente hardware.

Prima di procedere con la descrizione dei servizi messi a disposizione dai GPs,
verifichiamo se il nostro dispositivo ne provvisto. Utilizzando il Nexus 5 con
Android 5.0 a nostra disposizione andiamo nella parte dei Settings relativa alle
applicazioni e cerchiamo appunto i Google Play services, che notiamo essere presenti
come visualizzato nella Figura 1.1 insieme ad altre app, sempre fornite da Google.
Se selezioniamo la voce corrispondente, otteniamo quanto rappresentato nella
Figura 1.2, nella quale possiamo notare come la versione disponibile sia proprio la
6.5.
NOTA
A questo punto il lettore potrebbe chiedersi perch stiamo utilizzando un dispositivo che monta la
versione di Android 5.0 senza utilizzare le API di questa nuova versione, che sappiamo chiamarsi
Lollipop. La prima ragione riguarda la scarsa distribuzione (almeno attualmente) di questa
versione, mentre la versione 4.x ormai compatibile con la quasi totalit dei dispositivi. Il
secondo motivo riguarda la volont di concentrarsi sui Google Play services e non su aspetti che
riguardano Lollipop. Le API che descriveremo sono perfettamente funzionanti su tutti i dispositivi
Android, dalla versione 2.3 in avanti e quindi anche sulle versioni 4.x e ovviamente 5.0.

Figura 1.1 Applicazione Google Play services tra quelle disponibili.

Questo aggiornamento viene fatto in automatico, per cui non dovremo in alcun
modo ricevere una notifica e procedere allaggiornamento nel Play Market.
Osservando la Figura 1.2 viene la tentazione di selezionare il pulsante Disable come
faremo pi avanti per descrivere che cosa succede nel caso in cui lapplicazione non
fosse disponibile. Al momento lasciamo al lettore la verifica, che porterebbe alla
visualizzazione di un avvertimento e alla cancellazione dellapplicazione, con
conseguente generazione di una serie di warning generati da tutte le applicazioni che
fanno uso di questi servizi.
NOTA
Nel caso, niente paura: come vedremo pi avanti, sar sufficiente andare sul Play Market o
semplicemente seguire le istruzioni successive alla selezione di una delle notifiche di warning per
reinstallare il tutto.

Figura 1.2 Le informazioni relative allapplicazione Google Play services nella versione 6.5.99.

Se andiamo a vedere le applicazioni installate di default sul dispositivo, notiamo


anche quella con licona rappresentata nella Figura 1.3, relativa alle possibili
configurazioni che ogni utente pu personalizzare. Come vedremo nei prossimi
capitoli, ogni utente potr eventualmente disabilitare i servizi di localizzazione o
gestire il proprio account Google Plus.

Figura 1.3 Icona per la configurazione dei Google Play services.

Si tratta di unapplicazione che viene mantenuta allineata con i Google Play


services, permettendo la configurazione da parte degli utenti delle propriet relative
ai servizi che la piattaforma mette a disposizione.
Prima di iniziare a sporcarci le mani comunque utile fare una panoramica di
tutte le funzionalit dei Google Play services che andremo a utilizzare per realizzare
unapplicazione che descriviamo brevemente e che amplieremo di capitolo in
capitolo.
Si tratta, in sostanza, di unapplicazione che ci permetter inizialmente di
visualizzare su una mappa la posizione nostra e di un insieme di nostri amici che
decideranno, dando ovviamente il loro consenso, di aggiungersi. In questo capitolo
vedremo come creare lo scheletro dellapplicazione, mentre nei successivi vedremo
come aggiungere funzionalit legate ai servizi di Google Play services che andremo
di volta in volta a esplorare. Per completezza, ci aiuteremo anche con un insieme di
altri esempi che ci permetteranno di testare funzionalit dei Google Play services non
strettamente legate al nostro caso duso, ma che possono essere comunque
importanti.

Figura 1.4 Elenco delle possibili configurazioni per i Google Play services.

Gestione della Location


Il primo passo consiste nellindividuare la posizione dei vari utenti. La
determinazione della Location stata una delle prime funzionalit trasferite a livello
dei Google Play services, in quanto le informazioni fisiche necessarie, in termini di
latitudine e longitudine, erano comunque gi disponibili dalle prime versioni della
piattaforma. Si trattato, quindi, di implementare nuovi algoritmi in grado di
elaborare queste informazioni e di dare maggiore accuratezza nella posizione nel caso
di luoghi aperti e, soprattutto, di luoghi chiusi. Per quanto riguarda la nostra
applicazione, svilupperemo lopzione Where I Am, che permetter di visualizzare le
coordinate in termini di latitudine e longitudine, cui cercheremo di dare una localit e
quindi un nome. Sempre in corrispondenza di questa voce di menu non ci

accontenteremo di sapere la nostra posizione, ma anche il modo in cui ci stiamo


spostando, attraverso le funzionalit di ActivityRecognition.
NOTA
curioso come il termine Activity, in questo caso, non faccia riferimento alla classe che
utilizziamo come base per tutte le schermate, ma al concetto di attivit ovvero al movimento
dellutente.

In questa fase vedremo tutto ci che riguarda la posizione, rimandando la


visualizzazione delle informazioni raccolte in una mappa al capitolo successivo. Si
tratter di un capitolo molto importante, in quanto ci permetter di affrontare concetti
anche non strettamente legati a quello di Location. Per realizzare queste funzionalit
abbiamo scelto la strada migliore, ma anche meno convenzionale, non documentata
in modo ufficiale, se non attraverso il JavaDoc. Da un punto vista puramente tecnico
vedremo, in sintesi, come ottenere le informazioni di Location allinterno di un Service
per poterle rendere persistenti in un ContentProvider. Ulteriore sfida la possibilit di
affrontare concetti di programmazione concorrente, non sempre descritti in altri testi.

Utilizzo delle Google Maps


Unaltra importantissima funzionalit fornita dai Google Play services
rappresentata dalla possibilit di visualizzare e gestire le Google Maps. Nel terzo
capitolo vedremo come sia semplice visualizzare una mappa e quali siano i principali
strumenti che ci permetteranno di evidenziare particolari Location. Vedremo che cosa
siano i Marker e come sia semplice personalizzarli attraverso icone o colori e come
gestire gli eventi associati attraverso la visualizzazione delle Info Window. Nel
secondo capitolo abbiamo imparato a tracciare delle posizioni, che ora vogliamo
visualizzare allinterno di una mappa sfruttando gli strumenti per il disegno di
percorsi o di figure geometriche. Le stesse API per il disegno verranno utilizzate
anche nella creazione e gestione dei Geofence, regioni nella mappa per le quali
possibile ricevere notifiche associate a particolari eventi. Alcuni di questi sono
relativi alla semplice entrata o uscita da tali zone, ma possiamo creare una notifica
nel caso un utente rimanesse nella stessa zona per un tempo maggiore di un periodo
fissato. Questo caso duso ci permetter anche di vedere come testare un servizio di
localizzazione attraverso Mock Location. Concluderemo il terzo capitolo spiegando
come utilizzare le Street View per visualizzare un percorso. Si tratta di un capitolo
molto impegnativo, che ci permetter di porre le basi per realizzare applicazioni con
funzionalit molto interessanti.

Google Drive
Finora abbiamo visto come creare delle FenceSession ovvero una collezione di
informazioni di Location e di attivit caratterizzate da un nome, una data di inizio e di
fine. Nel capitolo dedicato a Google Drive vogliamo utilizzare le Google Drive API
per rendere queste informazioni persistenti allinterno di un file, per ripristinarle,
eventualmente, in un secondo tempo. Vedremo in particolare come creare un file in
Google Drive, gestendone il contenuto. Allo stesso modo vedremo come leggere
questi file per importare informazioni di una specifica FenceSession. Vedremo come
utilizzare questi strumenti per eseguire ricerche oltre che per gestire Folder privati
della nostra applicazione (App Folder) o comunque accessibili anche in altro modo,
per esempio tramite il Web.

Google Plus
Nel capitolo dedicato a Google Plus ci occuperemo dellaspetto social della nostra
applicazione, attraverso le API che i Google Play services ci mettono a disposizione
per lintegrazione con Google Plus. Inizialmente vedremo come eseguire la login con
Google+ in modo da ottenere le informazioni dellutente senza doverle richiedere in
modo esplicito. Poi vedremo come accedere alle informazioni relative agli utenti
nelle proprie cerchie, per condividere informazioni di vario genere.

Google Cloud Messaging


Una delle tecnologie pi affascinanti che sono state aggiunte ai Google Play
services quella che permette linvio di notifiche push e che va sotto il nome di
Google Cloud Messaging. Attraverso questa tecnologia possibile inviare dei
messaggi in push a un insieme di dispositivi che si sono in precedenza registrati a un
server di terze parti che contiene la logica dellapplicazione. Nel nostro contesto il
server potrebbe, per esempio, memorizzare le informazioni relative ai Geofence creati e
quindi inviare notifiche ai nostri amici quando si nella stessa zona. Le possibili
applicazioni sono moltissime. Nel capitolo dedicato al Google Cloud Messaging
descriveremo i punti fondamentali della tecnologia attraverso la realizzazione di una
semplicissima chat. Come detto si tratta di una tecnologia che prevede unarchitettura
nel quale il server ha unimportanza fondamentale. Nel nostro caso abbiamo
realizzato un semplicissimo server con Node.js, ma il lettore potr realizzare il
proprio con la tecnologia che ritiene pi opportuna, purch vengano rispettate le

convenzioni relative ai servizi Rest che il server espone. In particolare abbiamo


realizzato un servizio /register per la registrazione del client di quello che si chiama
e che viene ottenuto dal server GCM di Google, e il servizio /send per

Registration Id

linvio di una notifica di push ai dispositivi associati a un particolare utente. Si tratta


di una tecnologia che possibile utilizzare in moltissimi modi diversi, secondo le
specifiche caratteristiche dellapplicazione che intendiamo realizzare.

Google Wear
Nel capitolo dedicato a Google Wear ci occupiamo dei nuovi strumenti di gestione
dei dispositivi wearable. Stiamo parlando di tutti quegli strumenti che possono essere
indossati e che permettono modalit di interazione diversa con le varie applicazioni.
In questo momento i dispositivi wearable sono principalmente orologi, caratterizzati
da display molto piccoli e modalit di interazioni limitate alla selezione di piccoli
tasti o allinvio di comandi vocali. In questo capitolo vedremo tutto questo partendo
dalla gestione e personalizzazione delle notifiche, fino alla realizzazione di
applicazioni che girano interamente sul dispositivo wearable. Vedremo nel dettaglio
tutti i componenti principali di una libreria di supporto specifica per questo tipo di
dispositivi rilasciata da Google, concludendo con la descrizione delle diverse
modalit con cui i diversi tipi di device comunicano tra loro, ovvero attraverso
sincronizzazione di dati oppure messaggi. Si tratta di un capitolo abbastanza
impegnativo, che ci permetter di aprire un nuovo mondo nello sviluppo delle
applicazioni Android.

Google Cast
Nel capitolo precedente abbiamo imparato a programmare un dispositivo esterno
allo smartphone o al tablet che sta al polso come un orologio, ma ovviamente ci
aspettiamo che altri dispositivi di vario genere, sempre wearable, vengano lanciati
nel mercato e programmati allo stesso modo. In questo capitolo ci occupiamo invece
di un altro genere di dispositivi esterni che si possono considerare come
unestensione dei device tradizionali per quello che riguarda la loro capacit di
riprodurre media. Stiamo pensando per esempio a schermi molto grandi, come TV,
oppure a dispositivi in grado di riprodurre suoni. In questo capitolo vedremo
inizialmente come funzionano le MediaRouter API, per poi applicarle a un caso
particolare ovvero lutilizzo del Chromecast. Si tratta di un dispositivo dal prezzo
molto accessibile, di poche decide di euro, che permette di riprodurre suoni o video
su schermi diversi da quello dello smartphone e che solitamente sono molto pi

grandi e di ottima risoluzione. Vedremo che cos un Sender e soprattutto che come si
programma un Receiver per questo tipo di dispositivi. un argomento che si discosta
leggermente da quelli trattati durante lo sviluppo dellapplicazione FriendFence, ma che
permette comunque di realizzare un certo insieme di applicazioni molto interessanti.

Google Fit
Nel capitolo dedicato a Google Fit ci occuperemo invece di alcune API che sono
state annunciate allultimo Google I/O come parte di Lollipop, ma che sono state
integrate ai Google Play services nella release 6.5. Le Google Fit sono un ecosistema
di API che permette di raccogliere informazioni relative alla nostra attivit fisica
attraverso dei sensori, per poi renderle persistenti allinterno di un database condiviso
che si chiama Google Fitness Store. Si tratta di una tecnologia appena allinizio e in
piena evoluzione. Vedremo i concetti fondamentali e faremo alcuni esempi su come
utilizzare le API per la raccolta, memorizzazione e visualizzazione dei dati.

Google Game
Nel capitolo dedicato a questo argomento ci occuperemo della descrizione delle
Game API, ovvero degli strumenti che Google ci mette a disposizione per gestire
laspetto social e non solo di un gioco. Vedremo infatti come creare e gestire degli
obiettivi che i giocatori dovranno raggiungere per poter sbloccare nuove funzionalit
o comunque progredire nel gioco. Il raggiungimento di un obiettivo permette la
raccolta di punti esperienza, che possono essere poi visualizzati attraverso
opportune classifiche (leaderboard) che impareremo a personalizzare e visualizzare.
La seconda parte del capitolo dedicata agli strumenti che permettono il multiplayer
in modalit real-time e a turno. Vedremo gli strumenti per invitare altri giocatori
attraverso i nostri amici nelle cerchie Google+ o attraverso la modalit Quick Start,
che permette la selezione casuale di un giocatore in attesa di giocare. Vedremo come
mettere in contatto questi giocatori e gestire lo scambio di messaggi. Vedremo poi
come gestire i Gift e Wish e soprattutto come raccogliere informazioni sui giocatori
del nostro gioco, in modo da tararlo di conseguenza. Il capitolo permette di capire, e
soprattutto mettere in pratica, i concetti principali nello sviluppo di un gioco.

Google In-app Billing


Quindi ci occuperemo delle API che permettono la vendita di prodotti multimediali
dalla nostra applicazione. Vedremo quali passi seguire per configurare lapplicazione

e per creare il nostro catalogo. Poi ci occuperemo dellintegrazione di queste API


allinterno della nostra applicazione. Vedremo come sia possibile ottenere
informazioni sui prodotti del nostro catalogo, ma soprattutto come poterli acquistare.
Faremo molta attenzione a quelli che sono gli aspetti di test, attraverso la creazione di
una semplice applicazione.

Google Analytics
Segue un capitolo dedicato allutilizzo di alcuni strumenti molto importanti per
comprendere i nostri utenti e creare applicazioni che li soddisfino il pi possibile.
Vedremo come, attraverso le Google Analytics API sia possibile comprendere quali
siano le schermate utilizzate dagli utenti e le modalit di navigazione. Vedremo come
possibile registrare tutte le azioni dellutente, raggruppate per categorie, per poi
visualizzarle attraverso la console di Google Analytics. Esamineremo inoltre gli
strumenti disponibili per misurare le performance della nostra applicazione e per
tenere traccia degli eventuali errori ed eccezioni.

Google Ads
Lultimo capitolo dedicato alle API per la visualizzazione dei banner allinterno
della nostra applicazione. Vedremo inizialmente come creare normali banner, per poi
passare ai cosiddetti Interstitial. Un capitolo molto breve che permette di farsi
unidea dei meccanismi di advertisement nelle applicazioni Android.

Installazione e setup di unapplicazione


Dopo aver descritto i servizi offerti dai GPs, iniziamo a creare il nostro progetto
utilizzando Android Studio nella versione disponibile al momento, ovvero la 1.0. Per
creare lo scheletro della nostra applicazione utilizzeremo un semplice wizard, messo
a disposizione dallIDE.
NOTA
Anche se lambiente dovesse cambiare, il lettore potr comunque creare la stessa struttura da
zero seguendo la descrizione di seguito. Preferiamo non legarci troppo allIDE e specialmente ad
Android Studio, in continua evoluzione.

Iniziamo selezionando lopzione di creazione di un nuovo progetto, ottenendo la


schermata rappresentata nella Figura 1.5, che ci mette a disposizione diverse opzioni.
NOTA
Con le ultime versioni di Android Studio possibile che venga visualizzata una finestra diversa,
che permette di selezionare una configurazione standard o personalizzata dellIDE. In questo
caso, basta selezionare il pulsante Cancel per arrivare alla schermata per la creazione di un
nuovo progetto.

In questa prima schermata inseriamo semplicemente il nome dellapplicazione, il


corrispondente package e il percorso in cui salvare il progetto nel nostro file system.

Figura 1.5 Finestra di creazione del progetto FriendFence.

Ovviamente il lettore potr avere un percorso diverso.

NOTA
Supponiamo che il lettore sia gi a conoscenza dei concetti base della programmazione Android.
Descriveremo solamente gli aspetti in qualche modo collegati ai GPs.

Fino a qui nessun problema, se non quello di sottolineare come il package sia di
fondamentale importanza, in quanto definisce in modo univoco la nostra applicazione
e non potr pi essere modificato una volta che questa sia stata pubblicata.
NOTA
Forse per questo motivo viene chiesto il dominio dellazienda o persona che crea lapplicazione e
quindi viene creato in automatico il package. Lerrore in questa fase abbastanza frequente.

Selezionando il pulsante Next otteniamo la schermata rappresentata nella Figura 1.6


che, alla luce delle novit annunciate al Google I/O del 2014, assume unimportanza
fondamentale.

Figura 1.6 Definizione degli ambienti in cui lapplicazione potr essere eseguita e relative versioni.

In questa fase andiamo infatti a definire gli ambienti in cui la nostra applicazione
potr essere eseguita, con relative versioni minime. Come possiamo notare ci sar la

possibilit di eseguire lapplicazione su un ambiente TV, su un dispositivo Wear che al


momento rappresentato da un orologio oppure attraverso i Google Glasses. Al
momento selezioniamo solamente la prima delle opzioni, scegliendo come minima
versione di riferimento quella relativa a Gingerbread ovvero un API Level 10 o SDK
di versione 2.3.3. Come possiamo vedere nella figura, questo ci permette di
raggiungere il 99,2% degli utenti che possiamo considerare pi che accettabile.
NOTA
In realt i Google Play services esistono anche per Froyo (API Level 8), ma sono contenuti in una
libreria distinta. Laggiunta di questa versione avrebbe comportato lintroduzione di una
complessit non giustificata da un aumento minimo della percentuale di dispositivi (0,8%) peraltro
in continua diminuzione.

A questo punto facciamo clic sul pulsante Next e otteniamo unaltra schermata che
ci permette di scegliere tra un insieme di Activity (Figura 1.7). Il lettore potr notare
come esista la possibilit di creare una Google Play services Activity e generare in
modo automatico tutto il codice di inizializzazione. Nel nostro caso abbiamo deciso
di essere il pi possibile indipendenti dallIDE, per cui scegliamo lopzione
evidenziata in figura: Blank Activity.
NOTA
Come sappiamo, lutilizzo dei Fragment da preferire in quanto ci permette di essere pi elastici
nel caso di un Tablet. Nel nostro caso la prima attivit da creare sar quella di Splash che non
necessita di questo accorgimento. comunque disponibile anche una Blank Activity with
Fragment.

Selezioniamo ancora il pulsante Next, arrivando alla schermata rappresentata nella


Figura 1.8, la quale ci permette di inserire le informazioni relative alla nostra
SplashActivity.
A questo punto il pulsante Finish abilitato e possiamo procedere con la creazione
del progetto e la conseguente generazione della struttura, che possiamo vedere in
Figura 1.9. Coloro che hanno gi utilizzato una versione precedente di Android
Studio troveranno una modalit di visualizzazione diversa. In questa nuova vista
possiamo vedere come la definizione del modulo app separata dai file di
configurazione di Gradle, che possiamo vedere nella parte inferiore.

Figura 1.7 Wizard per la creazione di una Activity.

Figura 1.8 Creazione della nostra SplashActivity.

Nel modulo app notiamo come il file di configurazione AndroidManifest.xml venga


visualizzato in una parte separata rispetto al codice. Infine notiamo come le risorse
con qualificatore diverso vengano comunque visualizzate come se appartenessero a
una stessa cartella; il caso delle risorse nei file dimens.xml.

Figura 1.9 Struttura del progetto generata in automatico da Android Studio.

Prima di descrivere tutto ci che stato generato, dobbiamo per assicurarci un


paio di cose. La prima riguarda la disponibilit di tutte le librerie di cui andremo a
definire le dipendenze nel file di configurazione di Gradle ovvero il file build.gradle
associato al modulo app. Per fare questo andiamo a selezionare licona per lavvio
dellSDK Manager (Figura 1.10) per verificare la presenza delle librerie relative ai
Google Play services, della libreria di supporto e dei vari Repository cui Gradle accede
in fase di build.

Figura 1.10 Verifichiamo la presenza delle librerie necessarie per la nostra applicazione.
NOTA
Notiamo come la versione dei Google Play services 6.5 disponibile al momento corrisponda alla
revisione 22. Qualora Google rilasciasse nuove versioni, tale valore sar ovviamente diverso.

La seconda riguarda lutilizzo delle librerie in Gradle oltre alla configurazione dei
Google Play services, come evidenziato nel seguente listato:

apply plugin: 'com.android.application'
android {

compileSdkVersion 21
buildToolsVersion 21.1.2
defaultConfig {
applicationId uk.co.massimocarli.friendfence
minSdkVersion 10
targetSdkVersion 21
versionCode 1
versionName 1.0
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(proguard-android.txt),
proguard-rules.pro
}

}
}
dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile 'com.android.support:appcompat-v7:21.0.3'
compile 'com.android.support:support-v4:21.+'
compile 'com.google.android.gms:play-services:6.5.+'
}


Ovviamente la versione cui fare riferimento sar quella disponibile al momento di
creazione del progetto. Come possiamo vedere abbiamo aggiunto la dipendenza dalle
librerie di supporto e ovviamente dai Google Play services, che rappresenta il primo
passo di configurazione per il loro utilizzo. Nel precedente codice abbiamo
evidenziato la definizione della seguente dipendenza, che permette di importare nel
nostro progetto tutte le API contenute allinterno dei Google Play services:

compile 'com.google.android.gms:play-services:6.5.+'


In realt questo non sempre necessario. Se realizziamo unapplicazione che
utilizza le Location API, forse non abbiamo bisogno anche delle API per la gestione
del gaming o dei dispositivi wearable. In realt per la maggior parte delle
applicazioni questo non un problema. Per applicazioni di una certa dimensione pu
invece accadere che si verifichi un errore in fase di build dovuto al fatto che esiste
comunque un limite nel numero dei metodi (framework compreso) del codice
contenuto allinterno di un APK. Non possiamo, infatti, avere un numero di metodi
superiore a 65.536. Per questo motivo, dalla versione 6.5, possibile importare anche
solo le API che servono. Nel caso della nostra applicazione vedremo di volta in volta
quali API utilizzare e quali dipendenze impostare. Facciamo quindi subito una prima
modifica, sostituendo la definizione precedente con :

compile 'com.google.android.gms:play-services-base:6.5.87'


Questo ci permette di importare le sole API base. Unaltra modifica riguarda
leliminazione del + con un valore esplicito di versione. La parte delle dipendenze
diventa quindi la seguente. Ricordiamo che le versioni potrebbero essere diverse per
il lettore, che rimandiamo alla documentazione ufficiale, dove trover il valore
corretto.


dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.android.support:appcompat-v7:21.0.3
compile com.android.support:support-v4:21.0.3
compile com.google.android.gms:play-services:6.5.87
compile com.google.android.gms:play-services-maps:6.5.87
}


Lapplicazione generata in modo automatico permette la semplice visualizzazione
di un messaggio HelloWorld, mentre nel nostro caso vogliamo realizzare una Splash, ma
soprattutto introdurre la logica che ci permetter di fare in modo che gli stessi Google
Play services siano disponibili nella versione corretta.
Partiamo da quanto generato in modo automatico e iniziamo con la dichiarazione
dei Google Play services nel file di configurazione della nostra applicazione, ovvero
allinterno dellAndroidManifest.xml. Avendo utilizzato il template relativo a una Blank
Activity, questa definizione non avvenuta in automatico, per cui dovremo apportare
manualmente la modifica evidenziata nel seguente documento:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package=uk.co.massimocarli.friendfence>
<application
android:allowBackup=true
android:icon=@drawable/ic_launcher
android:label=@string/app_name
android:theme=@style/AppTheme>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>

<activity
android:name=.SplashActivity
android:theme=@style/FullscreenTheme

android:label=@string/app_name>
<intent-filter>
<action android:name=android.intent.action.MAIN/>
<category android:name=android.intent.category.LAUNCHER/>
</intent-filter>
</activity>
</application>
</manifest>


In realt loperazione molto semplice, in quanto il riferimento della versione dei
Google Play services verso una risorsa di tipo intero, definita nella stessa libreria
importata in precedenza. Si tratta quindi di un valore che viene modificato
automaticamente nel momento in cui viene aggiornata la versione della libreria dei
Google Play services utilizzata.
NOTA
Questo significa che il valore non dovr pi essere modificato in caso di aggiornamenti dei
Google Play services.

Nel codice abbiamo evidenziato anche lutilizzo di un tema che ci permette di


visualizzare lActivity di Splash a tutto schermo, definita nel seguente modo nel file
. Dora in poi non ci occuperemo pi di questi dettagli, che il lettore potr

styles.xml

comunque cogliere nel codice relativo agli esempi.



<style name="FullscreenTheme" parent="android:Theme.NoTitleBar.Fullscreen"/>


Lultimo passo di configurazione riguarda Proguard, il tool di offuscamento e
ottimizzazione del codice, che viene richiamato nel caso di creazione dellAPK
firmato con il certificato di produzione. infatti necessario fare in modo che le nuove
classi non vengano offuscate e quindi rese inutilizzabili dallapplicazione. Anche in
questo caso si tratta di aggiungere il seguente testo al file proguard-rules.pro nella
cartella associata al modulo del progetto. Per i dettagli relativi a questa
configurazione rimandiamo alla documentazione ufficiale di Proguard
(http://proguard.sourceforge.net/).

-keep class * extends java.util.ListResourceBundle {

protected Object[][] getContents();


}
-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {

public static final *** NULL;


}
-keepnames @com.google.android.gms.common.annotation.KeepName class *
-keepclassmembernames class * {

@com.google.android.gms.common.annotation.KeepName *;
}
-keepnames class * implements android.os.Parcelable {

public static final ** CREATOR;


}


Per creare questo file possibile passare alla visualizzazione del progetto,
selezionando la corrispondente opzione (Figura 1.11).

Figura 1.11 Vista di progetto per la visualizzazione del file di Proguard.

Terminata la fase di configurazione iniziamo quella relativa al codice Java da


aggiungere alla nostra SplashActivity. Vogliamo infatti che venga visualizzato il nome
dellapplicazione e che dopo un certo tempo si proceda verso la Activity principale,

che al momento non abbiamo ancora preparato. Per inserire la logica relativa alla
gestione dei Google Play services facciamo pulizia partendo dal seguente codice:

public class SplashActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
}
}


Ecco il documento di layout nel file activity_splash.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools=http://schemas.android.com/tools
android:layout_width=match_parent
android:layout_height=match_parent
android:background=#0099cc
tools:context=.SplashActivity>
<TextView
android:id=@+id/fullscreen_content
android:layout_width=match_parent
android:layout_height=match_parent
android:keepScreenOn=true
android:textColor=#33b5e5
android:textStyle=bold
android:textSize=50sp
android:gravity=center
android:text=@string/app_name/>
</FrameLayout>


Per eliminare la ActionBar abbiamo anche dovuto modificare le risorse relative allo
stile per i dispositivi di API Level maggiore o uguale a 11 nel file /res/values:

v11/styles.xml


<resources>

<style name=FullscreenTheme
parent=android:Theme.Holo.NoActionBar.Fullscreen/>
</resources>


E anche nel file /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

<! Base application theme. >


<style name=AppTheme parent=Theme.AppCompat.Light.DarkActionBar>
<! Customize your theme here. >
</style>
<style name=FullscreenTheme parent=android:Theme.NoTitleBar.Fullscreen>
</style>
</resources>


Lutilizzo di queste risorse di tipo style sarebbe causa di un errore nel caso in cui la
nostra Activity estendesse la classe ActionBarActivity della relativa libreria come nel
codice generato automaticamente da Android Studio. La nostra Splash non necessita
di ActionBar, per cui abbiamo semplicemente esteso la classe Activity come evidenziato
nel precedente codice. Il tema dovr quindi essere associato alla SplashActivity, come
evidenziato nellAndroidManifest.xml. Se eseguissimo lapplicazione in questo momento
otterremmo quanto rappresentato nella Figura 1.12, che non nulla di speciale, ma
che ci permette di consolidare un punto di partenza.

Figura 1.12 La visualizzazione della SplashActivity come punto di partenza.

Non ci resta che fare in modo che un utente non possa avviare la nostra
applicazione se non con la versione corretta dei Google Play services. Si tratta di un
procedimento ormai standard, per cui le stesse API ci mettono a disposizione dei
metodi di utilit per raggiungere il nostro scopo in modo semplice.
NOTA
Nel nostro caso eseguiamo questo controllo allavvio dellapplicazione, in quanto i Google Play
services sono necessari da subito. Nel caso si trattasse di funzionalit secondarie possibile
implementare la logica di controllo in un altro punto dellapplicazione.

Il codice della nostra SplashActivity prevede sostanzialmente la gestione del


controllo sulla compatibilit (presenza o versione) dei Google Play services e, in caso
di successo, il passaggio automatico alla Activity principale, che abbiamo chiamato
e che vedremo come creare utilizzando un Wizard di Android Studio.

MainActivity


public class SplashActivity extends Activity {

/**
* The Tag for the Log
*/
private static final String TAG_LOG = SplashActivity.class.getName();
/**
* The delay to wait before going to the MainActivity
*/
private static final long SPLASH_DELAY = 2000;
/**
* The Request code to use for the Dialog management
*/
private static final int GPS_REQUEST_CODE = 1;
/**
* The Handler to manage the delay message
*/
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (!isFinishing()) {
final Intent mainIntent =
new Intent(SplashActivity.this, MainActivity.class);
startActivity(mainIntent);
finish();
}
}
};
@Override

protected void onCreate(Bundle savedInstanceState) {


super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
}
@Override
protected void onResume() {
super.onResume();
// We check if the GooglePlayServices are available
int resultCode =
GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (ConnectionResult.SUCCESS != resultCode) {
// In this case the Google Play services are not installed or
// in the wrong version so we have to launch the Play Store
// for the installation
GooglePlayServicesUtil.showErrorDialogFragment(resultCode,
this, GPS_REQUEST_CODE, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
// In this case we close the app
Toast.makeText(SplashActivity.this, R.string.gps_mandatory,
Toast.LENGTH_LONG).show();
finish();
}
});
} else {
// Here we implement the logic for the MainActivity
mHandler.sendEmptyMessageDelayed(0, SPLASH_DELAY);
}

}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (GPS_REQUEST_CODE == requestCode &&
Activity.RESULT_CANCELED == resultCode) {
Log.d(TAG_LOG, Dialog closed from the Play Market);
Toast.makeText(SplashActivity.this, R.string.gps_mandatory,
Toast.LENGTH_LONG).show();
}
}
}


Il primo controllo molto semplice e utilizza il seguente metodo statico della
classe GooglePlayServicesUtil:

public static int isGooglePlayServicesAvailable (Context context)


Questo restituisce un valore intero che indica la presenza o meno dei Google Play
services nella versione corretta. Si tratta di un valore corrispondente a una delle
seguenti costanti esplicative della classe ConnectionResult:

SUCCESS
SERVICE_MISSING
SERVICE_VERSION_UPDATE_REQUIRED
SERVICE_DISABLED
SERVICE_INVALID
DATE_INVALID


Nel nostro caso controlliamo se il codice restituito diverso da SUCCESS.
Fortunatamente non necessario gestire ogni singolo caso, in quanto la stessa classe
di utilit mette a disposizione il seguente metodo:

public static boolean showErrorDialogFragment (int errorCode, Activity activity,

int requestCode, DialogInterface.OnCancelListener cancelListener)



Questo permette di creare una DialogFragment con un messaggio corrispondente al
problema da risolvere e soprattutto con la soluzione del problema. I parametri di
questo metodo meritano un piccolo approfondimento. Il parametro errorCode il
valore ottenuto in precedenza, corrispondente a una delle precedenti costanti. Il
parametro activity lActivity che ospiter il Fragment, che nel nostro caso la
. Il parametro requestCode il valore che verr utilizzato in corrispondenza

SplashActivity

della chiamata del metodo onActivityResult() che nel nostro caso ha


unimplementazione che prevede la semplice visualizzazione, attraverso un Toast, di
un messaggio che indichi come i Google Play services siano necessari allesecuzione
dellapplicazione, per poi chiuderla attraverso la chiamata del metodo finish(). Il
quarto e ultimo parametro il riferimento allimplementazione dellinterfaccia

, che permette di gestire il caso in cui lutente prema Back

DialogInterface.OnCancelListener

dopo la visualizzazione della Dialog.


NOTA
Il metodo showErrorDialogFragment() prevede anche un overload senza la necessit di impostare
un valore per il parametro cancelListener.

importante comprendere come la cancellazione della Dialog comporti la chiamata


del particolare cancelListener, ma non la chiamata del metodo onActivityResult(), il quale
viene invece richiamato nel caso in cui loperazione venisse annullata direttamente
dalla schermata del Play Market in fase di installazione dei Google Play services.
Il secondo aspetto della nostra SplashActivity riguarda il passaggio alla MainActivity
nel caso in cui il test sui Google Play services sia andato bene e siano passati 2
secondi. Come possiamo notare, questo viene realizzato attraverso un Handler che ha,
come unica accortezza, quella di controllare attraverso il metodo isFinishing(), se
lActivity stata chiusa.
NOTA
Vista la semplicit della nostra Splash abbiamo deciso di non implementare lHandler con il
consueto accorgimento della classe interna statica, al fine di eliminare possibili memory leak.

Prima di passare a un semplice test sul funzionamento di quanto creato ci


ricordiamo di creare una MainActivity temporanea.

Figura 1.13 Creazione di una Activity di tipo Navigation Drawer.

Abbiamo deciso di utilizzare un Navigation Drawer per la selezione delle diverse


opzioni, per cui utilizziamo il Wizard che Android Studio ci mette a disposizione. Per
mettere un minimo di ordine abbiamo creato un package activity e quindi selezionato

lopzione indicata nella Figura 1.13, che ci porta alla finestra rappresentata nella
Figura 1.14.
La presenza della classe MainActivity permetter la compilazione della nostra
. Eseguendo lapplicazione otterremmo la visualizzazione della Splash

SplashActivity

della Figura 1.12 e quanto rappresentato nella Figura 1.15 che andremo a
personalizzare nei prossimi capitoli. Quanto visualizzato nella Figura 1.15 potr
essere diverso da quanto visualizzato dal lettore, a seconda della particolare versione
di Android utilizzata o del particolare dispositivo.
Quello descritto il funzionamento nel caso in cui tutto vada bene ovvero che nel
device sia disponibile lultima versione di Google Play services. Il prossimo
paragrafo ci permetter di verificare cosa succede nel caso in cui i Google Play
services non fossero invece disponibili o siano di versione diversa da quella richiesta.

Figura 1.14 Configurazione della MainActivity.

Figura 1.15 La MainActivity in esecuzione.

Aggiornamento dei Google Play services


In questo capitolo non abbiamo scritto moltissimo codice, ma abbiamo
implementato qualcosa di necessario in tutte le applicazioni che si basano sui Google
Play services. Per completezza interessante osservare che cosa succederebbe
qualora eseguissimo la nostra applicazione in un dispositivo non ancora dotato di
Google Play services.
NOTA
Il lettore non interessato a questo tipo di test pu saltare al prossimo capitolo, impiegando quanto
realizzato in precedenza.

La maggior parte dei dispositivi (approvati da Google) dovrebbe disporre della


ultima versione dei Google Play services. In caso contrario, la nostra applicazione
dovrebbe chiedere allutente di procedere alla loro installazione, o aggiornamento,
dal Play Market.
NOTA
Nella descrizione che segue abbiamo utilizzato un Nexus 5. In altri dispositivi questo meccanismo
potrebbe essere leggermente diverso e potrebbe comportare la perdita di alcune informazioni.
Lasciamo quindi al lettore la piena responsabilit su quanto segue.

Cerchiamo di simulare questo processo andando nella schermata rappresentata


nella Figura 1.2 e quindi selezionando il pulsante Disable. A questo punto si ha la
visualizzazione del messaggio rappresentato nella Figura 1.16: i dati memorizzati
fino a quel momento verranno cancellati.

Figura 1.16 La disabilitazione dei GPs comporta la perdita dei dati corrispondenti.

Selezioniamo il pulsante OK e otteniamo una finestra di dialogo (Figura 1.17) nella


quale ci viene chiesto di ripristinare lapplicazione nella sua versione originale,
ovvero quella installata inizialmente nel dispositivo.

Anche in questo caso selezioniamo il pulsante OK: un messaggio ci segnala che


loperazione che stiamo per effettuare necessita dei diritti di Administrator del device
(Figura 1.18).
Se siamo sicuri della nostra scelta selezioniamo il pulsante Manage device
administrators il quale ci conduce alla schermata rappresentata nella Figura 1.19 che
contiene una checkbox che deselezioniamo arrivando alla schermata rappresentata
nella Figura 1.20.

Figura 1.17 Ripristiniamo la versione iniziale.

Figura 1.18 La rimozione necessita dei diritti di Administrator.

Figura 1.19 Otteniamo il diritto di cancellare la nostra applicazione.

Figura 1.20 In questa schermata possiamo disattivare il ruolo di Administrator.

Selezionando il pulsante Deactivate in basso a destra torniamo alla schermata


rappresentata nella Figura 1.2, ma questa volta il pulsante Uninstall updates abilitato,
come possiamo vedere nella Figura 1.21.

Figura 1.21 Ora il pulsante Uninstall updates abilitato.

Non ci resta che selezionare il pulsante Uninstall updates e dare conferma nella
successiva finestra di dialogo, per procedere alla disinstallazione, seguita da una serie
di notifiche di lamentela da parte delle altre applicazioni, tra cui, nel nostro caso,
Hangout e Google+ (Figura 1.22).

Figura 1.22 Notifiche delle applicazioni che reclamano la presenza dellultima versione di GPs.

Selezionando una di queste notifiche sar possibile andare al Play Store e ottenere
quello che invece noi vogliamo testare attraverso la nostra applicazione, che avviamo
ottenendo il messaggio rappresentato nella Figura 1.23.

Figura 1.23 La nostra applicazione, giustamente, richiede lultima versione di GPs.

A questo punto lutente potrebbe premere il pulsante Back, in quanto non intendere
procedere allaggiornamento. Questa la parte del caso duso gestito dalla nostra
implementazione dellinterfaccia DialogInterface.OnCancelListener la quale termina
lapplicazione visualizzazione un messaggio attraverso un Toast (Figura 1.24). In
questo caso il metodo di callback onActivityResult() non viene richiamato.

Figura 1.24 Se lutente rifiuta laggiornamento dobbiamo uscire dallapplicazione.

Se invece selezioniamo il pulsante Update veniamo condotti al Play Market, dove


potremo procedere allaggiornamento. Qualora, una volta giunti al Play Market,
decidessimo di rifiutare laggiornamento scegliendo ancora il pulsante Back,
cadremmo nel caso corrispondente alla cancellazione della operazione. Verr
richiamato il metodo onActivityResult() con un valore di resultCode di cancellazione,
ovvero Activity.RESULT_CANCELED. Nel nostro caso il risultato lo stesso del caso
precedente e quindi la visualizzazione di un Toast con il messaggio rappresentato
nella Figura 1.24. Questo metodo permetterebbe anche la gestione di altri tipi di
problemi che si potrebbero verificare di ritorno dalla particolare Activity di
destinazione della Dialog, che non detto sia necessariamente quella del Play Store.
Procedendo invece allaggiornamento dei Google Play services e tornando alla
nostra applicazione noteremo come il funzionamento sia quello sperato, ovvero la

visualizzazione della MainActivity dopo il tempo di attesa impostato nella SplashActivity.


Al termine consigliamo il lettore di riattivare i vincoli legati al ruolo di
Administrator del dispositivo, attraverso la corrispondente opzione che nel Nexus 5
nei Settings alla voce Security e quindi Device Administrators.

Conclusioni
Nella prima parte di questo capitolo abbiamo voluto giustificare la realizzazione
dei Google Play services come un modo per diminuire la forte dipendenza della
piattaforma dai diversi produttori di dispositivi. Attraverso unaccorta separazione tra
quello che fortemente legato allhardware e quello che invece gestibile a livello
software stato possibile creare un meccanismo di aggiornamento della piattaforma
pi affidabile e quindi in grado di soddisfare maggiormente i diversi utenti oltre che
Google stessa.
Dopo aver dato una panoramica su quelli che saranno i servizi offerti dai Google
Play services abbiamo iniziato lo sviluppo dellapplicazione FriendFence, occupandoci
di quello che ogni applicazione di questo tipo dovr fare ovvero verificare la presenza
dei Google Play services nella corretta versione, gestendone eventualmente
laggiornamento attraverso laccesso al Play Market. Abbiamo quindi concluso
attraverso la simulazione di quello che accadrebbe nel caso in cui lutente non
disponesse dei Google Play services necessari alla nostra applicazione. Quello
ottenuto alla fine di questo capitolo lo scheletro di unapplicazione, con alcune voci
di menu che andremo a modificare e riempire di volta in volta nei prossimi capitoli.

Capitolo 2

La gestione della Location

Come abbiamo accennato nel capitolo introduttivo, quella della gestione della
location, in termini di latitudine e longitudine, stata una delle prime funzionalit
che Google ha deciso di spostare da quello che possiamo chiamare ambiente base ai
Google Play services. Oltre alle motivazioni gi descritte per tutti i servizi in genere,
la gestione della location ne ha anche unaltra legata alle prestazioni e, in particolare,
alla durata della batteria. Come vedremo, specialmente nel caso di unelevata
accuratezza, la gestione della location unoperazione piuttosto impegnativa.
Qualora pi applicazioni avessero la necessit di gestire una stessa informazione,
sicuramente vantaggioso fare in modo che essa venga gestita in un punto solo.
Questo, per esempio, permette a unapplicazione di utilizzare la stessa location
appena richiesta da unaltra, senza ulteriore overhead. Allo stesso modo, i Google
Play services possono decidere il livello di accuratezza pi efficiente per poter gestire
le richieste da parte di pi applicazioni. Unapplicazione che si accontenti di
unaccuratezza bassa potrebbe anche godere di uninformazione pi precisa
disponibile a seguito della richiesta da parte di unaltra applicazione. Laspetto
fondamentale che questa decisione ora centralizzata e pu essere gestita e
migliorata nelle versioni successive della piattaforma. In Android ogni componente
in grado di fornire informazioni di Location si chiama Provider; ne esistono diverse
implementazioni che differiscono per la modalit di acquisizione dei dati,
conseguente accuratezza e quindi costo per poterla ottenere. Lunificazione di cui
abbiamo parlato prima si quindi tradotta nella creazione di quello che viene
chiamato Fused Location Provider, ovvero di unimplementazione che permette di gestire
in modo ottimizzato le informazioni di localizzazione in relazione allo stato del
dispositivo e alle necessit delle diverse applicazioni. Questa unificazione offre
vantaggi da diversi punti di vista. Per il programmatore rappresenta unevidente
semplificazione nello sviluppo. Come vedremo non dovremo pi preoccuparci della
gestione dei singoli Provider, ma ci accontenteremo di specificare il grado di
accuratezza dellinformazione che ci serve, la sua durata oltre ad altri criteri che
vedremo in dettaglio. Proprio per il fatto che si tratta di un Provider condiviso, molto
probabile che la nostra applicazione disponga subito di uninformazione di Location
perch richiesta poco prima da unaltra applicazione, con conseguente estensione
dellutilizzabilit.

In questo capitolo vogliamo sfruttare questi servizi per implementare le


funzionalit dellapplicazione FriendFence, che utilizza in modo diretto le informazioni
di location. Implementeremo, come prima funzionalit , quella che chiamiamo Where
I Am, che ci permetter di conoscere la nostra posizione sia in termini di latitudine e
longitudine, sia in termini di indirizzo. Da uninformazione statica realizzeremo poi
la funzione Where I Go, che ci permetter di sapere dove stiamo andando e,
soprattutto, come, attraverso la nuova funzionalit di ActivityRecognition che ci dir
se stiamo camminando, se siamo fermi, se andiamo in bicicletta e cos via.
Memorizzeremo le informazioni in un nostro ContentProvider, che ci sar utile anche
nel prossimo capitolo per visualizzare le informazioni in una mappa. Infine vedremo
come definire delle regioni, chiamate GeoFence, alle quali associare una serie di eventi,
che andremo poi a utilizzare nei capitoli successivi.

Design for Change


Come accennato, la funzionalit Where I Am ci permetter di visualizzare le
informazioni relative alla nostra posizione in termini di latitudine e longitudine, cui
assoceremo un indirizzo attraverso un servizio di Geocoder. Prima di fare tutto
questo, dobbiamo per riprendere lapplicazione che abbiamo creato nel capitolo
precedente e creare una struttura che ci permetta di aggiungere funzionalit in modo
dinamico. Per farlo abbiamo deciso di creare un package per ciascun argomento,
allinterno del quale distingueremo i diversi Fragment dalle classi di utilit ed eventuali
. Nel caso di questa prima funzionalit vogliamo fare in modo che tutta la

Activity

logica sia in qualche modo incapsulata in un Fragment che chiamiamo


. Come abbiamo detto nella parte introduttiva del primo capitolo,

StaticLocationFragment

i Google Play services vanno in un certo senso inizializzati e la documentazione


ufficiale lega questo meccanismo al ciclo di vita di una Activity. Nel nostro caso, al
fine di semplificarne la descrizione, vogliamo rendere le funzionalit il pi possibile
indipendenti e quindi rendere i vari Fragment autoconsistenti.
NOTA
Nella struttura della nostra applicazione, i Fragment vengono aggiunti a una stessa Activity
attraverso il meccanismo del Navigation Drawer, per cui linizializzazione potrebbe avvenire in
modo centralizzato. Vedremo inoltre che sar necessario apportare una modifica alla MainActivity
in relazione al metodo onActivityResult() richiamato.

Prima di creare il nostro Fragment facciamo un po di refactoring per quanto riguarda


lelenco delle funzionalit che andremo a realizzare. Nel codice creato da Android
Studio notiamo la presenza della classe NavigationDrawerFragment, che contiene al proprio
interno la definizione di un array delle opzioni nel seguente modo (nel metodo
onCreateView()):

mDrawerListView.setAdapter(new ArrayAdapter<String>(

getActionBar().getThemedContext(),
android.R.layout.simple_list_item_1,
android.R.id.text1,
new String[]{
getString(R.string.title_section1),
getString(R.string.title_section2),
getString(R.string.title_section3),
}));

La nostra prima modifica consiste quindi nel creare una risorsa di tipo string-array
che abbiamo inserito nel file /res/array.xml e che al momento molto semplice:

<?xml version="1.0" encoding="utf-8"?>
<resources>

<string-array name=menu_options>
<item>@string/menu_item_where_i_am</item>
<item>@string/menu_item_where_i_go</item>
</string-array>
</resources>


Da notare solo come non siano state inserite direttamente le voci di menu, ma i
riferimenti ad altrettante risorse di tipo String, che potranno quindi essere
internazionalizzate. Questo ci porta a modificare il precedente codice con il seguente,
nel quale abbiamo evidenziato i cambiamenti

final String[] menuOptions = getActivity()
.getResources().getStringArray(R.array.menu_options);
mDrawerListView.setAdapter(new ArrayAdapter<String>(

getActionBar().getThemedContext(),
android.R.layout.simple_list_item_1,
android.R.id.text1,
menuOptions));

Con questa semplice modifica non dovremmo pi tornare su questa classe, in
quanto andremo a modificare le opzioni direttamente nella risorsa. Quello che
dobbiamo fare invece modificare la nostra MainActivity, dove viene gestita la
selezione della voce di menu. Dovremo quindi cambiare limplementazione dei
seguenti due metodi:

@Override
public void onNavigationDrawerItemSelected(int position) {

// update the main content by replacing fragments


FragmentManager fragmentManager = getSupportFragmentManager();

fragmentManager.beginTransaction()
.replace(R.id.container,
PlaceholderFragment.newInstance(position + 1))
.commit();
}
public void onSectionAttached(int number) {

switch (number) {
case 1:
mTitle = getString(R.string.title_section1);
break;
case 2:
mTitle = getString(R.string.title_section2);
break;
case 3:
mTitle = getString(R.string.title_section3);
break;
}
}


Il primo metodo viene infatti richiamato in corrispondenza della selezione di una
voce di menu, mentre il secondo permette di definire il titolo da visualizzare nella
ActionBar per la selezione stessa. Per non dover modificare pi neppure questa classe
abbiamo definito una semplice FragmentFactory, la cui responsabilit sar quella di
fornire il Fragment corrispondente alla selezione fatta. Il precedente codice diventa
quindi:

@Override
public void onNavigationDrawerItemSelected(int position) {

// update the main content by replacing fragments


FragmentManager fragmentManager = getSupportFragmentManager();
final Fragment optionFragment = FragmentFactory.get()
.getFragment(this, position);
mCurrentFragment = optionFragment;

fragmentManager.beginTransaction()
.replace(R.id.container, optionFragment)
.commit();
}
public void onSectionAttached(int number) {

// We return the label for the given option


mTitle = getResources().getStringArray(R.array.menu_options)[number];
}


In questo modo tutta la logica di creazione dei Fragment da visualizzare in
corrispondenza di ciascuna opzione nella FragmentFactory, che al momento sar molto
semplice.
NOTA
Nella classe MainActivity ci siamo anche preoccupati di cancellare completamente la classe
PlaceholderFragment, che era stata creata come esempio di Fragment e che ora non serve pi.

Come possiamo osservare nel precedente codice, la nostra classe FragmentFactory


molto semplice, in quanto implementa il pattern Singleton
(http://en.wikipedia.org/wiki/Singleton_pattern) e definisce un metodo di Factory che
abbiamo evidenziato e che conterr appunto la logica descritta in precedenza di
associazione fra voce di menu e corrispondente Fragment.

public final class FragmentFactory {

/**
* The Singleton Instance
*/
private static FragmentFactory sFragmentFactory;
/**
* Private constructor
*/
private FragmentFactory() {
}
/**

* @return The FragmentFactory Singleton


*/
public synchronized static FragmentFactory get() {
if (null == sFragmentFactory) {
sFragmentFactory = new FragmentFactory();
}
return sFragmentFactory;
}
public final Fragment getFragment(final Context context,
final int selection) {
return new Fragment();
}
}


Al momento non facciamo altro che restituire unistanza di Fragment, che non
porter alla visualizzazione di alcuna informazione.
Prima di procedere allimplementazione della prima funzionalit importante
descrivere un aspetto cui avevamo accennato in una precedente nota, relativo alla
gestione del ciclo di vita di inizializzazione di Google Play services legato a quello
della Activity. In alcuni casi abbiamo infatti visto come la comunicazione avvenga
attraverso la chiamata del metodo onActivityResult(). Il problema riguarda il fatto che
tale metodo non viene richiamato nel Fragment che gestisce, nel nostro caso, il processo
di inizializzazione, ma nella Activity contenitore. Questo il motivo per cui in
precedenza abbiamo memorizzato in una variabile distanza il riferimento al Fragment
corrente:

mCurrentFragment = optionFragment;


e, soprattutto, questo il motivo della seguente implementazione del metodo
onActivityResult() nella classe MainActivity.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {

super.onActivityResult(requestCode, resultCode, data);

// IMPORTANT!!!!!
// We need this because the lifecycle of the Google Play Service
// initialization is bounded to the Activity and not to the Fragment.
if (mCurrentFragment != null) {
mCurrentFragment.onActivityResult(requestCode, resultCode, data);
}
}


In questo modo le eventuali chiamate che provengono dal ciclo di inizializzazione
eseguito in un Fragment ritornano al Fragment stesso. Ora siamo per pronti alla nostra
prima funzionalit.

Inizializzazione dei servizi di Location


Come accennato in precedenza, lutilizzo dei Google Play services prevede sempre
una fase di inizializzazione che pu differire di poco da servizio a servizio e che
abbiamo deciso quindi di incapsulare allinterno di ciascuna funzionalit. Nel nostro
caso vogliamo creare la classe MyLocationFragment con lo scopo di implementare la
funzione Where I Am. Creiamo quindi un Fragment allinterno del package location
utilizzando il Wizard di Android Studio, ottenendo la schermata rappresentata nella
Figura 2.1.

Figura 2.1 Creazione di un Fragment con Android Studio.

Come possiamo vedere, abbiamo selezionato la possibilit di creare il documento


XML di layout, ma non il classico metodo di factory o leventuale interfaccia di
callback. Questo perch non si tratta di un Fragment che necessita di parametri di input,
n conterr uninterfaccia che permetta di notificare eventi allActivity che lo
contiene. Selezionando il pulsante Finish otteniamo quindi il classico codice di un
, cui abbiamo associato un documento XML di layout.

Fragment


public class MyLocationFragment extends Fragment {

public MyLocationFragment() {
// Required empty public constructor

}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_my_location,
container, false);
}
}


Prima di procedere, ricordiamoci di associare questo Fragment alla prima voce di
menu nella nostra classe FragmentFactory, nel seguente modo.

public final Fragment getFragment(final Context context, final int selection) {

switch (selection) {
case 0:
return new MyLocationFragment();
}
return new Fragment();
}


Torniamo finalmente al nostro MyLocationFragment e procediamo allinizializzazione
del servizio di localizzazione, che cambiato nelle ultime versioni dei Google Play
services e prevede, come per gli altri servizi che vedremo, linizializzazione di un
oggetto di tipo GoogleApiClient, al quale poi chiederemo il riferimenti ad altri
componenti relativi alle specifiche funzionalit.
NOTA
Nelle precedenti versioni dei Google Play services esisteva un grosso problema di design che
costringeva lo sviluppatore a inizializzare oggetti diversi, ciascuno dei quali possedeva un ciclo di
vita collegato a quello di una Activity o Fragment. Fortunatamente ora lunico oggetto di cui
necessario gestire il ciclo di vita quello di tipo GoogleApiClient.

Il primo passo consiste quindi nellinizializzazione di un oggetto di tipo


GoogleApiClient attraverso le seguenti righe di codice, che possiamo vedere nel nostro
metodo onCreate()

@Override
public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mGoogleApiClient = new GoogleApiClient.Builder(getActivity())
.addApi(LocationServices.API)
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedListener)
.build();
}


Come possiamo notare viene utilizzata unimplementazione del Builder Pattern
che ci permette di specificare quelle che sono le API di cui abbiamo bisogno (in
questo caso quelle di localizzazione) e quindi fornire le implementazioni di due
interfacce che ci permetteranno di gestire il caso di inizializzazione con successo e
quello di eventuale errore.
NOTA
Quelle che descriveremo sono operazioni che, come altre analoghe e ripetitive, non
descriveremo pi per le successive funzionalit, se non strettamente necessario.

Come vedremo anche successivamente la nuova architettura dei Google Play


services prevede la definizione di una costante per ogni possibile funzionalit. Nel
nostro caso la costante LocationServices.API permette appunto di specificare la necessit
dei servizi relativi alla location, che ci verranno poi messi a disposizione da un
oggetto di tipo FusedLocationProviderApi.
NOTA
Alla luce di quanto fatto nel Capitolo 1, siamo sicuri che comunque i Google Play services siano
disponibili, per cui non dobbiamo implementare quella logica di controllo sulla loro presenza e
sulla loro versione.

Linizializzazione di un oggetto di tipo GoogleApiClient non immediata e si rende


necessario gestire sia il caso di avvenuta inizializzazione, sia quello di errore. A tale
proposito necessario implementare due interfacce.
Nella variabile mConnectionCallbacks abbiamo memorizzato una nostra
implementazione dellinterfaccia GoogleApiClient.ConnectionCallbacks, responsabile della
gestione degli eventi di connessione e disconnessione. Nel nostro caso

limplementazione molto semplice, ma molto importante, in quanto ci mostra come


accedere al primo dei servizi di localizzazione di cui abbiamo bisogno, ovvero quello
che restituisce lultima posizione ottenuta dalla nostra o da unaltra applicazione.

private final GoogleApiClient.ConnectionCallbacks mConnectionCallbacks

= new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle bundle) {
Log.d(TAG_LOG, Connected);
// We update the data into the View with the first location
final Location firstLocation = LocationServices.FusedLocationApi
.getLastLocation(mGoogleApiClient);

displayLocation(firstLocation);
}
@Override
public void onConnectionSuspended(int i) {
Log.d(TAG_LOG, Disconnected. Please re-connect.);
}
};


Come possiamo notare, linterfaccia prevede la definizione di due operazioni
relative allavvenuta connessione o disconnessione con i Google Play services. In
caso di successo abbiamo evidenziato come la classe LocationServices disponga del
membro statico FusedLocationApi che ci mette a disposizione il metodo:

public Location getLastLocation()


Questo metodo ha lenorme vantaggio di non richiedere alcuna risorsa particolare.
Se in precedenza la stessa o unaltra applicazione aveva fatto richiesta di una
posizione, questa viene restituita attraverso un oggetto di tipo Location. Nel raro caso
in cui questa informazione non fosse disponibile, si otterrebbe semplicemente un
valore null, che dovremo gestire in fase di visualizzazione. Nel nostro caso abbiamo

richiamato semplicemente un nostro metodo di utilit, displayLocation(), per la


visualizzazione del dato sul display.
Non pu andare sempre tutto bene, per cui necessario gestire gli eventuali casi di
errore. Questa la ragione della presenza dellinterfaccia
GooglePlayServicesClient.OnConnectionFailedListener, che abbiamo implementato nel
seguente modo:

private final GoogleApiClient.OnConnectionFailedListener

mOnConnectionFailedListener =
new GoogleApiClient.OnConnectionFailedListener() {
Override
public void onConnectionFailed(ConnectionResult connectionResult) {
if (connectionResult.hasResolution()) {
try {
connectionResult
.startResolutionForResult(getActivity(),
CONNECTION_FAILURE_RESOLUTION_REQUEST);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
}
} else {
DialogFragment dialogFragment = new DialogFragment();
dialogFragment.show(getFragmentManager(),
Error: + connectionResult.getErrorCode());
}
}
};


Possiamo notare come la gestione degli errori non differisca di molto da quella
relativa alla verifica della presenza dei Google Play services fatta nel capitolo

precedente. Il metodo onConnectionFailed() contiene infatti un parametro di tipo


che ci fornisce alcune informazioni sullerrore. Attraverso la chiamata

ConnectionResult

del suo metodo hasResolution() siamo inoltre in grado di ottenere un Intent, lanciando il
quale si va automaticamente a una schermata per la risoluzione del problema. In caso
contrario, eventualit molto rara, visualizzeremo semplicemente una Dialog con un
messaggio di errore.
Come gi detto, quella della Location da un lato uninformazione sensibile e
dallaltro un dato che richiede diverse risorse da parte del dispositivo. Per questo
motivo, laccesso a questa informazione richiede la definizione di una Permission
attraverso il corrispondente elemento nel file di configurazione AndroidManifest.xml. Da
molto tempo esistono due tipi di Permission e precisamente

android.permission.ACCESS_COARSE_LOCATION


e

android.permission.ACCESS_FINE_LOCATION


che differiscono per il livello di accuratezza. La prima richiede al dispositivo
laccesso ai meccanismi meno costosi in termini di risorse (Wi-Fi e celle), in grado
per di dare informazioni solo indicative sulla posizione. La seconda permette invece
di richiedere lutilizzo di meccanismi in grado di fornire posizioni accurate come il
GPS (Global Positioning System).
NOTA
Come vedremo pi avanti, questa impostazione ha un importante impatto sullesito dellinvio di
una LocationRequest in relazione specialmente a quella che chiameremo Priority.

Linizializzazione di unistanza di GoogleApiClient come fatto nel metodo onCreate()


non porta comunque alla creazione di una connessione con il corrispondente servizio.
Per farlo necessario richiamare in modo esplicito il suo metodo:

public void connect()

Una volta utilizzato importante eseguire la disconnessione attraverso la chiamata


del suo metodo:

public void disconnect()


Non ci resta che legare la chiamata di questi metodi al ciclo di vita del Fragment e
quindi provvediamo allimplementazione dei seguenti due metodi:

@Override
public void onStart() {

super.onStart();
// Here we have to connection the client
mGoogleApiClient.connect();
}
@Override
public void onStop() {

// Here we have to disconnect the client


mGoogleApiClient.disconnect();
// And then call super.onStop()
super.onStop();
}


Nel metodo onStart() provvediamo alla chiamata del metodo connect(), mentre il
metodo disconnect() verr richiamato in corrispondenza del metodo onStop().
NOTA
Da notare la simmetria nelle chiamate dei metodi sul GoogleApiClient e i metodi di callback del
Fragment attraverso il riferimento super.

A questo punto la nostra prima funzionalit quasi completa, anche se non


abbiamo in alcun modo gestito laggiornamento della Location corrente e nemmeno la
visualizzazione del corrispondente indirizzo. Le informazioni visualizzate sono infatti
quelle ottenute dal metodo getLastLocation(), che quindi dipendono da quello che il
sistema e le altre applicazioni ci possono regalare; non abbiamo ancora fatto alcuna
richiesta esplicita. Quello di richiesta il termine giusto, in quanto linterazione con i
LocationServices avviene proprio attraverso la creazione di una particolare istanza della

classe LocationRequest, che ci permetter di impostare una serie di opzioni che verranno
valutate dai Google Play services per fornirci uninformazione di Location pi utile
possibile. Si tratta di un oggetto attraverso il quale imposteremo non solo le
informazioni relative allaccuratezza di cui abbiamo bisogno, ma anche informazioni
legate alla frequenza con cui vorremo essere notificati con nuovi dati.
Prima di proseguire diamo qualche indicazione sul codice del metodo di
visualizzazione della posizione, che non riportiamo per brevit, ma che il lettore pu
consultare direttamente nel progetto. A parte i riferimenti agli elementi
dellinterfaccia utente, notiamo come sia stata definita la seguente variabile

/**
* The last location
*/
private volatile Location mLastLocation;


la cui responsabilit quella di contenere lultima lettura di Location ottenuta. Da
notare lutilizzo del modificatore volatile, per fare in modo che tutti i Thread coinvolti
ne vedano lo stesso valore.
I programmatori Java sono solitamente familiari con il costrutto

synchronized (obj){

// Critic region
}


il quale permette di fare in modo che un solo Thread alla volta acceda a un blocco di
codice che si definisce regione critica. Si tratta dellapplicazione di un pattern che si
chiama Monitor Object (http://www.dre.vanderbilt.edu/~schmidt/PDF/monitor.pdf). un
argomento molto esteso, che quindi non affrontiamo in questa sede, ma che ci
permette di descrivere il modificatore volatile, il quale permette di gestire una
variabile come se fosse possibile accedervi solamente dallinterno della regione
critica. In questo modo, quando un Thread esegue una modifica, si sicuri che tutti gli
altri ne vedano subito il risultato. Si tratta di un aspetto non banale, che impedisce
allambiente di runtime di applicare alcune ottimizzazioni che si basano su copie
locali a ciascun thread della stessa variabile. Un altro aspetto che possiamo osservare
nel nostro metodo displayLocation() riguarda luso di alcuni metodi statici di utilit

della classe Location, che ci permettono di scegliere il formato di visualizzazione,


come il metodo

public static String convert (double coordinate, int outputType)


Lasciamo al lettore la descrizione del documento di layout fragment_my_location.xml,
peraltro molto semplice.

Determiniamo la nostra posizione


Per implementare la nostra prima funzionalit abbiamo la necessit di ottenere
uninformazione di Location aggiornata ogni volta che selezioniamo unaction nella
, come si pu vedere nella Figura 2.2. Vogliamo fare in modo che,

ActionBar

selezionando licona in alto a destra, si ottenga un aggiornamento dellinformazione


di Location da visualizzare poi sul display.

Figura 2.2 Le informazioni ottenute dal LocationServices.FusedLocationApi.

A differenza di quello che vedremo nel prossimo paragrafo, in questo caso


abbiamo bisogno di un unico aggiornamento. Possiamo ottenerlo attraverso il
seguente codice, che abbiamo inserito nel metodo updateLocation(), richiamato al
momento della selezione dellazione di aggiornamento.

private void updateLocation() {

// Here we create the LocationRequest to send to our client to get


// an updated position
LocationRequest locationRequest = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setNumUpdates(1)
.setExpirationDuration(LOCATION_DURATION_TIME);
LocationServices.FusedLocationApi
.requestLocationUpdates(mGoogleApiClient, locationRequest, new
LocationListener() {

@Override
public void onLocationChanged(Location location) {
// We update the location with the
displayLocation(location);
}
});
}


Come possiamo osservare, abbiamo due fasi distinte, che corrispondono alla
richiesta di Location e alla relativa ricezione del risultato per la conseguente
visualizzazione. La prima fase avviene attraverso la creazione di un oggetto di tipo
LocationRequest, di cui dobbiamo specificare la priorit attraverso il metodo:
public LocationRequest setPriority (int priority)

Si tratta di uninformazione fondamentale, che ha impatti sulle risorse utilizzate


dalla nostra applicazione e quindi sul consumo di energia. I possibili valori sono
descritti da altrettante costanti della stessa classe LocationRequest e precisamente:

PRIORITY_HIGH_ACCURACY
PRIORITY_LOW_POWER
PRIORITY_NO_POWER
PRIORITY_BALANCED_POWER_ACCURACY


Nel nostro esempio abbiamo utilizzato la prima di queste costanti, attraverso la
quale indichiamo la volont di richiedere linformazione di Location pi accurata
possibile. La brutta notizia consiste nel fatto che il risultato dipender da moltissimi
fattori, non sempre di facile individuazione. In genere diciamo che con questo valore
intendiamo richiedere la migliore informazione di Location che il dispositivo, con le
proprie caratteristiche e impostazioni, in grado di fornirci. Si tratta dellopzione pi
impegnativa, in quanto presuppone lutilizzo dellhardware preposto, ovvero il GPS
(inteso come Global Positioning System questa volta) oppure il Wi-Fi o le famose
celle. Non possiamo nemmeno dire che dipende dalle impostazioni di sicurezza, in
quando il sistema potrebbe darci informazioni richieste e ottenute anche da altre
applicazioni. Potremmo, per esempio, avere una Permission di tipo ACCESS_COARSE_LOCATION
e ottenere uninformazione accurata perch richiesta da unaltra applicazione con
Permission ACCESS_FINE_LOCATION. Qualora non fosse necessaria unelevata accuratezza,

possibile utilizzare una priorit corrispondente al valore PRIORITY_LOW_POWER, il quale


viene chiamato city in quanto offre una precisione di circa 10 Km, che permette
appunto di sapere in che citt o zona siamo. Si tratta di uninformazione che pu
essere raggiunta anche con il Wi-Fi o lutilizzo delle celle e che quindi non richiede
molte risorse. Molto interessante invece il significato della costante PRIORITY_NO_POWER,
la quale permette di ottenere linformazione di Location come conseguenza delle
richieste fatte dalle altre applicazioni; come se la nostra applicazione non
contribuisse in alcun modo al consumo di energia, ma utilizzasse le informazioni
precedentemente richieste dalle altre. Infine la costante PRIORITY_BALANCED_POWER_ACCURACY
ci permette di fare riferimento a uninformazione con unaccuratezza di 100 m, che si
pu considerare, per moltissime applicazioni, un buon compromesso in termini di
consumi e precisione. Quale valore utilizzare dipender dallapplicazione che
intendiamo realizzare. Un navigatore necessita della maggiore accuratezza possibile,
mentre la nostra applicazione si pu anche accontentare di unapprossimazione di
100 m.
NOTA
Notiamo come molti dei metodi della classe LocationRequest restituiscano un oggetto dello stesso
tipo. Si tratta di una tecnica che si chiama chaining e che ci permette di concatenare le diverse
chiamate una di seguito allaltra, come nel nostro esempio.

Come abbiamo gi detto, la nostra LocationRequest vuole ottenere una sola


informazione di Location, per cui possiamo utilizzare il seguente metodo, passando 1
come valore del parametro:

public LocationRequest setNumUpdates (int numUpdates)


Dobbiamo infatti fare attenzione, in quanto, di default, linformazione di Location
verrebbe inviata pi volte. Altro aspetto cui prestare attenzione riguarda la
cancellazione della richiesta, anche nel caso, come il nostro, in cui il numero di
aggiornamenti fosse unico. Questo perch, nel caso in cui tale informazione non
fosse disponibile, la richiesta resterebbe appesa indefinitamente. Per cancellare la
richiesta abbiamo due possibilit. La prima consiste nellutilizzo del metodo

public LocationRequest setExpirationDuration (long millis)

il quale permette di specificare il tempo oltre il quale la richiesta deve essere


considerata obsoleta e quindi rimossa. Nel caso volessimo specificare un tempo
preciso (comunque relativo al tempo di boot del dispositivo) anche possibile
utilizzare la seguente opzione:

public LocationRequest setExpirationTime (long millis)


In entrambi i casi dobbiamo fare attenzione a un paio di particolari. Il primo
consiste nel fatto che il tempo impostato relativo al momento della chiamata del
corrispondente metodo e non allinstante in cui la richiesta viene inviata al
LocationServices.FusedLocationApi. Il secondo accorgimento riguarda il fatto che se si
vuole riciclare una richiesta scaduta, necessario chiamare nuovamente gli stessi
metodi.
Nel nostro esempio abbiamo utilizzato il metodo setExpirationDuration(), passando
un valore di 5000 millisecondi definito in una nostra costante statica. importante
sottolineare come linvio della LocationRequest non presupponga necessariamente una
risposta. Se, per esempio, la durata impostata in precedenza troppo bassa, potrebbe
succedere che la richiesta venga invalidata prima ancora di ottenere in risposta un
valore.
NOTA
Come accennato, esistono anche metodi che permettono di rimuovere la LocationRequest dal
LocationServices.FusedLocationApi. Si tratta comunque di una modalit che approfondiremo nei

prossimi paragrafi.

Ma come facciamo a ottenere linformazione di Location corrispondente allinvio


della nostra LocationRequest? Esistono diverse modalit, ma quella da noi utilizzata
prevede il semplice utilizzo di unimplementazione di uninterfaccia di callback, e
precisamente linterfaccia LocationListener.

LocationServices.FusedLocationApi

.requestLocationUpdates(mGoogleApiClient, locationRequest, new


LocationListener() {
@Override
public void onLocationChanged(Location location) {
// We update the location with the

displayLocation(location);
}
});


Notiamo come questo avvenga in modo molto semplice. Il metodo
onLocationChanged() viene richiamato nel momento in cui linformazione richiesta
disponibile e quindi passata come parametro. Nel nostro caso non facciamo altro che
visualizzarla nello schermo attraverso il metodo displayLocation() descritta in
precedenza. Un aspetto interessante in questa fase riguarda il Thread nel quale questo
metodo di callback viene richiamato. La risposta molto semplice e prevede che esso
venga richiamato nello stesso Thread in cui avviene la chiamata del metodo
, il quale deve comunque disporre di un Looper opportunamente

requestLocationUpdates()

inizializzato.
NOTA
A differenza di quello che avveniva con una versione precedente delle API dei Google Play
services, moltissimi dei metodi che vedremo sono statici e necessitano, come parametro, del
riferimento alloggetto GoogleApiClient inizializzato nel modo descritto in precedenza.

Nel nostro caso, il Thread di riferimento quello dellinterfaccia utente (o Thread


principale), il quale ha gi un Looper, chiamato MainLooper, inizializzato
automaticamente in fase di creazione dellapplicazione. Questo il motivo per cui
possiamo richiamare senza alcun problema il metodo displayLocation(). Qualora
volessimo gestire il tutto in un Thread diverso, il metodo da utilizzare sarebbe il
seguente, al quale viene appunto passato il riferimento a un Looper:

public void requestLocationUpdates (LocationRequest request, LocationListener listener, Looper
looper)


NOTA
Il concetto di Looper esula dalla trattazione dei Google Play services, ma merita un veloce
riassunto. In Android a ogni Thread possibile associare un unico Looper, che sostanzialmente
i l Consumer di una coda di Message (chiamata appunto MessageQueue). Linterazione con
questa coda di messaggi avviene attraverso limpiego di uno o pi Handler. Gli Handler vengono
infatti utilizzati per la creazione e invio dei messaggi (Producer della coda) e, allo stesso tempo,
per lelaborazione delle informazioni in essi contenute. Laspetto chiave consiste nel fatto che
quando un Looper estrae un messaggio dalla propria MessageQueue, lo invia allo stesso
Handler che lo ha creato, il quale lo elabora nel Thread associato al Looper stesso.

Le motivazioni che portano a una soluzione come questa potrebbero essere


unelaborazione complessa della Location prima della conseguente visualizzazione. Se
questo avvenisse nel Thread principale, avremmo il pericolo di una ANR Application
Not Responding.
NOTA
Qualora loperazione da eseguire fosse particolarmente impegnativa e si avesse lesigenza di
disaccoppiarla dalla parte di UI, la classe LocationServices.FusedLocationApi prevede un terzo
meccanismo di gestione della Location attraverso un PendingIntent per linvio del dato a un
Service, come vedremo nei prossimi paragrafi.

Per quanto riguarda la funzionalit di visualizzazione della posizione corrente, non


ci resta che eseguire la nostra applicazione e osservare che cosa succede nel caso in
cui selezioniamo lazione di aggiornamento della posizione sulla ActionBar. A tale
proposito facciamo alcune semplici considerazioni. La prima riguarda il fatto che
allavvio dellapplicazione, se disponibile, viene visualizzata uninformazione di
Location che ha un tempo precedente a quello dellavvio stesso. Questo dovuto al
fatto che abbiamo richiamato il metodo getLastLocation(), che pu restituire
uninformazione ottenuta in precedenza da unaltra applicazione. Se procediamo a un
aggiornamento, notiamo come nella barra di stato venga visualizzata licona tipica
della Location, come possiamo anche vedere nella Figura 2.2. Terminata la fase di
reperimento della posizione, notiamo come licona sparisca e il dato venga
aggiornato sul display. Unaltra osservazione riguarda invece ci che succede in caso
di disabilitazione del servizio di Location, che possiamo ottenere nellapplicazione dei
Google Play services, come indicato nella Figura 2.3.
In questo caso lavvio della nostra applicazione porta alla visualizzazione del Toast
Location not Available, come il lettore potr verificare direttamente.

Figura 2.3 Disabilitiamo il servizio di Location.

Concludiamo il paragrafo con unosservazione legata alla visualizzazione


dellinformazione di altitudine, che notiamo essere vuota nella Figura 2.2. Si tratta di
uninformazione che disponibile solamente nel caso di unelevata accuratezza,
come quella che si pu ottenere attraverso il GPS. Nel nostro caso, per, limmagine
stata presa al chiuso e quindi in una situazione in cui i satelliti non sono visibili.
Lasciamo al lettore la verifica che lesecuzione dellapplicazione allaperto permetta
la visualizzazione anche di quellinformazione.

Utilizzo del Geocoder


La seconda parte della nostra funzionalit Where I Am prevede che venga visualizzato
lindirizzo corrispondente alla Location ottenuta. Si tratta di uninformazione che
possiamo ottenere da una particolare classe che si chiama Geocoder e che viene ora
messa a disposizione dei Google Play services.
In realt si tratta di una funzionalit molto semplice, che presenta lunica
problematica di dover essere utilizzata in un Thread separato rispetto a quello
principale; , infatti, unoperazione che necessita della connessione a un server, e
quindi potrebbe richiedere del tempo, se non addirittura fallire. Anche in questo caso
dovremo verificare innanzitutto la disponibilit del server e poi, in caso affermativo,
utilizzare per la georeferenziazione alcuni metodi statici della classe Geocoder. Si tratta
di un meccanismo molto semplice, che abbiamo incapsulato nel seguente metodo, il
quale viene richiamato a seguito della selezione della corrispondente azione aggiunta
nella ActionBar attraverso lediting della risorsa di tipo menu in /res/menu/my_location.xml.

private void geoCodeLocation(final Location location) {

if (null != location) {
// We check if the Geocoder is present
if (Geocoder.isPresent()) {
// Here we have to execute the geocode in asynchronously
final GeoCoderAsyncTask geoCoderAsyncTask =
new GeoCoderAsyncTask(this, MAX_GEOCODE_RESULTS);
geoCoderAsyncTask.execute(location);
} else {
// In this case the Geocoder service is not available
Toast.makeText(getActivity(),
R.string.my_location_geocoder_not_available,
Toast.LENGTH_SHORT).show();
}
} else {

// In this case theres no location to geocode


Log.w(TAG_LOG, No Location to geocode!);
Toast.makeText(getActivity(), R.string.my_location_not_available,
Toast.LENGTH_SHORT).show();
}
}


Qualora non fosse disponibile alcuna informazione di Location, visualizzeremo un
messaggio di errore attraverso un Toast. Se invece la Location disponibile, dobbiamo
verificare se il server del servizio di Geocoder attivo. Per farlo sufficiente
utilizzare il metodo statico

public static boolean isPresent()


della stessa classe Geocoder, come possiamo vedere nel codice evidenziato nel listato
precedente. Se tutto va bene, procediamo con laccesso al servizio richiesto, il quale,
come abbiamo detto, deve avvenire in un Thread separato rispetto a quello
dellinterfaccia utente. Questo il motivo per cui abbiamo utilizzato una particolare
specializzazione di AsyncTask, che abbiamo descritto attraverso la classe
. I due parametri sono relativi al riferimento al Fragment ospitante e al

GeoCoderAsyncTask

numero di risultati che intendiamo ricevere (nel nostro caso uno solo). Della classe
GeoCoderAsyncTask descriviamo solamente limplementazione del metodo doInBackground(),
il quale contiene tutta la logica di nostro interesse.
NOTA
A parte questo, possiamo solo notare come GeoCoderAsyncTask abbia come tipo di input la
Location e come tipo di risultato una List di Address; si tratta della classe che incapsula i diversi
risultati del Geocoder. Gli altri metodi di callback sono i classici metodi che permettono la
visualizzazione di una ProgressDialog. Da notare anche lutilizzo di un WeakReference per il
riferimento al Fragment ospitante, usato al fine di evitare memory leak.

Come abbiamo detto, il metodo che contiene tutta la logica di accesso ai servizi del
Geocoder il seguente, il quale, ripetiamo, viene eseguito in background:

@Override
protected List<Address> doInBackground(Location params) {

// If the context is not available we skip


final MyLocationFragment fragment = mFragmentRef.get();
if (fragment == null) {
Log.w(TAG_LOG, Context is null!);
return null;
}
// We have to create the Geocoder instance
final Geocoder geocoder =
new Geocoder(fragment.getActivity(), Locale.getDefault());
// We get the Location to geocode
final Location location = params[0];
// We get the Addresses from the Location
List<Address> geoAddresses = null;
try {
geoAddresses = geocoder
.getFromLocation(location.getLatitude(),
location.getLongitude(), mMaxResult);
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG_LOG, Error getting Addressed from Location:
+ location);
}
return geoAddresses;
}


A parte i vari controlli sullesistenza del riferimento al Fragment contenitore,
notiamo come venga creata unistanza di Geocoder attraverso un costruttore, il quale
prevede, oltre allimmancabile Context, il riferimento a un Locale. Questultimo ci
permette di specificare la lingua da utilizzare per la rappresentazione di informazioni

relative a un indirizzo che sono incapsulate in un oggetto di tipo Address. Abbiamo


quindi utilizzato il metodo

public List<Address> getFromLocation (double latitude, double longitude, int maxResults)


che restituisce una List di Address, contenente al pi maxResults valori, corrispondenti
a una posizione specificata attraverso i valori di Latitude e Longitude. La lista di oggetti
di tipo Address viene poi restituita, a meno di errori, per la conseguente visualizzazione
del metodo onPostExecute() di callback dellAsyncTask.
Per completezza, possiamo poi notare come la classe Geocoder disponga di altri due
metodi molto utili, che permettono, per esempio, di completare le informazioni di un
indirizzo parziale

public List<Address> getFromLocationName (String locationName, int maxResults)


specificando eventualmente una regione in cui cercare

public List<Address> getFromLocationName (String locationName, int maxResults, double
lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double
upperRightLongitude)


Si tratta di metodi molto utili che, insieme ad altri, sono ottimamente descritti nella
documentazione ufficiale. Per testare questa nostra ultima funzionalit vogliamo per
utilizzare gli strumenti messi a disposizione dallAndroid Device Monitor e in
particolare quello che ci permette di simulare alcune posizioni. Per fare questo,
lostacolo maggiore rappresentato dal fatto che si tratta di un tool che funziona
solamente con lemulatore. Questo presenta per il problema di non disporre
dellultima versione dei Google Play services, che necessario installare per
accedere alle ultime funzionalit. Nel nostro caso, per esempio, stiamo utilizzando la
6.5.87 quando quella installata nellemulatore la 4.4.52. Aggiornare i Google Play
services un processo in teoria semplice, che per non funziona quasi mai, in quanto
dipendente dalle diverse versioni e da problemi legati alla firma dei vari file.
NOTA
In teoria basterebbe infatti procurarsi lAPK corrispondente ed eseguire un aggiornamento
attraverso il comando adb install -r <apk file>.

Nel nostro caso specifico meglio raggiungere un compromesso e testare


lapplicazione con la versione 4.4.52 dei Google Play services semplicemente
modificando la seguente definizione nel file di configurazione di Gradle.

//compile 'com.google.android.gms:play-services:6.5.87'
compile 'com.google.android.gms:play-services:4.4.52'


Non abbiamo infatti utilizzato alcuna funzionalit specifica della versione 6.5, per
cui, in questo caso, possiamo semplicemente procedere allesecuzione
dellapplicazione. Una volta eseguita lapplicazione, avviamo lAndroid Device
Manager visualizzando la finestra relativa allinvio di location allapplicazione, come
possiamo vedere nella Figura 2.4.

Figura 2.4 Invio di coordinate allemulatore.

Un metodo alternativo per inviare le informazioni consiste nellutilizzare la riga di


comando, e in particolare nelleseguire le seguenti istruzioni:

telnet localhost 5554
geo fix 12.4923 41.8902


In questo caso le informazioni di posizione vengono visualizzate, dopo la selezione
delle corrispondenti azione nella ActionBar, nel modo rappresentato nella Figura 2.5.

Figura 2.5 Utilizzo del Geocoder.

Come possiamo notare, si trattava delle coordinate di latitudine e longitudine del


Colosseo, a Roma. Ora ripristiniamo la corretta dipendenza con la versione 6.5 dei
Google Play services e ci accingiamo a sviluppare la seconda funzionalit, che sar
alquanto impegnativa.

Tracciamento della nostra posizione


La seconda funzionalit che vogliamo implementare si chiama Where I Go e ci
permetter di tracciare i nostri movimenti salvando le informazioni in un
ContentProvider, a cui accederemo pi avanti, per la visualizzazione dei percorsi in una
mappa. Lapproccio che abbiamo scelto bottom-up: parte dal layer di persistenza
per andare a quello di servizio e quindi di visualizzazione, come mostrato nella
Figura 2.6.

Figura 2.6 Design per il tracciamento della posizione.

Il primo passo consiste nella creazione di un database che abbiamo incapsulato in


un ContentProvider, al fine di sfruttare al meglio tutti gli strumenti che Android ci mette
a disposizione, come i Loader, ma anche per mantenere un certo livello di astrazione.
In sintesi definiremo due entit, FenceSession e FencePosition, che rappresentano
rispettivamente un percorso e le relative posizioni. Come vedremo nel prossimo
paragrafo sar importante definire opportuni Uri per la rappresentazione di ciascuna
di queste informazioni, che sono legate da una relazione di uno-a-molti, in quanto
ciascuna sessione sar composta da diverse rilevazioni di posizione. Il passo
successivo sar quello di raccolta delle informazioni di posizione, che ci verranno
inviate a intervalli regolari dal servizio di localizzazione, a seconda di come andremo
a configurare loggetto LocationRequest gi incontrato in precedenza. Nel nostro caso
abbiamo deciso di utilizzare il meccanismo forse pi complesso, ma che ci

permetter di registrare le nostre posizioni anche quando lapplicazione viene chiusa.


Per fare questo abbiamo infatti bisogno di un Service, il quale, come vedremo, sar
una particolare specializzazione della classe IntentService, che avr la responsabilit di
registrare, in modo asincrono, le informazioni ricevute nel nostro ContentProvider. La
parte di interfaccia ci permetter di avviare o terminare la sessione di registrazione
attraverso linterazione con un oggetto di tipo FusedLocationApi oltre che visualizzare le
informazioni raccolte in una ListView attraverso un Loader. Lutilizzo di un CursorLoader
ci permetter di aggiornare le informazioni visualizzate a ogni inserimento, senza
dover implementare alcun componente ad hoc. Si tratta di una funzionalit che
potrebbe essere realizzata in modo molto pi semplice, ma allo stesso tempo molto
meno affidabile. La nostra scelta ci permetter invece di affrontare anche concetti
molto importanti, come la personalizzazione dei Cursor e lutilizzo di PendingIntent.
NOTA
La stessa funzionalit potrebbe essere realizzata in modo pi semplice come descritto nella
documentazione ufficiale di Android. Proprio per questo motivo abbiamo invece deciso di seguire
unimplementazione pi completa e utile in un contesto professionale.

Approfitteremo anche della creazione del nostro ContentProvider per iniziare ad


aggiungere qualche classe di test, che in questo tipo di applicazioni diventa molto
importante.

Definizione del ContentProvider


Il primo passo nella realizzazione della funzionalit Where I Go consiste nella
creazione di un ContentProvider per la memorizzazione delle informazioni di percorso.
Ma perch un ContentProvider e non un semplice database o addirittura un file di testo?
La risposta risiede nella disponibilit di una serie di strumenti che Android ci mette a
disposizione e che sono stati progettati appositamente per gestire informazioni
contenute in questo tipo di componente. Il tipico esempio quello del Loader, ma
anche della possibilit di poter interagire in modalit asincrona attraverso lutilizzo di
opportune ContentProviderOperation (come vedremo successivamente). Un motivo molto
importante consiste anche nella possibilit di astrarre ciascuna informazione
attraverso il concetto di Uri, come avviene solitamente in unarchitettura REST.
NOTA
Anche se la tecnologia sostanzialmente diversa, pratica comune associare le operazioni che
si eseguono su dati di un ContentProvider rappresentati da Uri, ai servizi REST che si possono
richiamare per gestire altrettante risorse rappresentate da URI.

Nel caso specifico abbiamo deciso di gestire due diverse entit che possiamo
rappresentare, come indicato nella Figura 2.7. Per semplicit abbiamo deciso di
descrivere i vari campi attraverso lo stesso script che utilizzeremo per la creazione
delle corrispondenti tabelle.
Come possiamo vedere, una FenceSession rappresentata da un campo che identifica
lutente, da una data di inizio, da una data di fine e dalla distanza totale percorsa.
Lentit FencePosition contiene le informazioni che estrarremo da un oggetto di Location,
ma soprattutto il riferimento alla FenceSession a cui la stessa fa riferimento attraverso il
corrispondente session_id.

Figura 2.7 Le entit gestite dal nostro ContentProvider.

Abbiamo detto che gli elementi di un ContentProvider sono rappresentati da


opportuni Uri, che permettono di identificare uno o pi entit. Nel nostro caso
abbiamo deciso di utilizzare questi Uri anche per definire le relazioni tra le entit.
Ricordando che le Uri relative ai ContentProvider utilizzano uno schema del tipo
e che la nostra AUTHORITY rappresentata dalla stringa

content://


uk.co.friendfence.authority.friendfence


per indicare linsieme di tutte le possibili FenceSession utilizzeremo un Uri del
seguente tipo:

content://uk.co.friendfence.authority.friendfence/session


caratterizzato dal PATH /session. Secondo le tipiche convenzioni, in questo caso la
particolare FenceSession sar rappresentata da un Uri del tipo:

content://<authority>/session/<sessionId>


Per esempio, una FenceSession con identificatore 23 (e quindi _id pari a 23) sar
caratterizzata dallUri

content://uk.co.friendfence.authority.friendfence/session/23


La parte interessante riguarda per la gestione delle entit di tipo FencePosition, le
quali sono sempre in relazione alle precedenti (non esiste una FencePosition senza una
corrispondente FenceSession). Per questo motivo abbiamo deciso di rappresentare tutte
le FencePosition relative a una particolare FenceSession nel seguente modo:

content://<authority>/session/<sessionId>/position


Per esempio, tutte le FencePosition della precedente sessione saranno individuate
dal seguente Uri:

content://uk.co.friendfence.authority.friendfence/session/23/position


A questo punto risulta evidente come una specifica FencePosition per una specifica
dovr essere rappresentata da un Uri del tipo:

FenceSession


content://<authority>/session/<sessionId>/position/<positionId>


La FencePosition di id 88 corrispondente alla FenceSession precedente sar quindi
individuata dal seguente Uri:

content://uk.co.friendfence.authority.friendfence/session/23/position/88


Il ContentProvider dovr essere coerente con questo tipo di Uri, e quindi gestire i
diversi casi, come vedremo successivamente. Per il momento, invitiamo invece il
lettore a osservare la classe FenceDB nel package content, la quale contiene la
definizione delle tipiche costanti relative alle diverse entit gestite da un
ContentProvider, oltre che alcuni metodi di utilit per la gestione dei corrispondenti Uri.
In particolare notiamo la presenza dei seguenti due metodi:

public static Uri getPositionUriForSession(final long sessionId) {

final String uriPath = new StringBuilder()


.append(sessionId).append(/)
.append(FencePosition.PATH).toString();
return Uri.withAppendedPath(FenceSession.CONTENT_URI, uriPath);
}
public static Uri getPositionUri(final long sessionId,

final int positionId) {


final String uriPath = new StringBuilder()
.append(sessionId).append(/)
.append(FencePosition.PATH).append(/)
.append(positionId).toString();
return Uri.withAppendedPath(FenceSession.CONTENT_URI, uriPath);
}


Questi metodi permettono di comporre in modo automatico gli Uri relativi alle
FencePosition, mentre quelli relativi alle FenceSession si possono ottenere attraverso i
classici metodi della classe Uri, per esempio:

public static Uri withAppendedPath (Uri baseUri, String pathSegment)


Il passo successivo consiste nella creazione di una specializzazione della classe
SQLiteOpenHelper, che nel nostro caso si chiama FenceDbHelper. Si tratta di una classe che

ci permette di gestire in modo semplice la creazione e laggiornamento del DB.


Anche qui abbiamo per voluto preparare qualcosa di particolare, ovvero una
personalizzazione dellimplementazione di Cursor dipendente dalle varie entit. Per
comprendere quando ci accingiamo a fare supponiamo di disporre di un riferimento a
un Cursor risultato di una query sullentit FenceSession. Se volessimo accedere al
campo session_owner, dovremmo scrivere il seguente codice, che notiamo essere
piuttosto ripetitivo oltre che molto prolisso:

String owner = cursor.getString(cursor.getColumnIndex(

FenceDB.FenceSession.SESSION_OWNER));

La nostra idea invece quella di fare in modo che il metodo di query del nostro
ContentProvider restituisca unimplementazione di Cursor che ci permetta di accedere
alla stessa informazione nel seguente modo, sicuramente molto pi conciso e
leggibile:

String owner = cursor.getSessionOwner();


Incapsulare laccesso alle informazioni in un Cursor nel modo che descriveremo ci
garantisce anche un certo livello di ottimizzazione, come quello legato alla gestione
del metodo getColumnIndex(), che potr essere richiamato una volta sola per colonna.
possibile inoltre gestire le propriet definite come derivate, ovvero come
elaborazione o composizione di altre propriet dello stesso oggetto.
La magia per poter ottenere questo risultato risulta evidente nel costruttore della
classe FenceDbHelper, che riportiamo di seguito:

public FenceDbHelper(Context context) {

super(context, FenceDB.DB_NAME,
new FenceCursorFactory(), FenceDB.DB_VERSION);
this.mContext = context;
}

Abbiamo fornito unimplementazione dellinterfaccia CursorFactory descritta dalla


nostra classe FenceCursorFactory, la cui responsabilit quella di creare e restituire le
giuste implementazioni di Cursor per le diverse entit. Lasciamo al lettore la visione
del codice relativo a questa classe, invitandolo a osservare come il tutto venga
complicato dal fatto di dover gestire due diverse implementazioni: prima e dopo
HONEYCOMB. Questo ci ha costretto ad astrarre ulteriormente i Cursor definendo una coppia
di interfacce per le due entit.

/**
* The Interface with the operations that a FenceSessionCursorData
* should have
*/
public static interface FenceSessionCursorData extends Cursor {
}
/**
* The Interface with the operations that a FencePositionCursorData
* should have
*/
public static interface FencePositionCursorData extends Cursor {
}


Al momento non abbiamo aggiunto alcuna operazione per il semplice motivo che
non sappiamo ancora quale ci sar utile; comunque importante aver preparato il
terreno. In questo contesto facciamo anche notare la presenza della classe
CursorResolver, che ci permetter di risolvere un problema che una conseguenza della
nostra decisione di personalizzare i Cursor. Spesso i componenti di Android non ci
restituiscono direttamente il Cursor fornito dalla nostra Factory, ma lo incapsulano in
unistanza della classe CursorWrapper, la quale non ci permette di accedere al Cursor
contenuto, se non da HoneyComb in poi attraverso il corrispondente metodo getCursor().
Non potendo quindi semplicemente eseguire un cast sul CursorWrapper, abbiamo avuto
la necessit di creare un CursorResolver, che quindi, a seconda della versione di
Android nella quale viene eseguito, si preoccupa di accedere al Cursor attraverso la
chiamata del metodo getCursor() oppure attraverso introspection.
Tornando alla nostra classe FenceDbHelper, notiamo come gli altri metodi siano stati
implementati in modo abbastanza classico. Attraverso la nostra classe di utilit
ResourceUtils non facciamo altro che leggere dalle risorse gli script per la creazione del
DB. In caso di aggiornamento dello schema, non facciamo altro che cancellare e
ricreare il tutto.
NOTA

In fase di aggiornamento vi potrebbero essere delle logiche diverse che permettano di non
perdere alcun dato.

Dopo tutta questa fatica, siamo finalmente giunti alla creazione del ContentProvider,
che nel nostro caso descritto dalla classe FenceContentProvider. La gestione dellentit
abbastanza classica, a differenza di quello che succede con lentit

FenceSession

, a causa della nostra gestione della relazione. Anche in questo caso

FencePosition

invitiamo il lettore allo studio del codice, di cui descriveremo solamente le parti
fondamentali. La prima di queste sicuramente la configurazione della classe
UriMatcher, che utilizziamo per eseguire il matching degli Uri, che il nostro
gestisce associandoli al caso duso corrispondente.

ContentProvider


private final static UriMatcher URI_MATCHER =

new UriMatcher(UriMatcher.NO_MATCH);
private final static int SESSION_DIR_INDICATOR = 1;
private final static int SESSION_ITEM_INDICATOR = 2;
private final static int POSITION_DIR_INDICATOR = 3;
private final static int POSITION_ITEM_INDICATOR = 4;
static {

// The Uri for all the FenceSession is of the type AUTHORITY/session


URI_MATCHER.addURI(FenceDB.AUTHORITY,
FenceDB.FenceSession.PATH, SESSION_DIR_INDICATOR);
// The Uri AUTHORITY/session/<sessionId>
URI_MATCHER.addURI(FenceDB.AUTHORITY,
FenceDB.FenceSession.PATH + /#, SESSION_ITEM_INDICATOR);
// The Uri for all the position of a given session is of the type
// AUTHORITY/session/<sessionId>/position
URI_MATCHER.addURI(FenceDB.AUTHORITY, FenceDB.FenceSession.PATH
+ /#/ + FenceDB.FencePosition.PATH, POSITION_DIR_INDICATOR);
// The Uri for a given position of a given session is of the type
// AUTHORITY/session/<sessionId>/position/<positionId>
URI_MATCHER.addURI(FenceDB.AUTHORITY,
FenceDB.FenceSession.PATH
+ /#/ + FenceDB.FencePosition.PATH + /#,

POSITION_ITEM_INDICATOR);
}


Come di consuetudine, si definiscono delle costanti per ciascun tipo di accesso alle
risorse, associandovi successivamente un template di Uri. Nel codice abbiamo
evidenziato il caso pi complicato, relativo a una specifica informazione di posizione
allinterno di una sessione. Notiamo come il simbolo # ci permetta di specificare la
parte variabile dellUri.
Limplementazione del metodo onCreate() dovr contenere la creazione delloggetto
di tipo FenceDBHelper che utilizzeremo per laccesso al DB:

/**
* The DbHelper for this ContentProvider
*/
private FenceDbHelper mDbHelper;
@Override
public boolean onCreate() {

// We create the DbHelper


mDbHelper = new FenceDbHelper(getContext());
return true;
}


Il resto della classe prevede limplementazione dei metodi per lesecuzione delle
classiche operazioni di CRUD (Create, Retrieve, Update e Delete) che possiamo
comunque notare essere molto simili tra loro. Lunica complessit relativa alla
gestione della relazione attraverso un attento uso di quella parte di Uri che si chiama
e che composta di segmenti. Se consideriamo il tipo di Uri pi complesso, del

Path

tipo:

content://uk.co.friendfence.authority.friendfence/session/23/position/88


il cui PATH

/session/23/position/88

Questo pu essere pensato come composto di segmenti, cui possiamo accedere


attraverso il metodo della classe Uri:

public abstract List<String> getPathSegments()


il quale restituisce i vari segmenti in una List. Nellesempio precedente i segmenti
sono nellordine:

session
23
position
88


e possiamo utilizzarli per comporre la query che andremo a eseguire per
lestrazione dei dati voluti. Questo quanto succede, per esempio, nella gestione del
caso pi complicato dellUri precedente nel metodo di query che riportiamo di seguito:

final String sessionId = uri.getPathSegments().get(1);
final String positionId = uri.getPathSegments().get(3);
// We build the query as before using these two information
final StringBuilder where = new StringBuilder("( ")
.append(FenceDB.FencePosition.SESSION_ID)
.append(" = ").append(sessionId).append(" AND ")
.append(FenceDB.FencePosition._ID).append(" = ")
.append(positionId).append(" ) ");
if (!TextUtils.isEmpty(selection)) {
// If we have to append the filter based on the _ID
where.append(" AND ").append(selection);
}
// We use our constrains for the query
cursor = mDbHelper.getReadableDatabase()
.query(FenceDB.FencePosition.TABLE_NAME, projection,
where.toString(), selectionArgs, null, null, sortOrder);


Come notiamo, otteniamo i valori dei segmenti relativi agli id, che poi andiamo a
utilizzare per la composizione della parte WHERE della nostra query. Notiamo inoltre
come sia comunque necessario considerare anche gli eventuali vincoli passati come
parametri del metodo query stesso.
Un aspetto da non trascurare quello relativo alla notifica, attraverso il Cursor, della
possibile modifica di informazioni, che potrebbe produrre un update in fase di
visualizzazione. Questo il motivo della presenza delle seguenti righe di codice:

if (cursor != null) {

// We notify the query on the cursor for the requested Uri


cursor.setNotificationUri(getContext().getContentResolver(), uri);
// We return the cursor itself
return cursor;
} else {

// It means that the Uri didnt match


throw new UnsupportedOperationException(The given Uri
+ uri + is not supported);
}


Listruzione evidenziata ci permette di registrare il cursore alle eventuali modifiche
eseguite sui dati rappresentati dallUri passato come parametro, che nel nostro caso
quello relativo alla query richiesta. Si tratta di unistruzione fondamentale, che alla
base del funzionamento dei Loader che andremo a realizzare.
Altro aspetto da considerare con attenzione riguarda limplementazione del metodo
delete() di cancellazione delle entit. Quando eliminiamo una o pi FenceSession
vogliamo infatti provvedere alleliminazione di tutte le relative FencePosition. Si tratta
di unoperazione relativamente semplice nel caso di sessione unica; nel caso di pi
sessioni dobbiamo prima eseguire una query per verificare quali siano le sessioni da
cancellare. Come possibile vedere nel seguente sorgente, si tratta di una procedura
macchinosa, che contiene come unica complessit linserimento delle varie
operazioni in una transazione che, come sappiamo, ne migliora in modo notevole le
performance.

SQLiteDatabase db = mDbHelper.getWritableDatabase();
try {

db.beginTransaction();
// We get all the session with the given selection
final Cursor sessionsToDeleteCursor =
db.query(FenceDB.FenceSession.TABLE_NAME,
new String[]{FenceDB.FenceSession._ID}, selection,
selectionArgs, null, null, null);

// We delete all the related FencePosition


final int idIndex = sessionsToDeleteCursor
.getColumnIndex(FenceDB.FenceSession._ID);
final String whereClause = new
StringBuilder(FenceDB.FencePosition.SESSION_ID)
.append(= ?).toString();
while (sessionsToDeleteCursor.moveToNext()) {
final long sessionId = sessionsToDeleteCursor
.getLong(idIndex);
final String[] whereArgs = {String.valueOf(sessionId)};
db.delete(FenceDB.FencePosition.TABLE_NAME,
whereClause, whereArgs);
}
sessionsToDeleteCursor.close();
// Now we can delete the sessions
deletedCount = db.delete(FenceDB.FenceSession.TABLE_NAME,
selection, selectionArgs);
db.setTransactionSuccessful();
} finally {

db.endTransaction();
}


Come il lettore avr intuito, non faremo in modo, per motivi di risorse e consumi,
che le informazioni di Location arrivino con una frequenza troppo alta, per cui non
avremo alcun problema nelle operazioni di inserimento nel DB. In altri casi si ha
invece la necessit di eseguire un elevato numero di query (tipicamente insert o update
e delete) in una modalit che si chiama batch, attraverso un metodo che il
mette a disposizione dei propri client attraverso un ContentResolver e il

ContentProvider

metodo:

public ContentProviderResult[] applyBatch (String authority,

ArrayList<ContentProviderOperation> operations)

In questi casi le performance possono essere importanti, per cui preferibile fare
in modo che tutte le operazioni vengano eseguite in una stessa transazione. , infatti,
semplice dimostrare come le prestazioni aumentino in modo sensibile con una sola
transazione. A tale proposito abbiamo eseguito lovverride del metodo applyBatch() nel
seguente modo:

@Override
public ContentProviderResult[]

applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
// We have overridden the applyBatch to improve performances.
// In this was we can execute them into a transaction
final SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransaction();
try {
final int numOperations = operations.size();
final ContentProviderResult[] results =
new ContentProviderResult[numOperations];
for (int i = 0; i < numOperations; i++) {
results[i] = operations.get(i).apply(this, results, i);
}
db.setTransactionSuccessful();
return results;
} finally {
db.endTransaction();
}
}

Si tratta sostanzialmente della riscrittura del metodo esistente, a cui abbiamo


applicato una transazione attraverso le istruzioni evidenziate. Questo permetter di
ottenere prestazioni migliori nel caso di applicazione di query batch. Al momento
non sappiamo se ci sar utile, ma comunque buona prassi fare questa modifica alla
propria implementazione di ContentProvider.
A questo punto facile convincersi di come sia importante che il codice scritto sia
perfettamente funzionante. Nella nostra architettura fondamentale che ciascun layer
possa utilizzare con fiducia i servizi forniti dal layer sottostante. quindi importante
verificare che il nostro ContentProvider funzioni secondo quelle che sono state le nostre
assunzioni iniziali e in particolare quelle relative allutilizzo dei diversi tipi di Uri per
le diverse risorse. A tale proposito abbiamo creato alcune classi di test che
permettono di verificare laccesso alle diverse entit attraverso le corrispondenti Uri e
la creazione delle nostre implementazioni di Cursor descritte in precedenza. Lasciamo
al lettore la visione delle classi FenceSessionCPTest, FencePositionCPTest e
. Facciamo solo notare come si tratti di estensioni della classe

FenceSessionCursorTest

del framework di testing di Android, il quale ci permette di accedere

ProviderTestCase2

a un ContentProvider di test attraverso un MockContentResolver. importante osservare


come il trucco sia tutto nella definizione del costruttore, come per esempio nel
seguente caso:

public FenceSessionCursorTest() {

super(FenceContentProvider.class, FenceDB.AUTHORITY);
}


e quindi nellutilizzo del ContentResolver ottenuto attraverso il metodo
.

getMockContentResolver()

Reperimento delle informazioni di Location


Ora che abbiamo costruito il nostro ContentProvider, ci vogliamo occupare della parte
pi interessante, relativa allutilizzo delle API per la gestione della Location. Vogliamo
fare in modo che loggetto FusedLocationApi ci fornisca le informazioni di Location sulla
base di alcuni criteri che abbiamo in parte gi visto durante la realizzazione della
precedente funzionalit. Esistono sostanzialmente due modi. Il primo prevede
lutilizzo di uninterfaccia di callback analoga a quella usata per determinare la

posizione; in questo caso per le notifiche dovrebbero essere fornite per un periodo
prolungato. La seconda opzione invece quella che abbiamo deciso di implementare
e che prevede la definizione di un servizio che abbiamo chiamato LocationService.
Abbiamo scelto questa seconda opzione non solo perch la meno documentata (e
quindi pi utile al lettore), ma soprattutto perch pi adatta al nostro caso duso.
Vogliamo infatti che le informazioni di Location vengano registrate nel ContentProvider
anche qualora lapplicazione venisse chiusa; essa ci servir solamente per attivare o
fermare la registrazione delle posizioni. Si tratta di unopzione che consiste nel dire
al FusedLocationApi quale debba essere il particolare Intent da lanciare, con
linformazione di Location come extra. Il servizio che creiamo nella classe
, dovr ricevere questo Intent, estrarre le informazioni di Location e

LocationService

quindi renderle persistenti attraverso laccesso al nostro ContentProvider. Come


sappiamo, se avessimo esteso direttamente la classe Service avremmo dovuto gestire
due aspetti fondamentali ovvero:
1. esecuzione delle operazioni in un Thread separato da quello dellinterfaccia
utente;
2. chiusura del servizio nel caso di non utilizzo.
Si tratta di due caratteristiche che ci vengono fornite gratuitamente dalla classe
IntentService, che quindi abbiamo deciso di estendere implementando il solo metodo
. Senza entrare nei dettagli, possiamo dire che la classe IntentService si

onHandleIntent()

preoccupa di creare una coda, nella quale vengono inseriti dei messaggi che
incapsulano il riferimento agli Intent che il servizio riceve. Il consumer di questa coda
rappresentato da un Thread che, in background, estrae i messaggi, e richiama il
metodo onHandleIntent() che le varie specializzazioni implementano. In questo modo
siamo sicuri che questo metodo venga richiamato in un Thread diverso da quello
dellinterfaccia utente e permetta lelaborazione in successione dei vari Intent
ricevuti. Nel nostro caso il metodo stato implementato nel seguente modo:

@Override
protected void onHandleIntent(Intent intent) {

if (intent != null) {
final String action = intent.getAction();
if (SAVE_LOCATION_ACTION.equals(action)) {
final long sessionId =

intent.getLongExtra(EXTRA_SESSION_ID, NO_SESSION_ID);
if (sessionId == NO_SESSION_ID) {
Log.w(TAG_LOG, No SessionId in extra!);
return;
}
final Location location = intent
.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
saveLocationData(sessionId, location);
}
}
}


Al momento, il servizio utilizzato per ununica funzionalit, ma abbiamo
comunque deciso di prevedere lutilizzo di una action, che nel nostro caso
rappresentata dalla nostra costante SAVE_LOCATION_ACTION

/**
* This is the Action we use to send a Location information to this service
*/
private static final String SAVE_LOCATION_ACTION = Conf.PKG +

.action.SAVE_LOCATION_ACTION;

Nel caso di questa azione non facciamo altro che estrarre lextra contenente
linformazione di Location, quello relativo allid di sessione, e quindi richiamare il
nostro metodo saveLocationData().
NOTA
Notiamo come linformazione relativa alla action possa comunque essere utilizzata anche nel
caso in cui il servizio venga richiamato attraverso un Intent esplicito, come vedremo
successivamente.

Il nome dellextra relativo alla location indicato dalla costante KEY_LOCATION_CHANGED


della classe FusedLocationProviderApi. Si tratta di un metodo, a questo punto banale, che
riportiamo di seguito e che non fa altro che utilizzare il ContentResolver per
lesecuzione di unoperazione di insert() per unentit di tipo FencePosition.


/**
* Handle action Foo in the provided background thread with the provided
* parameters.
*/
private void saveLocationData(long sessionId, final Location location) {

final ContentValues locationValues = new ContentValues();


locationValues.put(FenceDB.FencePosition.POSITION_TIME,
location.getTime());
locationValues.put(FenceDB.FencePosition.ALTITUDE,
location.getAltitude());
locationValues.put(FenceDB.FencePosition.LATITUDE,
location.getLatitude());
locationValues.put(FenceDB.FencePosition.LONGITUDE,
location.getLongitude());
// We insert this data into the ContentProvider
final Uri sessionUri = FenceDB.FencePosition
.getPositionUriForSession(sessionId);
getContentResolver().insert(sessionUri, locationValues);
}


Unultima osservazione, relativamente al nostro servizio, riguarda il seguente
metodo statico di factory.

public static Intent getLocationIntent(final Context context,

final long sessionId) {


Intent intent = new Intent(context, LocationService.class);
intent.setAction(SAVE_LOCATION_ACTION);
intent.putExtra(EXTRA_SESSION_ID, sessionId);
return intent;
}

Questo metodo ci permette di incapsulare nella nostra classe tutte le informazioni


relative alla action e alle informazioni extra, che quindi sono definite attraverso
costanti statiche e private. Ultimo passo consiste nella definizione del servizio nel file
AndroidManifest.xml di configurazione della nostra applicazione nel seguente modo:

<service android:name=".service.LocationService"

android:exported=false>
</service>


Finora abbiamo semplicemente creato un servizio che rende persistenti le
informazioni che riceve attraverso una serie di Intent. Laspetto interessante riguarda
invece chi si occupa di inviare queste informazioni, ovvero il nostro FusedLocationApi.
Per vedere come avviene linvio di informazioni, dobbiamo descrivere il componente
che contiene la logica di avvio e stop, ovvero la classe MyMoveFragment. Innanzitutto si
tratta del Fragment relativo alla nostra seconda funzionalit, che quindi andremo a
registrare nella classe FragmentFactory nel seguente modo:

public final Fragment getFragment(final Context context, final int selection) {

switch (selection) {
case 0:
return new MyLocationFragment();
case 1:
return new MyMoveFragment();
}
return new Fragment();
}


Come il precedente, anche in questo caso abbiamo implementato la logica di
inizializzazione dei Google Play services, che quindi diamo per scontata. La parte pi
importante riguarda invece quella di avvio e stop del servizio di ricezione della
location. Il problema principale riguarda la modalit con cui lapplicazione si accorge
se il servizio di localizzazione attivo oppure no. Nel nostro caso abbiamo deciso di

utilizzare una classe di utilit molto semplice, che si chiama ServiceState. Attraverso
unistanza di questa classe non facciamo altro che rendere persistente, tramite le
SharedPreferences, lo stato del nostro servizio e quindi sappiamo se questo stato
avviato oppure no. Attraverso il seguente metodo notificheremo lavvio del servizio
di localizzazione salvandone il corrispondente id di sessione.

private synchronized void start(final long currentSessionId) {

mPrefs.edit().putLong(CURRENT_SESSION_ID,
currentSessionId).commit();
}


Attraverso il metodo

private synchronized void stop() {

mPrefs.edit().remove(CURRENT_SESSION_ID).commit();
}


notifichiamo invece lavvenuta interruzione del servizio, per accedere al cui stato
possiamo utilizzare il metodo

private synchronized boolean isRunning() {

return getSessionId() != NO_SESSION_ID;


}


Infine, possibile ottenere il riferimento alleventuale sessione attiva attraverso il
seguente metodo:

private synchronized long getSessionId() {

return mPrefs.getLong(CURRENT_SESSION_ID, NO_SESSION_ID);


}


Per vedere come stato utilizzato questo oggetto, descriviamo i metodi che ci
permettono di avviare il servizio e successivamente di interromperlo. Innanzitutto

notiamo la seguente implementazione del metodo:



@Override
public void onStart() {

super.onStart();
// Here we have to connection the client
mGoogleApiClient.connect();
// We check the state of the button
mStartedButton.setChecked(mServiceState.isRunning());
}


Nel codice evidenziato, notiamo come venga letto lo stato del servizio, per attivare
lo stato corretto di un ToggleButton.
NOTA
Il lettore potr notare come i metodi siano tutti privati. Questo possibile in quanto sono metodi di
una classe interna e quindi possibile accedervi dal codice contenuto nello stesso file.

Se il servizio attivo, il pulsante ci permetter di fermarlo e viceversa. La prima


volta siamo sicuri che il servizio non in esecuzione, per cui il pulsante sar nello
stato off. A questo pulsante abbiamo poi registrato un OnClickListener nel seguente
modo:

mStartedButton.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
final boolean serviceState = mStartedButton.isChecked();
if (serviceState) {
// We start the tracking
startTracking();
} else {
// We stop the tracking
stopTracking();

}
}
});


Notiamo come per lavvio del servizio venga richiamato il metodo startTracking(),
mentre per lo stop venga richiamato il metodo stopTracking(); si tratta dei metodi che
contengono la logica di avvio e stop del servizio di localizzazione. Nel metodo di
startTracking() dobbiamo sostanzialmente fare tre cose:
1. preparare loggetto LocationRequest;
2. creare la nuova sessione e ottenere il relativo id;
3. avviare il servizio attraverso il metodo requestLocationUpdates().
Il primo passo consiste nella creazione di un LocationRequest, analogamente a quanto
fatto nel caso della semplice determinazione della location, con qualche parametro in
pi.

LocationRequest locationRequest = LocationRequest.create()

.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setFastestInterval(FASTEST_INTERVAL)
.setSmallestDisplacement(MIN_DISPLACEMENT)
.setInterval(UPDATE_INTERVAL);

Come nel caso precedente abbiamo utilizzato un valore di priority, ma in questo
caso abbiamo impiegato il metodo setFastestInterval(), il quale merita una breve
descrizione. Abbiamo gi visto come una delle principali caratteristiche di questo
modo centralizzato di gestione della Location sia appunto quello di utilizzare
informazioni provenienti da altre applicazioni. Potrebbe quindi capitare che queste
informazioni vengano notificate alla nostra applicazione con una frequenza troppo
elevata per le nostre necessit. Per questo motivo possibile dire al sistema che non
si intendono ricevere informazioni di Location con una frequenza superiore a quella
corrispondente al valore indicato attraverso il metodo setFastestInterval(), che nel
nostro caso di 1 secondo. Quello che possibile fare nel tempo, possibile anche
nello spazio, attraverso il metodo setSmallestDisplacement(), che ci permette di indicare

la minima distanza (in metri) tra due Location per poter essere notificate. Nel nostro
caso abbiamo deciso di utilizzare una distanza minima di 20 metri, ma si tratta di un
valore che dipende dal tipo di applicazione. Infine il nostro caso duso prevede la
generazione di un insieme ripetuto di eventi che, considerando sempre i vincoli
impostati in precedenza, dovrebbe avvenire dopo ogni intervallo di una durata
indicata attraverso il metodo setInterval(). Nel nostro caso lintervallo di 1 secondo,
corrispondente a quello che abbiamo utilizzato come intervallo minimo di notifica
attraverso il metodo setFastestInterval().
Il passo successivo la creazione di una nuova FenceSession, attraverso la chiamata
del seguente metodo di utilit:

final Uri newSessionUri = FenceDB.FenceSession

.createNewSession(getActivity(), Conf.DEFAULT_USER);

Lo abbiamo implementato nella classe FenceDB nel seguente modo:

public static Uri createNewSession(final Context context, final String owner) {

final ContentValues values = new ContentValues();


// The owner information
values.put(FenceSession.SESSION_OWNER, owner);
// We insert the data for the startDate
final Date now = new Date();
values.put(FenceSession.START_DATE, now.getTime());
// We insert the data
final Uri newSessionUri = context.getContentResolver()
.insert(FenceSession.CONTENT_URI, values);
// We return the Uri
return newSessionUri;
}

Si tratta di codice che dovrebbe essere ormai familiare al lettore, in quando


consiste nella semplice creazione di una FenceSession con i valori relativi allowner e
data di creazione. Attraverso il ContentResolver accediamo al ContentProvider e otteniamo
lUri dellelemento creato, che restituiamo.
Una volta creata la sessione, dobbiamo avviare il servizio di aggiornamento della
Location, attraverso la chiamata del seguente metodo del nostro FusedLocationApi:

public abstract PendingResult<Status> requestLocationUpdates(GoogleApiClient

client, LocationRequest request, PendingIntent callbackIntent)



Notiamo come il primo parametro sia il nostro GoogleApiClient, inizializzato nel
modo ormai noto. Il secondo parametro rappresentato dalla LocationRequest creata in
precedenza, mentre il terzo un PendingIntent. Ma che cos un PendingIntent e perch
non basta un semplice Intent? Il problema che lIntent, con le informazioni relative
alla posizione, non viene inviato dalla nostra applicazione, ma dal corrispondente
componente allinterno dei Google Play services, il quale in esecuzione in un
processo diverso. Un PendingIntent sostanzialmente un wrapper, nel quale inseriamo
un nostro Intent insieme alle credenziali che ne permettono linvio come se questo
avvenisse dalla nostra applicazione; si tratta quindi di una specie di delega che diamo
al sistema di notifica della posizione, per poter interagire con un componente
accessibile solamente dalla nostra applicazione.
NOTA
Un aspetto interessante riguarda il fatto che questo PendingIntent possa essere utilizzato anche
dopo che lapplicazione che lo ha generato stata chiusa.

Per comprendere meglio di cosa si tratta, descriviamo il seguente codice, che


permette di creare un PendingIntent per la gestione degli Intent di notifica della
posizione.

final long newSessionId = FenceDB.FenceSession.getSessionId(newSessionUri);
final Intent locationIntent = LocationService

.getLocationIntent(getActivity(), newSessionId);
final PendingIntent callbackIntent = PendingIntent

.getService(getActivity(), UPDATE_LOCATION_REQUEST_CODE,
locationIntent, PendingIntent.FLAG_UPDATE_CURRENT);

LocationServices.FusedLocationApi

.requestLocationUpdates(mGoogleApiClient,
locationRequest, callbackIntent);
mServiceState.start(newSessionId);


La prima istruzione permette di estrarre semplicemente lid di sessione dallUri che
abbiamo ottenuto in fase di creazione. Si tratta di un semplice metodo di utilit che
riceve un Uri del tipo:

content://AUTHORITY/session/<sessionId>


ed estrae il valore di sessionId. Questa informazione ci utile, in quanto dovr
essere contenuta in ogni Intent relativo a una posizione. Come abbiamo visto, ci serve
infatti per associare la posizione alla corretta sessione. Questo il motivo per cui
questa informazione viene passata al seguente metodo di factory della nostra classe
LocationService, ovvero:

public static Intent getLocationIntent(final Context context,

final long sessionId) {


Intent intent = new Intent(context, LocationService.class);
intent.setAction(SAVE_LOCATION_ACTION);
intent.putExtra(EXTRA_SESSION_ID, sessionId);
return intent;
}


Notiamo come si tratti di un Intent esplicito, che permette lavvio del nostro
. Avendo specificato la classe direttamente, si tratta di un Intent che non

LocationService

pu essere lanciato direttamente da unaltra applicazione. In ogni caso vi associata


unaction per il salvataggio della location e quindi inseriamo il valore di sessionId
come extra.
NOTA
La definizione di questo metodo statico di Factory ci permette di definire le varie costanti relative
agli extra e alle action nella classe LocationService stessa. In questo modo i diversi componenti

possono non esserne a conoscenza e quindi si tratta di configurazioni che possiamo modificare
senza impatti sugli eventuali client.

Quello che abbiamo memorizzato nella variabile final locationIntent un template


dei vari Intent che il nostro servizio ricever in corrispondenza di ciascuna
informazione di Location.
Il passo successivo consiste finalmente nella creazione di un PendingIntent,
attraverso il corrispondente metodo di Factory della classe stessa. Innanzitutto notiamo
come esista un metodo di Factory diverso per ciascuno dei componenti che lIntent
vuole attivare. Nel nostro caso il metodo si chiama getService(), in quanto vuole
attivare un Service, ma esistono i metodi corrispondenti al lancio di una Activity o di
un BroadcastReceiver. Oltre allimmancabile Context, il metodo getService() prevede come
secondo parametro un valore intero che si chiama requestCode e che ci permette di
identificarlo nella nostra applicazione. Il terzo parametro il nostro Intent, mentre il
quarto un flag che ci permette di specificare che cosa succede nel caso lo stesso
sia stato utilizzato in precedenza. Noi abbiamo utilizzato il valore

PendingIntent

corrispondente alla costante PendingIntent.FLAG_UPDATE_CURRENT, che ci permette di dire che


se lo stesso PendingIntent stato utilizzato in precedenza, questo debba essere
mantenuto, ma gli extra contenuti nel relativo Intent devono essere sostituiti con
quelli contenuti nel nuovo Intent. Nel nostro caso, infatti, le informazioni che possono
cambiare sono solamente quelle relative allidentificatore di sessione o di posizione,
che vengono memorizzate come extra allinterno dellIntent da inviare al servizio.
Lutilizzo di questo flag nasconde uninsidia legata al criterio che ci permette di
sapere quando un PendingIntent uguale a un altro. Come facciamo, infatti, a sapere
che un nuovo PendingIntent corrisponde a uno analogo inviato in precedenza?
Fortunatamente in questo gli extra non contano nulla, ovvero due PendingIntent non
sono diversi se si differenziano per i valori degli extra dei corrispondenti Intent. La
regola di uguaglianza la stessa implementata nel metodo filterEquals() della classe
, che in genere viene utilizzato dallalgoritmo di Intent Resolution per capire quali

Intent

componenti possono rispondere a un particolare Intent. Si tratta delle stesse regole


che si utilizzano per la definizione degli Intent Filter nel file di configurazione
e che prevedono quindi lutilizzo di action, category e dati. Nel

AndroidManifest.xml

nostro caso, per creare un PendingIntent equivalente sar sufficiente crearne uno che fa
riferimento allo stesso servizio.
NOTA

Come vedremo tra poco, questa una buona notizia, in quanto non avremo bisogno di rendere
persistente un PendingIntent per poter fermare il servizio di localizzazione.

bene, comunque, fare attenzione al fatto che due PendingIntent associati a due
diversi sono considerati diversi, anche se il metodo Intent.filterEquals()

requestCode

restituisce true per i corrispondenti Intent. In ogni caso lutilizzo del flag
permette di cancellare il PendingIntent precedente e di

PendingIntent.FLAG_UPDATE_CURRENT

sostituirlo con il nuovo. Per un approfondimento sui vari flag rimandiamo alla
documentazione ufficiale.
A questo punto non ci resta che avviare il servizio di localizzazione attraverso la
chiamata del corrispondente metodo del nostro FusedLocationApi.

public abstract PendingResult<Status> requestLocationUpdates(GoogleApiClient

client, LocationRequest request, PendingIntent callbackIntent)



Lultima istruzione permette quindi di salvare lo stato del nostro servizio nella
classe di utilit ServiceState descritta in precedenza.
NOTA
Si potrebbe obiettare sulla mancata transazionalit dellavvio del servizio e del salvataggio del
corrispondente stato attraverso il ServiceState. A parte considerazioni di convenienza, in
unapplicazione di questo tipo non abbiamo voluto introdurre ulteriore complessit.

Una volta avviato con successo, il servizio ricever le informazioni di Location


associate a una particolare sessione e ne render persistenti le informazioni nel modo
visto in precedenza.
Una volta terminata la registrazione del nostro percorso, ci dobbiamo occupare
della chiusura della sessione e quindi dello stop del nostro servizio, che abbiamo
descritto nel seguente metodo stopTracking(), che viene richiamato nel caso in cui il
fosse inizialmente nello stato ON:

ToggleButton


private final void stopTracking() {

final Intent locationIntent = LocationService


.getLocationIntent(getActivity(), 0L);
final PendingIntent callbackIntent = PendingIntent
.getService(getActivity(), UPDATE_LOCATION_REQUEST_CODE,

locationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
LocationServices.FusedLocationApi
.removeLocationUpdates(mGoogleApiClient, callbackIntent);
long currentSessionId = mServiceState.getSessionId();
mServiceState.stop();
FenceDB.FenceSession.setSessionAsClosed(getActivity(),
currentSessionId);
}


Lo strumento che il FusedLocationApi ci mette a disposizione per interrompere
lacquisizione delle informazioni di Location rappresentato dal seguente metodo:

public abstract PendingResult<Status> removeLocationUpdates(GoogleApiClient

client, PendingIntent callbackIntent)



Notiamo che ha come secondo parametro un oggetto di tipo PendingIntent, che dovr
essere lo stesso, o equivalente, a quello utilizzato in fase di avvio. a questo punto
che risultano fondamentali le osservazioni fatte in precedenza. Qualora la nostra
applicazione fosse sempre attiva, non avremmo avuto difficolt a utilizzare
addirittura la stessa istanza. Qualora la nostra applicazione venisse invece chiusa, si
ha la necessit di creare un nuovo PendingIntent come descritto nel codice riportato
sopra. Come specificato prima, gli extra non hanno alcuna importanza nella
determinazione dellequivalenza o uguaglianza tra due PendingIntent. Per questo
motivo richiamiamo il metodo getLocationIntent() di creazione dellIntent, passando
uno 0 come id di sessione. Analogamente al caso precedente, creiamo poi il
PendingIntent che andiamo a utilizzare per lo stop del servizio. Infine rendiamo il
nuovo stato persistente attraverso il solito oggetto ServiceState. Come ultima istruzione
non facciamo altro che richiamare un metodo di utilit della classe FenceDB, che ci
permette di aggiornare la data di fine di una sessione.

public static void setSessionAsClosed(final Context context,

final long sessionId) {

final ContentValues values = new ContentValues();


final Date now = new Date();
values.put(FenceSession.END_DATE, now.getTime());
final Uri sessionUri = Uri.withAppendedPath(CONTENT_URI,
String.valueOf(sessionId));
AsyncQueryHandler asyncQueryHandler =
new AsyncQueryHandler(context.getContentResolver()) {
@Override
protected void onUpdateComplete(int token, Object cookie, int result) {
super.onUpdateComplete(token, cookie, result);
// Nothing to do
}
};
asyncQueryHandler.startUpdate(0, null, sessionUri, values, null, null);
}


Il metodo descritto nel precedente codice ci permette di notare lutilizzo della
classe AsyncQueryHandler, utile nel caso in cui si debbano eseguire delle query in
successione in un thread di background avendo eventualmente la notifica della loro
esecuzione.
NOTA
Si tratta dellimplementazione di un pattern di nome Asynchronous Completion Token
(http://www.cs.wustl.edu/~schmidt/PDF/ACT.pdf) che permette lesecuzione in catena di pi
operazioni, ricevendo di volta in volta una notifica di callback. Nel nostro caso il suo uso un po
forzato, ma ci ha comunque permesso di accedere al ContentProvider in modo asincrono.

Il lettore potr provare ad avviare e fermare il servizio a piacimento, verificando il


salvataggio delle informazioni di location attraverso alcuni messaggi di Log. Se questi
messaggi non dovessero arrivare, possibile modificare i parametri di creazione della
LocationRequest, come per esempio mettere a 0 la distanza minima in metri tra due
location successive.

Visualizzazione delle informazioni

Prima di proseguire con le funzionalit, vogliamo descrivere brevemente la


modalit con cui vengono visualizzate le informazioni relative alle sessioni e alle
posizioni. Si tratta del codice che abbiamo inserito in alcuni Fragment e in particolare
quelli descritti dalle classi FenceSessionListFragment e FencePositionListFragment.
NOTA
fondamentale sottolineare come la semplicit di queste classi sia dovuta in gran parte alla
fatica fatta in precedenza nella creazione di un ContentProvider e nellastrazione introdotta
attraverso la nostra implementazione di Cursor.

La classe FenceSessionListFragment non presenta nulla di particolare, se non lutilizzo


di un Loader, che sappiamo essere un meccanismo che ci permette di ricevere notifica
del cambiamento in una certa base dati per poterne aggiornare la corrispondente
visualizzazione. bene sottolineare come ci sia collegato al ContentProvider e
precisamente alle istruzioni del tipo

if (cursor != null) {

// We notify the query on the cursor for the requested Uri


cursor.setNotificationUri(getContext().getContentResolver(), uri);
// We return the cursor itself
return cursor;
}


nel metodo di query oltre alle seguenti

if (updatedCount >= 0) {

// We notify the deletion


getContext().getContentResolver().notifyChange(uri, null);
// We return the number of updated items
return updatedCount;
}


nel caso di modifica del DB. In questo modo non dobbiamo implementare alcun
meccanismo di notifica, che gi presente nellutilizzo di un Loader<Cursor>. In questo

modo il codice per la visualizzazione delle informazioni di sessione diventa banale e


precisamente:

@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {

CursorLoader loader = new CursorLoader(getActivity(),


FenceDB.FenceSession.CONTENT_URI, null, null, null,
FenceDB.FenceSession.START_DATE + DESC );
return loader;
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {

setListShown(true);
mSessionCursorData = CursorResolver.
CURSOR_RESOLVER.extractSessionCursor(cursor);
mAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {

mAdapter.swapCursor(null);
}


Nel metodo onCreateLoader() non facciamo altro che creare un CursorLoader,
utilizzando la query che ci permette di estrarre le sessioni, ordinate per data. Si tratta
della query che viene eseguita a ogni modifica della base dati. Il metodo onLoadFinish()
viene richiamato al termine dellesecuzione della query e presenta come unica
particolarit la presenza del nostro CursorResolver. Come accennato in precedenza si
tratta di un modo per estrarre il Cursor vero e proprio dalloggetto di tipo CursorWrapper
che viene restituito dal Loader.
NOTA
Si tratta di codice non complicato, ma molto prolisso, che il lettore pu vedere direttamente nel
sorgente della classe CursorResolver insieme a quello contenuto nella classe
FenceCursorFactory, che la frammentazione di Android rende anchesso molto prolisso. Lutilit di

questo approccio dipende dalla necessit o meno di accedere agli elementi del DB in modo
veloce, come vedremo successivamente nel caso dellAdapter.

Infine, il metodo onLoaderReset() non fa altro che eseguire il reset del Cursor
nelladapter, come nella maggior parte delle sue implementazioni.
Il secondo aspetto degno di nota in questa classe riguarda la creazione delladapter,
nel seguente modo:

mAdapter = new SimpleCursorAdapter(getActivity(),

R.layout.fragment_session_item, null, FROM, TO, 0);


mAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {

@Override
public boolean setViewValue(View view, Cursor cursor, int i) {
if (R.id.fence_session_start_date == view.getId()) {
// We have the date so we have to format it and show
final Date startDate = mSessionCursorData.getStartDate();
final TextView dateView = (TextView) view;
dateView.setText(Conf.SIMPLE_DATE_FORMAT.format(startDate));
return true;
}
return false;
}
});


Definendo un ViewBinder, gestiamo la visualizzazione del dato corretto nella
corrispondente posizione; nel nostro caso la sola data di inizio. Da notare come
possiamo accedervi semplicemente attraverso la chiamata del metodo getStartDate(),
che non prevede alcuna gestione dellindice della corrispondente colonna del DB.
Questo il vantaggio dellaver definito una nostra implementazione di Cursor. Nello
specifico, il relativo codice contenuto nella classe interna SessionCursorDelegate della
classe FenceCursorFactory.

private Date getStartDate() {

final long startDate = mCursor.getLong(mStartDateIndex);

final Date date = new Date();


date.setTime(startDate);
return date;
}


Lindice di ciascuna colonna viene ottenuto una volta sola nel costruttore e quindi
riutilizzato solo alloccorrenza nei relativi metodi getXXX(), come nel caso
precedente.
NOTA
La classe SessionCursorDelegate stata creata per gestire in un unico posto le due
implementazioni di cursor relative ad API Level precedenti o seguenti ad Honeycomb.

Infine notiamo come sia stato gestito un CRUD delle sessioni attraverso le
classiche opzioni contestuali attivate con il long-clic di vari elementi.
Anche la classe FencePositionListFragment non ha niente di particolare, se non il fatto
di utilizzare due diversi Loader, caratterizzati da un diverso identificatore. Il primo
relativo alle informazioni generali della sessione, mentre il secondo relativo alla
visualizzazione delle posizioni.
Concludiamo la parte relativa alla visualizzazione facendo notare come sia stato
utilizzato un FragmentManager particolare, ovvero quello associato non a una Activity, ma
a un altro Fragment.

@Override
public void onSessionSelected(int position, long id) {

// Here we want to show the information related to the position for given session
final FencePositionListFragment positionFragment =
FencePositionListFragment.create(id);
getChildFragmentManager().beginTransaction()

.addToBackStack(null)
.replace(R.id.info_container, positionFragment,
POSITION_LIST_TAG).commit();
}

A tale proposito facciamo notare un bug relativo al non funzionamento del


backstack. Per questo motivo abbiamo dovuto gestire in modo esplicito la pressione
del Back nella classe MainActivity nel seguente modo:

@Override
public void onBackPressed() {

FragmentManager fm = getSupportFragmentManager();
for (Fragment frag : fm.getFragments()) {
if (frag.isVisible()) {
FragmentManager childFm = frag.getChildFragmentManager();
if (childFm.getBackStackEntryCount() > 0) {
childFm.popBackStack();
return;
}
}
}
super.onBackPressed();
}


Confidiamo che venga risolto quanto prima in una release della compatibility
library.

La necessit di mantenere uno stato


Per come stato pensato, il nostro servizio da considerarsi stateless, nel senso
che viene attivato ogni volta che si ha la necessit di memorizzare uninformazione di
Location. Come abbiamo visto, si tratta di una specializzazione della classe
, che quindi viene fermata in modo automatico qualora non vi fossero

IntentService

eventi di Location da gestire. La classe LocationService non quindi da considerarsi un


luogo ideale per la memorizzazione di uno stato che, ricordiamo, dovr persistere
anche qualora la nostra applicazione venisse chiusa; questo il motivo per cui
abbiamo implementato il tracking delle nostre location attraverso un servizio.

Una possibile alternativa rappresentata da una nostra specializzazione della


classe Application, la quale ha un ciclo di vita che corrisponde con quello
dellapplicazione; se eseguiamo il kill dellapplicazione, anche la corrispondente
Application perde il proprio stato senza alcuna notifica. Se poi il servizio LocationService
venisse eseguito in un altro processo attraverso lattributo evidenziato nella seguente
definizione, listanza di Application vista dallapplicazione non sarebbe addirittura
nemmeno la stessa.

<service

android:name=.service.LocationService android:exported=false
android:process=:locationProcess />

Proprio per questo motivo si ha spesso la necessit di gestire qualcosa di diverso.
Fortunatamente non abbiamo la necessit di memorizzare informazioni che variano
molto velocemente, per cui possiamo utilizzare delle SharedPreferences per la loro
memorizzazione. Lassenza di un meccanismo di notifica nella chiusura
dellapplicazione ci impedisce di utilizzare un approccio pi lazy, che ne permetta la
persistenza solo in caso di bisogno.
NOTA
Ricordiamo che le SharedPreferences vengono salvate su file system, per cui non sono
comunque molto leggere dal punto di vista delle performance.

Ovviamente molto dipende dallaffidabilit che intendiamo raggiungere.


Potremmo infatti decidere di salvare le informazioni nellApplication facendo
affidamento sul fatto che lapplicazione sia comunque sempre in esecuzione,
trascurando semplicemente il dato in caso di eventuale eliminazione della stessa. In
questo caso comunque importante che il ServiceLocation sia in esecuzione nello stesso
processo e quindi condivida la stessa istanza. In realt abbiamo gi incontrato lo
stesso problema in precedenza in relazione alla necessit di ricordare se il servizio di
tracking fosse attivo oppure no. In quel caso abbiamo creato una classe interna di
nome ServiceState, con la responsabilit di contenere, appunto, lo stato del nostro
servizio. Si tratter quindi di estendere le responsabilit di quella classe
aggiungendole anche la possibilit di memorizzare lultima informazione di Location e
quindi le distanze relative allultimo tratto e al totale. Per questo motivo abbiamo
dovuto eseguire un refactoring che ha promosso la classe ServiceState a top class (e

quindi non pi interna) a cui abbiamo aggiunto le responsabilit relative alla


posizione.
NOTA
Quanto ottenuto prevede che le informazioni vengano salvate di volta in volta nelle
SharedPreferences. Non il massimo dellefficienza, ma il prezzo che dobbiamo pagare per
garantire che il tutto funzioni anche qualora lapplicazione venisse chiusa con un kill.

I metodi di interesse in questa versione della classe ServiceState sono i seguenti, per
lavvio e stop del servizio di tracking.

public synchronized void start(final long currentSessionId) {

mPrefs.edit()
.putLong(CURRENT_SESSION_ID, currentSessionId)
.commit();
}
public synchronized void stop() {

mPrefs.edit()
.remove(CURRENT_SESSION_ID)
.remove(LAST_LATITUDE)
.remove(LAST_LONGITUDE)
.putFloat(CURRENT_DISTANCE, 0.0f)
.commit();
}


Abbiamo quindi aggiunto il metodo per il calcolo della distanza. Da notare
lutilizzo della classe Pair dellSDK di Android, che ci ha permesso di restituire due
valori contemporaneamente.

public synchronized Pair<Float, Float> addLocation(final Location newLocation) {

final float distance;


final double newLatitude = newLocation.getLatitude();
final double newLongitude = newLocation.getLongitude();
if (mPrefs.contains(LAST_LATITUDE)) {

// In this case we have a previous Location


final float previousLatitude = mPrefs.getFloat(LAST_LATITUDE, 0.0f);
final float previousLongitude = mPrefs.getFloat(LAST_LONGITUDE, 0.0f);
// We calculate the distance
final float[] distances = new float[1];
Location.distanceBetween(previousLatitude, previousLongitude,
newLatitude, newLongitude, distances);
distance = distances[0];
} else {
// We dont have a previous location
distance = 0.0f;
}
mCurrentDistance = mPrefs.getFloat(CURRENT_DISTANCE, 0.0f);
mCurrentDistance = mCurrentDistance + distance;
mPrefs.edit()
.putFloat(LAST_LATITUDE, (float) newLatitude)
.putFloat(LAST_LONGITUDE, (float) newLongitude)
.putFloat(CURRENT_DISTANCE, mCurrentDistance)
.commit();
Log.d(TAG_LOG, Distance to position [ + newLatitude + , + newLongitude
+ ] is + distance + total: + mCurrentDistance);
return new Pair<Float, Float>(distance, mCurrentDistance);
}


In questo caso non abbiamo fatto altro che verificare la presenza di una location
precedente, per eventualmente usarla per il calcolo delle distanze. Lultima posizione
viene quindi salvata nelle SharedPreferences.
Quando si ha a che fare con informazioni condivise, sempre bene chiedersi se
possono sorgere problemi legati alla concorrenza, ovvero allaccesso delle stesse

informazioni da parte di processi diversi. Nel nostro caso il LocationService interagisce


con il ServiceState solamente attraverso il metodo addLocation, che quindi non presenta
particolari criticit in questo senso.
Per gestire la funzionalit relativa alla distanza percorsa andiamo a modificare il
metodo saveLocationData(), in modo da rendere persistente anche questa informazione.

private void saveLocationData(final long sessionId, final Location location) {

final Pair<Float, Float> distances =


mServiceState.addLocation(location);
final ContentValues locationValues = new ContentValues();
locationValues.put(FenceDB.FencePosition.POSITION_TIME,
location.getTime());
locationValues.put(FenceDB.FencePosition.ALTITUDE,
location.getAltitude());
locationValues.put(FenceDB.FencePosition.LATITUDE,
location.getLatitude());
locationValues.put(FenceDB.FencePosition.LONGITUDE,
location.getLongitude());
locationValues.put(FenceDB.FencePosition.DISTANCE, distances.second);
final Uri positionUri = FenceDB.FencePosition
.getPositionUriForSession(sessionId);
final ArrayList<ContentProviderOperation> updateOps =
new ArrayList<ContentProviderOperation>
(CONTENT_PROVIDER_OP_SIZE);
updateOps.add(ContentProviderOperation.newInsert(positionUri)
.withValues(locationValues).build());
final Uri sessionUri = Uri.withAppendedPath(FenceDB
.FenceSession.CONTENT_URI, String.valueOf(sessionId));
final ContentValues sessionValues = new ContentValues();

sessionValues.put(FenceDB.FenceSession.TOTAL_DISTANCE,
distances.second);
updateOps.add(ContentProviderOperation.newUpdate(sessionUri)
.withValues(sessionValues).build());
// We insert this data into the ContentProvider
try {
getContentResolver().applyBatch(FenceDB.AUTHORITY, updateOps);
} catch (RemoteException e) {
e.printStackTrace();
} catch (OperationApplicationException e) {
e.printStackTrace();
}
}


Da notare come nella parte evidenziata sia stato utilizzato il metodo applyBatch() del
, per eseguire in una singola transazione (vedi implementazione del

ContentResolver

) sia laggiornamento dellentit FenceSession, sia linserimento dellentit

ContentProvider

FencePosition

Iniziamo a gestire le Notification


Come vedremo in occasione del capitolo dedicato ad Android Wear, le notifiche
assumono unimportanza particolare. Per il momento ci prepariamo il terreno creando
una semplice classe di utilit che ci permetta di visualizzare una notifica con la
distanza percorsa in ogni momento. Al momento la classe FenceNotificationHelper ci
permette di visualizzare la notifica e di eliminarla. I metodi che abbiamo
implementato sono i seguenti:

public void showDistanceNotification(final long sessionId,

final float distance) {


final Intent launchAppIntent = new Intent(mContext, MainActivity.class);

launchAppIntent.putExtra(MainActivity.FIRST_FRAGMENT_INDEX, 1);
final PendingIntent launchPendingIntent = PendingIntent
.getActivity(mContext, LAUNCH_APP_REQUEST_CODE,
launchAppIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// The distance text
final String distanceText = DistanceUtil
.formatDistance(mContext, distance);
// We create the Builder
NotificationCompat.Builder builder =
new NotificationCompat.Builder(mContext)
.setContentText(mContext
.getString(R.string.notification_distance_format,
sessionId, distanceText))
.setSmallIcon(R.drawable.ic_launcher)
.setOngoing(true)
.setContentIntent(launchPendingIntent)
.setAutoCancel(false);
mNotificationManager.notify(DISTANCE_NOTIFICATION_ID, builder.build());
}


Si tratta di una notifica abbastanza semplice di tipo ongoing (non pu essere
eliminata attraverso unazione dellutente) e che visualizza la distanza percorsa fino a
quel momento. Da notare la presenza di un PendingIntent che permetta di lanciare
lapplicazione per eventualmente fermare il servizio stesso. Una sola complicazione
dovuta allutilizzo di un extra, evidenziato nel codice, per informare lActivity
principale di quale Fragment visualizzare; quello di default non sarebbe infatti quello
relativo al tracking delle posizioni. Nel metodo onCreate() della classe MainActivity
abbiamo pertanto aggiunto le seguenti semplici righe di codice, che permettono di
selezionare il Fragment corrispondente alla voce di menu indicata:

int firstFragmentIndex = getIntent().getIntExtra(FIRST_FRAGMENT_INDEX, 0);


onNavigationDrawerItemSelected(firstFragmentIndex);


Infine il metodo di cancellazione della notifica alquanto banale e precisamente:

public void dismissDistanceNotification() {

mNotificationManager.cancel(DISTANCE_NOTIFICATION_ID);
}


Il passo conclusivo consiste nel trovare i punti ideali in cui richiamare la
visualizzazione e quindi la cancellazione della notifica. A tale proposito utilizziamo
quanto creato in precedenza nella classe ServiceState, e precisamente in
corrispondenza del metodo addLocation() e stop(). Il primo viene richiamato in
corrispondenza del salvataggio di uninformazione di location, che quindi
presuppone che il servizio sia in esecuzione. Il secondo viene invece richiamato in
corrispondenza dello stop esplicito del servizio. Lasciamo al lettore la consultazione
del codice e il test dellapplicazione, che ora dovrebbe essere dotata di notifica,
selezionando la quale possibile tornare allapplicazione per leventuale interruzione
del servizio di tracking. Questa funzionalit di supporto ci ha permesso di creare la
classe FenceNotificationHelper che utilizzeremo successivamente per la gestione degli
altri tipi di notifiche.

Activity Recognition
Dopo limplementazione di un servizio di tracking delle posizioni, ci vogliamo
occupare di una funzionalit molto interessante, che prende il nome di Activity
Recognition, la quale ci permetter di avere indicazioni su come ci stiamo muovendo,
ovvero se stiamo camminando, correndo, andando in bici e cos via. Prima di
procedere allimplementazione, facciamo qualche considerazione sul perch queste
informazioni potrebbero esserci utili. Il tutto legato al tipo di applicazione. Qualora
volessimo creare unapplicazione che calcola il numero di calorie consumate,
potremmo s interfacciarci con un dispositivo cardiofrequenzimetro, ma anche fare
una stima in base al nostro peso e a quello che siamo facendo. La corsa, in genere,
porta un dispendio di calorie maggiore rispetto alla camminata o alla bicicletta. Il
secondo aspetto invece legato a quello che lapplicazione deve poter fornire
allutente. Alcune funzionalit che potrebbero distrarre lutente non dovrebbero
essere rese disponibili quando questo impegnato in alcune attivit come landare in
bicicletta. Si potrebbero poi introdurre eventuali ottimizzazioni per preservare
lutilizzo della batteria. Nella nostra applicazione, per esempio, potremmo aumentare
la distanza minima per la generazione di un evento di Location nel caso in cui ci si
muova in auto. Le applicazioni di questa funzionalit sono moltissime e dipendono
solo dalla fantasia dello sviluppatore. Nella nostra applicazione abbiamo deciso di
rendere il tutto molto semplice, preoccupandoci solo di aggiungere questa
informazione a quelle che salviamo nel nostro ContentProvider.
Il primo passo molto semplice: consiste nellaggiunta di un nuovo permesso a
quelli che abbiamo definito nellAndroidManifest.xml. Questo dovuto al fatto che
linformazione che si vuole ottenere da ritenersi sensibile e lutente ne deve essere
quindi informato a priori e dare il proprio assenso. Aggiungiamo quindi la seguente
definizione:

<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />


La buona notizia riguarda la modalit con cui andremo a raccogliere le
informazioni che ci interessano, che sar analoga a quella descritta in corrispondenza
delle Location. Anche in questo caso dovremo semplicemente abilitare la funzionalit
in fase di inizializzazione delloggetto GoogleApiClient e quindi utilizzare un altro
componente di tipo ActivityRecognitionApi per la registrazione o meno al servizio.
NOTA

Una curiosit riguarda il fatto che questa funzionalit non necessita dei permessi relativi alla
Location che abbiamo aggiunto per la precedente funzionalit.

Riprendiamo in mano la classe MyMoveFragment e modifichiamo listruzione di


inizializzazione delloggetto GoogleApiClient nel modo evidenziato di seguito:

mGoogleApiClient = new GoogleApiClient.Builder(getActivity())

.addApi(LocationServices.API)
.addApi(ActivityRecognition.API)
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedListener)
.build();

Abbiamo quindi semplicemente aggiunto le API identificate dalla costante
ActivityRecognition.API.
NOTA
Attenzione: nella documentazione ufficiale di Android si fa riferimento in modo erroneo alla
costante ActivityRecognitionServices.API.

Analogamente a quanto fatto nel caso della Location, ora i Google Play services ci
mettono a disposizione un oggetto di tipo ActivityRecognitionApi, cui accediamo sempre
attraverso la classe LocationServices. Anche ora dovremo quindi creare un PendingIntent
da utilizzare per linvio dellinformazione di ActivityRecognition a un opportuno
servizio, che nel nostro caso sar sempre descritto dalla classe
ActivityRecognition.ActivityRecognitionApi. Per mantenere linformazione ottenuta in
relazione allattivit dellutente da utilizzare per valori successivi di Location,
aggiungeremo unopportuna propriet della classe SessionState, che abbiamo creato
per la gestione dello stato.
Occupiamoci per il momento dellavvio del servizio di ActivityRecognition e quindi
modifichiamo il metodo startTracking() aggiungendo le seguenti righe di codice:

// We manage the ActivityRecognition
final Intent activityRecognitionIntent = LocationService

.getActivityRecognitionIntent(getActivity());
final PendingIntent activityIntent = PendingIntent

.getService(getActivity(), UPDATE_ACTIVITY_REQUEST_CODE,
activityRecognitionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
ActivityRecognition.ActivityRecognitionApi

.requestActivityUpdates(mGoogleApiClient,
ACTIVITY_INTERVAL, activityIntent);

Innanzitutto creiamo lIntent relativo al nostro LocationService, attraverso il metodo
statico di factory getActivityRecognitionIntent(), che in questo caso molto semplice,
ovvero:

public static Intent getActivityRecognitionIntent(final Context context) {

Intent intent = new Intent(context, LocationService.class);


return intent;
}


Vedremo infatti come non vi sia alcun bisogno di action o extra particolari nel
servizio stesso. Si tratta di un Intent che andiamo a utilizzare nellinizializzazione del
corrispondente PendingIntent che non si differenzia di molto da quello creato nel caso
della Location. Abbiamo infatti utilizzato semplicemente un identificatore diverso e,
ovviamente, il nuovo PendingIntent. Laspetto interessante riguarda invece lutilizzo
delloggetto di tipo ActivityRecognition.ActivityRecognitionApi, che mette a disposizione il
seguente metodo:

public abstract PendingResult<Status> requestActivityUpdates(GoogleApiClient

client, long detectionIntervalMillis, PendingIntent callbackIntent)



Questo metodo prevede lutilizzo di tre parametri. Il primo lormai noto oggetto
GoogleApiClient che abbiamo inizializzato in precedenza. Il secondo parametro ci
permette invece di specificare lintervallo per determinare linformazione di attivit.
Un valore troppo basso porta a un maggiore utilizzo delle risorse e quindi a un
maggior dispendio di energia. I Google Play services cercano infatti di capire che
tipo di attivit sta svolgendo lutente attraverso lutilizzo di tutti i sensori di cui

dispone. Alcuni sono a basso consumo, mentre altri richiedono maggiore energia.
Come nel caso della Location, potrebbe capitare di ricevere linformazione sullattivit
con una frequenza maggiore di quella indicata in precedenza; questo dovuto al fatto
che anche altre applicazioni potrebbero aver richiesto lo stesso servizio. Per
preservare la batteria, potrebbe anche accadere che il servizio non riceva alcuna
informazione, in quanto il dispositivo fermo. Per poter fare questo necessario
disporre di un particolare sensore associato alla costante Sensor.TYPE_SIGNIFICANT_MOTION.
Infine il terzo parametro il PendingIntent relativo al servizio che ricever le
informazioni volute e che nel nostro caso descritto dalla classe LocationService che
descriveremo tra poco.
A questo punto il metodo che ci permette di interrompere il servizio di
ActivityRecognition molto semplice e analogo a quanto fatto nel caso della Location.
Nel nostro caso abbiamo semplicemente aggiunto le seguenti righe di codice al
metodo stopTracking():

final Intent activityRecognitionIntent = LocationService

.getActivityRecognitionIntent(getActivity());
final PendingIntent activityIntent = PendingIntent

.getService(getActivity(), UPDATE_ACTIVITY_REQUEST_CODE,
activityRecognitionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
ActivityRecognition.ActivityRecognitionApi

.removeActivityUpdates(mGoogleApiClient, activityIntent);

Anche alla luce di quanto descritto in precedenza in relazione ai PendingIntent,
abbiamo semplicemente ricreato il PendingIntent e quindi utilizzato il seguente metodo
della classe ActivityRecognition.ActivityRecognitionApi:

public abstract PendingResult<Status> removeActivityUpdates(GoogleApiClient

client, PendingIntent callbackIntent)



A questo punto lavvio e la terminazione del servizio di ActivityRecognition stato
implementato in modo molto semplice. Il passo successivo consiste nella gestione
della relativa informazione nella classe LocationService.

NOTA
Il lettore potrebbe obiettare, a ragione, che i metodi startTracking() e stopTracking() stiano
diventando un po troppo corposi. Sicuramente una soluzione migliore prevedrebbe
lincapsulamento di queste istruzioni nei metodi start() e stop() della classe ServiceState.
Lasciamo al lettore come esercizio limplementazione di questa piccola operazione di refactoring.

Prima di questo ci accorgiamo della necessit di estendere la nostra base dati, e in


particolare quello che riguarda lentit FencePosition, che ora necessita di una colonna
che memorizzer linformazione di attivit, come possiamo vedere nella Figura 2.8 e
che abbiamo chiamato activity. Come possiamo notare, si tratta di un valore
numerico, che otterremo dal nostro servizio e che corrisponde ad altrettante costanti
di una classe di nome DetectedActivity.
Il tutto risulta chiaro osservando il codice che abbiamo aggiunto nel metodo
onHandleIntent() del nostro LocationService e precisamente:

if (ActivityRecognitionResult.hasResult(intent)) {

final ActivityRecognitionResult activityResult =


ActivityRecognitionResult.extractResult(intent);
final DetectedActivity mostProbableActivity =
activityResult.getMostProbableActivity();
final int activityType = mostProbableActivity.getType();
mServiceState.updateActivityType(activityType);
}

Figura 2.8 Le entit gestite dal nostro ContentProvider dopo laggiunta del campo activity.

Innanzitutto notiamo come in questo caso non sia stata utilizzata una specifica
action, ma sia stato utilizzato un metodo di utilit della classe ActivityRecognitionResult e
in particolare il metodo:

public static boolean hasResult(Intent intent)


Si tratta di un modo per incapsulare, e quindi poter cambiare successivamente
senza alcun impatto, la modalit con cui linformazione memorizzata nellIntent.
NOTA
Ci aspettavamo una cosa simile anche per la Location; probabilmente Google uniformer il tutto
in una successiva versione dei Google Play services.

Se il test ha esito positivo, possiamo accedere alle informazioni attraverso il


seguente metodo, sempre della classe ActivityRecognitionResult.

public static ActivityRecognitionResult extractResult(Intent intent)


Si tratta di un metodo di Factory che ci restituisce, appunto, unistanza di
che incapsula tutte le informazioni di cui abbiamo bisogno. Il

ActivityRecognitionResult

sistema di riconoscimento delle attivit dellutente non perfetto, per cui non pu
stabilire con certezza se lutente stia camminando o correndo o altro. Il tutto
addirittura impossibile se pensiamo a che cosa ognuno di noi considera come
camminare o correre. Per questo motivo abbiamo a disposizione due metodi diversi.

Il primo ci fornisce una lista di oggetti di tipo DetectedActivity, ciascuno dei quali
contiene informazioni relative a una possibile attivit

public List<DetectedActivity> getProbableActivities()


Il secondo ci permette invece di accedere direttamente allattivit che, secondo il
nostro dispositivo, la pi probabile

public DetectedActivity getMostProbableActivity()


Questultimo proprio il metodo che abbiamo utilizzato per stimare lattivit
dellutente in quel momento. Ogni oggetto di tipo DetectedActivity contiene alcune
informazioni, relative, come abbiamo detto, al tipo di attivit e quindi alla
corrispondente attendibilit. Possiamo accedere a queste informazioni attraverso i
seguenti metodi:

public int getConfidence()
public int getType()


Lattendibilit rappresentata da un valore numerico compreso tra 0 e 100.
Maggiore il valore e maggiore la probabilit che lattivit percepita sia quella
corretta. La documentazione sostiene che lattendibilit sia da considerarsi certa nel
caso di un valore maggiore di 75, mentre alquanto incerta nel caso di un valore
inferiore a 50.
NOTA
Dobbiamo inoltre fare attenzione che alcune attivit, come vedremo, non escludono altre. Per
esempio il fatto di essere a piedi (ON_FOOT) viene considerato come una generalizzazione del
camminare (WALKING) o del correre (RUNNING).

In ogni caso il tipo di attivit caratterizzato da un intero, che pu assumere uno di


questi valori, corrispondenti ad altrettante costanti statiche della classe DetectedActivity

IN_VEHICLE (VEH)
IN_BICYCLE (BIC)
ON_FOOT (FOOT)
RUNNING (RUN)
STILL (STL)
TILTING (TIL)
UNKNOWN (UKN)

WALKING (WAL)


Tra parentesi abbiamo indicato il codice che utilizzeremo in fase di
visualizzazione. Il valore corrispondente allattivit viene poi salvato nel nostro
oggetto ServiceState attraverso la chiamata del metodo updateActivityType(), che non fa
altro che rendere persistente la corrispondente informazione nelle SharedPreferences. La
stessa informazione viene poi utilizzata nel nostro metodo saveLocationData() e quindi
resa persistente insieme ai dati di Location. In questo caso stato sufficiente
aggiungere la seguente riga di codice in corrispondenza della creazione delloggetto
ContentValues dellentit di tipo FencePosition da inserire:

locationValues.put(FenceDB.FencePosition.ACTIVITY,

mServiceState.getActivityType());

Ultimo passo quello di aggiungere questa informazione in fase di visualizzazione
della lista di Location.
NOTA
Al posto delle icone, che avrebbero reso lapplicazione sicuramente pi gradevole, abbiamo
deciso di utilizzare semplici codici, che nel precedente elenco abbiamo messo tra parentesi.
Lasciamo al lettore leventuale personalizzazione dellinterfaccia.

Concludiamo aggiungendo la corrispondente informazione nella lista di


visualizzazione delle posizioni. Si tratta di unoperazione ormai banale, che il lettore
pu consultare nel codice allegato.

Conclusioni
In questo capitolo abbiamo creato un numero elevato di funzionalit attraverso le
API base di Android e di quelle fornite dai Google Play services in relazione alla
determinazione delle informazioni di Location. Innanzitutto abbiamo visto come
inizializzare loggetto GoogleApiClient, che poi abbiamo utilizzato sia per i servizi di
localizzazione sia per quello di determinazione dellattivit dellutente. Il primo caso
duso stato molto semplice e ci ha permesso di ottenere la posizione corrente oltre
che un indirizzo attraverso loggetto GeoCoder. Siamo poi passati allimplementazione
di una funzionalit pi complessa, ovvero il tracking delle posizioni e della relativa
modalit, attraverso la Activity Recognition. Come abbiamo detto, la descrizione dei
Google Play services stata anche il pretesto per vedere allopera alcuni concetti
fondamentali della programmazione Android. Abbiamo visto come realizzare (e
testare) un ContentProvider per la memorizzazione delle informazioni di Location.
Abbiamo visto in particolare come creare una nostra implementazione di Cursor, che
ci permetta di incapsulare laccesso ai vari campi, semplificandone lutilizzo in fase
di visualizzazione. In questo caso specifico abbiamo dovuto gestire le versioni
precedenti alla 4.0, implementando linterfaccia CursorFactory in modo leggermente pi
complicato rispetto al caso in cui tali versioni non dovessero essere supportate dalla
nostra applicazione. Abbiamo visto, nel caso del GeoCoder, come utilizzare un AsyncTask
per lesecuzione di operazioni in background che avessero comunque un feedback
nellinterfaccia utente. Abbiamo parlato poi di un PendingIntent e di come possa essere
utilizzato per gestire in modo affidabile le informazioni di Location e Activity
. Abbiamo poi preparato il terreno per la gestione delle diverse tipologie di

Recognition

notifiche. Infine abbiamo creato un prototipo di applicazione che, sebbene scarna dal
punto di vista grafico, ci ha permesso di affrontare moltissimi temi di fondamentale
importanza.
Ora prepariamoci a mettere in pratica la fatica fatta finora e a realizzare
funzionalit ancora pi interessanti, come quella legata alla gestione delle Google
Maps, che vedremo nel prossimo capitolo.

Capitolo 3

Mappe e Geofence

Nel Capitolo 2 abbiamo visto come ottenere informazioni di location attraverso


due diversi meccanismi. Il primo ci ha permesso di conoscere la nostra posizione
anche attraverso un servizio di Geocoding. Il secondo ci ha permesso di tracciare i nostri
movimenti non solo in termini di Location, ma anche di Activity, ovvero di sapere se
stiamo camminando, correndo, utilizzando un mezzo e cos via. In questo capitolo ci
occuperemo invece di unaltra grande funzionalit che fa parte dei Google Play
services ovvero le Google Maps. Estenderemo quindi le funzionalit della nostra
applicazione FriendFence con la possibilit di visualizzare la nostra posizione in mappa
e quindi il percorso che abbiamo registrato. Approfitteremo di queste funzionalit per
descrivere le API a nostra disposizione per la visualizzazione e personalizzazione
delle mappe. Concluderemo con la descrizione di due funzionalit molto interessanti,
cui abbiamo accennato nellIntroduzione. La prima permette di creare GeoFence, che
non sono altro che zone alle quali possibile associare eventi: possiamo ricevere
notifica se entriamo o usciamo in una di queste zone. Si tratta di una funzionalit
fornita dalle API per la gestione della Location, che abbiamo preferito descrivere in
questo capitolo, in quanto le mappe ne semplificano la creazione dal punto di vista
dellinterfaccia utente. Concluderemo con la descrizione delle API per la
visualizzazione delle Street View, che ci permetteranno di visualizzare il nostro
percorso in modo particolare. Anche in questo caso la nostra applicazione un
campo di sperimentazione, per cui tralasceremo gli aspetti puramente grafici, ma ci
concentreremo su quelli di gestione dei dati e di utilizzo delle API.

Inizializzazione delle Google Maps


Il primo passo verso lutilizzo delle Google Maps consiste nella loro abilitazione e
inizializzazione. Come vedremo anche per altre funzionalit, come i Google Cloud
Messaging, per utilizzare queste API occorre abilitarle nella console delle
applicazioni di Google. Andiamo quindi al seguente indirizzo, utilizzando il nostro
account: http://code.google.com/apis/console
Il primo passo consiste nella creazione dellapplicazione, selezionando il pulsante
Create Project nellinterfaccia che possiamo vedere nella Figura 3.1.

Figura 3.1 Creazione del progetto nella console di Google.

Selezionando il pulsante di creazione del progetto otteniamo la visualizzazione


della finestra di dialogo indicata nella Figura 3.2, nella quale vengono richieste due
informazioni. La prima riguarda il nome del progetto nella console. Quello che
caratterizza invece lapplicazione il secondo valore, il ProjectId, che possiamo farci
suggerire dal sistema selezionando licona con la freccia di aggiornamento. Notiamo
come questo sia composto da parole in minuscolo separate dal trattino (). Nel nostro
caso abbiamo inserito come identificativo dellapplicazione il valore apogeo-friendfence
e poi abbiamo selezionato il pulsante Create. Si tratta di unoperazione non immediata
che, dopo la visualizzazione di una Progress Dialog, ci porta alla schermata Project
, che contiene opzioni relative ad altre funzionalit offerte dalla piattaforma

Dashboard

Google. Quello che ci interessa invece nel menu nella parte sinistra, ovvero
lopzione relativa alle API e autorizzazioni (Figura 3.3).
Innanzitutto selezioniamo il link APIs per labilitazione della funzionalit relativa
alle mappe. Arriviamo a una schermata che contiene un lunghissimo elenco di tutte le
API che si possono utilizzare. Come vediamo nella Figura 3.4, utilizziamo lopzione

di ricerca per visualizzare le Google Maps Android API v2 che andiamo ad abilitare
selezionando lo Swatch Status sulla destra, che inizialmente OFF.
Una volta abilitate, le API vengono elencate tra quelle disponibili per la nostra
applicazione (Figura 3.5).

Figura 3.2 Inserimento delle informazioni del progetto FriendFence.

Figura 3.3 Alcune opzioni per il progetto FriendFence.

Figura 3.4 Cerchiamo le Google Maps tra le API disponibili.

Il passo successivo consiste nellottenere una chiave per lutilizzo delle Google
Maps, la quale dovr essere associata al certificato utilizzato per la nostra
applicazione; in questa fase di sviluppo il certificato sar quello di debug.
Selezioniamo la voce Credentials nel menu a sinistra e quindi lopzione Create New Key.
Nella finestra di dialogo successiva selezioniamo lopzione Android, ottenendo il form
rappresentato nella Figura 3.6.

Figura 3.5 Le Google Maps sono ora abilitate.

Figura 3.6 Creazione della chiave per lutilizzo delle Google Maps in Android.

Possiamo notare come sia stato selezionato Android come tipo di applicazione e
quindi vengano richieste due informazioni fondamentali. La prima consiste nel
codice SHA1 associato al certificato che utilizzeremo per firmare lapplicazione,
mentre la seconda il package dellapplicazione stessa.
Quando lapplicazione dovr essere pubblicata sullo Store, il certificato utilizzato
dovr essere quello di produzione, mentre in questo caso utilizzeremo quello di
debug, che solitamente nella cartella .android nella home del nostro utente sulla
nostra macchina di sviluppo. Per ottenere questa informazione dobbiamo utilizzare
gli strumenti di Java, e in particolare il comando keytool, nel seguente modo:

keytool -list -v -keystore debug.keystore -alias androiddebugkey -storepass android -keypass
android


Come possiamo notare, esistono dei nomi predefiniti in relazione allalias ed alla
password. In ogni caso eseguendo questo comando otteniamo quanto rappresentato
nella Figura 3.7 nella quale possiamo notare la presenza dellinformazione relativa
alla firma SHA1, che quindi copiamo nel precedente form, seguita dal carattere ; (punto
e virgola) e quindi dal package dellapplicazione.

Figura 3.7 Otteniamo il codice SHA1 dal certificato di debug.

Selezionando il pulsante Create notiamo come venga effettivamente creata una


chiave che utilizzeremo poi in fase di inizializzazione delle Google Maps nella nostra
applicazione. Abbiamo concluso lutilizzo della console di Google, per cui possiamo
tornare alla nostra applicazione e in particolare dobbiamo inserire nel file
AndroidManifest.xml le configurazioni necessarie. Occorre innanzitutto inserire la chiave
appena ottenuta, attraverso un elemento di tipo <meta-data/> come figlio dellelemento
.

<application/>

NOTA
Si tratta di un altro elemento simile a quello che abbiamo aggiunto nellinstallazione dei Google
Play services.

Figura 3.8 stata creata una chiave per laccesso alle Google Maps.

In questo caso si tratta della seguente definizione, dove in corrispondenza di <API


dovremo mettere la precedente chiave.

KEY>


<meta-data android:name="com.google.android.maps.v2.API_KEY"

android:value=<API KEY> />


Come facile intuire, le Google Maps richiedono labilitazione di alcune


funzionalit, che necessitano di alcuni permessi. Dobbiamo quindi aggiungere le
seguente dichiarazioni:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />


Si tratta di permessi che non si direbbero richiesti per visualizzare una mappa, per
cui ne diamo breve giustificazione. Il permesso android.permission.INTERNET serve per
permettere laccesso alla Rete e per eseguire il download dei tile che compongono le
mappe.
NOTA
Un tile (mattonella) e una parte della mappa, le cui dimensioni dipendono dal livello di zoom. Le
tile vengono scaricate dalla Rete in base alle esigenze.

In caso contrario non ci sarebbe alcuna possibilit da parte di Android di scaricare


le informazioni, a meno di non averle gi tutte in memoria locale. Anche il permesso
android.permission.ACCESS_NETWORK_STATE e utilizzato dalle mappe per sapere se e possibile
o meno eseguire il download delle precedenti tile o riprenderlo qualora ritornasse una
connessione prima assente. Le mappe, al fine di ridurre lutilizzo della Rete, hanno la
necessit di creare una cache su file system. Per questo motivo dobbiamo definire
anche lutilizzo del permesso android.permission.WRITE_EXTERNAL_STORAGE. Infine, per
laccesso ai web service di Google e stato definito un permesso personalizzato
associato alla costante com.google.android.providers.gsf.permission.READ_GSERVICES. Un
ultimo sforzo, legato al fatto che le mappe utilizzano le OpenGL ES versione 2.
Questa definizione serve solamente per impedire il download dellapplicazione dal
Market a quei dispositivi che non supportano tali librerie. Aggiungiamo quindi la
definizione

<uses-feature android:glEsVersion="0x00020000" android:required="true"/>


Siamo ora pronti ad aggiungere una mappa per la visualizzazione della posizione
corrente, che associamo alla selezione delloperazione di GeoCoder. Vogliamo fare in
modo che, quando selezioniamo il pulsante di geolocalizzazione, venga visualizzata
una mappa con la posizione corrente nella parte inferiore del display, al posto di
quella che prima era la visualizzazione di un indirizzo.

Come abbiamo visto nel Capitolo 1, una delle novit dei Google Play services 6.5
quella di permettere lutilizzo delle sole librerie che unapplicazione utilizza. Nel
precedente capitolo abbiamo definito la dipendenza solamente con la parte generica
mentre ora dobbiamo aggiungere la parte relativa alle Google Maps aggiungendo la
definizione evidenziata nel seguente codice allinterno del file build.gradle del nostro
progetto:

dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.android.support:appcompat-v7:21.0.3
compile com.android.support:support-v4:21.0.3
compile com.google.android.gms:play-services:6.5.87
compile com.google.android.gms:play-services-maps:6.5.87
}


In questo modo stiamo importando solamente le librerie per la gestione delle
mappe.

Visualizzazione della posizione corrente


Nelle versioni precedenti di Android, le mappe erano disponibili solamente
attraverso una particolare specializzazione della classe Activity, la classe MapActivity.
Questo aveva lo svantaggio di occupare tutto lo schermo e, soprattutto, permetteva la
visualizzazione di una sola mappa alla volta. Le nuove API permettono invece di
utilizzare la classe MapFragment e che, come dice il nome stesso, una specializzazione
della classe Fragment, la quale pu essere utilizzata sia direttamente in un documento di
layout, sia istanziata e quindi aggiunta dinamicamente come un qualunque altro
Fragment. Esiste comunque un problema, legato al fatto che nella nostra applicazione
stiamo gi utilizzando dei Fragment. Android non permette di definire un elemento di
tipo <fragment/> in un documento di layout gi utilizzato da un altro Fragment. Lunica
opzione che abbiamo quindi quella di definire un anchor nel layout
fragment_my_location.xml, al quale agganciamo in modo dinamico il Fragment con la
mappa. Nel documento di layout abbiamo sostituito la TextView per la
visualizzazione dellinformazione di Geocoding con un anchor che ospiter la nostra
mappa.

<FrameLayout

android:id=@+id/my_location_map_anchor
android:layout_width=match_parent
android:layout_height=match_parent
android:layout_below=@+id/my_location_geocoder_title />

Laggiunta della mappa diventa quindi molto semplice e consiste nellaggiunta
delle seguenti righe di codice nel metodo onCreateView() della classe MyLocationFragment
che, ricordiamo, contiene la logica relativa alla funzione Where I Am.

mMapFragment = SupportMapFragment.newInstance();
getChildFragmentManager().beginTransaction()

.replace(R.id.my_location_map_anchor, mMapFragment)
.commit();

comunque fondamentale notare come non sia stata utilizzata la classe MapFragment,
ma la classe SupportMapFragment. Si tratta di un accorgimento necessario, in quanto
stiamo utilizzando la libreria di compatibilit, la quale utilizza la classe Fragment del
, che non compatibile con la classe Fragment del package

package android.support.v4.app

esteso dalla classe MapFragment. Dopo aver temporaneamente commentato il

android.app

codice relativo al precedente campo di testo per la visualizzazione dellindirizzo


associato alla Location, notiamo come il risultato sia quello rappresentato nella Figura
3.9.
Notiamo la presenza della mappa nella parte inferiore del display oltre ai controlli
per lo zoom e un indicatore (bussola) in alto a sinistra.

Figura 3.9 Visualizzazione di una Google Maps.

Non ci basta visualizzare una mappa: abbiamo la necessit di interagire con essa
per visualizzare una particolare Location, alcuni indicatori e infine un percorso. Serve

quindi un meccanismo che ci permetta di interagire con la mappa e questo loggetto


di tipo GoogleMap, di cui otteniamo un riferimento attraverso il metodo:

public void getMapAsync (OnMapReadyCallback callback)


che notiamo avere un parametro di tipo OnMapReadyCallback che descrive
uninterfaccia di callback che definisce a sua volta la seguente operazione:

public abstract void onMapReady (GoogleMap googleMap)


Si tratta di un accorgimento che stato opportunamente aggiunto nella versione
6.5 dei Google Play services; nelle versioni precedenti si rendeva infatti necessaria la
creazione da parte dello sviluppatore di un meccanismo analogo estendendo la classe
SupportMapFragment. La necessit di un approccio di questo tipo si resa necessaria per il
fatto che loggetto di tipo GoogleMap non subito disponibile ma richiede un tempo di
inizializzazione al termine del quale si ha appunto linvocazione del metodo di
callback con loggetto GoogleMap pronto alluso.
Nel nostro codice abbiamo creato loggetto di tipo SupportMapFragment attraverso il
seguente metodo statico di factory

public static SupportMapFragment newInstance()


ma osservando la documentazione, possiamo notare come esista un secondo
metodo statico di Factory che preveda come parametro un oggetto di tipo
, il quale permette di specificare, in fase di creazione, alcune

GoogleMapOptions

caratteristiche della mappa, ovvero:


lo stato iniziale, inteso come Location, il livello di zoom e i valori bearing e tilt,
che vedremo successivamente;
il tipo di mappa;
la visualizzazione dei controlli di zoom;
linsieme delle gesture che lutente pu eseguire sulla mappa (pinch, zoom e
cos via).

Il nostro problema iniziale consiste comunque nella visualizzazione della posizione


corrente (che abbiamo ottenuto nel modo descritto nel Capitolo 2) allinterno della
nostra mappa. Prima di tutto bene fare alcune considerazioni generali sul
funzionamento delle Google Maps. Come possiamo vedere nella Figura 3.9 la mappa
viene visualizzata come se esistesse una videocamera inizialmente in posizione
verticale, rivolta verso il basso. Sebbene la Terra sia approssimativamente sferica,
viene rappresentata su un piano attraverso una proiezione, che si chiama mercator
projection. Gli strumenti che descriveremo permetteranno di modificare la posizione
della telecamera, con conseguente modifica del risultato visualizzato. Il primo di
questi quello che ci permetter di posizionare la telecamera in termini di latitudine e
longitudine, che conosciamo una volta ottenuto un oggetto di tipo Location. Per fare
questo si utilizza un oggetto di tipo CameraUpdate, di cui otteniamo un riferimento
attraverso uno dei metodi di Factory della classe CameraUpdateFactory. In pratica si tratta
di un oggetto che utilizzeremo per incapsulare tutte le informazioni relative a quello
che vogliamo visualizzare nella mappa; una sorta di richiesta che viene poi applicata
successivamente in modo asincrono. Una prima implementazione del metodo
showLocationInMap() che abbiamo creato per centrare la mappa la seguente:

private void showLocationInMap(final Location location) {

if (location == null || mGoogleMap == null) {


// Location not available
return;
}
final LatLng currentLatLng = new LatLng(location.getLatitude(),
location.getLongitude());
final CameraUpdate cameraUpdate = CameraUpdateFactory
.newLatLng(currentLatLng);
mGoogleMap.moveCamera(cameraUpdate);
}


Innanzitutto facciamo un controllo sulla presenza di tutto quello che ci serve,
ovvero una Location da visualizzare e un oggetto di tipo GoogleMap inizializzato nel
modo descritto in precedenza. Notiamo come le informazioni di latitudine e

longitudine vengano incapsulate in un oggetto di tipo LatLng, che poi utilizziamo per
creare unistanza di CameraUpdate attraverso il corrispondente metodo di Factory. A
questo punto non ci resta che applicare alla nostra mappa loggetto CameraUpdate
attraverso il metodo

public final void moveCamera(CameraUpdate update)


Quello descritto per la location un procedimento molto generale, che vedremo
essere applicato anche per operazioni pi complesse. In sintesi si crea un oggetto di
tipo CameraUpdate con le istruzioni relative alla richiesta da fare alla mappa per poi
applicarlo secondo diversi criteri, che possono prevedere lutilizzo di unanimazione
o meno. Se colleghiamo la chiamata di questo metodo allazione nella ActionBar in
precedenza relativa al GeoCoding notiamo come il risultato sia quello rappresentato
nella Figura 3.10.

Figura 3.10 Visualizzazione della Location corrente.

In realt la mappa dovrebbe essere centrata su Londra, ma il livello di zoom tale


da non rendere significativa questa informazione. Per questo motivo decidiamo
quindi di utilizzare un altro metodo di Factory della classe CameraUpdate, che prevede di
specificare il livello di zoom:

public static CameraUpdate newLatLngZoom(LatLng latLng, float zoom)


Notiamo come lo zoom sia rappresentato da un valore di tipo float, che pu
assumere valori compresi tra 0.0 f e un altro che dipende dal tipo di mappa, dalle
dimensioni di display e dalla zona visualizzata. Maggiore il valore specificato e
maggiore il dettaglio. Aggiungere un valore di 1.0 f corrisponde a dividere per 2 la
zona visualizzata sullo schermo.
Unaltra modifica che facciamo riguarda la modalit con cui si vogliono applicare
le modifiche incapsulate nelloggetto CameraUpdate. Possiamo infatti fare in modo che
queste vengano applicate non in modo immediato, ma attraverso unanimazione. In
questo caso gli strumenti ci vengono messi a disposizione dalla classe GoogleMap e
precisamente utilizzeremo il seguente metodo:

public final void animateCamera (CameraUpdate update, int durationMs,

GoogleMap.CancelableCallback callback)

Si tratta di un overload del metodo con il maggior numero di parametri che
corrispondono alloggetto CameraUpdate, alla durata dellanimazione e quindi
allimplementazione di uninterfaccia di callback relativa alla terminazione o meno
dellanimazione stessa. Linterfaccia GoogleMap.CancelableCallback definisce infatti due
metodi distinti, che ci permettono di sapere se lanimazione stata completata in
modo naturale o se stata interrotta, per esempio, a seguito di una gesture
dellutente o di un errore. Il nostro metodo showLocationInMap() diventa quindi:

private void showLocationInMap(final Location location) {

if (location == null || mGoogleMap == null) {


// Location not available

return;
}
final LatLng currentLatLng = new LatLng(location.getLatitude(),
location.getLongitude());
final CameraUpdate cameraUpdate = CameraUpdateFactory
.newLatLngZoom(currentLatLng, DEFAULT_ZOOM);
mGoogleMap.animateCamera(cameraUpdate, MAP_ANIMATION_DURATION,
new GoogleMap.CancelableCallback() {
@Override
public void onFinish() {
Log.d(TAG_LOG, Animation completed successfully!);
}
@Override
public void onCancel() {
Log.d(TAG_LOG, Animation Cancelled!);
}
});
}


In questo caso lasciamo al lettore la verifica di quello che succede, ovvero la
centratura della mappa nella posizione corrente attraverso unanimazione, la cui
durata dipende dal valore della costante che abbiamo chiamato MAP_ANIMATION_DURATION.
Abbiamo inoltre implementato linterfaccia GoogleMap.CancelableCallback visualizzando
due messaggi di log in corrispondenza del completamento naturale (metodo
onFinish()) o a seguito di una cancellazione (metodo onCancel()). Nel nostro caso,
lutilizzo di un livello di zoom pari a 6 ha portato al risultato rappresentato nella
Figura 3.11, dove notiamo una visualizzazione dellinformazione di Location pi
accurata.

Figura 3.11 Visualizzazione della Location corrente specificando il livello di zoom.

Abbiamo capito come loggetto CameraUpdate ci permetta di istruire la mappa in


relazione alle informazioni che intendiamo visualizzare. Per completare il discorso
sulla gestione dello zoom notiamo come esistano i seguenti metodi:

public static CameraUpdate zoomIn()
public static CameraUpdate zoomOut()
public static CameraUpdate zoomTo(float zoom)
public static CameraUpdate zoomBy(float amount)
public static CameraUpdate zoomBy(float amount, Point focus)


I primi due permettono di aumentare o ridurre il livello di zoom corrente di 1.0 e
sono equivalenti a quello che succede selezionando uno dei due pulsanti in basso a
destra nella mappa.
Il metodo zoomTo() permette invece di impostare un livello di zoom assoluto, a
differenza del metodo zoomBy(), che permette invece di impostare un valore di zoom

relativo. Per esempio, se il livello di zoom attuale 10, lapplicazione del metodo
zoomBy(2) porta lo zoom a un livello 12, mentre zoomBy(-3) lo porta a 7. Loverload del
metodo zoomBy() che prevede un secondo parametro di tipo Point molto utile, in
quanto permette di specificare nello stesso momento il livello di zoom e il punto nella
mappa da considerare come il centro dello zoom stesso e quindi da tenere fermo
(prima e dopo lo zoom quel punto indica la stessa location). Notiamo come questo
punto venga specificato attraverso un oggetto Point, che quindi rappresenta un punto
dellimmagine della mappa e non una Location. Il valore di default rappresentato dal
punto centrale della mappa. Notiamo infine come questi metodi restituiscano un
riferimento alloggetto CameraUpdate e quindi possano essere utilizzati in chaining.
Abbiamo visto come gestire il livello di zoom, il quale strettamente legato
allestensione della zona che intendiamo visualizzare. Maggiore il livello di zoom e
minore lestensione della zona visualizzata. Spesso abbiamo invece lesigenza di
voler rappresentare un numero di punti sulla mappa e abbiamo bisogno di un livello
di zoom tale da permetterci di visualizzarli tutti sul display senza la necessit di
traslare la mappa. Per fare questo le Google Maps API ci mettono a disposizione un
oggetto di tipo LatLngBounds, il quale permette di definire una regione contenente un
insieme di punti. Si tratta di un oggetto che possiamo utilizzare come parametro del
metodo di Factory della classe CameraUpdateFactory, da applicare poi alla mappa come
fatto in precedenza. Questo meccanismo ci sar utile quando avremo la necessit di
visualizzare tutti i punti del nostro percorso. Per il momento creiamo un semplice
esempio, che possiamo richiamare nella nostra applicazione a solo scopo di test per
verificarne il comportamento. Supponiamo di voler visualizzare alcune citt come
Roma, Napoli, Milano, Trieste e Palermo di cui conosciamo le coordinate. La classe
LatLngBounds dispone del seguente costruttore

public LatLngBounds(LatLng southwest, LatLng northeast)


Il costruttore ha due parametri, che rappresentano rispettivamente lestremo
inferiore sinistro (southwest) e quello superiore destro (northeast) del rettangolo che lo
stesso vuole rappresentare. Qualora avessimo pi punti, potremmo inizialmente
calcolare il punto pi a sud-ovest e quello pi a nord-est e poi utilizzare il costruttore.
In realt esiste un modo pi semplice, che prevede lutilizzo del seguente metodo, il
quale fornisce automaticamente il riferimento a un oggetto LatLngBounds tenendo conto
di un nuovo punto passato come parametro:


public LatLngBounds including(LatLng point)


Nel seguente codice abbiamo evidenziato come questo avvenga attraverso il
metodo including() in chaining. Notiamo infine come il procedimento sia analogo a
quello precedente, ovvero la creazione di un oggetto di tipo CameraUpdate e quindi
lutilizzo di un metodo della classe GoogleMap per poterlo rendere effettivo.

private void showCitiesInMap() {

if (mGoogleMap == null) {
// Location not available
return;
}
// We define the cities we want to represent
final LatLng rome = new LatLng(41.872389, 12.48018);
final LatLng naples = new LatLng(40.851775, 14.268124);
final LatLng milan = new LatLng(45.465422, 9.185924);
final LatLng trieste = new LatLng(45.649526, 13.776818);
final LatLng palermo = new LatLng(38.115688, 13.361267);
// We create the LatLngBounds object using chaining
final LatLngBounds bounds = new LatLngBounds(rome, rome)
.including(naples)
.including(milan)
.including(trieste)
.including(palermo);
// Create a CameraUpdate
final CameraUpdate cameraUpdate = CameraUpdateFactory
.newLatLngBounds(bounds, DEFAULT_PADDING);
// Apply the CameraUpdate to the Map

mGoogleMap.moveCamera(cameraUpdate);
}


Da notare anche lutilizzo del metodo statico di factory newLatLngBounds(), il quale
utilizza come secondo parametro un valore intero che rappresenta il padding da
applicare alla mappa per fare in modo che alcuni punti non vengano posizionati sui
margini della mappa e quindi non siano completamente visibili. Il risultato del nostro
esempio quello rappresentato nella Figura 3.12, dove notiamo che tutte le citt
elencate sono comprese nel dettaglio di zoom scelto dal sistema a seguito della nostra
richiesta.

Figura 3.12 Zoom tale da visualizzare contemporaneamente un insieme di Location.

In precedenza abbiamo per parlato anche di tilt e bearing, di cui vogliamo dare
qui una descrizione intuitiva, rimandando per i dettagli alla documentazione ufficiale.
Il bearing rappresenta la rotazione della mappa che stiamo osservando rispetto al

nord. Possiamo pensare a questo concetto ricordando cosa succede quando si


percorre una strada con un navigatore; osserviamo la mappa ruotare a seconda della
nostra direzione di marcia. Il tilt invece un modo per descrivere linclinazione con
cui si osserva la mappa. In posizione normale stiamo osservando la mappa da una
posizione perpendicolare, mentre il tilt ci permette di abbassarci e quindi di osservare
la mappa come se ci stessimo volando sopra. Per impostare tutte queste informazioni
in un colpo solo, le API ci mettono a disposizione la classe CameraPosition, che
implementa anchessa il Builder Pattern e di cui viene utilizzata unistanza per la
creazione del solito oggetto CameraUpdate come fatto in precedenza. Senza perderci in
dettagli, abbiamo creato il seguente metodo, che ci permette di impostare in un colpo
solo la posizione attuale, il livello di zoom e alcune informazioni di tilt e bearing.

private void showLocationInMapWithTiltAndBearing(final Location location) {

if (location == null || mGoogleMap == null) {


// Location not available
return;
}
final LatLng currentLatLng = new LatLng(location.getLatitude(),
location.getLongitude());
final CameraPosition cameraPosition = CameraPosition.builder()
.target(currentLatLng)
.zoom(DEFAULT_ZOOM)
.bearing(90)
.tilt(30)
.build();
final CameraUpdate cameraUpdate = CameraUpdateFactory
.newCameraPosition(cameraPosition);
mGoogleMap.moveCamera(cameraUpdate);
}

Otteniamo il risultato rappresentato nella Figura 3.13, dove notiamo che la mappa
ruotata di 90 gradi (bearing), mentre il valore di tilt presente, anche se non molto
evidente. Lasciamo al lettore lesecuzione di alcuni test a proposito.
Concludiamo questa parte con gli strumenti che ci permettono di gestire il tipo di
mappa da utilizzare. A tale proposito abbiamo deciso di abilitare i Settings, a cui
possiamo accedere attraverso la corrispondente azione nella ActionBar (i tre puntini in
alto a destra). A tale proposito abbiamo eliminato la definizione del menu nellattivit
principale descritta dalla classe e utilizzato quella invece definita nella nostra classe
MyLocationFragment. Ricordiamo che per fare questo necessario abilitare le opzioni
associate al Fragment attraverso la seguente istruzione nel metodo onCreate():

setHasOptionsMenu(true);


Poi occorre gestirne la selezione nel metodo onOptionsItemSelected(). A parte la
gestione della selezione del tipo di mappa nelle impostazioni, a noi interessa la
modalit con cui questa viene impostata in fase di visualizzazione. sufficiente
utilizzare il seguente metodo della classe GoogleMap:

public final void setMapType(int type)

Figura 3.13 Esempio di utilizzo di bearing e tilt con loggetto CameraPosition.

Il parametro un valore intero, corrispondente a una delle seguenti costanti della


stessa classe, che abbiamo rappresentato con altrettante opzioni:

public static final int MAP_TYPE_NORMAL
public static final int MAP_TYPE_NONE
public static final int MAP_TYPE_SATELLITE
public static final int MAP_TYPE_TERRAIN
public static final int MAP_TYPE_HYBRID


A parte la sola difficolt di convertire un valore di tipo String nei Settings nella
corrispondente costante, la selezione del tipo di mappa diventa quindi banale. Nel
nostro caso non facciamo altro che definire il seguente metodo di utilit, che viene
richiamato sia quando il riferimento alloggetto GoogleMap disponibile, sia quando si
ritorna dalla visualizzazione della schermata dei Settings, ovvero in corrispondenza
del metodo onStart():


private void updateMapType() {

if (mGoogleMap == null) {
// if not present we do nothing
return;
}
// We read the value from the SharedPreferences
SharedPreferences sharedPref = PreferenceManager
.getDefaultSharedPreferences(getActivity());
final String mapTypeStr = sharedPref
.getString(MAP_TYPE_PREFS_KEY, DEFAULT_MAP_TYPE);
final int mapType = Integer.parseInt(mapTypeStr);
final int oldMapType = mGoogleMap.getMapType();
if (oldMapType != mapType) {
mGoogleMap.setMapType(mapType);
}
}


Come semplice ottimizzazione abbiamo fatto in modo di modificare il tipo di
mappa solamente nel caso in cui questa fosse diversa da quella corrente.

Personalizzazione della mappa


Nel paragrafo precedente abbiamo imparato a utilizzare le mappe di Google
attraverso i metodi principali che la classe GoogleMap ci mette a disposizione.
Attraverso la selezione di unopportuna action abbiamo visualizzato una mappa,
centrandola nella nostra posizione corrente. In questo paragrafo vogliamo per fare
qualcosa di pi, ovvero utilizzare i marker, che permettono di evidenziare determinate
attraverso icone particolari. Ma prima vogliamo semplicemente visualizzare

Location

la nostra posizione attraverso una modalit standard, che possibile attivare


attraverso il seguente metodo della classe GoogleMap
public final void setMyLocationEnabled(boolean enabled)

il quale permette di abilitare o meno la visualizzazione della posizione corrente


nella mappa, oltre a unindicazione del bearing, ovvero della direzione. In
corrispondenza dellinizializzazione della GoogleMap aggiungiamo listruzione
evidenziata nel seguente codice:

mMapFragment = SupportMapFragment.newInstance();
mMapFragment.getMapAsync(new OnMapReadyCallback() {

@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
// Enable my location
mGoogleMap.setMyLocationEnabled(true);
// We update the type of the map
updateMapType();
}
});


Otteniamo il risultato rappresentato nella Figura 3.14.

Figura 3.14 Visualizzazione della posizione corrente e relativa direzione.

Nella nostra applicazione vogliamo per andare oltre, e visualizzare un marker in


corrispondenza della posizione corrente, a cui poi assoceremo le informazioni
relative allindirizzo ottenuto attraverso il Geocoder, come descritto nel Capitolo 2.
Un marker non altro che un segnaposto sulla mappa, che possiamo personalizzare in
vari modi. In particolare possiamo:
scegliere il colore per il pin standard;
scegliere unicona personalizzata;
aggiungere un anchor point.
Per gestire queste informazioni le Google Maps API hanno definito la classe Marker,
le cui istanze vengono create attraverso il seguente metodo di factory della classe
GoogleMap:

public final Marker addMarker(MarkerOptions options)


Notiamo come questo metodo riceva in input unistanza della classe MarkerOptions,
che utilizzeremo per impostare tutte le caratteristiche volute. Si tratta, infatti, di una
sorta di implementazione del Builder Pattern (non completo, in quanto non dispone

del classico metodo build()) attraverso la quale possiamo impostare le seguenti


informazioni:
posizione del marker;
abilitazione o meno al suo spostamento da parte dellutente;
titolo;
snippet;
visibilit;
trasparenza;
flatness, ovvero la piattezza rispetto alla mappa;
posizione dellanchor point;
angolo di rotazione;
icona.
La posizione uninformazione obbligatoria, che pu essere impostata attraverso il
seguente metodo della classe MarkerOptions, il quale pu essere, come gli altri,
utilizzato in chaining:

public MarkerOptions position(LatLng position)


Le altre impostazioni sono tutte opzionali e si possono impostare attraverso
altrettanti metodi che descriviamo velocemente. Per abilitare o meno la possibilit da
parte dellutente di eseguire un drag-and-drop esiste il metodo:

public MarkerOptions draggable(boolean draggable)


il quale entra in gioco attraverso un evento di LongClick. Se il clic semplice,
possiamo fare in modo che venga visualizzata unetichetta che possiamo impostare
attraverso il seguente metodo:

public MarkerOptions title(String title)


Se letichetta non fosse sufficiente, esiste comunque la possibilit di visualizzare
uno snippet, il cui contenuto pu essere impostato attraverso il seguente metodo:

public MarkerOptions snippet(String title)


Prima di proseguire possiamo gi provare lesecuzione delle seguenti righe di
codice:

private void showSimplePin(final LatLng pinPosition) {

// We show a simple Marker with title and snippet


final MarkerOptions markerOptions = new MarkerOptions()
.position(pinPosition)
.title(Im here!)
.draggable(true)
.snippet(Here well show the address after Geocoding);
final Marker simpleMarker = mGoogleMap.addMarker(markerOptions);
}


Queste permettono di visualizzare un pin standard con unetichetta del tipo Im
here e uno snippet che andremo ad alimentare successivamente con la nostra
informazione di indirizzo, come visualizzato nella Figura 3.15.
Lasciamo al lettore la verifica di come il pin visualizzato possa essere spostato
attraverso unoperazione di drag attivata da un long clic. Facciamo solo attenzione al
fatto che potremmo aggiungere un marker attraverso la selezione della
corrispondente action nella ActionBar, spostarlo e quindi premere nuovamente la action.
In questo modo il nostro codice aggiunger ogni volta un nuovo marker. Si tratta di un
aspetto da tenere in considerazione in casi analoghi.

Figura 3.15 Visualizzazione di un marker con titolo e snippet.


NOTA
Il lettore si sar accorto che per la verifica del funzionamento di queste e altre API abbiamo
creato dei metodi di utilit che potranno essere richiamati a seconda dei casi. Tutto il codice di
questi metodi contenuto nelle corrispondenti classi.

A volte utile abilitare o meno la visualizzazione di alcuni di questi marker. Per


farlo sufficiente utilizzare il seguente metodo:

public MarkerOptions visible(boolean visible)


Nellelenco delle caratteristiche di un marker abbiamo in precedenza elencato
anche lanchor point, il quale pu essere impostato attraverso il seguente metodo:

public MarkerOptions anchor(float u, float v)


Si tratta di un modo per decidere la posizione in cui collocare licona del pin
rispetto alla Location vera e propria che si vuole rappresentare. Per capire di cosa si
tratta, sufficiente visualizzare ci che si ottiene con i valori 0.0 e 0.0, che
impostiamo attraverso il seguente codice:


private void showPinZeroAnchorPoint(final LatLng pinPosition) {

// We show a simple Marker with title and snippet


final MarkerOptions markerOptions = new MarkerOptions()
.position(pinPosition)
.title(Im here!)
.draggable(true)
.anchor(0.0f, 0.0f)
.snippet(Here well show the address after Geocoding);
final Marker simpleMarker = mGoogleMap.addMarker(markerOptions);
}


Otterremo il risultato rappresentato nella Figura 3.16. Come possiamo notare
come se licona fosse rappresentata da un rettangolo di dimensioni W H; lanchor
point rappresenta la posizione in questo rettangolo in cui si trova il punto della
Location. In questo caso il punto che rappresenta la Location nella posizione (0,0)
ovvero in alto a sinistra.

Figura 3.16 Utilizzo di un anchor point pari a 0.0 f, 0.0 f.

Un valore relativo allanchor point (1.0, 1.0) porterebbe quindi alla visualizzazione
rappresentata nella Figura 3.17. La posizione dellanchor point viene infatti
specificata in modo relativo alla dimensione dellicona.
Da quanto descritto, si capisce come lanchor point di default sia quello in
posizione (0.5,1.0).

Nelle figure abbiamo visto come il pin abbia la classica forma Google, che in
qualche caso si ha la necessit di personalizzare attraverso il seguente metodo della
classe GoogleMap che notiamo avere un unico parametro di tipo BitmapDescriptor:

public MarkerOptions icon(BitmapDescriptor icon)


Un oggetto di questo tipo ci permette di specificare quali modifiche intendiamo
fare rispetto al pin standard; per farlo possiamo utilizzare alcuni metodi della classe
BitmapDescriptionFactory. Se volessimo, per esempio, modificare semplicemente il
colore del pin, dovremmo utilizzare un oggetto di tipo BitmapDescriptor ottenuto
attraverso la seguente istruzione:

BitmapDescriptor bluPin = BitmapDescriptorFactory

.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)

Figura 3.17 Utilizzo di un anchor point pari a 1.0 f, 1.0 f.

Come possiamo notare, il metodo defaultMarker() accetta solamente alcuni valori,


corrispondenti ad altrettante costanti relative a un sottoinsieme di tonalit (HUE). Per
trasformare il pin degli esempi precedenti in blu sar quindi sufficiente aggiungere
listruzione evidenziata di seguito:

private void showBluePin(final LatLng pinPosition) {

// We show a simple Marker with title and snippet


final MarkerOptions markerOptions = new MarkerOptions()

.position(pinPosition)
.title(Im here!)
.draggable(true)
.icon(BitmapDescriptorFactory
.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))
.snippet(Here well show the address after Geocoding);
final Marker simpleMarker = mGoogleMap.addMarker(markerOptions);
}


Il risultato rappresentato nella Figura 3.18.
Qualora volessimo invece modificare completamente licona, possiamo utilizzare
uno dei seguenti metodi che ci mette a disposizione la classe BitmapDescriptionFactory e
che possiamo impiegare analogamente a quanto fatto per il colore:

public static BitmapDescriptor fromAsset(String assetName)
public static BitmapDescriptor fromBitmap(Bitmap image)
public static BitmapDescriptor fromFile(String fileName)
public static BitmapDescriptor fromPath(String absolutePath)
public static BitmapDescriptor fromResource(int resourceId)


Il loro significato piuttosto intuitivo, in quanto permettono di specificare diverse
modalit di accesso alloggetto Bitmap che rappresenta licona.
Gli altri metodi messi a disposizione dalla classe MarkerOptions sono intuitivi, per cui
rimandiamo il lettore alla documentazione ufficiale o lo invitiamo a fare qualche
semplice test.

Figura 3.18 Personalizzazione del colore del pin.

Selezione di un Marker
Abbiamo visto come creare un Marker posizionandolo allinterno della mappa, per
poi personalizzarne la visualizzazione cambiando il colore o licona utilizzata.
Lultimo aspetto che vogliamo descrivere riguarda la Info Window, ovvero il pop-up
che viene visualizzato nel momento della selezione del pin. In precedenza una prima
personalizzazione consisteva nellimpostazione di un titolo e di uno snippet, ma ora
vogliamo fare qualcosa di pi. La prima considerazione riguarda il fatto che se si
hanno pi marker sulla stessa mappa, solamente uno di essi pu visualizzare la Info
Window. La visualizzazione delle informazioni di un pin nasconde leventuale pop-up
di quello selezionato in precedenza. interessante notare che questo pu avvenire
comunque anche da programma, attraverso la chiamata dei seguenti metodi della
classe Marker.

public void showInfoWindow()
public void hideInfoWindow()


Lo stato di visualizzazione pu essere interrogato attraverso il seguente metodo:

public boolean isInfoWindowShown()


La Info Window di default quella che abbiamo visto nella Figura 3.15, ma le
Google API ci mettono a disposizione gli strumenti per poterne creare di
personalizzate attraverso opportune implementazioni dellinterfaccia
GoogleMap.InfoWindowAdapter, la quale prevede la definizione delle seguenti due
operazioni:

public abstract View getInfoWindow(Marker marker)
public abstract View getInfoContents(Marker marker)


Limplementazione di questa interfaccia viene poi assegnata alloggetto GoogleMap
attraverso la chiamata del metodo:

public final void setInfoWindowAdapter(GoogleMap.InfoWindowAdapter adapter)


Questo significa che possibile assegnare a una GoogleMap una sola
implementazione dellinterfaccia responsabile della personalizzazione delle Info
Window. Notiamo, comunque, che entrambe le operazioni ricevono il marker
selezionato come parametro e possono contenere la logica che permette di produrre
risultati diversi a seconda del particolare elemento selezionato.
La modalit con cui vengono utilizzate queste due operazioni un po particolare.
Il metodo getInfoWindow() pu infatti restituire una View oppure null. Nel primo caso si
tratta della View che verr utilizzata come Info Window. Qualora il valore restituito fosse
, il framework utilizzer la View di default, che potr essere personalizzata

null

attraverso ci che viene restituito dalla seconda operazione, ovvero da


getInfoContents(). Un aspetto fondamentale in tutto questo riguarda il fatto che
leventuale View restituita dal metodo getInfoWindow() non dovr essere attiva, ovvero
non potr contenere alcun pulsante o altro elemento di interazione; dovr quindi
essere equivalente a unimmagine, che potr essere solo selezionata con un evento di
clic. Nel nostro caso vogliamo creare unimplementazione che permetta una
personalizzazione della Info Window tale da visualizzare lindirizzo ottenuto in
precedenza dal Geocoder. Si tratta di una funzionalit che merita molta attenzione, in
quanto il metodo getInfoWindow() viene richiamato nel Thread dellinterfaccia utente,
mentre la chiamata del Geocoder richiede una connessione a un server che pu fallire,
ma soprattutto pu richiedere troppo tempo e quindi causare una ANR (Application
Not Responding). Nella nostra implementazione, a scopo didattico, abbiamo deciso di
utilizzare la classe CountDownLatch. una classe che fa parte di Java standard e
precisamente delle Concurrent API. In pratica si tratta di un oggetto che permette di
bloccare un Thread fino a che un altro Thread non ha eseguito un certo numero di
operazioni. Si tratta sostanzialmente di un contatore che nel nostro caso viene posto a
1 e che altri Thread possono decrementare fino a fargli raggiungere il valore 0, che
permette lo sblocco del Thread in attesa. Nel nostro caso il Thread principale si metter
in attesa delluscita da un altro Thread, responsabile del Geocoding, per creare la View da
restituire alla mappa. Per impedire unANR utilizzeremo un particolare overload del
metodo di attesa, che prevede un timeout che abbiamo impostato in 1 secondo per
non rendere linterfaccia troppo poco reattiva.
NOTA
Sappiamo che il tempo necessario alla generazione di unANR di 5 secondi per linterfaccia
utente, ma nel nostro caso non vogliamo rendere il tutto troppo lento. Ricordiamo che si tratta di
una soluzione che pu essere ulteriormente ottimizzata.

Questo accorgimento necessario, in quanto una volta creata la View per la Info
, questa non potr pi essere modificata, per cui non possibile inserire una

Window

sorta di indicatore di caricamento e quindi un risultato. Nel nostro caso abbiamo


creato la classe GeocoderInfoWindowAdapter, la cui logica interessante contenuta nel
nostro metodo createInfoView(). Questo ci permetter di richiamarlo sia dal metodo
sia dal metodo getInfoContents() per osservarne la differenza.

getInfoWindow()


private View createInfoView(final Marker marker) {

// We get the info from the Marker


final double latitude = marker.getPosition().latitude;
final double longitude = marker.getPosition().longitude;
final String locationString = mContext
.getString(R.string.info_window_location,
Location.convert(latitude, Location.FORMAT_DEGREES),
Location.convert(longitude, Location.FORMAT_DEGREES));
// We set the address
mHolder.mLocationTextView.setText(locationString);
// We manage Geocoder
final CountDownLatch countDownLatch = new CountDownLatch(1);
// We launch the Tread to get Geocoding
GeoCodingThread geoCodingThread =
new GeoCodingThread(countDownLatch, marker.getPosition());
new Thread(geoCodingThread).start();
try {
countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (mAddress != null) {

// The Country info


final String countryInfo =
mContext.getString(R.string.info_window_country,
mAddress.getCountryCode(), mAddress.getCountryName());
mHolder.mCountryTextView.setText(countryInfo);
// The address info
final StringBuilder addressBuilder = new StringBuilder();
for (int i = 0; i < mAddress.getMaxAddressLineIndex(); i++) {
addressBuilder.append(mAddress.getAddressLine(i)).append( );
}
mHolder.mAddressTextView.setText(addressBuilder.toString());
// This fields are visible
mHolder.mCountryTextView.setVisibility(View.VISIBLE);
mHolder.mAddressTextView.setVisibility(View.VISIBLE);
} else {
// The information is not present
mHolder.mCountryTextView.setVisibility(View.GONE);
mHolder.mAddressTextView.setVisibility(View.GONE);
}
return mInfoWindowView;
}


La prima parte del metodo molto semplice e consiste nella visualizzazione delle
informazioni di Location che si possono ottenere dalloggetto Marker selezionato,
passato come parametro.

final double latitude = marker.getPosition().latitude;
final double longitude = marker.getPosition().longitude;
final String locationString = mContext

.getString(R.string.info_window_location,

Location.convert(latitude, Location.FORMAT_DEGREES),
Location.convert(longitude, Location.FORMAT_DEGREES));
// We set the address
mHolder.mLocationTextView.setText(locationString);


In questa fase possiamo notare come sia stato utilizzato lHolder Pattern
caratteristico delle ListView, che, analogamente a questa classe, contengono una logica
di riciclo. Infatti questa stessa istanza di InfoWindowAdapter verr, come gi detto,
utilizzata per la visualizzazione delle InfoWindow di ogni Marker.
Il passo successivo quello cui abbiamo accennato in precedenza. Sostanzialmente
creiamo un CountDownLatch inizializzato a 1 e attendiamo un tempo massimo che
abbiamo impostato attraverso la costante TIMEOUT. Attraverso il suo metodo await()
attendiamo che si verifichino due situazioni. Se tutto va per il meglio, il Thread
responsabile del Geocoding otterr il risultato voluto e lo notificher portando a 0 il
CountDownLatch. In questo caso il metodo await() si sblocca e il risultato sar disponibile
nella variabile distanza mAddress. Nel caso in cui questo non dovesse avvenire, il
metodo await() si sbloccher in ogni caso, ma mAddress non avr il risultato voluto, per
cui non provvederemo alla visualizzazione delle informazioni in esso contenute. Il
codice molto semplice:

mAddress = null;
GeoCodingRunnable geoCodingRunnable =

new GeoCodingRunnable(countDownLatch, marker.getPosition());


new Thread(geoCodingRunnable).start();
try {

countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {

e.printStackTrace();
}
if (mAddress != null) {

// We show Address info


} else {

// We hide Address Info


}

Il Thread che abbiamo descritto attraverso la classe interna GeoCodingRunnable


responsabile del Geocoding ed anchesso molto semplice. Ne riportiamo solamente il
metodo run(), ovvero:

@Override
public void run() {

// We get the Addresses from the Location


List<Address> geoAddresses = null;
try {
geoAddresses = mGeocoder
.getFromLocation(mLocation.latitude,
mLocation.longitude, MAX_RESULT);
if (geoAddresses != null && geoAddresses.size() > 0) {
mAddress = geoAddresses.get(0);
}
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG_LOG, Error getting Addressed from Location:
+ mLocation);
} finally {
mCountDownLatch.countDown();
}
}


Da notare come, in caso sia di successo, sia di fallimento del Geocoding, dovremo
portare il CountDownLatch a 0 per lo sblocco del metodo await() in attesa.
Per poter abilitare questa funzionalit abbiamo semplicemente modificato il
seguente codice nel metodo onCreateView() del nostro Fragment MyLocationFragment:

mMapFragment = SupportMapFragment.newInstance();
mMapFragment.getMapAsync(new OnMapReadyCallback() {

@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
// Enable my location
mGoogleMap.setMyLocationEnabled(true);
// We update the type of the map
updateMapType();
// We create the specific InfoWindowAdapter
mInfoWindowAdapter = new GeocoderInfoWindowAdapter(getActivity());
mGoogleMap.setInfoWindowAdapter(mInfoWindowAdapter);
} });

Non ci resta che verificarne il funzionamento. La prima prova che facciamo
prevede che la View descritta venga restituita dal metodo getInfoWindow() e quindi venga
considerata come una completa personalizzazione della Info Window. In questo caso la
nostra implementazione delle principali operazioni sar la seguente:

@Override
public View getInfoWindow(Marker marker) {

return createInfoView(marker);
}
@Override
public View getInfoContents(Marker marker) {

return null;
}


Il risultato ottenuto sar quello rappresentato nella Figura 3.19, dove notiamo
essere presente anche linformazione ottenuta dal Geocoder.

Figura 3.19 Personalizzazione completa della InfoWindow.

Qualora avessimo implementato i precedenti metodi nel seguente modo:



@Override
public View getInfoWindow(Marker marker) {

return null;
}
@Override
public View getInfoContents(Marker marker) {

return createInfoView(marker);
}


il risultato sarebbe stato quello rappresentato nella Figura 3.20, dove notiamo come
il contenitore delle Info sia quello di default.

Figura 3.20 Personalizzazione del solo contenuto della InfoWindow.

Per completare la discussione relativa alla InfoWindow vediamo come gestire gli
eventuali eventi a essa associati. Per intercettare la selezione di un Marker sar
sufficiente creare unimplementazione dellinterfaccia GoogleMap.OnMarkerClickListener,
che prevede la definizione della sola operazione:

public abstract boolean onMarkerClick(Marker marker)


e quindi registrarla nelloggetto GoogleMap attraverso il seguente metodo:

public final void setOnMarkerClickListener(GoogleMap.OnMarkerClickListener listener)


Si tratta della classica implementazione dellObserver Pattern. Un aspetto
importante da sottolineare invece legato al valore restituito dai metodi di questo
tipo, che un valore di tipo boolean che indica, come avviene anche in altri contesti, se
levento completamente consumato o se deve essere inviato ad altri handler. Se il
valore restituito true, levento stato completamente consumato e quindi non verr
propagato ad altri ascoltatori. Nel caso del metodo onMarkerClick(), per esempio, il
valore true non farebbe apparire la Info Window. Altri eventi legati a un Marker sono
descritti dallinterfaccia OnMarkerDragListener, la quale definisce le seguenti operazioni:

public abstract void onMarkerDrag(Marker marker)
public abstract void onMarkerDragEnd(Marker marker)
public abstract void onMarkerDragStart(Marker marker)


le cui implementazioni possono essere registrate al GoogleMap attraverso il metodo:

public final void setOnMarkerDragListener(GoogleMap

.OnMarkerDragListener listener)

Qualora volessimo gestire gli eventi associati alla selezione della Info Window,
linterfaccia da implementare GoogleMap.OnInfoWindowClickListener, che prevede la
definizione delloperazione

public abstract void onInfoWindowClick(Marker marker)


In questo caso il metodo da utilizzare per informare la GoogleMap di questa
implementazione il seguente:

public final void setOnInfoWindowClickListener(GoogleMap

.OnInfoWindowClickListener listener)

Oltre a questi, ci sono altre interfacce per gestire diversi tipi di eventi, per i quali
rimandiamo alla documentazione ufficiale.

Visualizzazione di un percorso
Nel paragrafo precedente abbiamo visto come aggiungere una mappa e quali siano
gli strumenti che le Google Maps API ci mettono a disposizione per la loro
personalizzazione. Nella funzionalit che abbiamo chiamato Where I Go abbiamo
imparato come tracciare e memorizzare una serie di informazioni relative alla Location
insieme a quelle relative al tipo di attivit. In questo paragrafo vogliamo invece fare
in modo di visualizzare in una mappa le informazioni relative a quella che abbiamo
chiamato FenceSession. Si tratta di una funzionalit che aggiungiamo a quelle che
possibile fare su una FenceSession e descritte da altrettante opzioni nel menu
contestuale definito nella classe FenceSessionListFragment. Diversamente da quanto ci si
potrebbe aspettare, quella della visualizzazione del path una funzionalit molto
semplice da realizzare, anche alla luce di tutto il lavoro di preparazione fatto in
precedenza. Lo strumento che abbiamo a disposizione un oggetto di tipo Polyline, il
quale rappresenta una successione di linee sulla mappa, di cui possiamo specificare
alcune caratteristiche, tra cui il colore e lo spessore. Per visualizzare il path sar
quindi sufficiente leggere il nostro ContentProvider, creare un oggetto Polyline con le
informazioni relative alla location e aggiungerlo alla mappa attraverso il seguente
metodo della classe GoogleMap.

public final Polyline addPolyline(PolylineOptions options)


Il meccanismo che permette di creare una Polyline simile a quanto visto in
precedenza per gli oggetti di tipo LatLngBounds ovvero prevede limpiego di una classe
che funge da Builder, che in questo caso si chiama PolylineOptions. Abbiamo descritto
questo procedimento nella classe ShowSessionInMapFragment, la quale utilizza un Loader per
gestire anche il caso in cui le informazioni dovessero cambiare dinamicamente,
ovvero qualora fosse attivo un processo di tracking. La parte interessante relativa al
seguente codice:

@Override
public void onLoadFinished(android.support.v4.content.Loader<Cursor>

cursorLoader, Cursor cursor) {


// We read the data from the DB and create the Polygon
final FenceCursorFactory.FencePositionCursorData positionCursorData =

CursorResolver.CURSOR_RESOLVER.extractPositionCursor(cursor);
LatLngBounds.Builder bounds = new LatLngBounds.Builder();
final PolylineOptions rectOptions = new PolylineOptions();
while (cursor.moveToNext()) {
final LatLng position = new LatLng(positionCursorData.getLatitude(),
positionCursorData.getLongitude());
rectOptions.add(position);
bounds.include(position);
}
mLastPolyline = getMap().addPolyline(rectOptions);
final CameraUpdate cameraUpdate =
CameraUpdateFactory.newLatLngBounds(bounds.build(),
DEFAULT_PADDING);
getMap().moveCamera(cameraUpdate);
}


Questo codice corrisponde al metodo richiamato dal LoaderManager a seguito di una
variazione delle informazioni nel ContentProvider. Notiamo come vengano gestiti sia i
punti delloggetto di tipo Polyline, sia quelli allinterno di un LatLngBounds, per fare in
modo che venga scelto un livello di zoom tale da visualizzare tutto il percorso. Nel
nostro caso il risultato del precedente codice su un percorso quello rappresentato
nella Figura 3.21. Possiamo notare come il percorso venga rappresentato da una linea
nera piuttosto sottile. Il passo successivo che vogliamo implementare consiste
nellutilizzare una linea di colore corrispondente allattivit rilevata, uninformazione
a nostra disposizione nel ContentProvider. Approfittiamo degli strumenti a disposizione
anche per aumentare lo spessore della linea. Per farlo esistono i seguenti due metodi,
che possono essere utilizzati in chaining sulloggetto PolylineOptions:

public PolylineOptions color(int color)
public PolylineOptions width(float width)

Nel nostro caso la personalizzazione del colore del percorso non banale, in
quanto le propriet di colore e spessore del tratto, come altre che vedremo, sono di
tutto un path e non di ogni singolo segmento.

Figura 3.21 Visualizzazione di un percorso attraverso un Polyline.

Per questo motivo si rende necessario creare oggetti Polyline distinti, che sulla
mappa appariranno invece come uno solo. Abbiamo quindi modificato il precedente
codice nel seguente modo:

@Override
public void onLoadFinished(android.support.v4.content.Loader<Cursor>

cursorLoader, Cursor cursor) {


// We read the data from the DB and create the Polygon
final FenceCursorFactory.FencePositionCursorData positionCursorData =
CursorResolver.CURSOR_RESOLVER.extractPositionCursor(cursor);

LatLngBounds.Builder bounds = new LatLngBounds.Builder();


PolylineOptions currentPolyline = null;
int lastActivityType = -1;
LatLng position = null;
LatLng previous = null;
while (cursor.moveToNext()) {
final int currentActivityType = positionCursorData.getActivityType();
if (lastActivityType != currentActivityType) {
// In this case the activity type is different so we add
// the previous Polyline and create a new one
if (currentPolyline != null) {
getMap().addPolyline(currentPolyline);
}
lastActivityType = currentActivityType;
currentPolyline = new PolylineOptions()
.color(getActivityColor(currentActivityType))
.width(PATH_WIDTH);
}
if (previous != null) {
currentPolyline.add(previous);
}
position = new LatLng(positionCursorData.getLatitude(),
positionCursorData.getLongitude());
previous = position;
currentPolyline.add(position);
bounds.include(position);
}
// We add the last

getMap().addPolyline(currentPolyline);
final CameraUpdate cameraUpdate =
CameraUpdateFactory.newLatLngBounds(bounds.build(),
DEFAULT_PADDING);
getMap().moveCamera(cameraUpdate);
}


un nostro metodo di utilit che ci permette di associare dei

getActivityColor()

colori ai diversi tipi di attivit. La creazione dei Polyline leggermente pi complessa


e il codice evidenziato indica i metodi per la modifica del colore e dello spessore
delle linee. Il risultato rappresentato nella Figura 3.22.

Figura 3.22 Utilizzo di un colore personalizzato per il path.

Nella nostra soluzione abbiamo deciso di creare il path a ogni aggiornamento. Una
soluzione pi efficiente potrebbe invece prevedere lutilizzo del seguente metodo
della classe Polyline per modificare i punti di un oggetto gi inserito in mappa.

public void setPoints(List<LatLng> points)


Oltre agli oggetti di tipo Polyline, le Google Maps ci permettono di gestire altri due
tipi di oggetti: quelli descritti dalle classi Polygon e Circle. Analogamente a quanto
visto in precedenza facile gestire questi oggetti attraverso le classi PolygonOptions e
. I Polygon si differenziano dai Polyline per il fatto che rappresentano regioni

CircleOptions

chiuse, mentre un Circle rappresenta un cerchio. Anche se non strettamente legato al


nostro caso duso, un rettangolo pu essere creato attraverso le seguenti righe di
codice:

private void showRectangle(final LatLng center) {

final float WIDTH = 0.1f;


final float HEIGHT = 0.1f;
final LatLng p1 = new LatLng(center.latitude WIDTH / 2,
center.longitude + HEIGHT / 2);
final LatLng p2 = new LatLng(center.latitude + WIDTH / 2,
center.longitude + HEIGHT / 2);
final LatLng p3 = new LatLng(center.latitude + WIDTH / 2,
center.longitude HEIGHT / 2);
final LatLng p4 = new LatLng(center.latitude WIDTH / 2,
center.longitude HEIGHT / 2);
PolygonOptions polygonOptions = new PolygonOptions()
.add(p1).add(p2).add(p3).add(p4);
getMap().addPolygon(polygonOptions);
}


Applicate alla citt di Roma queste righe produrrebbero il rettangolo rappresentato
nella Figura 3.23.

Figura 3.23 Creazione di un Polygon.

Se invece volessimo creare un cerchio, sarebbero sufficienti le seguenti righe di


codice, dove il raggio viene specificato in metri:

private void showCircle(final LatLng center, final double radius) {

CircleOptions circleOptions = new CircleOptions()


.center(center)
.radius(radius);
getMap().addCircle(circleOptions);
}


Usando lo stesso punto dellesempio precedente e un raggio di 10 Km, il risultato
sarebbe quello rappresentato nella Figura 3.24.

Figura 3.24 Esempio di utilizzo di Circle.

Anche in questo caso esistono diversi metodi per la personalizzazione del colore o
del tipo di tratto, per i quali rimandiamo alla documentazione ufficiale.

I Geofence
Dopo aver visto come disegnare su una mappa, ci occupiamo di una nuova
funzionalit che ci permetter di ricevere notifiche qualora entrassimo o uscissimo da
una particolare zona. Le possibili applicazioni di questa funzionalit sono moltissime:
una sveglia che si attiva nel momento in cui arriviamo in una particolare zona, oppure
un alert che ci notifica che siamo nelle vicinanze di una panetteria e ci ricorda cosa
comprare. Oppure semplicemente sapere quando siamo vicino a casa o al lavoro. Si
tratta di una funzionalit che ci viene offerta dai Google Play services e che prende il
nome di GeoFence. Si tratta di uno strumento che ci viene offerto dalle API per la
gestione della Location, che abbiamo deciso di trattare in questo capitolo, in quanto
vogliamo visualizzare queste zone in una mappa, per poi poterle creare, modificare o
cancellare. Si tratta di una funzionalit che implementiamo attraverso unopportuna
voce di menu, per la quale abbiamo creato il Fragment descritto dalla classe
.

GeoFenceFragment

NOTA
Laggiunta di una nuova funzione presuppone la modifica della corrispondente risorsa di tipo
array e della corrispondente aggiunta allinterno della classe FragmentFactory.

Prima di iniziare a sviluppare questa funzionalit bene notare come si renda


necessario utilizzare questo permesso per laccesso a informazioni accurate di
Location:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />


La parte di inizializzazione analoga a quella eseguita per la funzionalit Where I
, la quale prevede sia linizializzazione delloggetto di tipo GoogleApiClient sia la

Am

mappa attraverso un oggetto di tipo GoogleMap. Linterfaccia che abbiamo descritto nel
documento di layout fragment_geofence.xml molto semplice e prevede nella parte
superiore lelenco dei Geofence creati e in quella inferiore la visualizzazione di una
mappa. Allinizio la mappa sar centrata nella nostra posizione, ma successivamente
vorremmo selezionare un Geofence dalla lista, per poterlo evidenziare nella mappa. Ma
che cos, esattamente un Geofence? Sappiamo che si tratta di una zona nella mappa,
cui possibile associare degli eventi, ma non abbiamo specificato quale forma abbia
e quali siano le altre propriet. Diciamo quindi che un Geofence caratterizzato da:

una Location;
un raggio;
una durata;
il tipo di transizione che si intende intercettare;
un identificatore.
Dalla prima voce in elenco capiamo come un Geofence possa avere solamente forma
circolare, caratterizzata da una Location (espressa in latitudine e longitudine) e da un
raggio (espresso in metri). Una volta creato, un Geofence pu essere caratterizzato da
una durata, oltre la quale viene automaticamente cancellato dallelenco di quelli da
gestire. La transizione ci permette di decidere che tipo di evento intercettare, ovvero
se generare un alert quando si entra nella zona corrispondente, quando si esce o per
entrambi i casi. Un aspetto molto importante di un Geofence rappresentato dal suo id,
che assume particolare importanza se pensiamo al fatto che la gestione della
persistenza dei vari Geofence di completa responsabilit dello sviluppatore. Le API
non ci permettono, al momento, di chiedere quali siano i Geofence monitorati, e quindi
in qualche modo dovremo tenerne traccia. Una possibile soluzione potrebbe essere
quella di utilizzare le SharedPreferences, ma ormai abbiamo imparato a usare i
, e cos faremo anche in questo caso. Sempre con il nostro approccio

ContentProvider

bottom-up abbiamo quindi aggiunto una nuova entit, FenceDB.Geofence, che possiamo
rappresentare attraverso il diagramma rappresentato nella Figura 3.25:
Per la gestione della nuova tabella abbiamo creato due nuovi file come risorse di
tipo raw e precisamente i file create_geofence_table e drop_geofence_table, che abbiamo
utilizzato nella classe FenceDbHelper in corrispondenza dei metodi di creazione e
aggiornamento dello schema del DB. Baster quindi aggiungere le seguenti righe nel
metodo onCreate()

final String createGeofenceSql = ResourceUtils.getRawAsString(mContext,

R.raw.create_geofence_table);
db.execSQL(createGeofenceSql);

Figura 3.25 Creazione dellentit di tipo Geofence.

e quindi le seguenti altre nel metodo onUpgrade():



final String dropGeofenceSql = ResourceUtils.getRawAsString(mContext,

R.raw.drop_geofence_table);
db.execSQL(dropGeofenceSql);


Il passo successivo consiste nella definizione di una classe interna FenceDB.Geofence,
con i nomi di tutte le colonne definite in precedenza, analogamente a quanto fatto nel
caso delle altre entit.
NOTA
Aumentando il valore della costante FenceDB.DB_VERSION forziamo la creazione di un nuovo DB,
evitando quindi la disinstallazione e reinstallazione dellapplicazione.

La parte forse pi impegnativa consiste nella personalizzazione del nostro cursore,


che in questo caso possiamo sfruttare per estrarre direttamente un GeofenceData senza il
bisogno di estrapolare tutti i campi uno alla volta in fase di visualizzazione. Si tratta
di una classe che abbiamo creato e che ci permette di incapsulare le informazioni
relative a un Geofence. Da un oggetto di tipo Geofence non riusciamo, infatti, a ottenere
le informazioni che lo caratterizzano, se non lid, in quanto non esistono i
corrispondenti metodi di query. Lo stesso dicasi per limplementazione di
CursorResolver.
NOTA

Si tratta di un passo complicato, per il semplice fatto che abbiamo deciso di gestire anche
Honeycomb. Se invece decidessimo di gestire solo le versioni successive alla 4.0, la
personalizzazione del Cursor si semplificherebbe drasticamente. Il CursorResolver verrebbe infatti
completamente eliminato, in quanto inutile.

A questo punto il nostro ContentProvider pronto a ospitare anche le informazioni


relative ai Geofence di cui, ricordiamo, dobbiamo gestire anche la persistenza.
Notiamo, comunque, come la creazione di un oggetto di tipo GeofenceData a partire
dalle informazioni che abbiamo descritto in precedenza e che memorizzeremo nel
ContentProvider, sia molto semplice attraverso lennesima implementazione del Builder
:

Pattern


public GeofenceData getGeofence() {

// We create the GeofenceData


return GeofenceData.create(mCursor.getString(mIdIndex),
mCursor.getFloat(mLatitudeIndex),
mCursor.getFloat(mLongitudeIndex))
.withExpirationDuration(mCursor.getLong(mDurationIndex))
.withTransitionType(mCursor.getInt(mTransitionTypeIndex));
}


Il tutto viene quindi incapsulato nel Cursor, che abbiamo astratto attraverso
linterfaccia GeofenceCursorData.
NOTA
Notiamo come non sono state impostate tutte le possibili informazioni di un Geofence, le quali
dipendono anche dal tipo di transizione da monitorare.

bene precisare da subito che la creazione di unistanza della classe GeofenceData


non corrisponde allavvio del servizio che ne permette il monitoraggio (lo vedremo
tra poco). Insieme a tutte queste definizioni dovremo gestire le operazioni di CRUD
allinterno del nostro ContentProvider, nel modo ormai noto.

Creazione di un Geofence
La prima operazione di cui ci vogliamo occupare consiste nella creazione di un
Geofence attraverso la selezione della relativa posizione con un evento di LongClick sulla

mappa. Come abbiamo visto in precedenza, la mappa ci permette di gestire diversi


eventi, tra cui, appunto, quello associato allinterfaccia GoogleMap.OnMapLongClickListener,
che prevede la definizione della seguente operazione, che ci permette di ottenere il
riferimento al punto selezionato nella mappa attraverso un oggetto di tipo LatLng.

public abstract void onMapLongClick(LatLng point)


Nel nostro caso abbiamo semplicemente aggiunto le seguenti righe di codice in
corrispondenza del metodo che ci permette di sapere che la mappa disponibile e che
abbiamo inserito nel metodo onCreateView() del nostro Fragment descritto nella classe
:

GeofenceFragment


mMapFragment = SupportMapFragment.newInstance();
mMapFragment.getMapAsync(new OnMapReadyCallback() {

@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
mGoogleMap.setMyLocationEnabled(true);
// We update the type of the map
updateMapType();
// Show the current location
showLocationInMap(mCurrentLocation);
// We register the listener for the long click
mGoogleMap.setOnMapLongClickListener(new
GoogleMap.OnMapLongClickListener() {
@Override
public void onMapLongClick(LatLng latLng) {
startNewGeofence(latLng);
}
});

}
});

Nel codice non facciamo altro che richiamare il metodo di utilit startNewGeofence(),
che permette di inviare una richiesta di creazione del Geofence attraverso il metodo

public abstract PendingResult<Status> addGeofences (GoogleApiClient client, GeofencingRequest
geofencingRequest, PendingIntent pendingIntent)


delloggetto di tipo GeofencingApi, cui accediamo in modo simile a quanto fatto per i
servizi di localizzazione, ovvero attraverso la classe LocationService.

private void startNewGeofence(final LatLng position) {

// We create the GeofenceData


final String geofenceId = Geofence - + System.currentTimeMillis();
final GeofenceData geoFenceData = GeofenceData.create(geofenceId,
position.latitude, position.longitude);
// We create a GeofenceRequest
final GeofencingRequest geofencingRequest = new
GeofencingRequest.Builder().addGeofence(geoFenceData.buildGeofence()).build();
final Intent intent = new Intent(getActivity(), GeofenceService.class);
final PendingIntent geoPendingIntent = PendingIntent.getService(getActivity(), 1,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
final PendingResult<Status> result =
LocationServices.GeofencingApi.addGeofences(mGoogleApiClient,
geofencingRequest, geoPendingIntent);
result.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
// We check the status to know if the geofence was
// successfully added or not
if (status.isSuccess()) {
// In this case we save the data into the DB
FenceDB.Geofence.save(getActivity(), geoFenceData);

} else {
// In this case it failed so we do nothing
Toast.makeText(getActivity(), R.string.geofence_error,
Toast.LENGTH_SHORT).show();
}
}
});
}


Nel codice abbiamo evidenziato lutilizzo della nuova classe GeofencingRequest che,
attraverso il corrispondente Builder, ci permette di aggiungere pi Geofence in
chaining.
Innanzitutto determiniamo un valore per quello che sar lidentificatore del Geofence
che intendiamo creare. Nel nostro caso abbiamo utilizzato semplicemente
uninformazione legata al timestamp, ma si tratta di uninformazione che si potrebbe
far editare allutente attraverso unopportuna interfaccia. A questo punto creiamo
unistanza della nostra classe GeofenceData, che ricordiamo essere una classe di utilit
che ci permette di memorizzare le informazioni di un Geofence e di accedervi
successivamente. Il lettore potr vedere di come si tratti di una classe molto semplice,
che ci permette, tra le altre cose, di utilizzare alcuni valori di default per il raggio, la
durata e cos via. Nel nostro caso inseriamo solamente le informazioni obbligatorie,
tra cui la posizione selezionata in mappa. Analogamente a quanto fatto per la
gestione della posizione, notiamo come le istruzioni successive consistano nella
creazione di un PendingIntent, che permetter al sistema di attivare il servizio che nel
nostro caso sar descritto dalla classe GeofenceService. Nel codice evidenziato, ora
questo dovrebbe risultare abbastanza chiaro. Degno di nota, invece, il valore
restituito dal metodo addGeofences(), il quale non crea il Geofence immediatamente, ma
necessita di alcune operazioni che avvengono in background. Potrebbe anche
accadere che, per vari motivi, il Geofence non possa essere creato; esistono infatti dei
limiti al numero di Geofence che si possono creare. Tutte le operazioni dei Google Play
services di questo tipo restituiscono un oggetto di tipo PendingResult<T> che notiamo
essere generico. Nel nostro caso si tratta del tipo PendingResult<Status> e quindi ci
permetter di accedere a un oggetto di tipo Status. Il modo pi semplice per fare
questo attraverso limplementazione dellinterfaccia ResultCallback, come evidenziato

nel nostro codice. Nellimplementazione non faremo altro che verificare se


loperazione ha avuto successo o meno, e poi salvare le informazioni del Geofence nel
nostro ContentProvider. Qualora qualcosa andasse male visualizzeremo un messaggio,
ma non inseriremo il dato nel nostro database.
Abbiamo detto che nella parte superiore del display vogliamo visualizzare un
elenco dei Geofence che creiamo. Per fare questo abbiamo utilizzato il classico Loader
che si accorge delle modifiche del DB e si preoccupa di aggiornare, in questo caso,
una ListView con alcune informazioni sui Geofence stessi. In questa il lettore potr notare
come sia stato utilizzato un ViewBinder per intercettare le operazioni di aggiornamento
della lista e quindi aggiungere o eliminare anche dei Circle alla mappa.

mCursorAdapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {

@Override
public boolean setViewValue(View view, Cursor cursor, int i) {
final FenceCursorFactory.GeofenceCursorData mCursor =
CursorResolver.CURSOR_RESOLVER.extractGeofenceCursor(cursor);
final GeofenceData geoFenceData = mCursor.getGeofence();
if (view.getId() == R.id.geofence_id) {
// We add the item to the Map
final LatLng position =
new LatLng(geoFenceData.getLatitude(),
geoFenceData.getLongitude());
final Circle circle =
drawCircle(position, geoFenceData.getRadius());
mCircles.add(circle);
}
return false;
}
});

Per tenere traccia dei Circle aggiunti, per cancellarli in corrispondenza di ogni
aggiornamento, ci siamo poi aiutati con una variabile di tipo Set<Circle>. Per quanto
riguarda la cancellazione abbiamo seguito un meccanismo analogo, agganciato
questa volta a un evento di LongClick sullelemento corrispondente della lista. La
logica di cancellazione, questa volta contenuta nel seguente metodo:

private void requestForDelete(final long itemId) {

// We get the data related to the given Geofence


final Uri geofenceUri =
Uri.withAppendedPath(FenceDB.Geofence.CONTENT_URI,
String.valueOf(itemId));
final Cursor cursor = getActivity().getContentResolver()
.query(geofenceUri, null, null, null, null);
final FenceCursorFactory.GeofenceCursorData geoFenceCursor =
CursorResolver.CURSOR_RESOLVER.extractGeofenceCursor(cursor);
if (geoFenceCursor.moveToNext()) {
final GeofenceData geofenceData = geoFenceCursor.getGeofence();
final String idToDelete = geofenceData.getRequestId();
final ArrayList<String> idsToDelete = new ArrayList<String>(1);
idsToDelete.add(idToDelete);
final PendingResult<Status> result =
LocationServices.GeofencingApi
.removeGeofences(mGoogleApiClient, idsToDelete);
result.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
// In this case we delete the data into the DB
getActivity().getContentResolver()

.delete(geofenceUri, null, null);


} else {
// In this case it failed so we do nothing
}
}
});
}
cursor.close();
}


Notiamo come il meccanismo sia analogo e che il metodo da richiamare questa
volta sia il seguente:

public abstract PendingResult<Status> removeGeofences(GoogleApiClient client,

List<String> geofenceRequestIds)

Infine abbiamo gestito levento di clic sugli elementi della ListView, in modo da
centrare la mappa sul Geofence corrispondente. A tale scopo abbiamo utilizzato quanto
visto in precedenza, in relazione al posizionamento della mappa.
La gestione di unapplicazione di questo tipo richiede un numero maggiore di
accorgimenti, legati al fatto, per esempio, che il nostro ContentProvider non tiene conto
della durata del Geofence e quindi non provvede alla sua cancellazione automatica
dopo un certo tempo. Lapplicazione potrebbe inoltre permettere lediting dei Geofence,
ma si tratta di una funzionalit che pu essere svolta dal lettore come esercizio.
Abbiamo descritto come creare e cancellare dei Geofence servendoci di una mappa
per la loro visualizzazione, come possiamo vedere nella Figura 3.26.

Figura 3.26 Creazione e cancellazione dei Geofence.

Come accennato in precedenza, i Geofence permettono di definire delle aree, ma


soprattutto degli eventi a esse associati. Possiamo infatti ricevere delle notifiche nel
caso in cui lutente entri o esca da determinate zone. Interessante anche la
possibilit di ricevere una notifica nel caso in cui un utente entrasse in una zona e non
ne uscisse in un tempo massimo. Questo possibile impostando come tipo di
transizione quella associata alla costante

Geofence.GEOFENCE_TRANSITION_DWELL


e quindi associando il tempo massimo di permanenza attraverso il metodo della
classe Geofence.Builder utilizzata appunto per la sua creazione:

public Geofence.Builder setLoiteringDelay(int loiteringDelayMs)

Gestione degli eventi di Geofence


Nel paragrafo precedente abbiamo visto come creare dei Geofence, ma non abbiamo
descritto come intercettare gli eventi associati alle corrispondenti transizioni.
Abbiamo per capito che il tutto viene intercettato da un particolare servizio che
abbiamo definito in fase di creazione del PendingIntent e che abbiamo descritto
attraverso la classe GeofenceService:

public class GeofenceService extends IntentService {

/**
* The Tag for the Log
*/
private static final String TAG_LOG = GeofenceService.class.getName();
/**
* The Default Constructor
*/
public GeofenceService() {
super(GeofenceService);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
// We get the GeofencingEvent from the received intent
final GeofencingEvent geofencingEvent =
GeofencingEvent.fromIntent(intent);
// We check if we have errors
if (!geofencingEvent.hasError()) {
// If everything is ok we can intercept

// which is the triggered event


FenceNotificationHelper.get(this).showGeofenceNotification(this, intent);
} else {
// In this case we got an error
// so the only thing we can do is to Log it
int errorCode = geofencingEvent.getErrorCode();
Log.e(TAG_LOG, Error receving Geofence notification. Error code: +
errorCode);
}
}
}
}


Come possiamo notare si tratta di una specializzazione della classe IntentService di
cui abbiamo descritto in precedenza le caratteristiche. Nel codice evidenziato
abbiamo messo in risalto lutilizzo della classe GeofencingEvent che stata aggiunta
proprio nella versione 6.5 dei Google Play services in sostituzione della ormai
deprecata classe LocationClient. Si tratta di una classe che dispone del metodo statico di
factory

public static GeofencingEvent fromIntent(Intent intent)


che ci permette di estrarre tutte le informazioni dallIntent ricevuto, in questo caso,
dal sistema di Geofence che ha attivato il servizio stesso. Attraverso loggetto di tipo
GeofencingEvent possiamo infatti verificare la presenza di un errore ed il corrispondente
tipo attraverso i due metodi:

public boolean hasError()
public int getErrorCode()


Nel caso in cui tutto fosse andato per il verso giusto ora possibile lanciare una
notifica invocando un nuovo metodo di utilit che abbiamo aggiunto alla classe

e precisamente il seguente:

FenceNotificationHelper


public void showGeofenceNotification(final Context context, final GeofencingEvent
geofencingEvent) {

final int transitionType = geofencingEvent.getGeofenceTransition();


// We get the location that triggered the event
final Location triggeredLocation = geofencingEvent.getTriggeringLocation();
// We get the Geofence that generates the event
final List<Geofence> triggeredGeofences =
geofencingEvent.getTriggeringGeofences();
final String geoFenceId;
if (triggeredGeofences != null && triggeredGeofences.size() > 0) {
geoFenceId = triggeredGeofences.get(0).getRequestId();
} else {
geoFenceId = mContext.getString(R.string.geofence_unknown_geofence);
}
// We build the title
final String title;
if (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT) {
title = mContext.getString(R.string.geofence_exit_event, geoFenceId);
} else if (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER) {
title = mContext.getString(R.string.geofence_enter_event, geoFenceId);
} else {
title = mContext.getString(R.string.geofence_unsupported_event);
}
final String message = mContext.getString(R.string.info_window_location,
triggeredLocation.getLatitude(), triggeredLocation.getLongitude());
// We build the notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext)

.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.ic_launcher)
.setAutoCancel(true);
mNotificationManager.notify(DISTANCE_NOTIFICATION_ID, builder.build());
}


Come prima cosa notiamo come sia stato passato come parametro lo stesso oggetto
di tipo GeofencingEvent che abbiamo creato nel servizio; questo ci permette di non dover
leggere due volte le stesse informazioni dallIntent. Attraverso il seguente metodo:

public int getGeofenceTransition()


otteniamo un identificatore del tipo di evento corrispondente ad una delle costanti
previste dalla classe Geofence ovvero:

GEOFENCE_TRANSITION_DWELL
GEOFENCE_TRANSITION_ENTER
GEOFENCE_TRANSITION_EXIT


Il metodo

public Location getTriggeringLocation()


ci permette invece di avere indicazioni relative alla Location in cui questo evento si
verificato. Molto pi importante comunque lutilizzo del metodo:

public List<Geofence> getTriggeringGeofences()


che fornisce un elenco dei Geofence a cui levento fa riferimento. Si tratta di
informazioni che utilizziamo per comporre il testo da visualizzare allinterno della
notifica che intendiamo lanciare.

Testiamo i Geofence
Abbiamo visto come gestire i Geofence e ricevere notifiche in corrispondenza dei
loro eventi di ingresso e uscita definendo un servizio, che nel nostro caso abbiamo
descritto con la classe GeofenceService. In casi come questo nasce per lesigenza di
testare lapplicazione, ovvero di verificare se le notifiche vengono effettivamente
generate. Un modo sicuramente quello di spostarsi fisicamente con il dispositivo
nella zona relativa a un Geofence e vedere che cosa succede. facile intuire come
questa soluzione non sia sempre praticabile, per cui si ha la necessit di un
meccanismo diverso che le Location API chiamano Mock Location. Esiste infatti la
possibilit di inviare al nostro dispositivo delle posizioni fittizie per verificare il
comportamento della nostra applicazione. Se impieghiamo un emulatore, una
possibile soluzione consiste nellutilizzare lapposito strumento dellAndroid Device
Monitor, come possiamo vedere nella Figura 3.27. Attraverso i Location Controls
possiamo infatti inserire una posizione e quindi inviarla allemulatore verificando che
cosa succede.
Come possiamo notare, vi anche la possibilit di inviare una serie di Location
attraverso file che seguono lo standard GPX (GPS Exchange Format) o KML
(Keyhole Markup Language). Si tratta di due formati che permettono di specificare,
secondo standard diversi, una serie di Location. Il secondo meccanismo per linvio di
informazioni fittizie allemulatore prevede lutilizzo del comando geo attraverso
lADB (Android Debug Bridge). Se lemulatore associato, come avviene di solito,
alla porta 5554, sar sufficiente collegarsi in Telnet attraverso il comando:

telnet localhost 5554


e quindi utilizzare comandi del tipo:

geo fix <longitude value> <latitude value>


inviando le informazioni di longitudine e latitudine.

Figura 3.27 Utilizzo dei Location Controls per linvio di posizioni fittizie allemulatore.

Il discorso un po diverso qualora si volesse testare lapplicazione in un


dispositivo reale. Innanzitutto necessario che sia abilitata la corrispondente opzione
nelle configurazioni e pi precisamente in corrispondenza delle impostazioni
sviluppatore. Sono quindi richieste alcune modifiche nellapplicazione. La prima di
queste consiste nella definizione di un permesso particolare, identificato dalla stringa
android.permission.ACCESS_MOCK_LOCATION, nel file AndroidManifest.xml associato alla versione
di debug della nostra applicazione. In sostanza dobbiamo definire la seguente
versione del file di configurazione:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission
android:name=android.permission.ACCESS_MOCK_LOCATION />
</manifest>


e inserirlo nella cartella visualizzata nella Figura 3.28.

Figura 3.28 Il file AndroidManifest.xml relativo al profilo di debug.


NOTA
Si consiglia il lettore di utilizzare la vista Project nella parte in alto a sinistra in figura in quanto la
vista Android non permette la creazione della suddetta cartella in modo semplice.

A questo punto dobbiamo anche abilitare la possibilit di inviare informazioni di


Location fittizie attraverso il seguente metodo, cui accediamo attraverso la ormai nota
variabile LocationServices.FusedLocationApi:

public abstract PendingResult<Status> setMockMode(GoogleApiClient client,

boolean isMockMode)

Poi dobbiamo inviare le informazioni attraverso il seguente metodo:

public abstract PendingResult<Status> setMockLocation(GoogleApiClient client,

Location mockLocation)

Per ottenere questo risultato abbiamo creato una nuova opzione nel menu
contestuale associato alla lista dei Geofence, opzione che ci permetter di simulare uno

spostamento del dispositivo da una posizione esterna al Geofence al centro dello stesso.
La logica di questa funzione contenuta nel metodo startMockLocations(), che
descriviamo nelle sue parti fondamentali. Innanzitutto il parametro di questo metodo
dato dallid del Geofence selezionato, per il quale abbiamo bisogno, come fatto in
precedenza, di ottenere le informazioni di latitudine e longitudine. Per fare questo
abbiamo usato le seguenti righe di codice, che dovrebbero essere ormai di semplice
comprensione:

// We get the data related to the given Geofence
final Uri geofenceUri = Uri.withAppendedPath(FenceDB.Geofence

.CONTENT_URI, String.valueOf(geofenceId));
final Cursor cursor = getActivity().getContentResolver()

.query(geofenceUri, null, null, null, null);


final FenceCursorFactory.GeofenceCursorData geoFenceCursor =

CursorResolver.CURSOR_RESOLVER.extractGeofenceCursor(cursor);
GeofenceData geofenceData = null;
if (geoFenceCursor.moveToNext()) {

geofenceData = geoFenceCursor.getGeofence();
}
cursor.close();


Una volta ottenute le informazioni sul Geofence selezionato abbiamo creato
unistanza di un Thread, la cui responsabilit sar quella di inviare informazioni di
fittizie a intervalli regolari. Interessante il seguente codice di

Location

inizializzazione che abbiamo inserito in un inizializzatore non statico della classe


anonima relativa al Thread stesso.

double currentLatitude = finalData.getLatitude() DISTANCE;
double currentLongitude = finalData.getLongitude() DISTANCE;
{

final LatLng currentLatLng = new LatLng(currentLatitude,


currentLongitude);
final CameraUpdate cameraUpdate = CameraUpdateFactory
.newLatLngZoom(currentLatLng, DEFAULT_ZOOM);
mGoogleMap.moveCamera(cameraUpdate);
}


Ricordiamo che un inizializzatore non statico viene richiamato in corrispondenza
della creazione di ogni istanza di una classe. Nel caso di classi anonime, come la
nostra, questo lunico meccanismo di inizializzazione che il linguaggio Java ci
mette a disposizione; una classe anonima non ha nome, per cui non possibile
definirne il costruttore.
Il corpo del Thread esegue tre operazioni principali, che nellordine sono:
Abilitazione delle Mock Location
Invio delle Location fittizie a intervalli regolari
Disabilitazione delle Mock Location
Il primo e ultimo passo sono molto semplici e consistono nella semplice
esecuzione delle seguenti due istruzioni:

LocationServices.FusedLocationApi.setMockMode(mGoogleApiClient, true);
- - LocationServices.FusedLocationApi.setMockMode(mGoogleApiClient, false);


Pi interessante il codice che permette di creare le informazioni di Location
fittizie:

final Location testLocation = new Location(MOCK_PROVIDER);
testLocation.setLatitude(currentLatitude);
testLocation.setLongitude(currentLongitude);
testLocation.setAccuracy(ACCURACY);
testLocation.setTime(System.currentTimeMillis());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {

testLocation.setElapsedRealtimeNanos(SystemClock.
elapsedRealtimeNanos());
}


di fondamentale importanza che vengano definite tutte le informazioni relative
alla posizione, accuratezza e tempo come nel precedente codice. In caso contrario le
informazioni di Location non verranno considerate valide e la simulazione non avr
effetto. La parte di invio al sistema descritta nel seguente codice:

final PendingResult<Status> mockLocResponse = LocationServices

.FusedLocationApi.setMockLocation(mGoogleApiClient, testLocation);
mockLocResponse.setResultCallback(new ResultCallback<Status>() {

@Override
public void onResult(Status status) {
if (status.isSuccess()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// Center the map on the location
}
});
} else {
Log.w(TAG_LOG, Error setting Mock Location +
status.getStatusMessage());
}
}
});


Il codice evidenziato permette di inviare al sistema la Location fittizia e quindi di
verificarne lesito attraverso loggetto di tipo PendingResult<Status> che abbiamo
imparato a gestire in precedenza. In caso di successo non faremo altro che centrare la
mappa nella posizione fittizia corrente attraverso le ormai classiche istruzioni.

LatLng newPos = new LatLng(currentLatitude, currentLongitude);
mGoogleMap.animateCamera(CameraUpdateFactory

.newLatLngZoom(newPos, DEFAULT_ZOOM));

Molto pi importante invece notare come le operazioni di interazione con la
mappa debbano avvenire nel Thread principale, ovvero di quello che gestisce
linterfaccia utente. Questo il motivo dellutilizzo del metodo runOnUiThread()
dellActivity contenitore.

A questo punto sar quindi possibile selezionare un Geofence nella lista e quindi
richiamare lopzione contestuale di nome Simulate Mock Location. Il lettore potr
quindi notare come la posizione corrente rappresentata dal pallino blu si avvicini alla
zona circolare associata al Geofence fino alla generazione del corrispondente evento.

Figura 3.29 La posizione corrente simulata si sposta lentamente verso il centro del Geofence.

In questo caso dobbiamo fare attenzione che se il tempo trascorso dalla posizione
corrente nella zona del Geofence troppo breve, levento associato potrebbe non essere
generato.

Integrazione di StreetView
Una caratteristica sicuramente affascinante delle Google Maps rappresentata da
Street View ovvero dalla possibilit di visualizzare una foto della zona corrispondente
a una particolare Location. Non si tratta di una sola foto statica, ma di una PanoramaView,
ovvero di un oggetto che possibile osservare a 360 gradi come se ci avvolgesse. Per
dare dimostrazione di questa funzione abbiamo definito la classe
FenceStreetViewActivity, che abbiamo associato a un nuova voce nel menu contestuale
relativo allelenco di FenceSession che abbiamo chiamato Show in Street View. Questa
attivit utilizza il seguente documento di layout presente nel file
activity_street_view.xml e che contiene la definizione di una Fragment di tipo
. Si tratta una classe che le stesse API ci mettono a

SupportStreetViewPanoramaFragment

disposizione per la visualizzazione delle immagini di StreetView.


NOTA
Notiamo come la classe contenga il prefisso Support, in quanto estende la classe Fragment della
libreria di compatibilit. Anche in questo caso, questo prefisso potr essere omesso nel caso in
cui si utilizzasse limplementazione di Fragment nativa della piattaforma.

Nella parte inferiore del display abbiamo poi inserito una SeekBar, che ci permetter
di muoverci avanti e indietro tra le varie posizioni disponibili.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools=http://schemas.android.com/tools
android:id=@+id/drawer_layout
android:layout_width=match_parent
android:layout_height=match_parent>
<fragment
android:id=@+id/fence_street_view
class=com.google.android.gms.maps.SupportStreetViewPanoramaFragment
android:layout_width=match_parent
android:layout_height=match_parent
android:layout_above=@+id/fence_stree_view_seekbar />
<SeekBar

android:id=@+id/fence_stree_view_seekbar
android:layout_width=wrap_content
android:layout_height=wrap_content
android:layout_alignParentBottom=true
android:layout_alignParentEnd=true
android:layout_alignParentLeft=true
android:layout_alignParentRight=true
android:layout_alignParentStart=true />
</RelativeLayout>


La classe FenceStreetViewActivity molto semplice e consiste semplicemente
nellinizializzazione di un oggetto di tipo StreetViewPanorama, attraverso il quale avviene
linterazione con limmagine di Street View visualizzata. Si tratta di un oggetto di cui
si ottiene un riferimento direttamente dal Fragment attraverso il seguente metodo
analogamente a quanto avveniva nel caso delloggetto di tipo GoogleMap e il relativo
:

Fragment


public final StreetViewPanorama getStreetViewPanorama()


Una volta ottenuto tale oggetto baster semplicemente richiamare il seguente altro
metodo per portarsi alla posizione indicata:

public void setPosition(LatLng position)


Osservando il codice della classe FenceStreetViewActivity notiamo come il riferimento
alloggetto di tipo StreetViewPanorama avvenga nel metodo onStart() nel seguente modo:

@Override
protected void onStart() {

super.onStart();
if (mStreetViewPanorama == null) {

mStreetViewPanorama = ((SupportStreetViewPanoramaFragment)
getSupportFragmentManager()
.findFragmentById(R.id.fence_street_view))
.getStreetViewPanorama();
}
}


Per il caricamento delle posizioni relative a una data FenceSession abbiamo utilizzato
ancora un Loader; la gestione del suo risultato avviene attraverso il seguente codice:

@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {

mPositionList.clear();
final FenceCursorFactory.FencePositionCursorData cursorData =
CursorResolver.CURSOR_RESOLVER.extractPositionCursor(cursor);
while (cursorData.moveToNext()) {
final LatLng latLng = new LatLng(cursorData.getLatitude(),
cursorData.getLongitude());
mPositionList.add(latLng);
}
// We go to the first position
if (mPositionList.size() > 0) {
showPosition(0);
mSeekBar.setMax(mPositionList.size());
mSeekBar.setProgress(0);
}
}


In sostanza leggiamo tutte le posizioni e le memorizziamo in una lista di oggetti di
tipo LatLng, la cui lunghezza determina anche i possibili valori della SeekBar, di cui

impostiamo la lunghezza. Per legare il movimento dellhandle della SeekBar a una


posizione, non facciamo altro che gestire il corrispondente evento attraverso il
seguente codice:

mSeekBar.setOnSeekBarChangeListener(new

SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
if (mPositionList.size() > progress) {
showPosition(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});


A ogni movimento della SeekBar corrisponde la chiamata del metodo showPosition(),
che quindi aggiorna la StreetView attraverso le seguenti poche righe di codice:

private void showPosition(final int positionIndex) {

if (mPositionList.size() > positionIndex) {


mStreetViewPanorama.setPosition(mPositionList.get(positionIndex));
}
}


Il risultato sar simile a quello rappresentato nella Figura 3.30.

Figura 3.30 Visualizzazione della StreetView del percorso associato a una FenceSession.

Lasciamo al lettore la verifica di come sia semplice tracciare un percorso per


visualizzarlo successivamente attraverso lo strumento delle StreetView. Sono
disponibili altre API per la gestione di questo strumenti, per le quali rimandiamo alla
documentazione ufficiale. Si tratta, comunque, di strumenti che riprendono i pattern
ormai noti descritti in questo capitolo e nel precedente.

Lite Mode
Come sappiamo da sempre le release di Android hanno avuto come obiettivo
quello di essere il pi possibile ottimizzate sia in termini di prestazioni ma soprattutto
in termini di risorse. Per quello che riguarda la gestione delle mappe, capita spesso di
utilizzare le Google Maps per la sola visualizzazione di una particolare zona senza la
necessit di alcuna interazione con lutente attraverso operazioni di zoom o pan. La
versione 6.5 dei Google Play services hanno quindi introdotto quella che si chiama

Lite Mode ovvero la possibilit di ottenere direttamente la sola immagine di una zona
senza attivare tutti gli altri strumenti. Per utilizzare questa modalit sufficiente
creare unistanza della classe MapFragment, o equivalenti, attraverso i metodi che hanno
come parametro loggetto di tipo GoogleMapOptions per i quali stata attivata la modalit
attraverso il seguente metodo:
public GoogleMapOptions liteMode (boolean enabled)

Infine sottolineiamo come tutti i metodi della modalit classica siano comunque
disponibili anche se molti di loro genereranno semplicemente un messaggio di
warning.

Conclusioni
In questo capitolo ci siamo occupati di tutto ci che riguarda la visualizzazione e
personalizzazione di Google Maps. Siamo partiti dalla semplice visualizzazione della
posizione corrente per arrivare alla creazione e personalizzazione di opportuni Marker.
Definendo Geofence abbiamo imparato a disegnare sulla mappa e a gestirne alcuni
degli eventi, per esempio il LongClick. Abbiamo inoltre visto come disegnare i nostri
percorsi, utilizzando colori e tratti diversi. Sempre in relazione alla gestione dei
Geofence abbiamo infine visto come testare lapplicazione attraverso Mock Location.
Abbiamo concluso il capitolo con una dimostrazione di come sia semplice
utilizzare le API per la visualizzazione delle Street View, avendo ulteriore occasione
di approfondire i concetti legati alla creazione di un ContentProvider, insieme ad alcuni
concetti relativi alla programmazione concorrente. Nel prossimo capitolo ci
occuperemo di qualcosa di diverso, che ci permetter, nel contesto della nostra
applicazione, di rendere persistenti i dati raccolti nel cloud.

Capitolo 4

Google Drive API

Una funzionalit molto importante dei Google Play services va sotto il nome di
Google Drive API. Attraverso questo strumento possibile accedere ai file contenuti
nel proprio account Google Drive attraverso API native della piattaforma Android e
quindi specializzate per essa. In precedenza era possibile realizzare le stesse
funzionalit attraverso librerie Java che era necessario aggiungere al progetto e
personalizzare per i casi duso mobile. Le funzionalit offerte da queste API sono
molto semplici, ma importanti soprattutto alla luce del fatto che ora funzionano per
dispositivi da Gingerbread in avanti, coprendo quindi quasi il 99% del mercato. Con
questi strumenti possiamo ora:
scrivere, leggere e aggiornare file e folder remoti come se fossero locali;
consultare strutture a directory;
gestire e visualizzare metadati;
usare delle interfacce native per linterazione con i File;
delegare al sistema le operazioni di sincronizzazione.
Nel nostro caso lultima funzionalit sar molto importante, in quanto ci
permetter di salvare le informazioni relative alle nostre sessioni per poi ripristinarle
anche da altri dispositivi o scambiarle con altri utenti.

Google Drive
Prima di procedere con la descrizione delle funzionalit strettamente legate alla
nostra applicazione bene fare una panoramica dei concetti alla base dellutilizzo dei
Google Drive. Ogni risorsa che possibile gestire attraverso queste API viene astratta
attraverso linterfaccia DriveResource, la quale definisce tutte le operazioni che
possibile effettuare su file e directory, astratte, rispettivamente, attraverso le
interfacce DriveFile e DriveFolder. In particolare ognuna di queste identificata da un
, cui possibile accedere attraverso la seguente operazione:

DriveId


public abstract DriveId getDriveId()


Attraverso un DriveId potremo infatti interagire con il corrispondente File o Folder.
NOTA
Perch un oggetto e non semplicemente una String? Osservando la classe DriveId notiamo
come questa sia innanzitutto Parcelable e quindi condivisibile tra pi processi (anche una
semplice String lo sarebbe), ma soprattutto permetta di separare quello che un id locale della
risorsa, da quello che invece un id remoto. Si tratta di informazioni che sono utili specialmente
in fase di sincronizzazione.

A ogni DriveResource poi possibile associare dei metadati, i quali sono stati astratti
dallinterfaccia Metadata, che permette di associare diversi tipi di informazioni, tra cui
una descrizione, la data di creazione, il link a cui la risorsa accessibile e,
soprattutto, il MimeType, che ricordiamo essere una stringa standard associata ai tipi di
file che permettono ai vari sistemi di scegliere il tool opportuno per poterli gestire.
Per esempio, un MimeType text/html verr interpretato da un parser, che lo visualizzer
come documento HTML, mentre un MimeType image/jpeg verr interpretato come
unimmagine. Per accedere ai metadati si utilizza la seguente operazione su una
risorsa qualsiasi:

public abstract PendingResult<DriveResource.MetadataResult>

getMetadata (GoogleApiClient apiClient)



Notiamo ancora il tipo restituito, PendingResult, e il parametro, di tipo GoogleApiClient,
come nel caso delle mappe e delle location. anche possibile modificare queste
informazioni, attraverso la seguente operazione:


public abstract PendingResult<DriveResource.MetadataResult>

updateMetadata(GoogleApiClient apiClient, MetadataChangeSet changeSet)



Il secondo parametro di tipo MetadataChangeSet e ci permetter di impostare le
diverse propriet attraverso lormai classico Builder implementato dalla classe
.

MetadataChangeSet.Builder

Per interagire con tutto questo ci serve un punto di ingresso, che in questo caso
rappresentato dalla classe DriveApi che dispone di una serie di metodi per linterazione
con tutte le funzionalit di Google Drive. Dato un DriveId, possibile, per esempio,
accedere al corrispondente DriveFile attraverso il seguente metodo:

public abstract DriveFile getFile(GoogleApiClient apiClient, DriveId id)


Ottenuto un DriveFile potremo accedere al suo contenuto attraverso il seguente
metodo, che notiamo gestire il tutto in modo asincrono:

public abstract PendingResult<DriveApi.DriveContentsResult>

open (GoogleApiClient apiClient, int mode,


DriveFile.DownloadProgressListener listener)

Questo conseguenza della necessit, da parte del sistema, di valutare se il file
effettivamente disponibile in locale, se nella versione aggiornata oppure se
richiederlo totalmente al server. Il fatto positivo che il meccanismo totalmente
trasparente a noi che sviluppiamo, la cui unica interfaccia di interazione data dalle
classi appena viste. Una cosa analoga si ha nel caso dei DriveFolder, i quali si possono
ottenere attraverso operazioni come:

public abstract DriveFolder getFolder(GoogleApiClient apiClient, DriveId id)


Sui DriveFolder possiamo eseguire altre operazioni, come quelle a cui possiamo
accedere attraverso i seguenti metodi:


public abstract PendingResult<DriveFolder.DriveFolderResult>

createFolder (GoogleApiClient apiClient, MetadataChangeSet changeSet)



oppure

public abstract PendingResult<DriveFolder.DriveFileResult>

createFile (GoogleApiClient apiClient, MetadataChangeSet changeSet,


DriveContents driveContents)

In precedenza abbiamo detto che queste nuove API si differenziano da quelle
precedenti per il fatto che sono strettamente collegate ad Android. Questa cosa si nota
soprattutto per la disponibilit di due nuove classi, che ci forniscono direttamente
linterfaccia utente per la creazione o lapertura di DriveFile. Attraverso la classe
, infatti, possibile creare un DriveFile, dando lopportunit di

CreateFileActivityBuilder

specificare la destinazione, il nome e alcuni metadati, tra cui il MimeType. Laspetto


interessante di questa classe che permette di creare il file in locale gestendo
leventuale mancata connettivit e provvedendo alla sincronizzazione qualora questa
dovesse essere nuovamente disponibile. Nel caso dellapertura di un DriveFile esiste
invece la classe OpenFileActivityBuilder, la quale ci fornir linterfaccia utente per
lapertura di un File.
Lultima delle funzionalit che vedremo quella di ricerca di una risorsa secondo
alcuni criteri, che possibile impostare attraverso un oggetto di tipo Query che
creeremo attraverso lennesimo Builder, descritto dalla classe Query.Builder. Lo stesso
ci permetter poi di usare questo oggetto come parametro del seguente

DriveApi

metodo per la ricerca di File che soddisfano i criteri assegnati:



public abstract PendingResult<DriveApi.MetadataBufferResult>

query (GoogleApiClient apiClient, Query query)


Inizializzazione di Google Drive


Prima di procedere con lutilizzo di queste API nella nostra applicazione,
descriviamo brevemente la procedura di inizializzazione, che ora diventa la seguente,
come possiamo vedere nella nuova versione della classe FenceSessionListFragment nel
metodo onCreate():

mGoogleApiClient = new GoogleApiClient.Builder(getActivity())

.addApi(Drive.API)
.addScope(Drive.SCOPE_FILE)
.addScope(Drive.SCOPE_APPFOLDER)
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedListener)
.build();

Abbiamo evidenziato la richiesta delle Drive.API e degli Scope relativi alla possibilit
di accedere sia ai file sia ai folder. Nel nostro caso abbiamo poi associato una nuova
voce del menu contestuale, che abbiamo chiamato semplicemente Export, e abbiamo
incapsulato tutta la logica nel metodo privato exportFenceSession(), che vedremo nel
prossimo paragrafo.
Come accennato nel Capitolo 1 i Google Play services ci permettono di definire le
dipendenze solamente con le librerie effettivamente utilizzate. Nel caso delle Drive
API abbiamo quindi aggiunto la definizione evidenziata qui di seguito nel file di
configurazione di gradle.

dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.android.support:appcompat-v7:21.0.3
compile com.android.support:support-v4:21.0.3
compile com.google.android.gms:play-services-base:6.5.87
compile com.google.android.gms:play-services-location:6.5.87

compile com.google.android.gms:play-services-maps:6.5.87
compile com.google.android.gms:play-services-drive:6.5.87
}


A questo punto fondamentale fare due considerazioni. La prima riguarda il fatto
che ogni applicazione che utilizza le Google Drive debba aggiungere tra le proprie
note legali quelle relative a questa nuova funzionalit. Per questo motivo le stesse
API ci mettono a disposizione il seguente metodo della classe GooglePlayServicesUtil, la
quale fornisce appunto il testo da includere nellapplicazione:

public static String getOpenSourceSoftwareLicenseInfo(Context context)


Per questo motivo abbiamo aggiunto una nuova voce alle opzioni
dellapplicazione, che abbiamo chiamato Legal Notes e che non fa altro che
visualizzare una schermata con il testo ottenuto, come possiamo vedere nella Figura
4.1.
La seconda riguarda invece il fatto che per accedere a Google Drive necessario
disporre di un account. Questo il motivo per cui in corrispondenza della
visualizzazione della nostra nuova versione della classe FenceSessionListFragment si ha
quanto rappresentato nella Figura 4.2, ovvero la richiesta da parte del sistema di
quale tra gli account presenti nel dispositivo utilizzare per queste funzionalit.

Figura 4.1 Visualizzazione delle note legali di Google Drive.

Figura 4.2 Richiesta dellaccount da utilizzare per Google Drive.

Per arrivare qui comunque necessario tornare nella Google Console per
richiedere un Client ID per laccesso al server. Per fare questo selezioniamo la voce
Api & Auth sulla sinistra e quindi il pulsante Create new Client ID, il quale porter
alla visualizzazione della form rappresentato nella Figura 4.3.

Figura 4.3 Creazione del Client ID per laccesso alle funzioni di Google Drive.

Notiamo come il tipo di applicazione sia Installed application per Android e come
sia necessario inserire il package dellapplicazione insieme al certificato SHA1 come
fatto per la mappa. In questo caso per non abbiamo avuto bisogno di alcuna chiave,
ma solamente di abilitare il servizio lato server. Una volta confermate le informazioni
premendo il pulsante Create Client ID si pu procedere alla creazione del Client ID e
quindi possibile selezionare una delle mail precedenti per arrivare alla schermata
rappresentata nella Figura 4.4, la quale, ovviamente, verr visualizzata solo al primo
accesso.
Una volta data la conferma, lambiente sar pronto a utilizzare tutti gli strumenti
visti in precedenza, come vedremo nella descrizione del nostro caso duso.
A dire il vero, la documentazione ufficiale in questa fase non molto chiara. Esiste
infatti un problema legato alla visualizzazione della precedente schermata e collegato
al ciclo di vita delloggetto di tipo GoogleApiClient. In precedenza abbiamo infatti

provveduto alla connessione nel metodo onStart() e alla sconnessione nel metodo
. In questo caso abbiamo invece avuto la necessit di spostare la chiamata di

onStop()

questi metodi direttamente in onCreate() per la connessione e onDestroy() per la


disconnessione. In caso contrario la visualizzazione della schermata rappresentata
nella Figura 4.4 porterebbe lapplicazione in un loop infinito.
Concludiamo dicendo che la precedente schermata dipender dal tipo di Scope
richiesti e dalla particolare configurazione che si pu fare attraverso gli strumenti
messi a disposizione dalla Google Console.

Figura 4.4 Richiesta del permesso di accedere a Google Drive per lapplicazione corrente.

Esportazione delle informazioni di una


FenceSession
Dopo aver configurato le API Google Drive iniziamo finalmente lo sviluppo della
funzionalit di esportazione delle informazioni di una FenceSession. A tale proposito
abbiamo deciso di seguire lapproccio pi semplice, che consiste nellutilizzo della
classe CreateFileActivityBuilder, che abbiamo visto fornirci anche linterfaccia utente
per la memorizzazione del file associato. Come abbiamo gi detto, la selezione
dellopzione contestuale relativa al salvataggio dei dati di una sessione associata
allesecuzione del nostro metodo exportFenceSession(), che merita una descrizione
approfondita, in quanto non banale e non accuratamente spiegato nella
documentazione ufficiale di Google.

private void exportFenceSession(final long sessionToExportId) {

// We create the Callback for the creation


// of the Content for the file
ResultCallback<DriveApi.DriveContentsResult> contentCallback = new
ResultCallback<DriveApi.DriveContentsResult>() {
@Override
public void onResult(DriveApi.DriveContentsResult contentsResult) {
if (contentsResult.getStatus().isSuccess()) {
// We update the last session
mCurrentSavedSession = sessionToExportId;
// We set the initial Metadata
final MetadataChangeSet metadataChangeSet = new MetadataChangeSet.Builder()
.setMimeType(JSON_MIME_TYPE)
.setTitle(DEFAULT_DRIVE_TITLE)
.build();
// We Start the CreateFileActivityBuilder
final IntentSender intentSender = Drive.DriveApi
.newCreateFileActivityBuilder()

.setActivityTitle(getString(R.string.session_context_export))
.setInitialMetadata(metadataChangeSet)
.setInitialDriveContents(contentsResult.getDriveContents())
.build(mGoogleApiClient);
try {
getActivity()
.startIntentSenderForResult(intentSender,
REQUEST_CODE_SAVE_SESSION, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG_LOG, Error , e);
}
}
}
};
Drive.DriveApi.newDriveContents(mGoogleApiClient)
.setResultCallback(contentCallback);
}


Innanzitutto notiamo come il primo passo consista nella creazione del Contents, il
contenuto del file che andiamo a creare. In realt non si tratta del contenuto vero e
proprio, ma di un placeholder di un file, che andremo a riempire successivamente.
Loperazione di richiesta della creazione di un Contents data dallistruzione
evidenziata nel precedente codice, nella quale notiamo lutilizzo delloggetto
Drive.DriveApi e del suo metodo newDriveContents(). Come ormai sappiamo si tratta di un
metodo che esegue il proprio lavoro in modo asincrono; questo il motivo per cui
impiega un parametro di tipo ResultCallback<DriveApi.DriveContentsResult>, che abbiamo
definito nella prima parte del metodo stesso. Un oggetto di questo tipo ci permette di
ricevere in modalit asincrona un oggetto di tipo DriveApi.ContentsResult, il quale
conterr sia lesito delloperazione, sia, in caso affermativo, il riferimento al
contenuto creato, cui accediamo attraverso il metodo getContents(). Una volta

verificato che la creazione del contenuto sia avvenuta con successo attraverso
listruzione

contentsResult.getStatus().isSuccess()


ci siamo occupati della creazione dei parametri necessari alla visualizzazione
dellinterfaccia di creazione del file. Il primo rappresentato dalle informazioni
associate ai Metadata e che contengono le caratteristiche del file che stiamo andando a
creare. Nel nostro caso abbiamo utilizzato le seguenti istruzioni:

final MetadataChangeSet metadataChangeSet =

new MetadataChangeSet.Builder()
.setMimeType(JSON_MIME_TYPE)
.setTitle(DEFAULT_DRIVE_TITLE)
.build();

Attraverso di esse abbiamo impostato il nome del file, ma soprattutto il
corrispondente MimeType, che per noi quello di un JSON, ovvero application/json.
Per la visualizzazione del componente visuale di creazione di un File abbiamo
creato un oggetto di tipo IntentSender. Si tratta sostanzialmente di un oggetto che
incapsula tutte le informazioni necessarie al lancio di un Intent, al fine di avviare una
o un Service. Nel nostro caso abbiamo utilizzato le seguenti istruzioni:

Activity


final IntentSender intentSender = Drive.DriveApi

.newCreateFileActivityBuilder()
.setActivityTitle(getString(R.string.session_context_export))
.setInitialMetadata(metadataChangeSet)
.setInitialContents(contentsResult.getDriveContents())
.build(mGoogleApiClient);

Abbiamo richiesto, nel codice evidenziato, la visualizzazione della Activity per la


creazione di un nuovo File. Abbiamo quindi specificato un titolo, ma soprattutto
abbiamo passato le informazioni relative ai Metadata e quindi il riferimento alloggetto
iniziale.

Contents

NOTA
Qualcuno si sar forse chiesto quale possa essere la differenza tra un IntentSender e un
PendingIntent.

Non deve trarre in inganno il fatto che nel precedente caso si ottenga un

IntentSender a partire dalloggetto DriveApi; in ogni caso lunico modo per ottenere un oggetto di

quel tipo passare per un PendingIntent e quindi richiamare il suo metodo getIntentSender(). In
breve potremmo dire che, mentre un PendingIntent contiene tutte le informazioni relative al lancio
di un Intent, un IntentSender contiene anche informazioni sul componente che in quel momento
pu inviare lIntent stesso.

Attraverso un IntentSender possiamo inviare un Intent per lavvio di un Service o di


una Activity; questo ci che avviene con le ultime istruzioni, ovvero:

try {

getActivity().startIntentSenderForResult(intentSender,
REQUEST_CODE_SAVE_SESSION, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {

Log.e(TAG_LOG, Error , e);


}


importante notare come la modalit di utilizzo dellIntentSender preveda lattesa di
un risultato attraverso il classico metodo onActivityResult(), nel quale dovremo gestire i
vari casi in cui lutente abbia annullato loperazione selezionando back e il caso in
cui tutto sia andato per il meglio. Nel nostro caso specifico, come identificatore della
particolare richiesta abbiamo utilizzato la costante REQUEST_CODE_SAVE_SESSION. Il secondo
passo consiste nellimplementazione della corrispondente parte del metodo
onActivityResult(), che per noi la seguente:

public void onActivityResult(int requestCode, int resultCode, Intent data) {

if (CONNECTION_FAILURE_RESOLUTION_REQUEST == requestCode
&& Activity.RESULT_OK == resultCode) {
- - -

} else if (REQUEST_CODE_SAVE_SESSION == requestCode) {


if (Activity.RESULT_OK == resultCode) {
final DriveId createdFileId = data.getParcelableExtra(
CreateFileActivityBuilder.EXTRA_RESPONSE_DRIVE_ID);
if (mCurrentSavedSession >= 0) {
PutDataIntoDiveFileTask saveTask =
new PutDataIntoDiveFileTask(mCurrentSavedSession);
saveTask.execute(createdFileId);
} else {
Toast.makeText(getActivity(),
R.string.action_drive_create_failed,
Toast.LENGTH_SHORT).show();
}
}
}
}


Nel caso in cui loperazione sia andata a buon fine possiamo ottenere il riferimento
al DriveId del file appena creato attraverso uno degli extra restituiti, il cui nome ci
viene dato da unapposita costante della classe CreateFileActivityBuilder. A questo
punto il file stato creato e dobbiamo preoccuparci di riempirlo con i dati della
nostra sessione. Si tratta di unoperazione che deve essere eseguita in background,
per la quale abbiamo creato unopportuna implementazione del seguente AsyncTask:

private class PutDataIntoDiveFileTask

extends AsyncTask<DriveId, Void, DriveFile> {


private final long mSessionId;
public PutDataIntoDiveFileTask(final long sessionId) {
this.mSessionId = sessionId;
}

@Override
protected DriveFile doInBackground(DriveId params) {

// We get the reference to the file


DriveFile newFile = Drive.DriveApi.getFile(mGoogleApiClient, params[0]);
try {
DriveApi.DriveContentsResult contentsResult = newFile.open(
mGoogleApiClient, DriveFile.MODE_WRITE_ONLY, null).await();
if (!contentsResult.getStatus().isSuccess()) {
return null;
}
OutputStream outputStream =
contentsResult.getDriveContents().getOutputStream();
final String sessionJson =
FenceDriveUtil.fenceSessionAsJson(getActivity(), mSessionId);
outputStream.write(sessionJson.getBytes());
MetadataChangeSet changeSet =
new MetadataChangeSet.Builder()
.setLastViewedByMeDate(new Date()).build();
com.google.android.gms.common.api.Status commitStatus =
contentsResult.getDriveContents().commit(mGoogleApiClient,
changeSet).await();
if (!commitStatus.isSuccess()) {
return null;
}
return newFile;
} catch (IOException e) {
Log.e(TAG_LOG, IOException while appending to the output stream, e);
}
return null;

@Override
protected void onPostExecute(DriveFile driveFile) {
super.onPostExecute(driveFile);
if (driveFile != null) {
// The session has been saved
mCurrentSavedSession = -1;
// We show a Toast for success
} else {
// We show Toast for error
}
}
}


Nel metodo doBackground() del nostro Task utilizziamo lidentificatore DriveId del file
creato e otteniamo il riferimento al corrispondente Contents attraverso la seguente
istruzione:

DriveApi.DriveContentsResult contentsResult = newFile.open(

mGoogleApiClient, DriveFile.MODE_WRITE_ONLY, null).await();



di fondamentale importanza notare come questa volta non sia stata utilizzata
uninterfaccia di callback, ma sia stato richiamato semplicemente il metodo await()
che ricordiamo essere bloccante. Essendo in fase di scrittura del file, abbiamo
utilizzato la costante DriveFile.MODE_WRITE_ONLY, che identifica appunto una modalit di
accesso al file solamente in scrittura.
NOTA
Lutilizzo di un metodo bloccante come await() non porta alcun problema, in quanto siamo
comunque in un Thread in background.

Se lapertura avvenuta con successo, otteniamo il riferimento a un OutputStream,


che utilizziamo per la scrittura del Json con le informazioni della particolare

. La lettura delle informazioni di una FenceSession e la creazione del

FenceSession

corrispondente documento JSON sono state incapsulate in un metodo di utilit della


classe FenceDriveUtil che il lettore potr consultare nel codice del progetto.
bene sottolineare che la scrittura del contenuto del nostro file nelloggetto di tipo
Contents non corrisponde automaticamente al salvataggio delle informazioni in locale
e/o su Google Drive. Lultimo passo consiste, infatti, nellesecuzione delle seguenti
istruzioni:

com.google.android.gms.common.api.Status commitStatus = contentsResult

.getDriveContents().commit(mGoogleApiClient, changeSet).await();

Ovvero del seguente metodo caratteristico di ogni oggetto DriveContents:

public abstract PendingResult<Status> commit (GoogleApiClient apiClient,

MetadataChangeSet changeSet)

Come possiamo notare il secondo parametro un oggetto di tipo MetadataChangeSet
che permette di impostare in corrispondenza del salvataggio, anche le informazioni
relative ad alcuni metadati. Nel nostro caso abbiamo creato un oggetto che ci ha
permesso di aggiornare la data del file come possiamo vedere nel seguente codice:

MetadataChangeSet changeSet = new MetadataChangeSet.Builder()

.setLastViewedByMeDate(new Date()).build();

Osservando la documentazione notiamo come sia presente un overload del
precedente metodo con la seguente firma:

public abstract PendingResult<Status> commit (GoogleApiClient apiClient,
MetadataChangeSet changeSet, ExecutionOptions executionOptions)


il quale ha un terzo parametro di tipo ExecutionOptions che ci permette di gestire
eventuali conflitti durante il salvataggio del file. Un conflitto si verifica, per esempio,

se uno stesso file viene caricato da due applicazioni o dispositivi diversi, modificato
diversamente, e quindi nuovamente sincronizzato. Loggetto di tipo ExecutionOptions ci
permette di scegliere tra alcune politiche di risoluzione dei conflitti previste dalla
piattaforma.
Se anche questa operazione viene eseguita con successo, possiamo finalmente dire
di aver concluso la creazione e il salvataggio delle informazioni di una FenceSession su
Google Drive.
Per testarne il funzionamento creiamo quindi il tracking di un percorso e
selezioniamo la voce Export Locations nel menu contestuale, avviando la schermata
rappresentata nella Figura 4.5:
Notiamo come nella parte superiore sia visualizzato il titolo che abbiamo
impostato in fase di creazione dellIntentSender, mentre il nome del file quello
impostato di default. Notiamo poi come siano presenti le informazioni relative al
nome dellaccount e al folder di root, che si chiama My Drive. A questo punto
selezioniamo la cartella My Drive per visualizzarne il contenuto. Ora utilizziamo
licona in alto a destra per creare un nuovo folder che chiamiamo FriendFence, nel
quale vogliamo inserire il nostro file. Facciamo clic sul pulsante Select in basso a
destra, tornando alla schermata rappresentata nella Figura 4.6 dove ora il nome del
folder diverso.

Figura 4.5 Interfaccia di Google Drive per la creazione di un file.

Figura 4.6 Abbiamo creato e selezionato un folder diverso di nome FriendFence.

A questo punto selezioniamo il pulsante Save, sempre in basso a destra, per


confermare la creazione del file e il salvataggio del corrispondente contenuto. La
nostra applicazione torna allelenco delle FenceSession, visualizzando un Toast con un
messaggio di successo.

Figura 4.7 Il file creato disponibile anche attraverso linterfaccia web.

Vogliamo per andare a vedere su Google Drive nel Web se effettivamente esiste il
file che abbiamo appena creato. Il risultato rappresentato nella Figura 4.7, dove
notiamo la presenza del nostro File, di cui vogliamo per verificare anche il
contenuto. Per questo motivo selezioniamo il file scaricandolo in locale. Nel nostro
caso si ottiene il seguente risultato, abbreviato per motivi di spazio:

{

positions: [
{
position_time: 2014-09-28T22:03:11.298Z,
distance: 0,
longitude: -0.2131488025188446,
latitude: 51.444557189941406,
activity: 3
},
- - {
position_time: 2014-09-28T22:04:26.621Z,
distance: 44.0030403137207,
longitude: -0.21316149830818176,
latitude: 51.4445686340332,
activity: 3
}
],
session_owner: defaultUser,
end_date: 2014-09-28T22:04:26.907Z,
start_date: 2014-09-28T22:03:11.203Z,
totalDistance: 44.0030403137207
}

Quello descritto un caso duso che ci ha permesso di imparare a creare un file su


Google Drive, attraverso informazioni create od ottenute allinterno di
unapplicazione Android. Sebbene ci siano altre modalit per svolgere la stessa
operazione, queste non si discostano di molto nel metodo e dallo schema applicati.
Nel precedente caso abbiamo utilizzato linterfaccia utente fornita con le Google
Drive API e abbiamo visto come siano richiesti due passi distinti. Il primo permette
di scegliere e creare il file, mentre il secondo permette di scrivere nel file i dati della
nostra FenceSession. Se conosciamo il nome del file e il folder di destinazione,
possiamo seguire un procedimento pi diretto, che abbiamo implementato nel metodo
exportDirectFenceSession(), che ci accingiamo a descrivere nel dettaglio. Innanzitutto
notiamo come la struttura del metodo sia analoga a quella precedente, ovvero:

private void exportDirectFenceSession(final long sessionToExportId) {

// We create the Callback for the creation of the Content for the file
ResultCallback<DriveApi.DriveContentsResult> contentCallback =
new ResultCallback<DriveApi.DriveContentsResult>() {
@Override
public void onResult(DriveApi.DriveContentsResult contentsResult) {
if (contentsResult.getStatus().isSuccess()) {
// Manage content
}
}
};
Drive.DriveApi.newDriveContents(mGoogleApiClient)
.setResultCallback(contentCallback);
}


Come in precedenza, richiamiamo il metodo newDriveContents(), passando come
riferimento limplementazione dellinterfaccia
ResultCallback<DriveApi.DriveContentsResult> e gestendo il caso in cui la creazione del
contenuto sia avvenuta con successo. In questa implementazione eseguiamo
fondamentalmente le seguenti operazioni.

Otteniamo il nome del file dai dati nel DB.


Definiamo i metadati del file che stiamo per creare.
Otteniamo il JSON della FenceSession inserendolo nel Contents del file.
Richiamiamo il metodo createFile() per leffettiva creazione del file, con la
corrispondente implementazione dellinterfaccia di callback.
Verifichiamo lesito delloperazione.
Come abbiamo detto, il primo passo consiste nel determinare il nome del file, che
pu essere una qualunque String che abbiamo dedotto dal nome dellowner e dalla data
di fine percorso. Si tratta comunque di una logica che abbiamo implementato in una
classe di utilit, che abbiamo utilizzato nel seguente modo:

final String sessionName = FenceDriveUtil

.getFenceSessionName(getActivity(), sessionToExportId);

Il passo successivo consiste nel creare loggetto di tipo MetadataChangeSet che
incapsula le informazioni relative ai metadati del file che intendiamo creare. Nel
nostro caso abbiamo semplicemente utilizzato le seguenti, poche, istruzioni,
analogamente a quanto fatto in precedenza, con la sola differenza dellutilizzo del
nome appena creato e del metodo setStarred(), che ci permette di segnalare il file
come importante.

final MetadataChangeSet metadataChangeSet =

new MetadataChangeSet.Builder()
.setMimeType(JSON_MIME_TYPE)
.setTitle(sessionName)
.setStarred(true)
.build();

A questo punto siamo comunque nel caso in cui la creazione delloggetto Contents
avvenuta con successo, per cui possibile accedervi attraverso il metodo getContents()
sulloggetto ricevuto come parametro del metodo di callback. La fase successiva

consiste nella scrittura dei dati attraverso un OutputStream, che si ottiene dallo stesso
attraverso il metodo getOutputStream().

Contents


OutputStream outputStream = contentsResult.getDriveContents()

.getOutputStream();
final String sessionJson = FenceDriveUtil

.fenceSessionAsJson(getActivity(), sessionToExportId);
try {

outputStream.write(sessionJson.getBytes());
} catch (IOException e) {

e.printStackTrace();
}


In questa fase bene precisare che lOutputStream non lunico modo di interagire
con loggetto di tipo DriveContents, ma quello corrispondente allaccesso al file
solamente in scrittura, come fatto anche nel caso precedente. Se avessimo voluto
accedere al file in lettura e scrittura avremmo dovuto utilizzare un altro meccanismo,
che prevede lutilizzo di un oggetto di tipo ParcelFileDescriptor, al quale si poteva
accedere attraverso il metodo getParcelFileDescriptor().
NOTA
Il nome di questa classe non deve intimorire. Si tratta sostanzialmente di un meccanismo che
prevede lutilizzo di File invece che direttamente di InputStream o OutputStream.

Il contenuto JSON della FenceSession ci viene sempre dato dal corrispondente


metodo di utilit che abbiamo creato nella classe FenceDriveUtil. Prima di richiamare il
metodo effettivo di creazione del file, provvediamo a creare la corrispondente
implementazione dellinterfaccia di callback, attraverso le seguenti istruzioni:

final ResultCallback<DriveFolder.DriveFileResult> fileCallback =

new ResultCallback<DriveFolder.DriveFileResult>() {
@Override
public void onResult(DriveFolder.DriveFileResult result) {
if (!result.getStatus().isSuccess()) {
Toast.makeText(getActivity(),

R.string.action_drive_create_failed,
Toast.LENGTH_SHORT).show();
return;
}
final String message = getString(
R.string.action_drive_create_success_name,
sessionName);
Toast.makeText(getActivity(), message,
Toast.LENGTH_SHORT).show();
}
};


Si tratta semplicemente di verificare lesito delloperazione per visualizzare un
Toast, con un messaggio di conferma o di errore.
Arriviamo infine alle istruzioni di creazione vera e propria del File, ovvero alle
seguenti righe di codice:

Drive.DriveApi.getRootFolder(mGoogleApiClient)

.createFile(mGoogleApiClient, metadataChangeSet,
contentsResult.getDriveContents())
.setResultCallback(fileCallback);

Attraverso loggetto Drive.DriveApi otteniamo il riferimento al DriveFolder principale
grazie al metodo getRootFolder() per creare poi un file al suo interno con il metodo
, che richiede come parametri, oltre alloggetto di tipo GoogleApiClient, il

createFile()

riferimento ai metadati e al relativo DriveContents. In chaining abbiamo poi


concatenato limplementazione dellinterfaccia di callback implementata al punto
precedente.
Possiamo dire che questo modo di procedere forse pi immediato, ma non lascia
allutente la possibilit di scegliere il nome del file e, soprattutto, il folder in cui
crearlo. Dipende dai casi duso e dallapplicazione che si vuole realizzare. Queste

istruzioni ci hanno permesso di comprendere ancora una volta i meccanismi che si


celano dietro la creazione di file in Google Drive.

Figura 4.8 Il file stato creato e marcato come importante.

Testando lapplicazione, possiamo notare come il file venga effettivamente creato


in Google Drive. La stella a lato ne indica limportanza, come impostato nella
definizione dei metadati.

Importazione delle informazioni di una


FenceSession
Nel paragrafo precedente abbiamo implementato la scrittura delle informazioni di
una FenceSession allinterno di un File in Google Drive. A questo punto vogliamo per
anche leggere le informazioni e quindi dobbiamo provvedere allimplementazione di
unoperazione di import, accessibile questa volta non come voce del menu
contestuale, ma come voce del menu delle opzioni.
NOTA
Non si tratta, infatti, di una funzione associata a una specifica FenceSession, ma di un qualcosa
associato allintera applicazione.

Anche in questo caso provvediamo alla doppia implementazione, ovvero quella


che prevede lutilizzo dellinterfaccia utente fornita dalle Drive API e quella diretta.
In corrispondenza della selezione dellopzione Import Locations richiamiamo il
seguente metodo:

private void importFenceSession() {

// We Start the CreateFileActivityBuilder


final IntentSender intentSender = Drive.DriveApi
.newOpenFileActivityBuilder()
.setActivityTitle(getString(R.string.action_drive_import))
.build(mGoogleApiClient);
try {
getActivity().startIntentSenderForResult(intentSender,
REQUEST_CODE_LOAD_SESSION, null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG_LOG, Error , e);
}
}


Si tratta di un metodo analogo a quello visto nel caso della creazione di un File. In
questo caso, per, lIntentSender quello relativo alla creazione dellinterfaccia utente

per la selezione di un file e non per la creazione del file. In questo caso non viene
richiesto alcun metadato, bens un titolo ed, eventualmente (non il nostro caso), un
Folder di partenza da proporre allutente. Lesecuzione di queste righe di codice
visualizza linterfaccia rappresentata nella Figura 4.9, per la selezione di un File.
La modalit di gestione del risultato quella classica, ovvero quella che abbiamo
implementato nel metodo di callback onActivityResult():

if (REQUEST_CODE_LOAD_SESSION == requestCode) {

if (Activity.RESULT_OK == resultCode) {
final DriveId openedFileId = data.getParcelableExtra(
CreateFileActivityBuilder.EXTRA_RESPONSE_DRIVE_ID);
readFenceSessionData(openedFileId);
}
}

Figura 4.9 Interfaccia per la selezione di un Drive File.

Dopo aver controllato, attraverso il resultCode, che lutente abbia effettivamente


selezionato un File, otteniamo il suo DriveId attraverso un opportuno Extra nellIntent di
ritorno. A questo punto passiamo la palla a un altro metodo di utilit, che si
preoccuper delleffettiva lettura del contenuto, ovvero il metodo che abbiamo
chiamato readFenceSessionData(). La logica da seguire prevede innanzitutto la chiamata
del seguente metodo, la cui responsabilit quella di fornire il riferimento alloggetto
Contents associato al File:

fenceSessionFile.open(mGoogleApiClient, DriveFile.MODE_READ_ONLY,

new DriveFile.DownloadProgressListener() {
@Override
public void onProgress(long l, long l2) {

Log.d(TAG_LOG, l + -> + l2);


}
}).setResultCallback(contentsOpenedCallback);


Notiamo come il primo parametro sia lormai immancabile oggetto di tipo
GoogleApiClient, mentre il secondo sia una costante che indica la modalit di accesso al
, che ora solamente in lettura. Questo ci permetter di accedere al contenuto del

File

attraverso un oggetto di tipo InputStream. Il terzo parametro unimplementazione

File

opzionale dellinterfaccia DriveFile.DownloadProgressListener, la cui esistenza dovuta al


fatto che il file richiesto potrebbe non essere disponibile in locale e quindi richiedere
unoperazione di download, la quale richiede tempo. Attraverso questa interfaccia
viene data lopportunit allo sviluppatore di creare una sorta di ProgressDialog che
indichi lo stato del caricamento. Lultimo parametro di fondamentale importanza e
rappresenta la solita implementazione dellinterfaccia
ResultCallback<DriveApi.DriveContentsResult> che nel nostro caso avr la responsabilit
della lettura e del parsing del contenuto del File. Il contenuto degno di nota di questa
implementazione descritto dalle seguenti righe di codice:

DriveContents contents = contentsResult.getDriveContents();
// We read the data from the Contents
try {

String contentAsString = IOUtils.toString(contents.getInputStream());


FenceDriveUtil.importFenceSessionJson(getActivity(), contentAsString);
} catch (Exception e) {

e.printStackTrace();
Toast.makeText(getActivity(), R.string.action_drive_load_failed,
Toast.LENGTH_SHORT).show();
}


Dopo aver ottenuto il riferimento alloggetto DriveContents, ne leggiamo il contenuto
e quindi ne svolgiamo il parsing e linserimento nel DB attraverso il metodo
importFenceSessionJson() della nostra classe di utilit FenceDriveUtil, che il lettore potr
consultare direttamente nel progetto. Ecco che la funzione di import ci permette di

selezionare un file precedentemente esportato e di caricarlo nuovamente nel nostro


DB.
NOTA
di fondamentale importanza sottolineare come questi metodi debbano il pi delle volte essere
eseguiti allinterno di un Thread diverso da quello dellinterfaccia utente. Nel nostro caso abbiamo
definito la logica allinterno di metodi di utilit dello stesso Fragment che visualizza lelenco di
FenceSession, ma solamente per comodit di lettura. Invitiamo, come esercizio, a spostare le
operazioni di import ed export di una FenceSession in unopportuna implementazione di
IntentService.

Qualora non volessimo utilizzare linterfaccia di selezione del file, dovremmo


necessariamente conoscerne il nome e quindi utilizzare il metodo diretto, che non si
differenzia di molto da quello appena esposto. In questo caso dobbiamo avere un
metodo alternativo per ottenere lo stesso DriveId ottenuto dallinterfaccia utente. Uno
di questi consiste nel conoscere a priori la stringa che identifica il file, cosa che noi
abbiamo inserito nella costante DRIVE_ID. La precedente funzionalit diventa in questo
caso banale, ovvero:

private void importDirectFenceSession() {

final DriveId fileId = DriveId.decodeFromString(DRIVE_ID);


readFenceSessionData(fileId);
}


Conoscendo lidentificatore del File sotto forma di String possibile utilizzare il
seguente metodo per ottenerne direttamente il riferimento:

public static DriveId decodeFromString(String s)

La gestione dei Folder


Come abbiamo visto allinizio del capitolo, le Drive API hanno definito le
DriveResource come generalizzazione sia di DriveFile sia di DriveFolder. Sappiamo che un
molto simile a un File, e questo uno dei motivi di questa generalizzazione.

Folder

Sebbene la nostra applicazione non richieda una gestione particolare dei Folder,
vogliamo dare una breve descrizione di quello che possibile fare. Innanzitutto
possibile accedere al Folder principale (la root) attraverso il seguente metodo
delloggetto Drive.DriveApi:

public abstract DriveFolder getRootFolder(GoogleApiClient apiClient)


Analogamente a quanto visto per i File, la creazione di un Folder prevede la
chiamata di un metodo che richiede dei metadati. Il metodo il seguente e restituisce
un oggetto di tipo PendingResult<DriveFolder.DriveFolderResult>, al quale possiamo
collegare unimplementazione dellinterfaccia di callback:

public abstract PendingResult<DriveFolder.DriveFolderResult>

createFolder(GoogleApiClient apiClient, MetadataChangeSet changeSet)



Si tratta di un metodo della classe DriveFolder per cui possibile senza problemi
creare dei Folder allinterno di altri. Con un meccanismo analogo possiamo utilizzare
il metodo per la creazione di un File nel Folder associato alloggetto DriveFolder sul
quale il metodo viene richiamato:

public abstract PendingResult<DriveFolder.DriveFileResult> createFile

(GoogleApiClient apiClient, MetadataChangeSet changeSet, DriveContents


contents)

Pi interessante sicuramente la presenza del seguente metodo, che permette di
ottenere il riferimento a tutti gli elementi contenuti nel Folder stesso:

public abstract PendingResult<DriveApi.MetadataBufferResult>

listChildren (GoogleApiClient apiClient)



In questo caso interessante osservare come loggetto ottenuto sia di tipo
MetadataBufferResult, che non altro che un particolare Iterable che restituisce
unimplementazione di un Iterator Pattern. In pratica, attraverso questo oggetto
possibile ottenere il riferimento a un Iterator delle informazioni relative al contenuto
di un Folder, sotto forma di Metadata. Questultima interfaccia unastrazione degli
oggetti che abbiamo visto per la definizione dei metadati di un File o di un Folder. Si
tratta dello stesso tipo di oggetto che si ottiene attraverso il seguente metodo, il quale
permette di eseguire delle ricerche in un Folder in base a criteri incapsulati in
unimplementazione dellinterfaccia Query, che vedremo comunque successivamente.
In questo caso un oggetto di tipo MetadataBufferResult ci permetter di accedere ai
risultati della query stessa:

public abstract PendingResult<DriveApi.MetadataBufferResult>

queryChildren (GoogleApiClient apiClient, Query query)


Eseguire la ricerca di un File


Un framework per la gestione dei File che si rispetti deve necessariamente
fornire strumenti per ricerche e questo il caso delle Drive API, attraverso diverse
implementazioni dellinterfaccia Query. Come per altre classi viste in precedenza,
anche in questo caso si utilizza unimplementazione del Builder Pattern che si
chiama Query.Builder, di cui possibile creare unistanza e quindi aggiungere
specifiche implementazioni di unaltra interfaccia di nome Filter. Se volessimo, per
esempio, ricercare un File per nome sar sufficiente utilizzare le seguenti righe di
codice:

Query query = new Query.Builder()

.addFilter(Filters.eq(SearchableField.TITLE, fileToSearch))
.build();

Poi potremo utilizzare loggetto ottenuto, come parametro del seguente metodo
della classe Drive.DriveApi:

public abstract PendingResult<DriveApi.MetadataBufferResult>

query (GoogleApiClient apiClient, Query query)



Il tipo restituito da questo metodo ancora unimplementazione dellinterfaccia
PendingResult, riferita questa volta a oggetti di tipo DriveApi.MetadataBufferResult. Come
accennato in precedenza, lo stesso procedimento si pu seguire per la ricerca di File o
allinterno di un particolare Folder, sul quale possibile richiamare il seguente

Folder

metodo:

public abstract PendingResult<DriveApi.MetadataBufferResult>

queryChildren (GoogleApiClient apiClient, Query query)



bene precisare come i metodi di callback nei due casi siano diversi tra loro.
Come primo esempio supponiamo di voler ricercare tutti i file di tipo JSON il cui

nome inizia per FenceSession, che ricordiamo essere il nome di default dei file relativi
ai vari percorsi. In questo caso baster eseguire il metodo che abbiamo chiamato
searchFenceSessionFiles(), il quale inizialmente non fa altro che creare la seguente Query:

final Query query = new Query.Builder()

.addFilter(Filters.contains(SearchableField.TITLE,
DEFAULT_DRIVE_TITLE))
.addFilter(Filters.eq(SearchableField.TRASHED, false))
.addFilter(Filters.eq(SearchableField.MIME_TYPE,
JSON_MIME_TYPE))
.build();

Notiamo come le implementazioni di Filter siano disponibili attraverso una classe
di nome Filters e come queste vengano aggiunte secondo il meccanismo del chaining.
La prima ci permette di selezionare solamente i file che contengono FenceSession nel
proprio nome. La seconda ci permette di filtrare solamente quelli di Mime Type
. La terza ci permette di selezionare solamente quelli che non sono stati

application/json

cancellati e quindi messi nella cartella di Trash.


NOTA
Il lettore potr consultare nella documentazione ufficiale le implementazioni di Filter disponibili.
Andando nel dettaglio potr anche verificare come linterfaccia Filter sia una tagging interface,
ovvero non contenga la definizione di alcuna operazione. Questo ci impedisce di crearne
implementazioni custom.

Il passo successivo consiste nel semplice utilizzo della precedente Query,


attraverso le seguenti righe di codice:

Drive.DriveApi.query(mGoogleApiClient, query)

.setResultCallback(new ResultCallback<DriveApi.MetadataBufferResult>() {
@Override
public void onResult(DriveApi.MetadataBufferResult
metadataBufferResult) {
for (Metadata metadata : metadataBufferResult

.getMetadataBuffer()) {
Log.i(TAG_LOG, metadata.getTitle() +
+ metadata.getCreatedDate());
}
}
});
}


Unico aspetto degno di nota lutilizzo che facciamo del parametro di tipo
DriveApi.MetadataBufferResult ricevuto dalloperazione di callback. Esso contiene un
altro oggetto di tipo MetadataBuffer, che un Iterable che ci permette di iterare su tutti
gli elementi di tipo Metadata in esso contenuti.
Non ci dilunghiamo su come eseguire delle Query su un oggetto di tipo DriveFolder
per limitare la ricerca in un singolo Folder. Pi interessante invece la possibilit di
impostare un ordinamento nei risultati attraverso opportune implementazioni
dellinterfaccia SortOrder, la cui creazione riprende quella degli oggetti di tipo Query.
Per ordinare i precedenti risultati per nome e per data di creazione, sarebbe
sufficiente definire il seguente oggetto:

final SortOrder sortOrder = new SortOrder.Builder()

.addSortAscending(SortableField.TITLE)
.addSortDescending(SortableField.CREATED_DATE)
.build();

Notiamo la possibilit di ordinare in modo crescente o decrescente i campi
specificati attraverso opportune costanti della classe SortableField. Loggetto creato
pu quindi essere aggiunto alla query attraverso il metodo evidenziato di seguito:

final Query query = new Query.Builder()

.addFilter(Filters.contains(SearchableField.TITLE,
DEFAULT_DRIVE_TITLE))

.addFilter(Filters.eq(SearchableField.TRASHED, false))
.addFilter(Filters.eq(SearchableField.MIME_TYPE,
JSON_MIME_TYPE))
.setSortOrder(sortOrder)
.build();

Ultimissima considerazione riguarda la possibilit di paginare i risultati qualora
questi fossero in un numero tale da richiederne lesigenza. Senza entrare troppo nel
dettaglio, ogni oggetto di tipo MetadataBuffer dispone del seguente metodo:

public String getNextPageToken()


Questo metodo restituisce una String, il cui significato quello di token o
identificatore da passare alla query da eseguire per ottenere leventuale pagina
successiva di risultati. In sintesi si eseguono questi passi:
1. Si esegue una Query.
2. Si itera su tutti i risultati. Al termine delliterazione si richiama il metodo
getNextToken(), per verificare se esistono altri risultati. Se si ottiene null allora ci si
ferma.
3. Se esiste un token, si esegue nuovamente la Query, cui per si passa il token
attraverso il metodo setPageToken().
4. Attraverso queste API possiamo eseguire delle ricerche delle risorse in Drive, in
modo molto semplice.

Download automatico di File pinnable


Allinizio del capitolo abbiamo accennato al fatto che i file gestiti con le Drive API
possono essere sincronizzati automaticamente. Questo significa che tutti i dispositivi
che ne hanno diritto avranno sempre a disposizione lultima versione del file e il tutto
avverr in modo trasparente allapplicazione e, soprattutto, a noi sviluppatori. Questa
funzionalit non vale per tutti i file, ma solamente per quelli che si indicano in modo
esplicito come Pinnable.
NOTA
Mantenere tutti i file sempre aggiornati potrebbe portare a generare un traffico dati elevato e
quindi anche a costi elevati, a seconda del piano telefonico scelto. Il tutto sarebbe anche costoso
dal punto di vista dellutilizzo della batteria.

Impostare un file come Pinnable molto semplice e consiste nellutilizzare il


seguente metodo della classe MetadataChangeSet.Builder:

public MetadataChangeSet.Builder setPinned(boolean pinned)


Analogamente possiamo sapere se un file Pinnable o meno attraverso il seguente
metodo, questa volta della classe MetadataChangeSet:

public Boolean isPinned()


Per aggiornare lo stato di Pinnable di un file dovremmo eseguire le seguenti
istruzioni, nelle quali si ottiene il riferimento del DriveFile, si crea un oggetto di
modifica dei metadati, che poi si applica attraverso il metodo updateMetadata() nella
modalit ormai nota:

DriveFile file = Drive.DriveApi.getFile(getGoogleApiClient(), mFileId);
MetadataChangeSet changeSet = new MetadataChangeSet.Builder()

.setPinned(true)
.build();
file.updateMetadata(getGoogleApiClient(), changeSet)

.setResultCallback(pinningCallback);

Dati privati di unapplicazione


Nel nostro progetto abbiamo salvato le informazioni relative alle FenceSession in una
cartella che avevamo creato manualmente o che avremmo potuto creare da
programma attraverso alcuni dei metodi descritti. In realt le Drive API ci mettono a
disposizione degli strumenti che ci permettono di salvare dei file caratteristici della
nostra applicazione. Si tratta in sostanza di un folder nascosto che privato di ogni
singola applicazione nel Drive associato a ogni singolo utente. Si tratta di quello che si
chiama App Folder, il quale viene automaticamente cancellato nel momento in cui
lutente disinstalla lapplicazione dal proprio dispositivo. Il primo passo per utilizzare
questa funzionalit consiste nel richiedere il corrispondente permesso, attraverso la
definizione evidenziata nel codice che abbiamo gi utilizzato in precedenza:

mGoogleApiClient = new GoogleApiClient.Builder(getActivity())

.addApi(Drive.API)
.addScope(Drive.SCOPE_FILE)
.addScope(Drive.SCOPE_APPFOLDER)
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedListener)
.build();

Accedere allApp Folder a questo punto la cosa pi semplice del mondo, in
quanto sufficiente richiamare il seguente metodo sulloggetto Drive.DriveApi e poi
utilizzare loggetto di tipo DriverFolder ottenuto come un qualunque altro Folder.

public abstract DriveFolder getAppFolder(GoogleApiClient apiClient)


Possiamo eseguire delle ricerche o creare dei file o altri Folder al suo interno,
cancellare o modificare file e cos via.
Infine potrebbe essere utile sapere se un File o un altro Folder contenuto in questa
App Folder oppure no. Si tratta di uninformazione accessibile attraverso il seguente
metodo delloggetto Metadata associato.


public boolean isInAppFolder()

Conclusioni
In questo capitolo ci siamo occupati delle Google Drive API, che ora sono
disponibili, attraverso i Google Play services, in una versione ottimizzata per la
piattaforma Android. Dopo una descrizione generale abbiamo visto come creare un
file attraverso uninterfaccia grafica predefinita o direttamente attraverso alcune righe
di codice. Abbiamo visto anche come eseguire loperazione inversa, ovvero accedere
a un file estrapolandone il contenuto. Nello specifico, abbiamo utilizzato questi
strumenti per rendere persistenti in Google Drive le informazioni relative a una
FenceSession, per poi ripristinarla successivamente. Abbiamo continuato con la
descrizione degli strumenti per lesecuzione di ricerche e per la gestione dei Folder,
per concludere con la definizione di App Folder che ricordiamo non essere altro che
un Folder visibile solamente a un particolare utente, che utilizza una data applicazione.
Il salvataggio delle informazioni di sessione rappresentano, se vogliamo, un primo
modo di condivisione. Nel prossimo capitolo descriveremo un altro insieme di API
che si occupano della parte Social dei Google Play services, ovvero le Google Plus
API.

Capitolo 5

Integrazione con Google Plus

Chiunque si occupi della programmazione Android sa cos Google Plus (o


Google+). Si tratta, molto sinteticamente, di un social network che permette di
raggruppare i propri collegamenti allinterno di cerchie. Esiste la cerchia degli
amici, quella dei familiari, quella dei colleghi di lavoro e cos via. Come per altri
social network (per esempio Facebook) esiste una timeline, nella quale possibile
condividere una serie innumerevole di informazioni, di tipo anche molto diverso tra
loro. Un aspetto molto importante degli strumenti di questo tipo quello legato
allautenticazione che, nella stragrande maggioranza dei casi, al giorno doggi
utilizza il protocollo OAUTH 2.0 (http://oauth.net/2/). Senza entrare in dettagli, per i
quali servirebbe un libro intero, possiamo dire che si tratta di un meccanismo di
autenticazione a token: se un utente si vuole autenticare a un particolare servizio,
deve inviare al server di autenticazione non solo le proprie credenziali, ma anche un
indicatore dei servizi cui vuole accedere. Potremmo, per esempio, inviare le nostre
credenziali per accedere a una serie di servizi di pura visualizzazione oppure a servizi
che prevedono linvio di dati da parte dellapplicazione. A seguito delle informazioni
inviate dal client, il server verifica le credenziali e quindi compone un token, cui
associa non solo il fatto che lutente stato riconosciuto, ma anche linsieme dei
servizi cui ha accesso. Linsieme di funzioni a cui un particolare token ha accesso si
indica con il termine Scope. Cosa ci facciamo con questo token? Il token ci permette di
accedere ai servizi senza dover ripetere la procedura di autenticazione. In questo
modo le singole applicazioni possono memorizzare questo token e utilizzarlo
successivamente per accedere ai vari servizi fino a che il token stesso non viene
invalidato; cosa che costringe il client e ripetere il processo di autenticazione. Il
processo che abbiamo descritto molto interessante qualora volessimo implementare
una funzione di login per la nostra applicazione. Una possibile soluzione potrebbe
essere quella di realizzare un nostro server di autenticazione che implementa, per
esempio, la verifica dellutente in base a username e password. Questo non sarebbe
neanche un problema, se in fase di registrazione non dovessimo anche costringere
lutente a inserire le proprie informazioni, come la sua email, una sua foto o altre
ancora pi riservate, sensibili, come il numero di telefono o lindirizzo di casa.
invece molto pi semplice affidarsi a un sistema esterno, come appunto Google+,
delegare a lui la fase di autenticazione e quindi richiedere in modo automatico le

informazioni di cui abbiamo bisogno attraverso le opportune API. Lutente dovr solo
acconsentire al fatto che la nostra applicazione acceda a queste informazioni, per
vedere precompilate, automaticamente, le informazioni aggiuntive.
Nel caso della nostra applicazione, questa funzionalit ci permetter di avere un
nome e quindi un owner diverso da quello di default, e, soprattutto, ci permetter di
accedere ai nostri amici per condividere le informazioni delle FenceSession.

Eseguiamo la login con Google+


Come abbiamo appena detto, la prima funzionalit che vogliamo implementare
quella relativa al login, attraverso i meccanismi offerti dalle Google Plus API. Prima
di scrivere del codice abbiamo comunque bisogno di una parte di amministrazione.
Andiamo alla nostra Google Console e abilitiamo questa nuova funzionalit. A
questo punto dovremmo gi avere un Client Id creato in corrispondenza della
gestione di Google Drive, per cui non dobbiamo fare altro che selezionare la voce
APIs nel menu a sinistra sotto lo voce APIs & auth e quindi cercare e abilitare quelle
relative a Google+, come possiamo vedere nella Figura 5.1.

Figura 5.1 Abilitazione delle API per Google+.

Prima di procedere con la scrittura del codice dobbiamo ricordarci di aggiungere la


definizione della dipendenza con la relativa libreria nel file build.gradle come
evidenziato di seguito:

dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.android.support:appcompat-v7:21.0.3
compile com.android.support:support-v4:21.0.3
compile com.google.android.gms:play-services-base:6.5.87
compile com.google.android.gms:play-services-location:6.5.87
compile com.google.android.gms:play-services-maps:6.5.87
compile com.google.android.gms:play-services-drive:6.5.87
compile com.google.android.gms:play-services-plus:6.5.87
}


Il passo successivo consiste nellimplementazione di una Activity con la logica di
inizializzazione delloggetto di tipo GoogleApiClient, come nei capitoli precedenti. In

questo caso, per, Android Studio ci viene in aiuto, in quanto permette la creazione
automatica di tutto il codice necessario, semplicemente scegliendo lopzione
opportuna. Per fare questo sufficiente selezionare il package di destinazione (che
abbiamo chiamato plus) con il tasto destro del mouse e quindi selezionare la voce New
, come indicato nella Figura 5.2:

> Google > Google Play Services Activity

Verr visualizzata la schermata rappresentata nella Figura 5.3, nella quale notiamo
come venga richiesto il nome della classe, il relativo package e quali servizi abilitare.

Figura 5.2 Creazione automatica di una Activity per i Google Play services.

Figura 5.3 Inseriamo il nome dellActivity e i servizi richiesti.


NOTA

Notiamo come tra i servizi disponibili vi sia anche quello di Google Drive, che abbiamo
implementato senza lausilio di questo tool, anche perch eravamo allinterno di un Fragment.

Non ci resta che fare clic sul pulsante Finish per generare la classe
, che andiamo a modificare leggermente e a descrivere nelle parti

GooglePlusLoginActivity

fondamentali. A parte qualche commento aggiunto, definiamo le implementazioni


delle interfacce di callback ConnectionCallbacks e OnConnectionFailedListener attraverso
delle variabili distanza, invece di farle implementare direttamente dalla nostra classe
di Activity. Questo ci permette di focalizzare meglio i metodi richiamati in
corrispondenza degli eventi di connessione o di errore.
NOTA
In genere sempre bene usare la composizione invece dellereditariet (composition over
inheritance). Per ereditariet qui si intende quella tra classi (implementation inheritance) e non
quella tra una classe e uninterfaccia (interface inheritance).

La parte pi interessante quella relativa alla modalit con cui viene inizializzato
loggetto di tipo GoogleApiClient, attraverso le seguenti righe di codice nel metodo
:

onCreate()


mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Plus.API)
.addScope(Plus.SCOPE_PLUS_LOGIN)

.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedLister)
.build();

Nelle righe evidenziate notiamo come le API richieste siano quelle relative a
Google Plus (utilizzo della costante Plus.API) e che la funzionalit richiesta sia quella
relativa alla funzione di login (attraverso Plus.SCOPE_PLUS_LOGIN). bene ricordare che
per utilizzare queste API necessaria la seguente definizione nel file di
configurazione AndroidManifest.xml per accedere alle informazioni del proprio account.

<uses-permission android:name="android.permission.GET_ACCOUNTS" />


Per quanto riguarda la nostra applicazione abbiamo deciso di creare una funzione
Social cui possibile arrivare attraverso unopportuna voce nel menu delle opzioni
dellattivit principale, descritta dalla classe MainActivity. Selezionando questa voce

non facciamo altro che lanciare lattivit descritta dalla classe GooglePlusLoginActivity,
alla quale sar affidata la responsabilit di inizializzare loggetto GoogleApiClient
necessario per la gestione di tutte le funzioni di Google Plus. La prima funzionalit
che vogliamo gestire quella di semplice login e visualizzazione delle informazioni
del profilo. A tale scopo abbiamo creato un layout descritto dal file
activity_plus_login.xml, il quale contiene nella parte superiore un pulsante che
inizialmente sar di login, come possiamo vedere nella Figura 5.4.

Figura 5.4 Pulsante di Login a Google+.

Si tratta di un pulsante fornito dalle stesse API, e che si pu visualizzare attraverso


il seguente frammento di codice nel documento di layout:

<com.google.android.gms.common.SignInButton

android:id=@+id/plus_login_button
android:layout_width=wrap_content
android:layout_height=wrap_content
android:layout_gravity=center_horizontal />

Nel nostro caso abbiamo utilizzato la versione estesa del pulsante che abbiamo
impostato, dopo aver ottenuto un riferimento al pulsante, attraverso la seguente
istruzione:

signInButton.setSize(SignInButton.SIZE_WIDE);


bene sottolineare come questo pulsante non sia agganciato automaticamente alla
funzione di login, la quale dovr essere richiamata da codice attraverso le seguenti
istruzioni, dopo aver ottenuto il riferimento alloggetto di tipo GoogleApiClient nel
modo ormai consueto:


signInButton.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
mGoogleApiClient.connect();
}
});


Come possiamo osservare, la pressione del pulsante di login richiama il metodo
connect(), che visualizza la schermata rappresentata nella Figura 5.5, nella quale viene
data la possibilit di scegliere tra usare un account esistente oppure creare un nuovo
account.
Una volta selezionato uno degli account, il sistema provveder automaticamente a
visualizzare una schermata con alcune informazioni dellaccount selezionato, insieme
ad altre informazioni dellapplicazione che intende utilizzare le informazioni. Si tratta
di una schermata che, di fatto, richiede le necessarie autorizzazioni per laccesso ai
dati, come possiamo vedere nella Figura 5.6.
A questo punto possibile selezionare il pulsante Sign In e quindi acconsentire
allutilizzo dei dati in Google+ da parte della nostra applicazione.

Figura 5.5 Selezione dellaccount da usare per la login.

Figura 5.6 Richiesta delle autorizzazioni.

Nel caso della nostra applicazione, non facciamo altro visualizzare la schermata
rappresentata nella Figura 5.7, nella quale visualizziamo alcune delle informazioni
ottenute nel modo che descriveremo subito dopo.
Notiamo inoltre come il pulsante di login si sia trasformato nel pulsante di logout.
A tale proposito bene notare come vi sia la necessit di memorizzare lesito
delloperazione di login e quindi rendere persistente il fatto che lutente sia
connesso o meno. Questo importante per due motivi. Il primo molto semplice e
consiste nella semplice regola che permette di visualizzare il pulsante di login o
quello di logout. Il secondo pi importante e permette di decidere quando
richiamare listruzione di connessione, ovvero:

mGoogleApiClient.connect();

Se lutente non connesso, tale istruzione va richiamata in corrispondenza della


pressione del pulsante di login.

Figura 5.7 Visualizzazioni delle informazioni di Google+.

Se invece lutente gi connesso, listruzione va eseguita subito dopo la


visualizzazione del nostro layout, e quindi in corrispondenza del metodo onStart()
della classe GooglePlusLoginActivity. Per memorizzare lo stato di connesso o meno
abbiamo creato la classe UserModel, la quale permette di memorizzare, attraverso le
, alcune informazioni sullutente connesso, tra cui username o

SharedPreferences

eventuale token. Alla luce di queste considerazioni, il metodo onStart() diventa quindi
il seguente:

protected void onStart() {

super.onStart();

if (mGoogleApiClient == null) {
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Plus.API)
.addScope(Plus.SCOPE_PLUS_LOGIN)
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedLister)
.build();
}
if (UserModel.get(this).isLogged()) {
mGoogleApiClient.connect();
}
}


Notiamo come venga utilizzata la classe UserModel per lavvio o meno della
connessione. Sia nel caso in cui la connessione venga avviata a seguito della
pressione del pulsante di login, sia qualora venga avviata perch gi connessi, il
metodo di callback richiamato in caso di successo quello riportato di seguito:

public void onConnected(Bundle bundle) {
final String username = Plus.AccountApi
.getAccountName(mGoogleApiClient);

UserModel.get(getApplicationContext()).login(username);
UI.findViewById(GooglePlusLoginActivity.this,
R.id.plus_login_button).setVisibility(View.GONE);
UI.findViewById(GooglePlusLoginActivity.this,
R.id.plus_logout_button).setVisibility(View.VISIBLE);
showPersonData();
}


Notiamo innanzitutto come la nostra interfaccia di accesso alle informazioni di
Google+ sia rappresentata dalloggetto Plus.AccountApi, un oggetto di tipo Account del

che permette di accedere a tutte le informazioni di

package com.google.android.gms.plus

gestione del nostro account, come quella che abbiamo evidenziato, che fornisce il
nome dellaccount stesso.
NOTA
bene fare attenzione che in Android esiste unaltra classe Account, gestita da un componente
che si chiama AccountManager. Si tratta di classi che fanno parte delle API per la gestione degli
account, come possibile vedere nelle impostazioni di ciascun dispositivo.

Linformazione relativa al nome dellaccount viene passata al metodo login() della


nostra classe UserModel, la quale lo rende persistente e quindi ne memorizza lo stato di
connesso. Le istruzioni seguenti permettono di nascondere il pulsante di login e
visualizzare quello di logout, per poi richiamare il metodo showPersonData() per
visualizzare alcune informazioni dellutente connesso. Si tratta di un metodo molto
semplice, che non fa altro che visualizzare il Fragment descritto dalla classe
nella parte inferiore del display.

ShowPersonFragment


private void showPersonData() {

final ShowPersonFragment personFragment = new ShowPersonFragment();


mCurrentFragment = personFragment;
getSupportFragmentManager().beginTransaction()
.replace(R.id.plus_anchor_point, personFragment)
.commit();
}


Si tratta di un Fragment che visualizza alcune informazioni dellutente, rappresentate
da un oggetto di tipo Person cui si accede attraverso le istruzioni che abbiamo inserito
nel seguente metodo, che merita ulteriori spiegazioni.

public Person getPerson() {

if (mGoogleApiClient != null) {
final Person currentPerson = Plus.PeopleApi
.getCurrentPerson(mGoogleApiClient);
return currentPerson;

} else {
return null;
}
}


Innanzitutto notiamo come questa volta loggetto utilizzato sia Plus.PeopleApi e il
metodo getCurrentPerson(). In generale si tratta di un oggetto di tipo People, il quale
permette di accedere a informazioni relative alle persone che fanno parte delle
cerchie di un utente; tra queste c anche lutente stesso. Tutte le informazioni
relative allutente o ai suoi amici vengono incapsulate allinterno di oggetti di tipo
Person. Ma perch non passare direttamente al Fragment loggetto Person? Il problema
che loggetto Person non disponibile immediatamente, ma necessita
dellinizializzazione delloggetto di tipo GoogleApiClient. Per legare il tutto al ciclo di
vita dellActivity definita innanzitutto la seguente interfaccia:

public static interface PersonProvider {

/**
* @return The Person to show
*/
Person getPerson();
}


Questa stata implementata dalla classe GooglePlusLoginActivity e quindi utilizzata
dal Fragment stesso allinterno del proprio metodo onAttach() nel seguente modo:

@Override
public void onAttach(Activity activity) {

super.onAttach(activity);
if (activity instanceof PersonProvider) {
mPersonProvider = (PersonProvider) activity;
}
}


Il Fragment disporr delloggetto Person di cui visualizzare le informazioni solamente
dopo che tali informazioni verranno rese disponibili dallattivit, a seguito del
proprio ciclo di vita e dellinizializzazione delloggetto di tipo GoogleApiClient.
Il Fragment descritto dalla classe ShowPersonFragment molto semplice e non fa altro che
visualizzare le informazioni contenute nelloggetto Person, che sono di vario tipo. La
prima informazione di cui ci siamo occupati relativa allimmagine dellutente:

if (person.hasImage() && person.getImage().hasUrl()) {

final String imageUrl = person.getImage().getUrl();


Picasso.with(getActivity())
.load(imageUrl).into(mPictureImageView);
}


Come per le altre propriet, solitamente vi un metodo del tipo hasXXX() che
restituisce true solo se loggetto dispone della corrispondente propriet accessibile
attraverso il metodo getXXX(). In questo caso abbiamo verificato lesistenza
dellimmagine e quindi del corrispondente Uri, per visualizzarlo attraverso una
libreria che si chiama Picasso (http://square.github.io/picasso/). Senza entrare nel
dettaglio di come funzioni questa libreria, diamo invece una breve descrizione di
come utilizzarla nel progetto. Lo stesso procedimento varr anche per altre librerie.
Innanzitutto selezioniamo il progetto con il tasto destro del mouse e selezioniamo la
voce Open Module Settings rappresentata nella Figura 5.8 ottenendo la visualizzazione
della schermata rappresentata nella Figura 5.9, nella quale possiamo osservare
lelenco delle dipendenze del nostro progetto con eventuali altri moduli o librerie.

Figura 5.8 Visualizziamo le impostazioni del nostro progetto.

Notiamo come nella parte destra sia stato selezionato il progetto, mentre nella parte
superiore sia stata selezionata la scheda relativa alle dipendenze.
A questo punto selezioniamo il pulsante + in basso a sinistra scegliendo lopzione
rappresentata nella Figura 5.10 per aggiungere una nuova libreria.
A questo punto si apre una finestra che ci permette di fare una ricerca della libreria
in base al nome. Si tratta di una ricerca in un repository Maven
(http://maven.apache.org/), che contiene moltissime librerie non solo di Android, ma del
mondo Java in genere. Scriviamo il nome Picasso, ottenendo quanto rappresentato
nella Figura 5.11:

Figura 5.9 Elenco delle dipendenze del nostro progetto.

Figura 5.10 Aggiunta alle dipendenze di una nuova libreria.

Figura 5.11 Ricerchiamo Picasso tra le librerie disponibili.

Non ci resta che selezionare la libreria indicata, per vederla visualizzata tra le
dipendenze del nostro progetto (Figura 5.12). bene sottolineare come questa voce
venga aggiunta automaticamente al file di configurazione di Gradle, il quale contiene
ora la seguente definizione:

dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.android.support:appcompat-v7:21.0.3
compile com.android.support:support-v4:21.0.3
compile com.google.android.gms:play-services-base:6.5.87
compile com.google.android.gms:play-services-location:6.5.87
compile com.google.android.gms:play-services-maps:6.5.87
compile com.google.android.gms:play-services-drive:6.5.87
compile com.google.android.gms:play-services-plus:6.5.87
compile com.squareup.picasso:picasso:2.3.4
}

Figura 5.12 Picasso aggiunto come dipendenza.

Una volta importata, questa libreria molto semplice da usare e permette di gestire
tutta la parte di caricamento asincrono di unimmagine e la relativa visualizzazione
allinterno di una ImageView, con il seguente codice:

Picasso.with(getActivity()).load(imageUrl).into(mPictureImageView);


Tornando alla nostra applicazione, possiamo notare come lo stesso meccanismo sia
stato utilizzato per visualizzare altre informazioni, come quella relativa allultimo
luogo in cui si abitato, al display name, fino al campo About Me del profilo.
Una volta che ci siamo connessi e abbiamo visualizzato le nostre informazioni,
vogliamo poterci disconnettere. Per farlo basta utilizzare il seguente metodo della
classe Plus.AccountApi, il quale ha come parametro restituito una nostra vecchia
conoscenza, ovvero un oggetto di tipo PendingResult<Status>.

public abstract PendingResult<Status> revokeAccessAndDisconnect (GoogleApiClient
googleApiClient)


Come spesso avviene, si tratta di unoperazione asincrona, che richiede quindi un
meccanismo di notifica del risultato; nel nostro caso lo abbiamo implementato nel
seguente modo in corrispondenza della selezione del pulsante di logout:

signOutButton.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
// We do logout

Plus.AccountApi.revokeAccessAndDisconnect(mGoogleApiClient)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
// We switch the buttons
signInButton.setVisibility(View.VISIBLE);
signOutButton.setVisibility(View.GONE);
UserModel.get(GooglePlusLoginActivity.this).logout();
mGoogleApiClient.disconnect();
clearFragment();
} else {
Toast.makeText(GooglePlusLoginActivity.this,
R.string.plus_logout_error,
Toast.LENGTH_SHORT).show();
}
}
});
}
});


Notiamo come leffettivo logout, attraverso la chiamata del metodo logout() sul
nostro oggetto di tipo UserModel, venga eseguita solamente nel caso in cui loperazione
sia avvenuta con successo, cosa che possibile verificare attraverso loggetto di tipo
Status, ottenuto attraverso linterfaccia di callback.

Invitiamo i nostri amici


Il passo successivo quello relativo alla visualizzazione dellelenco dei nostri
amici, per poter inviare loro un invito a utilizzare la nostra applicazione. In questo
caso abbiamo incapsulato la logica in un Fragment descritto dalla classe
. Si tratta di una specializzazione della classe ListFragment, il cui

ShowFriendsFragment

non fa altro che visualizzare le informazioni contenute nel nostro modello, che

Adapter

definito nel seguente modo:



private List<Person> mPersons = new LinkedList<Person>();


Unica complicazione riguarda la gestione della selezione multipla, che abbiamo
gestito nella ListView attraverso la seguente variabile:

private Set<Person> mSelectedPersons = new HashSet<Person>();


Questa in grado di contenere gli amici selezionati; la selezione multipla resa
possibile attraverso la seguente istruzione:

getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);


Pi interessante la modalit con cui accediamo alle informazioni relative ai nostri
amici, ovvero il seguente frammento di codice, che abbiamo inserito nel metodo
onStart():

if (googleApiClient != null) {

mPersons.clear();
Plus.PeopleApi.loadVisible(googleApiClient, null)
.setResultCallback(new ResultCallback<People.LoadPeopleResult>() {
@Override
public void onResult(People.LoadPeopleResult loadPeopleResult) {
final PersonBuffer personBuffer = loadPeopleResult

.getPersonBuffer();
for (Person person : personBuffer) {
mPersons.add(person);
}
mAdapter.notifyDataSetChanged();
setListShown(true);
}
});
}


Come possiamo notare, abbiamo utilizzato un altro metodo della classe di tipo
People, accessibile attraverso loggetto Plus.PeopleApi, il quale necessita di due
parametri; il primo limmancabile GoogleApiClient, mentre il secondo un token che
serve per gestire la paginazione qualora il numero degli elementi ottenuti fosse molto
grande.

public abstract PendingResult<People.LoadPeopleResult> loadVisible

(GoogleApiClient googleApiClient, String pageToken)



NOTA
Lutilizzo del token per la paginazione analogo a quello che abbiamo descritto in
corrispondenza dei risultati di un ricerca di un file vista nel Capitolo 4, dedicato a Google Drive. Si
tratta di un pattern molto comune nellutilizzo dei Google Play services.

Nel nostro caso non facciamo altro che inserire i dati relativi agli oggetti Person dei
nostri amici allinterno della List mPersons che rappresenta il nostro modello.
Il riferimento alloggetto di tipo GoogleApiClient stato ottenuto con lo stesso
meccanismo utilizzato in precedenza per loggetto di tipo Person, ovvero definendo
uninterfaccia, che in questo caso GooglePlayClientProvider, implementata dallattivit
ospitante, ovvero dalla classe GooglePlusLoginActivity.
Da notare come il modello non venga ricreato ogni volta, ma venga solamente
svuotato attraverso il metodo clear() prima che vi vengano inseriti nuovi oggetti di

tipo Person. Questo ci permette di non ricreare ogni volta unistanza di ArrayAdapter, ma
forzarne laggiornamento attraverso la chiamata del metodo notifyDataSetChanged().
Degna di nota la parte di selezione multipla, che abbiamo gestito attraverso la
variabile mSelectedPersons definita in precedenza e loggetto di tipo CheckedTextView nel
documento di layout nel file fragment_person_item.xml.

<CheckedTextView

android:id=@+id/person_item_display_name
android:layout_width=match_parent
android:layout_height=43dp
android:checkMark=?android:attr/listChoiceIndicatorMultiple
android:gravity=center_vertical
android:paddingLeft=10dp
android:text=Large Text
android:textAppearance=?android:attr/textAppearanceLarge />

Nella classe principale abbiamo collegato la visualizzazione di questo Fragment a
una voce del menu delle opzioni, che attiva solo se lutente connesso. Il tutto
stato gestito attraverso il seguente metodo, che sappiamo essere richiamato prima di
ogni richiesta di visualizzazione del menu delle opzioni:

@Override
public boolean onPrepareOptionsMenu(Menu menu) {

final boolean isUserLogged = UserModel.get(this).isLogged();


// This is visible only if the user is logged
menu.findItem(R.id.action_social_friends).setVisible(isUserLogged);
return super.onPrepareOptionsMenu(menu);
}


Il risultato ottenuto quello rappresentato nella Figura 5.13 con lelenco dei
display name e delle immagini dei nostri amici, con la possibilit di selezione

multipla.

Figura 5.13 Visualizzazione dellelenco di amici in Google+.

Prima di proseguire con lutilizzo delle informazioni selezionate, notiamo come lo


stesso oggetto People metta a disposizione anche altre operazioni, che permettono di
ottenere le stesse informazioni relative per a specifici utenti di cui si conosce
lidentificatore:

public abstract PendingResult<People.LoadPeopleResult>

load (GoogleApiClient googleApiClient, String personIds)



Eventualmente ordinandoli in base al nome o altri criteri attraverso il seguente
metodo:

public abstract PendingResult<People.LoadPeopleResult>

loadVisible (GoogleApiClient googleApiClient, int orderBy,


String pageToken)

Interessante infine la possibilit di accedere alle persone che hanno accettato
laccesso alla nostra stessa applicazione; si tratta del metodo, che potrebbe essere
utile per la scelta degli utenti a cui condividere la propria FenceSession o altre
informazioni:

public abstract PendingResult<People.LoadPeopleResult>

loadConnected (GoogleApiClient googleApiClient)



A questo punto il passo successivo consiste nellinviare informazioni agli amici
selezionati e questo possibile attraverso loggetto PlusShare, il quale, attraverso il
corrispondente builder PlusShare.Builder, ci permette di ottenere lIntent da lanciare per
la condivisione delle informazioni. Nel nostro caso abbiamo realizzato il seguente
metodo, che viene richiamato a seguito della selezione di unopportuna voce nel
menu delle opzioni:

private void shareToFriends() {

if (mSelectedPersons.isEmpty()) {
// Toast witj message
} else {
// We put the selected users into a List
final List<Person> selectedList =
new LinkedList<Person>(mSelectedPersons);
final Intent shareIntent = new PlusShare.Builder(getActivity())
.setRecipients(selectedList)
.setType(PLAIN_TEXT_MIME_TYPE)
.setText(getString(R.string.plus_share_text))
.getIntent();

getActivity().startActivityForResult(shareIntent, SHARE_REQUEST_CODE);
}
}


Nella prima parte controlliamo semplicemente che esista almeno un utente
selezionato; in caso contrario visualizziamo un messaggio, attraverso un Toast. Nel
caso fossero selezionati pi utenti, utilizziamo il PlusShare.Builder per inviare un
messaggio di tipo testuale a un insieme di utenti, contenuti questa volta in una List. Il
metodo di build questa volta getIntent(), che restituisce un Intent che lanciamo
attraverso il metodo startActivityForResult() associato a un request code che abbiamo
definito attraverso la costante SHARE_REQUEST_CODE.
NOTA
Attenzione: anche in questo caso si richiede un accorgimento allinterno della classe
GooglePlusLoginActivity e precisamente per il metodo onActivityResult(). Questo , infatti, il
metodo che viene richiamato in corrispondenza dellinvio dellIntent di sharing che abbiamo fatto
dallinterno di un Fragment. Abbiamo infatti bisogno che venga chiamato il corrispondente
metodo nel Fragment stesso.

Notiamo come in questo caso il contenuto sia semplice testo, per cui il MimeType
semplicemente text/plain. Si tratta di un parametro di fondamentale importanza, in
quanto un valore non previsto potrebbe portare a un errore, dovuto al fatto che non
esiste alcuna Activity dellapplicazione Google+ in grado di gestirlo.
NOTA
Le schermate che vedremo di seguito potrebbero anche cambiare a seconda della versione di
Google Play services o Google+ corrente.

A questo punto non ci resta che avviare la nostra applicazione, selezionare un


insieme di amici e attivare la voce di Sharing dal menu delle opzioni. Quello che si
ottiene visualizzato nella Figura 5.14:

Figura 5.14 Schermata di condivisione.

Notiamo come vengano visualizzate le informazioni relative alle persone


selezionate, insieme ad altri suggerimenti relativi a persone che possibile
aggiungere successivamente. Selezionando il pulsante Next in alto a destra si richiama
la schermata rappresentata nella Figura 5.15, nella quale viene evidenziato il
messaggio e linsieme dei destinatari scelti.
Non ci resta che confermare loperazione attraverso il pulsante di Share, per inviare
il messaggio di condivisione che arriver come messaggio sia nella mail del
destinatario, sia nel suo profilo Google+, come possiamo vedere nella Figura 5.16.

Figura 5.15 Messaggio da condividere.

Figura 5.16 Il messaggio condiviso nel profilo Google+ del destinatario.

Esaminando pi nel dettaglio loggetto PlusShare.Builder possiamo notare come sia


anche possibile esportare un contenuto di tipo diverso, attraverso il seguente metodo,
che permette appunto di condividere un contenuto diverso come, per esempio, delle
immagini:

public PlusShare.Builder setStream (Uri streamUri)


Nel nostro caso non abbiamo immagini da condividere, se non quelle relative alle
Google Maps, che non possibile utilizzare per motivi di copyright.
NOTA
La condivisione di immagini relative alle Google Maps non permesso. Quello che si deve fare in
questo caso la trasmissione delle informazioni di location, che il destinatario pu a sua volta
utilizzare attraverso le stesse Google Maps o altre librerie.

Come dimostrazione di questa possibilit abbiamo creato un metodo che


sicuramente non strettamente legato alla nostra applicazione, ma che ci permette di
fare lo Share di una foto tra quelle disponibili nella nostra gallery. Per la scelta
dellimmagine o video abbiamo creato il seguente metodo, collegato a una voce del
menu delle opzioni:

private void launchShareImageToFriends() {

if (mSelectedPersons.isEmpty()) {
Toast.makeText(getActivity(),
R.string.plus_share_no_friends_selected_message,
Toast.LENGTH_SHORT).show();
} else {
// We choose an existing picture
Intent photoPicker = new Intent(Intent.ACTION_PICK);
photoPicker.setType(video/*, image/*);
startActivityForResult(photoPicker, PICK_PICTURE_REQUEST_CODE);
}
}


Nelle righe di codice evidenziate non abbiamo fatto nulla di particolare, se non
richiamare lattivit in grado di permetterci di scegliere un media. Le informazioni
relative al media selezionato le possiamo raccogliere nel metodo onActivityResult()
attraverso il seguente codice:

if (PICK_PICTURE_REQUEST_CODE == requestCode) {

if (Activity.RESULT_OK == resultCode) {
// We share the selected image
shareImage(data.getData());
}
}

Questo, a sua volta, richiama il metodo che si occupa delleffettiva condivisione


del media:

private void shareImage(final Uri imageUri) {

final List<Person> selectedList = new


LinkedList<Person>(mSelectedPersons);
ContentResolver cr = getActivity().getContentResolver();
String mime = cr.getType(imageUri);
Intent shareImageIntent = new PlusShare.Builder(getActivity())
.setText(getString(R.string.action_social_share_image_text))
.addStream(imageUri)
.setRecipients(selectedList)
.setType(mime)
.getIntent();
getActivity().startActivityForResult(shareImageIntent,
SHARE_REQUEST_CODE);
}


Molto interessante lutilizzo del ContentResolver per determinare il MimeType
delloggetto selezionato, da utilizzare poi per la composizione dellIntent da lanciare
per la condivisione. Notiamo come lUri del contenuto sia stato utilizzato come
parametro del metodo addStream(). Il risultato quello rappresentato nella Figura 5.17.

Figura 5.17 Condivisione di unimmagine con Google+.

Lo stesso oggetto Share.Builder ci permette di condividere le informazioni relative a


un particolare sito di cui si conosce lURL. Per farlo sufficiente utilizzare il metodo:

public PlusShare.Builder setContentUrl(Uri uri)


nel modo che abbiamo descritto nel metodo shareAuthorPage() collegato a
unapposita opzione di menu.
Notiamo come lunica differenza consista appunto nellutilizzo del metodo
evidenziato.

private void shareAuthorPage() {

if (mSelectedPersons.isEmpty()) {

Toast.makeText(getActivity(),
R.string.plus_share_no_friends_selected_message,
Toast.LENGTH_SHORT).show();
} else {
// We put the selected users into a List
final List<Person> selectedList =
new LinkedList<Person>(mSelectedPersons);
final Intent shareIntent = new PlusShare.Builder(getActivity())
.setRecipients(selectedList)
.setText(getString(R.string.plus_share_text))
.setType(PLAIN_TEXT_MIME_TYPE)
.setContentUrl(Uri.parse(http://www.massimocarli.eu))
.getIntent();
getActivity().startActivityForResult(shareIntent, SHARE_REQUEST_CODE);
}
}


Lasciamo al lettore il compito di verificare cosa succede nel caso di utilizzo di
questultimo metodo.

Conclusioni
In questo capitolo ci siamo occupati dellutilizzo di alcune API che permettono
linterazione con Google+, ovvero lapplicazione social di Google. Nella prima parte
abbiamo visto come eseguire il login e quindi ottenere alcuni dati che, altrimenti,
lapplicazione avrebbe dovuto richiedere in modo esplicito allutente. Abbiamo visto
come utilizzare loggetto People per accedere alle informazioni dei propri amici, che
abbiamo poi utilizzato per la condivisione delle informazioni. Abbiamo, per quanto
possibile, integrato questa tecnologia nella nostra applicazione con diversi esempi. Si
tratta di un argomento che necessiterebbe di molto pi spazio, specialmente per le
implicazioni legate al marketing o alla promozione delle applicazioni.

Capitolo 6

Google Cloud Messaging

In questo capitolo ci occuperemo di Google Cloud Messaging (GCM) ovvero di


una tecnologia, al momento completamente gratuita, che permette di inviare dei
messaggi Push alle applicazioni Android. Attraverso una tecnologia di questo tipo
possibile non solo inviare brevi informazioni allutente, ma anche ottimizzare il
processo di sincronizzazione di uninformazione dal dispositivo a un server remoto.
Per comprendere meglio questo concetto ci aiutiamo con un esempio. Supponiamo di
aver realizzato unapplicazione per la visualizzazione della cronaca delle azioni di
una partita di calcio. Unapplicazione di questo tipo visualizzer i vari eventi
allinterno di una lista, la quale dovr essere aggiornata con le informazioni che un
amministratore, da unaltra parte, inserisce in un database remoto attraverso
uninterfaccia dedicata, di cui non ci occuperemo. Lunico punto di interazione con la
parte server sar rappresentato, per esempio, da un servizio REST associato a un path
/cronaca/{time}, la cui responsabilit sar quella di restituire tutte le azioni che si sono
verificate dopo listante time. Lapplicazione lato server non far altro che inserire le
informazioni in un database, che sar consultato dal servizio /cronaca/{time} con una
semplice query. La parte interessante riguarda il client, ovvero quando accedere alle
informazioni e inviare una richiesta al server. Il problema consiste nel limitare il
numero di queste richieste, che sono costose in termini di batteria. Una prima
soluzione potrebbe essere il polling, ovvero linvio di richieste al server a intervalli
regolari, per esempio ogni minuto. Il vantaggio di questa soluzione consiste nel fatto
che di facile implementazione, mentre lo svantaggio che non tutte le richieste
produrranno un risultato e quindi saranno da considerarsi inutili. Una scelta di questo
tipo porta anche a problemi di scalabilit per il server, il quale dovr sopportare un
numero di richieste che si pu rivelare molto superiore al necessario. Aumentando
lintervallo con cui il client accede al server si pu, in un certo senso, alleviare questo
problema, con per il pericolo di ricevere un gran numero di informazioni tutte in una
volta, con un effetto negativo sullusabilit dellapplicazione. Una soluzione
leggermente migliore potrebbe essere quella di modulare il periodo di chiamata in
base al numero di dati ricevuti con le richieste precedenti. Per esempio, se per un paio
di richieste eseguite a un intervallo T non si sono ottenuti risultati, possiamo
aumentare lintervallo a un periodo 2 T. Se ancora non otteniamo risultati
possiamo aumentare lintervallo a 4 T e utilizzare una regola simile, ma al

contrario, nel caso in cui invece le richieste ottengano sempre risultati. Anche in
questo caso tutta la responsabilit del client e nessuno pu prevedere esattamente
ogni quanto le informazioni saranno disponibili; dipender, in questo caso, dal tipo di
partita e dalla loquacit del cronista. Proviamo a immaginare una soluzione
completamente diversa, ovvero un modo attraverso il quale il server possa notificare
ai propri client la disponibilit di nuove informazioni. In questo caso i client
accederebbero al server solamente nel caso in cui fossero sicuri di ricevere dei dati.
La tecnologia che permette proprio questo in Android si chiama Google Cloud
Messaging (GCM), largomento di questo capitolo. Prima di cominciare bene dire
che anche in questo caso potrebbero esserci degli svantaggi. Se, come accennato in
precedenza, le informazioni fossero moltissime, i client riceverebbero un numero
elevato di notifiche e quindi invierebbero un numero elevato di richieste al server,
ricadendo in un caso ancora peggiore di quello di polling, che almeno permetteva di
raggruppare pi informazioni in una stessa risposta del server. Tutto questo per dire
che il GCM una tecnologia molto interessante, che per va usata con criterio nel
contesto di ciascuna singola applicazione. Nel nostro caso implementeremo una
funzione che ci permette di inviare messaggi ad altri utenti connessi con Google+ e
che quindi hanno associato al proprio account linstallazione dellapplicazione
FriendFence. A questi manderemo dei messaggi a cui potranno rispondere. La notifica
di un messaggio avverr attraverso linvio di una notifica push fatta da un server,
molto semplice, che implementeremo su Heroku (https://www.heroku.com/) utilizzando
Node.js (http://nodejs.org/).
NOTA
Informazioni al riguardo possono essere trovate al seguente indirizzo http://goo.gl/zzrBbK.

Per quanto riguarda il server ci limiteremo (pur fondendo il codice


precedentemente creato) alla descrizione dellinterazione con il client e con il sistema
GCM e non entreremo nella descrizione degli aspetti legati a Node.js oppure a
Heroku, per i quali si rimanda alla documentazione ufficiale. Il lettore potr
implementare il proprio server con la tecnologia che riterr pi opportuna. Si tratter
comunque di unimplementazione di chat molto semplice, che ci permetter di
descrivere nel dettaglio gli elementi pi importanti della tecnologia Google Cloud
Messaging.

Architettura base
Prima di procedere allimplementazione della chat bene dare una descrizione
dellarchitettura che andremo a realizzare e che possiamo vedere rappresentata nella
Figura 6.1. Come possiamo notare, esistono tre attori principali ovvero:
lapplicazione client nel nostro dispositivo;
il server che contiene i dati della nostra applicazione;
il server Google Cloud Messaging di Google.
Il primo attore la nostra applicazione, alla quale vogliamo mandare dei messaggi
in modalit push utilizzando, appunto, la tecnologia GCM. Lapplicazione potr
anche non essere in esecuzione al momento della ricezione dei messaggi, i quali
potranno contenere delle informazioni, delle dimensioni massime di 4 KB.

Figura 6.1 Architettura generale per lutilizzo di Google Cloud Messaging.

Limportante che il nostro dispositivo disponga dellapplicazione Google Play


Store e, per le versioni inferiori alla 4.0.4, anche di un utente registrato allo store. Per
le versioni superiori questa restrizione non esiste pi e sar infatti possibile testarla
con lemulatore.
NOTA
Questa tecnologia si basa sullutilizzo dei servizi che Google Play Store usa per la notifica degli
aggiornamenti delle applicazioni. inoltre lo stesso meccanismo utilizzato per la notifica della
disponibilit di messaggi di mail con il client di Gmail.

La seconda componente di fondamentale importanza rappresentata dal server con


cui la nostra applicazione interagisce per offrire i propri servizi. Nel nostro caso si
tratter di un server realizzato con Node.js, cui sono affidate sostanzialmente due
responsabilit:
mantenere un registro degli utenti che utilizzano lapplicazione;
implementare un meccanismo di notifica che utilizza il server GCM.
Al momento dellinstallazione di FriendFence, faremo infatti in modo che il servizio
GCM generi (Passo 1 della Figura 6.1) un identificatore di cui fondamentale capire
il significato. Si tratta, infatti, di una String che identifica linstallazione
dellapplicazione nel device e che viene utilizzata come indirizzo di destinazione per
linvio di messaggi. Questo identificatore si chiama Registration ID e dovr essere
inviato al nostro server (Passo 2 della Figura 6.1) insieme allinformazione che
identifica la particolare installazione dellapplicazione nel dispositivo dellutente, che
nel nostro caso sar la coppia username/deviceId. Questo ci permetter di far arrivare
leventuale messaggio in tutti i dispositivi associati allutente. In questo modo il
server sapr sempre quale Registration Id utilizzare per linvio di un messaggio a tutti i
dispositivi di un utente di cui si conosce lo username. Utilizzeremo quindi una tabella
come quella indicata nella Figura 6.2:

Figura 6.2 La tabella che associa il Registration Id ai vari utenti.

La seconda responsabilit del nostro server quella di gestire linvio dei messaggi
attraverso linterazione con il sistema GCM. Creeremo quindi un servizio Rest che ci
permetter di inviare un messaggio a un utente di cui conosciamo lo username;
loperazione semplice se si considera la precedente tabella.
In tutto questo scenario il server GCM ha un ruolo fondamentale: ha la
responsabilit di fornire al particolare client che ne ha fatto richiesta, il
corrispondente Registration Id e quindi permettere al server dellapplicazione di
inviare messaggi a un insieme di utenti. In pratica il server richiamer un particolare
servizio sul server GCM (Passo 3 della Figura 6.1), il quale si preoccuper di inviare
la notifica a tutti i client corrispondenti, gestendo in modo trasparente ogni aspetto
legato allaffidabilit del messaggio.

Da quanto descritto capiamo come gli aspetti di sicurezza siano fondamentali.


Innanzitutto serve un meccanismo per autenticare lapplicazione che fa richiesta del
Registration Id al server GCM. A tale scopo necessario ottenere un Application Id. Per
avere questo valore torniamo nella nostra console di amministrazione e abilitiamo
lopzione Google Cloud Messaging for Android, che quindi si aggiunge alle altre API
abilitate nei capitoli precedenti, come possiamo vedere nella Figura 6.3.
Interessante notare come Google abbia deciso di rendere (almeno per il momento)
completamente gratuito il servizio, senza alcun limite o quota. Linformazione
relativa allApplication Id la troviamo nella schermata riassuntiva dellapplicazione
sotto la voce Project Number, nella parte superiore della Figura 6.4.
Questa linformazione che memorizzeremo nella nostra applicazione e che
utilizzeremo per richiedere al server GCM la Registration Id, che poi invieremo al
nostro server insieme alle altre informazioni relative allutente e al suo dispositivo.
Un altro aspetto fondamentale quello relativo alla modalit con cui il server GCM
riconosce come attendibile il nostro server che richiede linvio dei messaggi in push.

Figura 6.3 Abilitazione delle API per il Google Cloud Messaging.

Figura 6.4 Informazione relativa allApplication Id.

Figura 6.5 Selezioniamo la Server key.

A tale proposito abbiamo la necessit di definire un Server Auth Token, che otteniamo,
al solito, attraverso le console di Google. Andiamo quindi alla voce APIs & auth >
e selezioniamo il pulsante Create new Key, visualizzando una finestra di

Credentials

dialogo come quella rappresentata nella Figura 6.5, dove selezioniamo il pulsante
relativo a una Server Key che porta alla visualizzazione del form rappresentato nella
Figura 6.6.
Si tratta di un form che ci permette di specificare unulteriore informazione al fine
di aumentare la sicurezza del nostro sistema. Ci permette infatti di specificare lIP del
server, ovvero lindirizzo da cui il server GCM vedr arrivare le richieste per linvio
dei messaggi in push. Questo permetter quindi di ignorare eventuali richieste
provenienti da server cui sono associati IP differenti.

Figura 6.6 Inseriamo lIP associato al server.

Come valore di test possiamo specificare 0.0.0.0/0, come suggerito dalla


documentazione e quindi selezionare il pulsante Create, il quale produrr la voce
rappresentata nella Figura 6.7, con linformazione relativa alla API Key che
utilizzeremo nel nostro server ogni volta che avremo la necessit di comunicare con il
sistema GCM.

Figura 6.7 Abbiamo ottenuto la API Key per il nostro server.

Prima di procedere con lo sviluppo delle varie componenti, facciamo un breve


riassunto di quello che abbiamo fatto. Abbiamo visto che in unarchitettura GCM vi
sono tre attori e precisamente un client, un nostro server e il server GCM. Come
descritto nella Figura 6.1, non appena lapplicazione viene installata sul dispositivo,
invia una richiesta al server GCM per ottenere il suo Registration Id. Per fare questo il
client ha bisogno, a sua volta, di un identificatore, che abbiamo ottenuto attraverso la
console di Google e che abbiamo chiamato Application Id. Una volta ottenuto il
, responsabilit del client inviarlo al proprio server, che lo memorizza

Registration Id

in una propria struttura dati, associandolo a informazioni relative allutente e al suo


dispositivo. Se ora il server ha necessit di inviare dei messaggi push a un insieme di
client, non dovr fare altro che comunicare con il server GCM indicando
linformazione da spedire e i destinatari attraverso i relativi Registration Id. Per poter
fare questo il server si deve comunque identificare al sistema GCM attraverso la API
Key che abbiamo creato sempre attraverso la console di Google. Quando il server,
detto di terze parti, invia quindi la richiesta al server GCM responsabilit di
questultimo linvio della notifica ai dispositivi. Abbiamo capito che i passi che
andremo a realizzare saranno i seguenti.
Predisposizione del client al Google Cloud Messaging.

Invio del Registration Id al server e relativa memorizzazione attraverso un


opportuno servizio.
Implementazione dei servizi di invio dei messaggi push dal server.
Ricezione dei messaggi sul client e relativa visualizzazione.
Abbiamo molta carne al fuoco, per cui bene metterci subito al lavoro.

Predisposizione del client


Come accennato in precedenza, il primo passo nella gestione delle notifiche con
GCM di responsabilit dellapplicazione client, la quale dovr richiedere al server
GCM un Registration Id. Per fare questo necessario seguire alcuni passi, che sono, a
dire il vero, abbastanza automatici e non danno molta libert di implementazione.
Innanzitutto bene ricordare che si tratta di API che sono state integrate da tempo nei
Google Play services, che dovranno essere disponibili tra le dipendenze della nostra
applicazione. Assodato questo, dobbiamo inserire alcune definizioni nel file di
configurazione, ovvero il documento AndroidManifest.xml.
NOTA
bene precisare come le API per la gestione del Google Cloud Messaging siano comprese nella
libraria base dei Google Play services per cui, questa volta, non abbiamo la necessit di definire
alcuna dipendenza come fatto nei capitoli precedenti.

La prima parte di queste definizioni riguarda alcuni permessi, e precisamente


quelli definiti dal seguente codice:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission

android:name=uk.co.massimocarli.friendfence.permission.C2D_MESSAGE
android:protectionLevel=signature />
<uses-permission

android:name=uk.co.massimocarli.friendfence.permission.C2D_MESSAGE />

Il primo permesso abbastanza evidente e permette di dichiarare che la nostra
applicazione dovr necessariamente utilizzare la Rete per linterazione con il nostro
server e quello GCM. Come accennato in precedenza, fino alla versione 4.0.4
necessario disporre di un utente Google e questo il motivo della necessit di
definire, per quelle versioni, il permesso associato al valore
android.permission.GET_ACCOUNTS. Molto interessante il motivo per cui si richiede il
permesso associato al valore android.permission.WAKE_LOCK, il quale, ricordiamo, permette,
sostanzialmente, di impedire al dispositivo di mettersi in standby. Come vedremo
successivamente, in alcuni casi , infatti, necessario assicurarsi che il dispositivo non
vada in sleep in un periodo compreso tra quello di ricezione della notifica e

lelaborazione delle informazioni in essa contenute. Il permesso successivo


caratterizzato dalla costante com.google.android.c2dm.permission.RECEIVE ed caratteristico
della tecnologie GCM: permette di dichiarare che la nostra applicazione vuole
ottenere un Registration Id per poter utilizzare questa tecnologia. Notiamo, infine,
come lultimo permesso dichiarato sia di tipo custom, associato alla nostra
applicazione di cui utilizza il nome del package. Esso deve infatti avere un nome che
segue la convenzione

<nome package applicazione>.permission.C2D_MESSAGE


e permette, sostanzialmente, di impedire che altre applicazioni nello stesso
dispositivo possano ricevere le notifiche che destiniamo invece alla nostra. Notiamo
come il livello di protezione, specificato attraverso lattributo android:protectionLevel
dellelemento permission, sia signature. Questo significa che eventuali altre applicazioni
che intendano ricevere queste notifiche dovranno obbligatoriamente definire la stessa
permission, oltre che essere firmate con lo stesso certificato.
Visto che ci siamo, concludiamo con la definizione di alcuni componenti che
saranno utili in fase di ricezione del messaggio e che esamineremo in dettaglio nei
prossimi paragrafi. Il primo corrisponde al tipo di componente che meglio si presta
alla gestione delle notifiche, ovvero un BroadcastReceiver. Si tratta del componente che
viene attivato in corrispondenza della ricezione di un messaggio push e che ha
responsabilit di dispatcher: deve infatti rimanere attivo per il solo tempo necessario
alla lettura del tipo di messaggio, per poi attivare un altro componente che ne elabori
le informazioni, per esempio un Service. Ecco perch abbiamo inserito le seguenti
definizioni nel documento AndroidManifest.xml della nostra applicazione.

<receiver

android:name=.gcm.GcmBroadcastReceiver
android:permission=com.google.android.c2dm.permission.SEND>
<intent-filter>
<action android:name=com.google.android.c2dm.intent.RECEIVE />
<category android:name=uk.co.massimocarli.friendfence />
</intent-filter>

</receiver>
<service

android:name=.gcm.GcmService
android:exported=false />

Come possiamo notare, alla classe GcmBroadcastReceiver associato un IntentFilter,
che la rende sensibile allazione corrispondente alla ricezione dellinformazione push
associata alla String:

com.google.android.c2dm.intent.RECEIVE


molto importante notare anche la presenza di una category che ha come valore il
nome del package associato allapplicazione. Ricordiamo che un IntentFilter deve
definire tutte le category a cui stato associato lIntent che intente ricevere. Molto
importante anche la definizione del permission attraverso lomonimo attributo, che ci
permette di dire che lattivazione del BroadcastReceiver pu avvenire solamente da parte
di applicazioni che dispongono di tale permesso. Infine abbiamo definito un semplice
Service, al quale delegheremo lelaborazione delle informazioni ricevute. La relazione
tra questi componenti descritta nella Figura 6.8. altres importante sottolineare
come questa non sia lunica soluzione possibile, in quanto il BroadcastReceiver potrebbe
semplicemente lanciare una notifica da visualizzare nellapposita sezione nel display.

Figura 6.8 Possibili scenari di gestione della notifica push.

Una volta fatte tutte le definizioni del caso, ci accingiamo a scrivere finalmente del
codice che si dovr occupare della richiesta al server del Registration Id e quindi del
suo invio al nostro server per la memorizzazione delle informazioni. In questo

processo ci sono diverse cose che potrebbero andare storte. Per esempio, potrebbe
fallire la richiesta del Registration Id oppure il suo invio al server. Per quanto ci
riguarda, faremo in modo che il processo di richiesta e invio si ripeta fino a che il
tutto non si conclude con successo oppure qualora dovesse cambiare la versione
dellapplicazione. Questultima affermazione legata alla frequenza con cui viene
aggiornato il Registration Id. In teoria si tratta di uninformazione legata alla
particolare installazione dellapplicazione sul dispositivo dellutente e quindi se la
stessa applicazione dovesse essere installata, disinstallata e poi reinstallata, il valore
di Registration Id dovrebbe essere lo stesso. Questo non assicurato nel caso in cui
lapplicazione dovesse cambiare e pertanto ne terremo conto attraverso unopportuna
informazione. Per gestire la persistenza del Registration Id abbiamo utilizzato la classe
, che avevamo creato nel Capitolo 5 in corrispondenza allutilizzo delle API

UserModel

di Google Plus. Per associare un valore del Registration Id per una specifica versione
dellapplicazione abbiamo implementato il seguente metodo, che permette il
salvataggio delle informazioni passate come parametro nelloggetto SharedPreferences
utilizzato per le informazioni di profilo:

public void setRegistrationId(final String regId, final int appVersion) {

mPrefs.edit().putString(Keys.REGISTRATION_ID, regId)
.putInt(Keys.APP_VERSION, appVersion)
.commit();
}


Pi interessante il metodo che abbiamo implementato per verificare la presenza o
meno dellinformazione:

public String getRegistrationId(final Context context,

final int currentVersion) {


String registrationId = mPrefs.getString(Keys.REGISTRATION_ID, );
if (registrationId.isEmpty()) {
Log.i(TAG_LOG, No registration Id found);
return ;
}

int registeredVersion = mPrefs.getInt(Keys.APP_VERSION,


Integer.MIN_VALUE);
if (registeredVersion != currentVersion) {
Log.i(TAG_LOG, App version changed.);
return ;
}
return registrationId;
}


Nella prima parte del metodo andiamo semplicemente a verificare se
linformazione salvata nelle preferenze. Qualora linformazione fosse gi presente,
non facciamo altro che verificare se la versione dellapplicazione cambiata o meno.
Qualora non fosse cambiata significa che abbiamo ottenuto il valore cercato. Se
invece la versione dellapplicazione cambiata, significa che il Registration Id non
pi valido, per cui restituiamo come valore la String vuota, come nel caso di
informazione assente. In questa fase importante osservare il modo in cui viene
determinato lintero che identifica la versione dellapplicazione. A tale scopo
abbiamo creato una classe di utilit di nome AppInfoUtil, la quale conterr dei metodi
per ottenere informazioni che utilizzeremo anche successivamente e che ci
permetteranno di identificare in qualche modo il dispositivo. In questo caso il metodo
il seguente, e utilizza le informazioni fornite dal PackageManager per ottenere, appunto,
la versione dellapplicazione:

public static int getAppVersion(final Context context) {

try {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException e) {
// If our app is executing this should never happen
throw new RuntimeException(Could not get package name: + e);
}


Ricordiamo che il PackageManager quel componente che si occupa della gestione di
tutte le applicazioni installate nel nostro dispositivo. In questo caso non facciamo
altro che richiedere le informazioni relative allapplicazione incapsulate in un oggetto
PackageInfo, per poi richiederne solo la versione attraverso la propriet versionCode.
NOTA
Simpatica la gestione delleccezione, la quale viene sollevata qualora lapplicazione che la sta
eseguendo non esista. Ma se non esistesse, quel codice non verrebbe mai eseguito.

A questo punto abbiamo definito la classe UserData, attraverso la quale


verificheremo la presenza e validit del Registration Id oppure lo memorizzeremo
successivamente. Ma dove avviene tutto ci? Dobbiamo infatti trovare il punto della
nostra applicazione in cui inserire il controllo e quindi lanciare leventuale richiesta,
con conseguente invio al nostro server delle informazioni. Dovr essere un punto in
cui siamo sicuri della presenza dei Google Play services. Nella nostra
implementazione abbiamo deciso di permettere la chat a chi registrato in Google+,
per cui lanceremo il tutto a seguito di unoperazione di login; solo in quel momento,
infatti, avremo a disposizione lo username da inviare al server. Si tratta di
unoperazione che pu essere eseguita in background, per cui abbiamo deciso di
gestire il tutto nella classe GcmService attraverso unapposita action. Lo strumento per
fare questo rappresentato dalla classe GoogleCloudMessaging, la cui inizializzazione non
richiede, fortunatamente, alcun lifecycle, ma pu essere fatta attraverso il suo metodo
di Factory getInstance(). Secondo uno schema ormai noto, abbiamo creato il seguente
metodo di utilit per lavvio del processo di gestione e invio del Registration Id.

public static void manageRegistrationId(final Context context,

final String username) {


Intent intent = new Intent(context, GcmService.class);
intent.putExtra(USERNAME_EXTRA, username);
intent.setAction(GET_REGISTRATION_ID_ACTION);
context.startService(intent);
}

Attraverso un metodo di questo tipo possibile nascondere alle altre classi il nome
dei vari extra, che in questo caso sono rappresentati dalla sola costante USERNAME_EXTRA.
La classe GcmService estende la classe IntentService, che quindi permette di eseguire le
operazioni associate ai vari Intent allinterno di un Thread in background. Il metodo
richiamato per ciascuno degli Intent ricevuti, nel nostro caso il seguente:

@Override

protected void onHandleIntent(Intent intent) {


// We initialize the GoogleCloudMessaging object
if (mGcm == null) {
mGcm = GoogleCloudMessaging.getInstance(this);
}
if (intent != null) {
final String action = intent.getAction();
if (GET_REGISTRATION_ID_ACTION.equals(action)) {
final String username = intent.getStringExtra(USERNAME_EXTRA);
handleRegistrationId(username);
}
}
}


Nella prima parte provvediamo, se non gi fatto, a intercettare loggetto
GoogleCloudMessaging attraverso il suo metodo di factory getInstance() che accetta, come
sempre, un Context. Anche un Service una specializzazione di Context, per cui abbiamo
semplicemente passato il riferimento this. Di seguito, dopo aver verificato che lIntent
ricevuto non sia null, utilizziamo la action per delegare il tutto al corrispondente
metodo, ovvero il metodo handleRegistrationId(), che contiene la logica descritta in
precedenza.

private void handleRegistrationId(final String username) {

final UserModel userModel = UserModel.get(this);

final int currentAppVersion = AppInfoUtil.getAppVersion(this);


String registrationId = userModel.getRegistrationId(this,
currentAppVersion);
if (TextUtils.isEmpty(registrationId)) {
try {
registrationId = mGcm.register(Conf.GCM_SENDER_ID);
sendRegistrationIdToServer(registrationId, username);
userModel.setRegistrationId(registrationId, currentAppVersion);
} catch (IOException e) {
e.printStackTrace();
}
} else {
Log.d(TAG_LOG, Registration Id already present!);
}
}


Innanzitutto otteniamo il riferimento alloggetto UserModel e utilizziamo la classe
per ottenere la versione corrente dellapplicazione. Si tratta di oggetti che

AppInfoUtil

ci servono per verificare se il Registration Id gi stato gestito oppure no. Per questo
sufficiente verificare se il metodo getRegistrationId() restituisce qualcosa oppure la
stringa vuota. Il significato della stringa vuota quello di Registration Id mai ottenuto
o relativo a unaltra versione dellapplicazione. Un altro motivo potrebbe essere il
fallimento nellinvio dellinformazione al server.
Le righe di codice seguenti si preoccupano proprio di questo. Per ottenere il
Registration Id si utilizza loggetto GoogleCloudMessaging attraverso il suo metodo
, che vuole come parametro lApplication Id che abbiamo ottenuto dalla

register()

Google Console in fase di inizializzazione.


NOTA
Spesso la documentazione ufficiale chiama questo dato di configurazione come Sender Id.

Si tratta di unoperazione che pu andare male, e quindi sollevare uneccezione di


tipo IOException. Nel caso in cui il tutto andasse bene provvediamo a inviare il dato al

server attraverso una logica che abbiamo definito nel metodo


sendRegistrationIdToServer() che descriveremo di seguito. Se anche questo va a buon
fine e non stata sollevata alcuna eccezione, non ci resta che richiamare il metodo
setRegistrationId() sulloggetto UserModel e quindi rendere persistenti le informazioni.
Notiamo come le tre operazioni siano racchiuse in un modulo try/catch che ne
gestisce, in un certo senso, la transazionalit.
Come abbiamo detto, il metodo sendRegistrationIdToServer() gestisce la connessione
al server. A tale proposito non abbiamo voluto scrivere codice superfluo, ma
utilizzare una libreria molto usata e comoda, che si chiama OkHttp
(http://square.github.io/okhttp/). Lutilizzo di questa libreria presuppone solamente
linserimento delle seguenti definizioni nel file di configurazione:

compile 'com.squareup.okhttp:okhttp-urlconnection:2.0.0'
compile 'com.squareup.okhttp:okhttp:2.0.0'


Il metodo di gestione della connessione diventa molto semplice e intuitivo, come
possiamo vedere da queste poche righe di codice:

private void sendRegistrationIdToServer(final String registrationId, final

String username) throws IOException {


// The url for the service
final String registerUrl = getString(R.string.registration_id_url);
// We create the Json object to send
final String jsonInput = GcmRequest.RegistrationBuilder.create()
.withRegistrationId(registrationId)
.withUsername(username)
.withDeviceId(AppInfoUtil.getDeviceId(this))
.getJsonAsString();
// We create the output
RequestBody body = RequestBody.create(JSON, jsonInput);
Request request = new Request.Builder()
.url(registerUrl)

.post(body)
.build();
Response response = httpClient.newCall(request).execute();
if (!response.isSuccessful()) {
IOException ioe = new IOException(Registration Error);
Log.e(TAG_LOG, Registration Error, ioe);
throw ioe;
}
}


Da notare solamente lutilizzo di un altro metodo di utilit getDeviceId() della classe
, il quale si preoccupa di fornire un identificativo unico del dispositivo,

AppInfoUtil

oltre a una nostra implementazione del Builder Pattern per la costruzione del
documento JSON di input del nostro servizio POST.

public static String getDeviceId(final Context context) {

return Settings.Secure.getString(context.getContentResolver(),
Settings.Secure.ANDROID_ID);
}


A questo punto manca lutilizzo del servizio GcmService nella nostra applicazione, in
corrispondenza dellavvenuta login dellutente. A questo proposito abbiamo
semplicemente aggiunto listruzione evidenziata nel seguente frammento di codice in
corrispondenza del metodo di callback relativo allavvenuta login.

private final GoogleApiClient.ConnectionCallbacks mConnectionCallbacks =

new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle bundle) {
Log.i(TAG_LOG, GoogleApiClient connected);
// - - -

GcmService
.manageRegistrationId(GooglePlusLoginActivity.this,
username);
// - - }
@Override
public void onConnectionSuspended(int i) {
Log.i(TAG_LOG, GoogleApiClient connection suspended);
retryConnecting();
}
};


Si trattava dellultimo passo relativo alla registrazione sul server, il quale ora in
grado di associare uno username a un dispositivo e un Registration Id da utilizzare per
linvio di messaggi.
Esiste comunque ancora un problema, legato alla nostra decisione di gestire
comunque utenti connessi di cui si conosce lo username. bene precisare che questo si
rende necessario, in quanto per mandare un messaggio di chat abbiamo comunque
bisogno di conoscere il destinatario. In altri contesti questa potrebbe essere
uninformazione completamente superflua. Pensiamo per esempio al caso in cui un
utente debba semplicemente dire al server di essere interessato a un certo argomento
nella gestione di un forum. In questo caso il server pu semplicemente utilizzare il
deviceId e il Registration Id per identificare linstallazione dellapplicazione cui inviare
i messaggi. Nel nostro scenario il problema si avrebbe qualora si eseguisse il login
con un account, si uscisse, e quindi rientrasse con un altro account. In questo caso i
due account condividerebbero gli stessi messaggi; avrebbero infatti lo stesso deviceId
e stesso Registration Id. Per ovviare a questo problema abbiamo due soluzioni. La
prima consisterebbe nel cancellare le informazioni dellaccount in corrispondenza del
logout. In questo caso dovremmo per sapere che linvio della richiesta di
cancellazione potrebbe andare comunque male. La seconda soluzione invece pi
semplice e consiste nel verificare, in fase di ricezione del messaggio, se lo username
dellutente connesso corrisponde con quella del destinatario del messaggio. Anche in

questo caso lasciamo al lettore la scelta, che nel nostro caso ricade sulla seconda
opzione.
NOTA
In questo modo possiamo concentrarci sul client, tralasciando logiche che dovrebbero stare sul
server, la cui implementazione non argomento di questo testo.

Se tutto andato per il verso giusto, noteremo come le informazioni inviate siano
effettivamente state salvate sul server. A tale proposito abbiamo implementato un
servizio associato al path /users, che restituisce un documento JSON con i dati relativi
agli utenti che si sono registrati.
Si tratta di un servizio che dovrebbe essere protetto allinterno di una parte di
amministrazione del server. Lo stesso Registration Id uninformazione da considerare
privata, che quindi non va condivisa. Nel nostro caso non ci occupiamo di aspetti di
sicurezza, ma ovviamente linvio delle informazioni al server potrebbe, anzi
dovrebbe in certi scenari, avvenire su protocollo di trasporto sicuro, ovvero HTTPS.

Implementazione del server


Come abbiamo detto, non ci occuperemo dellimplementazione del server in s,
ma solo delle configurazioni necessarie alla sua integrazione in unarchitettura GCM.
A tale proposito bene precisare che esistono due diverse tecnologie, e precisamente:
GCM http;
CCS.
La prima basata sul protocollo HTTP e prevede solamente uno scenario secondo
cui il nostro server invia messaggi al server GCM, il quale invia la notifica push ai
corrispondenti destinatari. La seconda, invece, basata sul protocollo XMPP e
permette non solo linvio di messaggi, come nel precedente caso, dal server ai client,
ma anche dai client al server. La tecnologia CCS avrebbe quindi potuto essere
utilizzata per linvio del Registration Id al server. Come abbiamo detto si tratta di
tecnologie relative allimplementazione del server, che in realt non vanno a
impattare molto sulle operazioni da svolgere sul lato client. Per quanto riguarda il
server, utilizzeremo GCM HTTP, mentre per i client descriveremo la modalit di
utilizzo di entrambi i casi.
Nel caso della tecnologia GCM HTTP, linterazione con il server GCM avviene
attraverso linvio di richieste HTTP in POST a un indirizzo (endpoint) fornito da
Google, e precisamente:

https://android.googleapis.com/gcm/send


Come tutte le richieste HTTP, anche queste saranno caratterizzate da un HTTP Header
e da un HTTP Body. Come sappiamo gli header contengono metainformazioni relative
alla richiesta che intendiamo inviare, al tipo di dati, al tipo di dati che attendiamo
come risposta e cos via. Nel caso di GCM HTTP dovranno necessariamente essere
contenuti due header, e precisamente quello per lautenticazione del nostro server
(che agisce da client, in questo caso) verso il server GCM e un altro relativo al tipo di
dati che intendiamo inviare. Precisamente dovranno essere presenti

Content-Type:<Content type>
Authorization:key=<Registration Ids>

Al momento sono supportati solamente due Content-Type, e precisamente quello per


linvio di documento JSON:

Content-Type:application/json


E quello corrispondente allinvio di contenuti di puro testo:

Content-Type:application/x-www-form-urlencoded;charset=UTF-8


Il secondo header invece relativo allinsieme dei destinatari identificati
attraverso i corrispondenti Registration Id. Nel nostro caso utilizzeremo solamente
documenti JSON, di cui discuteremo il formato per quanto riguarda la specifica
applicazione di chat. Al momento ci interessa solamente sapere che il messaggio che
invieremo attraverso una richiesta POST dovr, oltre ai precedenti header, avere un
body come il seguente:

{

registration_ids : [<Regid1>, <Regid2>, ,<RegidN> ],


data : {
<payload>
}
}


Prima di procedere, invitiamo il lettore a un semplice test che presuppone lutilizzo
del tool Postman (http://www.getpostman.com/).
NOTA
Postman un plug-in per Chrome Browser, che permette di simulare linvio di richieste HTTP. Si
tratta di un tool molto utile in casi come questi, in cui si ha la necessit di inviare richieste a un
server, esaminandone le risposte senza dover sviluppare un client. Ovviamente questo uno dei
tanti tool che servono a questo scopo. Il lettore potr utilizzare il tool con cui si trova pi a proprio
agio.

Attraverso questo tool proviamo a costruire una richiesta come quelle che il nostro
server invier ai vari dispositivi.

Figura 6.9 Postman come tool per la gestione di richieste HTTP.

Figura 6.10 Utilizzo di Postman per linvio di una richiesta al server GCM.

Utilizzando Postman abbiamo impostato gli header e il contenuto, come indicato


nella Figura 6.10. Attraverso gli appositi campi nella parte superiore abbiamo

impostato gli header, mentre il body del messaggio lo abbiamo inserito nella parte
apposita nella scheda Raw. Come valore dellattributo registration_ids abbiamo
inserito solamente un Registration Id, ma avremmo potuto inserirne fino a mille,
separati da una virgola.
NOTA
Nel caso di un numero di destinatari superiore a mille necessario implementare un meccanismo
che ne permetta la scomposizione in gruppi pi piccoli.

Selezionando il pulsante Send notiamo come il messaggio venga inviato e si ottenga


una risposta analoga alla seguente:

{

multicast_id: 7160979837239465000,
success: 1,
failure: 0,
canonical_ids: 0,
results: [
{
message_id: 0:1413040366090152%5f8cc2a9f9fd7ecd
}
]
}


Si tratta di un JSON con alcune informazioni relative allesito dellinvio. Innanzitutto
esiste un identificatore della richiesta, associato allattributo multicast_id. Se
dovessimo ripetere la stessa richiesta, si otterrebbe quindi un identificatore diverso.
Si tratta di uninformazione che pu essere utile per tracciare le varie richieste da
parte del server, per gestirne, eventualmente, il reinvio o per fare semplici statistiche.
Gli attributi success e failure sono abbastanza intuitivi e danno indicazione sul
numero di messaggi che sono giunti a destinazione con successo oppure falliti. Nel
nostro caso il destinatario unico, per cui abbiamo avuto un successo e nessun
fallimento. Invitiamo il lettore a provare ad aggiungere un Registration Id inesistente
di seguito a quello precedente, per verificare come in quel caso il valore dellattributo

verrebbe portato a 1. Linformazione successiva contenuta nellattributo

failure

ed uninformazione molto importante, in quanto legata alla fase di

canonical_ids

registrazione, ovvero di reperimento del Registration Id. Si tratta di un dato che


permette di evitare il verificarsi di situazioni in cui un dispositivo si registri pi volte
al server GCM e quindi, per un errore, sia presente pi volte nelle informazioni sul
server di terze parti. Si tratta di situazioni che potrebbero portare allinvio di
messaggi multipli.
NOTA
Nel nostro caso questo problema non si verifica, in quanto a ogni registrazione provvediamo alla
cancellazione delle eventuali informazioni associate allo stesso utente per lo stesso dispositivo.

Per capire come funziona il meccanismo, facciamo un semplice esempio.


Supponiamo che la nostra applicazione venga installata per la prima volta e ottenga
un Registration Id pari a REG1. Questa informazione viene quindi utilizzata per linvio
delle informazioni in push come descritto in precedenza. Supponiamo ora che una
qualche ragione o per errore lapplicazione esegua una nuova registrazione a un
avvio successivo, ottenendo un nuovo Registration Id che ora REG2. A questo punto, se
il server non gestisce in modo opportuno la situazione, ci potrebbero essere due
diverse entry per la stessa installazione. Se ora per provassimo a inviare un
messaggio utilizzando REG2, tutto andrebbe come previsto. La cosa particolare sta nel
fatto che il messaggio verrebbe spedito anche nel caso in cui si utilizzasse ancora
REG1, ma questa volta si otterrebbe una risposta del seguente tipo:

{

multicast_id: 4613684671456525000,
success: 1,
failure: 0,
canonical_ids: 1,
results: [
{
registration_id: REG2,
message_id: 0:1413041704860356%5f8cc2a9f9fd7ecd
}
]


Notiamo come il messaggio verrebbe comunque inviato, ma nella risposta
troveremmo lindicazione del fatto che il Registration Id non lultimo.
Linformazione relativa a REG2 contenuta nellelemento results. In sintesi, un valore
di canonical_ids diverso da 0 indica che il Registration Id utilizzato non lultimo e va
gestito di conseguenza.
Lultima parte del messaggio di risposta contiene invece un array di informazioni
relative ai singoli destinatari. In caso di successo abbiamo linformazione relativa
allid del messaggio, insieme ad altre, come possono essere quelle dellesempio
precedente. bene sottolineare come esista un elemento per ciascun destinatario del
messaggio. Se inserissimo due Registration Id inesistenti, otterremmo la seguente
risposta.

{

multicast_id: 5665368221991882000,
success: 1,
failure: 2,
canonical_ids: 0,
results: [
{
message_id: 0:1413042006703170%5f8cc2a9f9fd7ecd
},
{
error: InvalidRegistration
},
{
error: InvalidRegistration
}
]
}


Possiamo notare come i valori delle propriet failure e canonical_ids ci permettono di
decidere se eseguire il parsing dellattributo results; se entrambi sono a 0 non serve,
infatti, andare oltre. In caso di errore notiamo come la riposta contenga un messaggio
che ne descrive il tipo. Tra gli errori ve ne sono alcuni molto importanti. Un errore
corrispondente al messaggio Message Too Big permette di ricordarci come i messaggi
non possano superare i 4096 byte.
NOTA
Ricordiamo come questo non sia un limite. Alcuni messaggi possono contenere tutte le
informazioni, mentre altri possono essere solamente un meccanismo per indicare al client che
alcune informazioni sono disponibili indicando anche il luogo in cui reperirle, se non gi
conosciuto al client stesso.

Nel precedente esempio abbiamo creato un semplice messaggio con


uninformazione molto semplice come payload; nel prossimo paragrafo vedremo
come gestire questa informazione lato client. Esistono comunque anche altri attributi
che possiamo utilizzare in fase di invio del messaggio e che bene descrivere qui per
completezza, anche se non sono strettamente legati al nostro caso duso. Il primo di
questi lattributo collapse_key. Supponiamo di voler inviare a un client delle
informazioni relative al prezzo di unazione che varia molto frequentemente.
Immaginiamo che in una giornata vengano inviate circa 100 notifiche push,
caratterizzate del fatto che, in ogni momento, solo lultima ha un valore effettivo.
Allutente non interessa sapere che alle 5:00 lazione valeva 100, se adesso, che sono
le 8:00, lazione vale 200. Qualora il dispositivo dellutente fosse rimasto spento, non
vorremmo che, quando lutente lo riaccende, arrivassero centinaia di notifiche con
informazioni ormai obsolete. Per gestire questo scenario si fa quindi in modo che
tutte le richieste di invio della notifica siano associate alla stessa collapse_key e quindi
vengano automaticamente eliminate dalleventuale nuova notifica con lo stesso
valore per quella propriet. Si tratta, quindi, di un meccanismo che permette di
ottimizzare moltissimo linvio e la ricezione dei messaggi, con conseguente risparmio
di risorse. Nel caso della nostra chat, ovviamente non vogliamo perdere informazioni,
per cui questa non sar una delle opzioni utilizzate dal nostro server.
Un altro aspetto, sempre legato alla vita di un messaggio, impostabile attraverso
lattributo time_to_live. Se non specificato diversamente, un messaggio viene
mantenuto sul server GCM per quattro settimane, trascorse le quali viene eliminato.
Questo valore pu essere modificato attraverso lattributo time_to_live, che permette di
specificare il tempo (in secondi) oltre il quale il messaggio viene automaticamente
cancellato se non recapitato. Unaltra informazione relativa allattributo

, il quale ci permette di dire al server di non inviare il messaggio se il

delay_while_idle

dispositivo idle, ma di inviarlo solamente quando questo torna attivo. Si tratta di un


attributo che pu essere utilizzato insieme a collapse_key, e quindi il messaggio che
viene inviato al risveglio del dispositivo lultimo tra quelli con lo stesso valore di
collapse_key per ogni possibile collapse_key.
Per quanto riguarda il nostro server, abbiamo realizzato un servizio associato al
path /send, che permette di inviare un messaggio a un utente di cui si conosce lo
username. Si tratta di una richiesta POST con un documento del seguente tipo:

{

to: <destination username>,


senderUser: <sender username>,
msgBody:<body of the message>
}


Il server non far altro che cercare tutti i Registration Id associati allutente con lo
dato e inviare un messaggio utilizzando la modalit impiegata in precedenza

username

con il tool Postman. Vedremo al termine del capitolo come gestire anche pi dispositivi
dello stesso utente attraverso una nuova funzionalit, che si chiama User
Notifications.

Ricezione dei messaggi sul client


Nel paragrafo precedente abbiamo visto come inviare una notifica push a un
insieme di client cui sono stati associati degli identificatori, che abbiamo chiamato
Registration Id. Ora ci occupiamo di un aspetto fondamentale, ovvero di come questi
messaggi vengono gestiti nel dispositivo allinterno della nostra applicazione.
Abbiamo gi accennato allutilizzo di un BroadcastReceiver e di un Service, che ora
descriveremo in modo pi dettagliato. Quando il dispositivo riceve una notifica,
viene attivato un BroadcastReceiver, che abbiamo implementato attraverso la seguente
classe:

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {

private static final String TAG_LOG = GcmBroadcastReceiver.class.getName();


@Override
public void onReceive(Context context, Intent intent) {
GcmService.convertIntent(context, intent);
startWakefulService(context, intent);
setResultCode(Activity.RESULT_OK);
}
}


Come possiamo osservare, si tratta di una classe che estende WakefulBroadcastReceiver,
la quale permette di risolvere un problema che si potrebbe verificare in questi casi,
ovvero il fatto che il dispositivo vada nello stato idle nel periodo che passa dalla
ricezione del messaggio al momento in cui il servizio elabora il servizio stesso.
Probabilmente il nostro non uno scenario che rende necessario questo
accorgimento, ma comunque bene tenerne conto in altri contesti. Come facile
intuire, le informazioni che arrivano dalla notifica push sono contenute nel parametro
di tipo Intent. Attraverso il nostro metodo di utilit convertIntent() non facciamo altro
che riutilizzare lo stesso Intent per lattivazione del servizio GcmService. Si tratta, infatti,
di un metodo molto semplice, che modifica il valore della action per la selezione
appunto del nostro servizio:

public static void convertIntent(final Context context,

final Intent intent) {


ComponentName comp = new ComponentName(context.getPackageName(),
GcmService.class.getName());
intent.setComponent(comp);
intent.setAction(MANAGE_MESSAGE_ACTION);
}


Questo accorgimento si rende necessario per non perdere i valori degli extra in
esso contenuti. Attraverso il metodo startWakefulService() acquisiamo il lock, in modo
che il dispositivo non vada nello stato idle fino a che non rilasciamo il lock in modo
esplicito, al termine dellelaborazione delle informazioni della notifica push.
Limplementazione del nostro BroadcastReceiver termina con la chiamata del metodo
, che, lo ricordiamo, serve qualora questo componente fosse stato

setResultCode()

attivato attraverso il metodo sendOrderedBroadcast() del Context. In ogni caso il controllo


passa al nostro GcmService che, in corrispondenza dellazione associata alla costante
, non fa altro che delegare il tutto al suo metodo privato:

MANAGE_MESSAGE_ACTION


private void handleReceivedMessage(final Intent intent) {

// We get the extra from the received message


Bundle extras = intent.getExtras();
if (!extras.isEmpty()) {
// We read the type of the received message
String messageType = mGcm.getMessageType(intent);
// We only manage messages with informations
if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType))
{
// The message has some information
final String sender = intent.getStringExtra(SENDER_EXTRA);
final String message = intent.getStringExtra(MESSAGE_EXTRA);
// We use these information to show a notification

FenceNotificationHelper.get(this)
.showChatNotification(this, sender, message);
} else if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR
.equals(messageType)) {
// Here we have an error
Log.e(TAG_LOG, Send message error);
} else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED
.equals(messageType)) {
// The message has been deleted
Log.e(TAG_LOG, GCM message deleted);
}
}
// In Any case we have to release the lock
GcmBroadcastReceiver.completeWakefulIntent(intent);
}


Come possiamo notare, non facciamo altro che verificare la presenza di un Bundle
allinterno dellIntent ricevuto come parametro e quindi utilizzare loggetto
per determinare il tipo di messaggio. Qualora si tratti

GoogleCloudMessaging

effettivamente di un messaggio contenente informazioni, non facciamo altro che


leggere i valori degli extra relativi al sender e al messaggio stesso. A questo punto
di fondamentale importanza sottolineare come il messaggio inviato dal nostro server
sia del tipo:

{

registration_ids : destinations,
data : {
msgBody:<message body>,
senderUser:<sender user>
}


I dati in esso contenuti si estraggono dal Bundle associato agli extra dellIntent
attraverso i metodi

final String sender = intent.getStringExtra(SENDER_EXTRA);
final String message = intent.getStringExtra(MESSAGE_EXTRA);


dove

private static final String SENDER_EXTRA = "senderUser";
private static final String MESSAGE_EXTRA = "msgBody";


Esiste quindi una corrispondenza uno a uno tra gli attributi del JSON che
impostiamo come valore per lelemento data del messaggio inviato al server GCM e
quelli che leggiamo nellIntent ricevuto sul client.
NOTA
bene precisare che questo funziona solamente per i valori di primo livello. Se avessimo un
attributo con valore complesso, questo verrebbe letto come String e non come ulteriore oggetto
JSON da cui estrarre altri attributi con metodo analogo. Potremmo dire che si tratta pi di una
struttura dati vicina alla Map<String,String> che a un oggetto JSON vero e proprio.

Nel nostro caso specifico abbiamo delegato la gestione delle informazioni a un


altro metodo di utilit della nostra classe di gestione delle notifiche, ovvero la classe
di utilit FenceNotificationHelper che vedremo in dettaglio nel prossimo paragrafo.
Ultima importante osservazione riguarda la chiamata del metodo
completeWakefulIntent(), che sostanzialmente libera il lock precedentemente acquisito nel
. In realt la chiamata di questo metodo non corrisponde

BroadcastReceiver

necessariamente al rilascio del lock, il quale dipende dal numero di processi che lo
avevano acquisito. Il lock viene rilasciato quando il contatore incrementato in
corrispondenza della richiesta e decrementato in corrispondenza del rilascio,
raggiunge il valore 0. Lutilizzo di questo meccanismo non necessario nel caso di
operazioni molto semplici come la nostra, ma comunque bene considerarne sempre
lopportunit di utilizzo o meno.

Implementazione di una semplice chat


Nel paragrafo precedente abbiamo visto come implementare la logica di ricezione
di un messaggio push da un server che abbiamo simulato anche attraverso un tool
come Postman, che permette appunto linvio di richieste HTTP al server GCM di
Google. Abbiamo visto come le informazioni del messaggio vengano ricevute dalla
nostra applicazione allinterno di un Intent, che un BrodcastReceiver inoltra poi a un
servizio a cui data la responsabilit dellelaborazione del messaggio stesso. Nel
nostro caso questa parte molto semplice e prevede la visualizzazione di una
notifica, selezionando la quale sar possibile visualizzare il messaggio completo e
quindi rispondere.
NOTA
Come abbiamo detto allinizio del capitolo non svilupperemo una vera e propria chat, la quale
richiede uno sviluppo non banale dal lato server, ma descriveremo uno scenario di ricezione e
risposta che ci permetter comunque di esaminare gli strumenti messi a disposizione dalle
Google Cloud Messaging API.

Limplementazione del metodo di gestione della notifica quindi molto semplice:



public void showChatNotification(final Context context,

final String sender, final String message) {


// The PendingIntent to launch the application

final Intent chatIntent = new Intent(mContext,


SimpleChatActivity.class);
chatIntent.putExtra(SimpleChatActivity.SENDER_EXTRA, sender);
chatIntent.putExtra(SimpleChatActivity.MESSAGE_EXTRA, message);
final PendingIntent launchPendingIntent = PendingIntent
.getActivity(mContext, LAUNCH_APP_REQUEST_CODE,
chatIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// We create the Builder
NotificationCompat.Builder builder = new NotificationCompat
.Builder(mContext)
.setContentTitle(mContext
.getString(R.string.notification_chat_message, sender))

.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(launchPendingIntent)
.setAutoCancel(true);
mNotificationManager.notify(CHAT_NOTIFICATION_ID, builder.build());
}


Come possiamo notare, non facciamo altro che creare un PendingIntent che contiene
lIntent per il lancio dellattivit descritta dalla classe SimpleChatActivity che
utilizzeremo per la visualizzazione del messaggio e la possibilit di rispondere. In
corrispondenza della ricezione di un messaggio push, si avr quindi la
visualizzazione di una notifica, selezionando la quale seguir la visualizzazione di
una schermata come quella rappresentata nella Figura 6.11.

Figura 6.11 Visualizzazione del messaggio chat ricevuto attraverso notifica push.

Nella parte inferiore del display abbiamo inserito una EditText per linvio della
risposta. Anche in questo caso il codice molto semplice, specialmente per quanto
riguarda linvio della risposta, la quale non altro che una chiamata al servizio /send
che abbiamo realizzato nel paragrafo precedente. A questo punto potremmo anche
dire che quello che faremo in questa fase non ha quasi nulla a che fare con GCM, in
quanto tutta la logica appartiene al server. In ogni caso linvio della risposta
attraverso la chiamata del servizio /send sul nostro server stato implementato
allinterno di un AsyncTask. Avremmo potuto aggiungere unulteriore action al nostro
servizio, ma un AsyncTask ci permette una pi semplice interazione con linterfaccia
utente per visualizzare una ProgressDialog e per visualizzare il messaggio di avvenuto
invio o di errore.
NOTA
In casi come questo bene anche fare attenzione a chi sia il sender e chi sia il destinatario del
messaggio. Per la risposta, quello che prima era il sender, ora diventa infatti il destinatario ed
facile confondersi con i nomi delle variabili.

La parte di interesse riguarda linvio della richiesta secondo un pattern che


abbiamo gi utilizzato nel servizio GcmService:

protected Boolean doInBackground(String params) {

final String message = params[0];


final OkHttpClient httpClient = new OkHttpClient();
// The url for the service
final String registerUrl = getString(R.string.send_message_url);
// We create the Json object to send
final String jsonInput = GcmRequest.SendBuilder.create()
.withSender(mSender)
.withTo(mDestination)
.withMessageBody(message)
.getJsonAsString();
// We create the output
RequestBody body = RequestBody.create(JSON, jsonInput);
Request request = new Request.Builder()

.url(registerUrl)
.post(body)
.build();
try {
Response response = httpClient.newCall(request).execute();
return response.isSuccessful();
} catch (IOException e) {
e.printStackTrace();
return false;
}
}


Abbiamo utilizzato anche in questo caso la libreria OkHttp con unaltra
implementazione del Builder pattern descritta dalla classe GcmRequest.SendBuilder per la
creazione del body della richiesta. Il tipo restituito dal nostro AsyncTask un Boolean, il
quale ci permette di visualizzare un messaggio di successo nel caso true e di errore
nel caso false. In caso di successo provvediamo anche alla chiusura dellActivity.
Come accennato, questa implementazione non ha quasi nulla di collegato alla
gestione GCM, in quanto corrisponde alla semplice chiamata di un servizio Rest
attraverso linvio di una richiesta HTTP in POST. Il discorso sarebbe invece diverso
nel caso in cui avessimo utilizzato CCS e quindi il protocollo XMPP. In quel caso
possibile, infatti, creare una connessione persistente (o comunque gestita dalla
piattaforma) tra il dispositivo e il server e questa poteva essere utilizzata non solo per
la ricezione dei messaggi push, ma soprattutto per linvio di messaggi dal client al
server, come nel caso dellinvio di un messaggio chat. In questo caso il precedente
metodo avrebbe utilizzato le seguenti istruzioni:

Bundle data = new Bundle();
data.putString("to", destination);
data.putString("msgBody", message);
data.putString("senderUser", sender);
gcm.send(Conf.GCM_SENDER_ID + "@gcm.googleapis.com", id, data);

Avremmo quindi semplicemente inserito le informazioni da inviare allinterno di


un Bundle, da utilizzare poi come parametro di uno degli overload del metodo send()
delloggetto GoogleCloudMessaging. Come abbiamo detto, la creazione di un server XMPP
per lutilizzo della tecnologia CCS prevede delle configurazioni lato server che
esulano dallargomento del presente testo. comunque bene precisare che il suo
utilizzo lato client permetterebbe unulteriore semplificazione del meccanismo di
comunicazione con il server.

User Notifications
Come ultimo argomento di questo capitolo dedicato alla tecnologia Google Cloud
Messaging ci occupiamo della descrizione di una funzionalit che stata aggiunta
con le ultime versioni, ma che si occupa di una funzionalit che abbiamo in parte gi
sviluppato, ovvero linvio delle notifiche a pi dispositivi associati a uno stesso
utente. In parte perch attraverso la User Notifications possibile fare in modo che
tutti i dispositivi associati a un particolare utente vengano visti come uno solo, cos
da gestire in modo automatico il fatto che se una notifica viene gestita in un
dispositivo essa venga automaticamente rimossa dagli altri. Per fare questo il server
dovr implementare una logica che ci accingiamo a descrivere. Supponiamo, per
esempio, che un utente si registri al nostro servizio dal suo smartphone e ottenga il
Registration Id REG1. Lo stesso utente decide poi di utilizzare un tablet e quindi,
attraverso la stessa applicazione installata su un dispositivo diverso, lo stesso utente
ottiene un altro Registration Id che questa volta REG2. In questo momento sul server
esistono quindi due record relativi allo stesso utente. Qualora si volesse mandare un
messaggio a tutti i dispositivi di un utente, bisognerebbe fare quello che al momento
fa il nostro server, ovvero eseguire una query che restituisce tutti i Registration Id
associati allo stesso account e metterli come destinatari del messaggio push da
inviare al server GCM. Con la User Notifications il meccanismo invece diverso, e
prevede che lo stesso server richieda al server GCM una notification_key, associandola
a un notification_key_name, che uninformazione che identifica in modo univoco
lutente. Nella stessa richiesta al server GCM si inviano anche tutti i Registration Id
relativi allo stesso utente e che bisogna, in un certo senso, raggruppare. La richiesta
ha la seguente forma:

{

operation: create,
notification_key_name: <user identifier>,
registration_ids: [<REG1>, <REG2>, , <REGN>]
}


Essa va inviata a un endpoint che diverso da quello utilizzato in fase di
registrazione, ovvero

https://android.googleapis.com/gcm/notification


Anche in questo caso saranno necessarie le header per lautenticazione al server
GCM come fatto per tutte le altre chiamate. Il risultato di questa chiamata un
documento JSON con linformazione relativa alla notification_key del tipo:

{

notification_key: <the notification key>


}


La chiave ottenuta poi quella che utilizzeremo per identificare tutti i dispositivi
associati allutente, al posto dellinsieme dei Registration Id a esso associati. In pratica
per inviare un messaggio a tutti i dispositivi di un utente non dovremo utilizzare tutti
i Registration Id, ma solamente la notification key associata.
Un utente potrebbe accedere con un nuovo dispositivo successivamente, per cui
dovr esistere anche la possibilit di aggiungere un dispositivo e questo sar possibile
attraverso una richiesta del tipo:

{

operation: add,
notification_key: <notification key>
notification_key_name: <user identifier>,
registration_ids: [<REG1>, <REG2>, , <REGN>]
}


Per rimuoverlo si usa una richiesta con la seguente operation:

{

operation: remove,
notification_key: <notification key>
notification_key_name: <user identifier>,
registration_ids: [<REG1>, <REG2>, , <REGN>]


Possiamo notare come in questo caso si debba anche fornire il valore della
notification_key ottenuta in precedenza.
Questo meccanismo non permette solamente linvio di un messaggio a un insieme
di dispositivi ma, soprattutto, di gestire il fatto che se una notifica viene cancellata in
uno di questi, lo stesso accade negli altri. Questo avviene in modo automatico
attraverso linvio, ai vari dispositivi, di messaggi di tipo
GoogleCloudMessaging.MESSAGE_TYPE_DELETED, che quindi le applicazioni dovranno essere in
grado di gestire.

Conclusioni
In questo capitolo ci siamo occupati dello studio di una tecnologia molto
interessante, che permette linvio di notifiche in modalit push a un insieme di
dispositivi. Dopo una descrizione generica dellarchitettura, abbiamo visto come
gestire il processo di registrazione e quindi di invio di un messaggio attraverso la
realizzazione di un semplice server con Node.js. In realt ci siamo maggiormente
concentrati sui meccanismi di comunicazione, pi che sulla vera e propria
realizzazione del server. Nella prima parte ci siamo infatti occupati di gestire il
Registration Id e che identifica la particolare installazione della nostra applicazione su
un dispositivo. Abbiamo visto come ottenere tale valore e quindi inviarlo a un server
responsabile della sua memorizzazione e associazione al particolare utente. Abbiamo
visto come utilizzare questa informazione per inviare messaggi al server GCM al fine
di generare delle notifiche sul client. Abbiamo visto come gestire queste notifiche
definendo un BroadcastReceiver che delega lelaborazione del messaggio a un opportuno
. Abbiamo infine descritto la funzionalit di User Notifications. Come ultima

Service

osservazione bene dire che nella nostra chat, molto semplice, abbiamo inserito tutte
le informazioni allinterno del messaggio stesso. bene comunque ricordare che
molto spesso la notifica push viene utilizzata per notificare al client la disponibilit di
informazioni, che poi questo va a prendere attraverso una richiesta HTTP o di altro
genere. In questo modo possibile ottenere un buon compromesso tra utilizzo delle
risorse e freschezza dellinformazione.

Capitolo 7

Programmare i dispositivi wearable

Una delle principali novit introdotte allultimo Google I/O del 2014 stata
sicuramente quella che va sotto il nome di Android Wear, ovvero la possibilit di
utilizzare un orologio con un piccolo display come ulteriore modalit di interazione
con le applicazioni Android. Si tratta di un nuovo modo per visualizzare le
informazioni e interagire con le applicazioni, completamente diverso da quello che
avviene con uno smartphone o un tablet. Alcune informazioni dovranno quindi essere
lette dallutente allo stesso modo con cui si legge lora di un orologio e quindi in
modo non invasivo. Le linee guida da seguire per lo sviluppo di applicazioni per
questo nuovo tipo di dispositivo si basano su due importanti concetti: Suggest e Demand.
Il primo concetto riguarda la possibilit di visualizzare piccole quantit di
informazioni contenute allinterno di Card e che sono organizzate secondo una
sequenza verticale che si chiama Stream. Ogni applicazione, come vedremo in
dettaglio in questo capitolo, potr realizzare la propria Card e inserirla nello Stream.
Lutente potr scorrere le varie Card attraverso un semplice swap verticale verso lalto
o verso il basso, come indicato nella Figura 7.1.
Ciascuna di queste Card pu contenere informazioni visualizzate con unimmagine
di sfondo, che ciascuna applicazione potr personalizzare. Per esempio, la Card che
notifica la ricezione di una mail, potr visualizzare il Subject della mail sopra
unimmagine corrispondente alla foto del mittente. Alcune di queste Card potranno
anche contenere altri elementi, detti Page, cui si arriva attraverso uno swipe
orizzontale. Queste Page potranno contenere dei Button, selezionando i quali possibile
inviare comandi allapplicazione corrispondente. Rimanendo nellesempio
corrispondente alla visualizzazione del messaggio di mail, con uno swap da destra a
sinistra possiamo visualizzare una Page che contiene un Button per larchiviazione della
mail. Se invece applichiamo uno swap da destra a sinistra otteniamo leliminazione
della Card dallo Stream.

Figura 7.1 Rappresentazione dello Stream come insieme di Card disposte verticalmente.
NOTA
Il lettore non si preoccupi della terminologia che stiamo utilizzando; vedremo ciascuno di questi
componenti nel dettaglio nei prossimi paragrafi.

Come accennato, le informazioni visualizzate sui dispositivi Android Wear non


dovrebbero essere iniziate da unazione dellutente, ma da un evento generato dalle
applicazioni a seguito di un qualche evento che si intende notificare. Questo potrebbe
essere, per esempio, uninformazione relativa alla prossima metro, o al tempo
necessario per arrivare al lavoro o a casa, visualizzata al mattino o in corrispondenza
di quello che lorario che si soliti finire di lavorare. Il secondo concetto di Demand
prevede che lutente, attraverso il dispositivo Wear, possa inviare dei comandi anche
vocali tramite una particolare Card che si chiama Cue Card (letteralmente la Card per dare
lo spunto a Google relativamente allazione da fare). Si tratta di una Card particolare
che viene attivata attraverso il famoso comando vocale Ok Google (Figura 7.2)
oppure attraverso un tocco sullo sfondo del display quando visualizzata la
schermata principale.
A questo punto possibile dare un comando vocale oppure toccare ancora una
volta il display e visualizzare una serie di comandi predefiniti attraverso un menu a

scorrimento verticale. Alcuni di questi comandi permettono di create una memo,


visualizzare i passi fatti fino a quel momento, inviare un messaggio testuale, inviare
una mail, creare una voce nel calendario e molto altro ancora, tra cui la
visualizzazione delle impostazioni del dispositivo Wear.

Figura 7.2 La Cue Card per la richiesta del comando vocale.

In questo capitolo vedremo come collegare una serie di comandi vocali a specifici
task da eseguire nelle varie applicazioni. facile intuire che i vari comandi vocali
verranno trasformati dalla piattaforma in altrettanti Intent, che, utilizzando il classico
algoritmo di Intent Resolution, verranno raccolti dalle varie applicazioni che ne
elaboreranno il contenuto per rispondere di conseguenza. La risposta potr avvenire
attraverso lavvio di componenti sullo smartphone o la visualizzazione di una
specifica Card sullo Stream o di un semplice messaggio di conferma.
Dopo questa introduzione non ci resta che iniziare a utilizzare queste nuove e
divertenti API nella nostra applicazione. Secondo lo schema suggerito nella
documentazione ufficiale, iniziamo dalla gestione delle notifiche.

La gestione delle notifiche per Android


Wear
Come abbiamo appena accennato, la modalit di interazione con un dispositivo
Android Wear deve essere immediata e breve. Si dice che linformazione accessibile
da un dispositivo di questo tipo, debba essere letta con la coda dellocchio e quindi
senza interrompere quello che lutente stava facendo in quel momento. Lesempio pi
semplice e immediato quello delle notifiche. Quando un dispositivo Android Wear
collegato (paired) con uno smartphone, ne riceve in modo automatico tutte le
notifiche. La notifica relativa a un messaggio della Chat realizzata al capitolo
precedente appare come indicato nella Figura 7.3:

Figura 7.3 La notifica standard su Android Wear della ricezione di un messaggio di Chat.

Come possiamo vedere, si tratta di una notifica che visualizza le stesse


informazioni che comparirebbero nello smartphone. Ricordiamo che il codice
utilizzato per la generazione di questa notifica il seguente:

public void showChatNotification(final Context context, final String sender,

final String message) {


final Intent chatIntent = new Intent(mContext,
SimpleChatActivity.class);
chatIntent.putExtra(SimpleChatActivity.SENDER_EXTRA, sender);
chatIntent.putExtra(SimpleChatActivity.MESSAGE_EXTRA, message);
final PendingIntent launchPendingIntent =

PendingIntent.getActivity(mContext, LAUNCH_APP_REQUEST_CODE,
chatIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new
NotificationCompat.Builder(mContext)
.setContentTitle(mContext
.getString(R.string.notification_chat_message, sender))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(launchPendingIntent)
.setAutoCancel(true);
mNotificationManager.notify(CHAT_NOTIFICATION_ID, builder.build());
}


Abbiamo messo in evidenza sia lutilizzo della classe NotificationCompat.Builder, sia
lutilizzo di un PendingIntent per il lancio dellattivit per la visualizzazione del
messaggio. La classe NotificationCompat compresa nella libreria, la cui dipendenza
stata definita attraverso la seguente definizione nel file build.gradle di Gradle del
progetto:

compile "com.android.support:support-v4:20.0.+"


NOTA
Anche in questo caso dobbiamo ricordarci di aggiungere la dipendenza con la corrispondente
libreria nei Google Play services questa volta identificata dalla stringa
com.google.android.gms:play-services-wearable:6.5.87.

Come spesso accade, Google mette a disposizione delle librerie per gestire alcune
funzionalit anche per versioni precedenti della piattaforma; in questo caso la libreria
funziona dalla 1.6 in poi. Abbiamo messo in evidenza lutilizzo del PendingIntent in
quanto questo si traduce nellaggiunta di quella che abbiamo chiamato Page alla quale
possiamo arrivare attraverso uno swap da sinistra a destra. Il risultato quello
rappresentato nella Figura 7.4:
Selezionando il pulsante Open on phone si avr il lancio della corrispondente
Activity sul telefono.

NOTA
Lasciamo al lettore la verifica di come la Page con il pulsante di apertura dellattivit sul telefono
non sia disponibile qualora non venisse utilizzato il PendingIntent. Interessante poi vedere cosa
succede se non si abilita lopzione di autocancel.

Figura 7.4 Lutilizzo di un PendintIntent si traduce nellaggiunta di una Page con la corrispondente
azione.

Come sappiamo, la classe NotificationCompat.Builder ci permette di associare alla


notifica delle azioni, attraverso il seguente metodo:

public NotificationCompat.Builder addAction (int icon, CharSequence title,

PendingIntent intent)

oppure il seguente:

public NotificationCompat.Builder addAction (NotificationCompat.Action action)


Il primo permette di specificare unicona, una label e un PendingIntent che incapsula
lIntent che viene lanciato nel dispositivo quando viene selezionata lazione.
Loggetto di tipo NotificationCompat.Action descrive, sostanzialmente, un modo per
incapsulare le stesse informazioni in un unico oggetto. Come esempio di utilizzo di
questi strumenti abbiamo creato il metodo showChatNotificationWithActions(), che il
lettore potr sostituire al precedente nella classe FenceNotificationHelper per vari
esperimenti. In questa versione abbiamo aggiunto le seguenti righe di codice:

final Intent actionIntent1 = new Intent(context, MainActivity.class);


final PendingIntent pendingIntent1 = PendingIntent

.getActivity(context, GO_TO_MAIN_REQUEST_CODE,
actionIntent1, PendingIntent.FLAG_UPDATE_CURRENT);
final Intent actionIntent2 = new Intent(context, MainActivity.class);
actionIntent2.putExtra(MainActivity.FIRST_FRAGMENT_INDEX, 2);
final PendingIntent pendingIntent2 = PendingIntent.getActivity(context,

GO_TO_GEOFENCE_REQUEST_CODE, actionIntent2,
PendingIntent.FLAG_UPDATE_CURRENT);
Queste creano le due azioni, che poi abbiamo aggiunto alla notifica attraverso le
definizioni evidenziate di seguito:
NotificationCompat.Builder builder =

new NotificationCompat.Builder(mContext)
.setContentTitle(mContext
.getString(R.string.notification_chat_message, sender))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(launchPendingIntent)
.addAction(R.drawable.ic_launcher, Main, pendingIntent1)
.addAction(R.drawable.ic_launcher, Geofence, pendingIntent2)
.setAutoCancel(true);

In questo caso la notifica sul dispositivo come quella rappresentata nella Figura
7.5, dove notiamo la presenza di due azioni per la visualizzazione della Activity
principale o della stessa posizionata sul Fragment relativo ai Geofence.

Figura 7.5 Visualizzazione della notifica con azioni sul dispositivo.

Se andiamo invece a vedere come questo si traduce sul dispositivo Wear notiamo
quanto rappresentato nella Figura 7.6, nella quale possiamo vedere la prima Card e
quindi le tre successive Page relative alle altre possibili azioni.

Figura 7.6 Notifica con azioni sul dispositivo Google Wear.

Come sappiamo, Android prevede la possibilit di utilizzare anche versioni estese


delle notifiche, attraverso le classi interne BigTextStyle e BigPictureStyle della classe
. La prima permette di visualizzare informazioni testuali estese,

NotificationCompat

mentre la seconda permette di visualizzare unimmagine nel caso in cui la notifica sul
dispositivo fosse la prima. Anche in questo caso le notifiche sul dispositivo si
traducono automaticamente in altrettante notifiche sul dispositivo Wear. Come
dimostrazione di ci abbiamo creato altrettanti metodi di test, che invitiamo il lettore
a provare. Il primo si chiama showBigTextChatNotification() e visualizza un testo esteso
che compare nello smartphone, come indicato nella Figura 7.7, mentre sul dispositivo
Wear appare, come indicato nella Figura 7.8.
Il codice utilizzato lo stesso che si impiega per una normale notifica attraverso la
classe NotificationCompat.

NotificationCompat.Builder builder = new

NotificationCompat.Builder(mContext)
.setContentTitle(mContext
.getString(R.string.notification_chat_message, sender))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(launchPendingIntent)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(bigText)
.setBigContentTitle(bigContentTitle)
.setSummaryText(bigSummaryText))
.setAutoCancel(true);
mNotificationManager.notify(CHAT_NOTIFICATION_ID, builder.build());

Figura 7.7 Notifica BigTextStyle sullo smartphone.

Figura 7.8 Notifica BigTextStyle sul dispositivo Wear.

Interessante anche il risultato qualora si utilizzasse una notifica di stile


BigPictureStyle, come nel metodo showBigPictureChatNotification(), dove il codice il
seguente:

NotificationCompat.Builder builder = new

NotificationCompat.Builder(mContext)
.setContentTitle(mContext
.getString(R.string.notification_chat_message, sender))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(launchPendingIntent)
.setStyle(new NotificationCompat.BigPictureStyle()

.setBigContentTitle(bigContentTitle)
.bigPicture(bigImage)
.setSummaryText(bigSummaryText))
.setAutoCancel(true);

Il risultato sullo smartphone quello rappresentato nella Figura 7.9, mentre sul
dispositivo Wear quello rappresentato nella Figura 7.10.

Figura 7.9 Notifica BigPictureStyle sullo smartphone.

Figura 7.10 Notifica BigPictureStyle sul dispositivo Wear.

Quello descritto finora ci viene completamente gratis, senza scrivere alcuna riga
di codice per Google Wear. La piattaforma Android ci mette per a disposizione delle
nuove API, che permettono di creare notifiche personalizzate per i dispositivi Wear;
per questo si utilizza la nuova classe NotificationCompat.WearableExtender, che impareremo
a utilizzare. Si tratta sostanzialmente di un oggetto che decora le informazioni di una
notifica aggiungendo configurazioni specifiche del dispositivo Wear. Qualora queste
informazioni fossero presenti, la modalit di notifica al dispositivo Wear si dissocia da
quella standard. bene precisare fin da subito che per far funzionare questo
meccanismo sar necessario utilizzare unaltra classe, per linvio della notifica,
ovvero NotificationManagerCompat, e non pi quella che abbiamo utilizzato in precedenza,
ovvero NotificationManager.
In precedenza abbiamo visto come aggiungere a una notifica delle Action, le quali
vengono rappresentate sullo smartphone come semplici pulsanti, mentre sul
dispositivo Wear diventano delle Page, cui si arriva attraverso uno swipe orizzontale. Un
utilizzo classico della classe WearableExtender quello che permette linvio di action
specifiche per il dispositivo Wear attraverso il seguente metodo:

public NotificationCompat.WearableExtender

addAction(NotificationCompat.Action action)

semplice comprendere come la classe WearableExtender disponga di una serie di
metodi che permettono di impostare informazioni specifiche del dispositivo Wear
come, in questo caso, le action, che sono rappresentate da oggetti dello stesso tipo
NotificationCompat.Action delle notifiche standard. Come primo semplice esempio di
utilizzo di questo oggetto abbiamo implementato il metodo
showChatNotificationWithWearAction(), che permette di visualizzare sul dispositivo Wear una
sola delle azioni che avevamo definito nel metodo showChatNotificationWithActions(). In
questo caso abbiamo inizialmente creato loggetto di tipo Action nel seguente modo
(abbiamo evidenziato il nome della classe di Builder utilizzata):

NotificationCompat.Action wearAction =

new NotificationCompat.Action.Builder(R.drawable.ic_launcher,

Main, pendingIntent1)
.build();

Abbiamo utilizzato questo oggetto nella definizione del WearableExtender nel
seguente modo:

final NotificationCompat.WearableExtender extender =

new NotificationCompat.WearableExtender()
.addAction(wearAction);

Per poi applicarlo alla notifica utilizzando il nuovo metodo extend() evidenziato di
seguito:

NotificationCompat.Builder builder =

new NotificationCompat.Builder(mContext)
.setContentTitle(mContext
.getString(R.string.notification_chat_message, sender))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(launchPendingIntent)
.addAction(R.drawable.ic_launcher, WearMain, pendingIntent1)
.addAction(R.drawable.ic_launcher, Geofence, pendingIntent2)
.extend(extender)
.setAutoCancel(true);

Come abbiamo detto, questo permette di dire al sistema che le notifiche sul
dispositivo Wear ora sono disaccoppiate da quelle standard. Affinch questo abbia
effetto abbiamo la necessit di utilizzare un nuovo NotificationManager, descritto dalla
classe NotificationManagerCompat, di cui si ottiene il riferimento attraverso un metodo
statico di Factory, come indicato nel seguente codice:

NotificationManagerCompat notificationManager =

NotificationManagerCompat.from(mContext);
notificationManager.notify(CHAT_NOTIFICATION_ID, builder.build());

Leffettivo invio della notifica analogo a quello utilizzato in precedenza.
Generando ora la notifica, il risultato sullo smartphone rimane quello rappresentato
nella Figura 7.6, mentre sul dispositivo diventa quello rappresentato nella Figura
7.11.

Figura 7.11 Action specifica del dispositivo Wear.


NOTA
importante notare come lazione specifica attraverso il metodo setContentIntent() sia
comunque presente.

Osservando la classe WearableExtender notiamo la presenza di alcuni metodi


interessanti, che vogliamo descrivere attraverso limplementazione di qualche
semplice esempio. Il primo riguarda la gestione delle Page, che ricordiamo essere delle
schermate aggiuntive di una notifica, cui si arriva attraverso uno swipe verso sinistra
da una Card di uno stream. , infatti, possibile aggiungere una Page attraverso il
seguente metodo:

public NotificationCompat.WearableExtender addPage(Notification page)


Per inserire un insieme di Page si usa il seguente metodo:

public NotificationCompat.WearableExtender addPages(List<Notification> pages)


Le informazioni di ciascuna Page sono incapsulate in oggetti di tipo Notification, che
possiamo creare nel modo descritto in precedenza per una qualunque altra notifica.
Nel metodo showAdditionalPagesOnWear() abbiamo implementato il caso in cui una
semplice notifica sullo smartphone venga invece notificata sul dispositivo Wear,
attraverso tre pagine diverse. Ciascuna delle notifiche per le pagine stata creata
attraverso il seguente codice:

NotificationCompat.Builder page1Notification =

new NotificationCompat.Builder(mContext)
.setContentText(Page 1)
.setSmallIcon(R.drawable.ic_launcher)
.setAutoCancel(true);

Loggetto di tipo WearableExtender stato quindi inizializzato nel seguente modo:

final NotificationCompat.WearableExtender extender =

new NotificationCompat.WearableExtender()
.addPage(page1Notification.build())
.addPage(page2Notification.build())
.addPage(page3Notification.build());

e utilizzato nel modo descritto sopra. Il risultato che si ottiene quello
rappresentato nella Figura 7.12 dove, ovviamente, le Page sono molto semplici, ma
possono essere rese pi accattivanti nel modo gi descritto.

Figura 7.12 Visualizzazione di pi pagine per una notifica sul dispositivo Wear.

Un altro insieme di opzioni fornite dalla classe WearableExtender riguarda la gestione


dellicona caratteristica dellapplicazione, che possiamo vedere anche nella prima
Card della Figura 7.12. Attraverso il seguente metodo possibile richiedere al
dispositivo Wear di non visualizzare licona:

public NotificationCompat.WearableExtender setHintHideIcon(boolean hintHideIcon)


Attenzione che, come dice il nome stesso, si tratta di una richiesta che potrebbe
non essere accolta e quindi ignorata. Lo stesso si pu fare con limmagine di
background attraverso il metodo:

public NotificationCompat.WearableExtender setHintShowBackgroundOnly(boolean
hintShowBackgroundOnly)


Attraverso il seguente metodo infine possibile decidere dove posizionare licona,
ovvero allinizio o alla fine dello spazio disponibile:

public NotificationCompat.WearableExtender setContentIconGravity (int contentIconGravity)


Nellultimo esempio di questo paragrafo abbiamo creato il metodo
showNoIconNotification(), nel quale abbiamo generato una semplice notifica che richiede
la non visualizzazione dellicona.

Per fare questo il codice :



final NotificationCompat.WearableExtender extender =

new NotificationCompat.WearableExtender()
setHintHideIcon(true);

Il risultato quello rappresentato nella Figura 7.13.

Figura 7.13 Visualizzazione di una notifica senza licona.

Comandi vocali di input


Allinizio del capitolo abbiamo detto che un dispositivo Wear dovrebbe gestire le
due fasi di Suggest e Demand. La modalit con cui vengono inviate le notifiche descritte
nel paragrafo precedente appartengono sicuramente alla prima fase. La seconda
invece quella che permette, attraverso il dispositivo Wear, di inviare dei comandi. Si
tratta di unoperazione non banale, vista la completa assenza di una tastiera. I
dispositivi Wear danno comunque la possibilit agli utenti di selezionare dei comandi
veloci oppure di utilizzare dei comandi vocali. Tutto questo viene gestito attraverso
una nuova classe che si chiama RemoteInput, le cui istanze vengono associate alle Action
viste in precedenza.
NOTA
bene ricordare che la modalit di interazione standard che viene implementata in modo
automatico dai dispositivi Wear quella che abbiamo descritto in precedenza e che prevede
lutilizzo di PendingIntent impostati sia attraverso il metodo setContentIntent() del
NotificationCompat.Builder sia attraverso le varie Action.

A questo punto facile intuire come il comando vocale, alla fine, venga comunque
trasformato in un Intent che lapplicazione dovr gestire e proprio piacimento. Le
notifiche che abbiamo creato in precedenza relative alla ricezione di un messaggio si
prestano bene allutilizzo di questa funzionalit. Supponiamo di voler rispondere al
messaggio attraverso un comando vocale, per poi veder aprire la schermata di
risposta precompilata. Si tratta di una logica molto semplice, implementata nel
metodo showChatNotificationWithReply(). In questo metodo seguiamo alcuni passi.
Innanzitutto abbiamo definito una costante che utilizzeremo come nome dellextra
dellIntent che conterr il messaggio di risposta.

/**
* The name for the extra for the reply
*/
public static final String REPLY_TEXT_EXTRA_NAME = Conf.PKG +

.extra.REPLY_TEXT_EXTRA_NAME;

Si tratta di una costante che utilizziamo nel passo successivo, che consiste nella
creazione dellistanza di RemoteInput attraverso lormai classico Builder.

RemoteInput remoteInput = new RemoteInput.Builder(REPLY_TEXT_EXTRA_NAME)

.setLabel(context.getString(R.string.notification_reply_label, sender))
.build();

La precedente costante viene passata come parametro del costruttore del Builder
proprio perch informazione necessaria al funzionamento di tutto il meccanismo.
Attraverso il metodo setLabel() impostiamo la label che verr visualizzata nel
momento in cui andremo a fornire il comando vocale. Loggetto RemoteInput creato
descrive quindi una possibile modalit di interazione, la quale deve per essere
associata a una Action, la quale a sua volta necessita di un PendingIntent che necessita di
un Intent. Dobbiamo seguire lordine inverso, per cui abbiamo creato prima lIntent e
quindi il relativo PendingIntent con le seguenti righe di codice:

Intent replyIntent = new Intent(context, SimpleChatActivity.class);
PendingIntent replyPendingIntent =

PendingIntent.getActivity(context, 0, replyIntent,

PendingIntent.FLAG_UPDATE_CURRENT);

Nel codice evidenziato notiamo come lIntent faccia riferimento allattivit
descritta dalla classe SimpleChatActivity, che sar proprio quella che ricever il
comando vocale allinterno dellIntent di attivazione. Prima di vedere come questa
informazione viene raccolta dallattivit vediamo che cosa succede in caso di
ricezione della notifica sul dispositivo Wear (Figura 7.14).

Figura 7.14 Gestione di un input vocale associato a una Action.

Dopo la visualizzazione della notifica nel modo ormai classico, notiamo come sia
disponibile una Page con un comando associato alla label Reply che abbiamo
impostato nella definizione della Action. La label che abbiamo impostato attraverso

loggetto RemoteInput invece quella visualizzata nella terza schermata, cui si arriva
selezionando lazione. Notiamo come il dispositivo Wear sia in attesa del comando
vocale. Non appena il comando stato inserito, si ha la visualizzazione della quarta
immagine con il risultato e quindi unanimazione che ne conferma linvio.
Linvio del comando vocale raccolto ha come conseguenza il lancio di un Intent
che abbiamo specificato nel precedente codice attraverso la variabile replyIntent, che
quindi lancia lattivit descritta dalla classe SimpleChatActivity, la quale si dovr
aspettare, quindi, che nellIntent ricevuto ci possa essere un extra del nome, indicato
dalla costante REPLY_TEXT_EXTRA_NAME. Anche in questo caso il tutto molto semplice,
anche se non come ci si aspetterebbe.

Bundle remoteInput = RemoteInput.getResultsFromIntent(getIntent());
if (remoteInput != null) {

final CharSequence fromWear = remoteInput


.getCharSequence(FenceNotificationHelper
.REPLY_TEXT_EXTRA_NAME);
if (!TextUtils.isEmpty(fromWear)) {
replyEditText.setText(fromWear);
}
}


Come possiamo notare, la lettura dellinformazione raccolta dal dispositivo Wear
non avviene accedendo direttamente allextra dellIntent, ma il tutto deve passare per
loggetto RemoteInput stesso. Attraverso il metodo getResultsFromIntent() otteniamo le
informazioni dal dispositivo Wear allinterno di un Bundle, a cui poi accediamo nel
modo classico, utilizzando la costante definita in precedenza. Quelle ottenute sono
infatti informazioni trasmesse dal dispositivo Wear allo smartphone attraverso un
meccanismo pi complesso, che vedremo nei prossimi paragrafi. Il risultato quello
rappresentato nella Figura 7.15, con il lancio dellattivit con la risposta preinserita
nellEditText.
Concludiamo questo paragrafo relativo alla descrizione delloggetto RemoteInput con
la gestione di un caso duso particolare, che prevede un insieme predefinito di
risposte. Supponiamo che la nostra notifica permetta di rispondere a una domanda le

cui possibili risposte sono Yes, No e Maybe. In questo caso possibile utilizzare il
metodo setChoices() nel modo evidenziato di seguito:

RemoteInput remoteInput = new RemoteInput.Builder(REPLY_TEXT_EXTRA_NAME)

.setLabel(context.getString(R.string.notification_reply_label, sender))
.setChoices(context.getResources()
.getStringArray(R.array.possible_replies))
.build();

Le varie opzioni possono essere rappresentate da un array che abbiamo inserito in
una risorsa. In questo caso il risultato quello rappresentato nella Figura 7.16, cui si
arriva con uno swap verticale a partire dalla schermata di richiesta del comando
vocale. Notiamo come sia possibile selezionare una delle opzioni per vedere inviato il
corrispondente comando vocale.

Figura 7.15 La SimpleChatActivity con la risposta precompilata.

Figura 7.16 Comando vocale con opzioni predefinite.

Gestire notifiche multiple

Finora abbiamo visto come gestire le notifiche nei dispositivi Wear, ma si trattato
sempre di notifiche singole. Ma cosa succede se invece di un singolo messaggio ne
riceviamo molti e da pi utenti? In questo caso la classe NotificationCompat.Builder ci
mette a disposizione un metodo molto semplice, che si chiama setGroup() e che
permette appunto di raggruppare pi notifiche dello stesso tipo in una sola Card o Page.
Per dimostrare il funzionamento di questa opzione abbiamo creato un metodo
praticamente identico al precedente, con laccortezza evidenziata nel seguente
codice:

NotificationCompat.Builder builder =

new NotificationCompat.Builder(mContext)
.setContentTitle(mContext
.getString(R.string.notification_chat_message, sender))
.setSmallIcon(R.drawable.ic_launcher)
.setContentIntent(launchPendingIntent)
.extend(extender)
.setGroup(GROUP_NAME)
.setAutoCancel(true);

Oltre a questo bene sottolineare come nel presente metodo sia stato in qualche
modo gestito il fatto di utilizzare diversi notification id. In caso contrario ogni
notifica si sarebbe sovrapposta alla precedente. Questo il motivo del codice:

notificationManager.notify(CHAT_NOTIFICATION_ID + counter++, builder.build());


Qui counter una variabile statica definita nel seguente modo:

private static int counter;


In questo caso, simulando linvio di quattro notifiche, si otterrebbe quanto
rappresentato nella Figura 7.17, dove si ha la visualizzazione della prima notifica con

linformazione del numero di ulteriori notifiche in quello che si chiama notification


stack.

Figura 7.17 Visualizzazione di notifiche a stack.

Queste notifiche non appaiono sullo smartphone; per loro sarebbe necessario
creare una notifica apposita, magari utilizzando lo style NotificationCompat.InboxStyle.

Applicazioni per Wear


Fino a questo momento abbiamo visto come gestire linvio di notifiche al
dispositivo Wear. Dopo aver verificato come le notifiche vengano inviate di default al
dispositivo Wear, abbiamo esaminato gli strumenti che ne permettono la
personalizzazione attraverso la creazione di nuove Card o Page da inserire nel classico
. Il dispositivo Wear viene quindi utilizzato come ulteriore meccanismo di

Stream

notifica. In questo paragrafo vedremo invece come realizzare vere e proprie


applicazioni eseguibili direttamente nel dispositivo Wear. Si tratta di vere e proprie
applicazioni che, per la diversit del dispositivo, presentano alcune limitazioni. La
prima riguarda linsieme di strumenti a disposizione; non , infatti, possibile
utilizzare API appartenenti ai seguenti package:

android.webkit
android.print
android.app.backup
android.appwidget
android.hardware.usb


In ogni caso, anche per la gestione di dispositivi futuri, sempre bene interrogare
il particolare dispositivo sulle eventuali funzionalit disponibili, attraverso il seguente
metodo della classe PackageManager:

public abstract boolean hasSystemFeature(String name)


Altre differenze riguardano invece la modalit di interazione da parte dellutente.
Per esempio, qualora venisse visualizzata una schermata sul dispositivo Wear, essa
verrebbe automaticamente eliminata nel caso in cui il dispositivo andasse in sleep
dopo un periodo di inattivit. Al risveglio lutente non vedrebbe pi la schermata
dellapplicazione, ma la home del dispositivo, a differenza di quello che avviene sullo
smartphone. Ovviamente le informazioni che si andranno a visualizzare sul
dispositivo Wear saranno un sottoinsieme di quelle che solitamente vengono
visualizzate sullo schermo di uno smartphone; si tratta di informazioni essenziali, che
possono essere guardate con la coda dellocchio. Un aspetto invece pi pratico
riguarda la modalit con cui lapplicazione Wear viene installata nel dispositivo. A tale
proposito esistono due modalit, a seconda che si sia in fase di sviluppo o nel caso di
applicazione pubblicata sullo Store e quindi firmata con un certificato di produzione.

Nel primo caso sar sufficiente avere il dispositivo connesso con un cavo USB e
lanciare lapplicazione con lopzione Run di Android Studio. Nel secondo caso,
invece, lapplicazione Wear viene installata sullo smartphone insieme allapplicazione
principale e quindi inviata a ogni dispositivo Wear a esso accoppiato.
Da quanto descritto, capiamo che esistono diverse modalit di programmazione del
dispositivo Wear. La prima prevede linvio di informazioni precedentemente elaborate
sullo smartphone, mentre la seconda prevede la gestione dei dati nel dispositivo Wear
stesso. Questultimo disporr di un hardware meno potente rispetto a quello di uno
smartphone, ma potr, per esempio, accedere a un insieme di sensori diverso, come
quello del battito cardiaco o della pressione (se disponibili). Questa osservazione ci
invita quindi a riflettere su cosa elaborare nel dispositivo Wear e cosa invece gestire
nel pi potente smartphone.
Prima di fare i nostri esperimenti vediamo come creare questa applicazione Wear da
affiancare alla versione FriendFence per smartphone. bene dire fin da subito che
lutilizzo di dispositivi reali da preferire allutilizzo degli emulatori. Nel nostro
caso, per esempio, non disponiamo di un dispositivo Wear con display circolare, per
cui, come primo passo, ci occupiamo proprio della creazione di un AVD allo scopo.
Selezioniamo lopzione che permette di avviare lAVD Manager, ovvero licona
rappresentata nella Figura 7.18 oppure la voce di menu Tools > Android > AVD Manager di
Android Studio.

Figura 7.18 Icona per lavvio dellAVD Manager.

Nella finestra che compare selezioniamo il pulsante Create Virtual Device in basso a
sinistra, ottenendo unaltra schermata, nella quale selezioniamo le opzioni
rappresentate nella Figura 7.19. Qui notiamo la selezione della categoria Wear sulla
sinistra e quindi il dispositivo con una densit hdpi e display rotondo.

Figura 7.19 Creazione di un dispositivo Wear con display circolare.

Selezionando il pulsante Next otteniamo la schermata rappresentata nella Figura


7.20, nella quale selezioniamo lhardware che intendiamo simulare attraverso
unopportuna immagine. In questo caso notiamo la scelta del dispositivo con un
API Level 20 e unimmagine di sistema armeabi-v7a, basata appunto sul processore
ARM.
Selezionando il pulsante Show Advanced Settings anche possibile selezionare la
quantit di memoria disponibile, uno skin personalizzato e altre informazioni, che
invitiamo il lettore a consultare. A questo punto vogliamo lanciare il nostro
emulatore. Nel nostro caso, forse per la versione ancora beta di Android Studio, il
pulsante con la freccia verde per lavvio dellAVD non funziona. Per questo motivo
abbiamo deciso di utilizzare il comando emulator direttamente da riga di comando.
Selezionando nuovamente il pulsante Next si ottiene la schermata rappresentata
nella Figura 7.21, nella quale possiamo selezionare altre caratteristiche del nostro
AVD, tra cui il nome.

Figura 7.20 Selezione del tipo di immagine.

Figura 7.21 Ultime informazioni sullAVD tra cui il nome.

Il lettore potr vedere come nella cartella <ANDROID_HOME>/tools dellSDK Android


esistano pi versioni del comando emulator. Nel nostro caso utilizziamo quello che si
chiama

emulator64-arm


ma ovviamente dipende dallarchitettura della macchina per lo sviluppo. di
fondamentale importanza dire che per lesecuzione di questi emulatori necessario
installare un tool che si chiama HAXM e che si pu scaricare attraverso il tool SDK
Manager come indicato nella Figura 7.22 e che poi si trova nel seguente

PATH <ANDROID_HOME>/extras/intel/Hardware_Accelerated_Execution_Manager

Figura 7.22 Come scaricare lHardware Accelerated Execution Manager.

A questo punto dobbiamo vedere quali sono gli AVD disponibili e quindi lanciare
quello appena creato con il comando emulator64-arm. Per visualizzare linsieme degli
AVD sufficiente lanciare il seguente comando:

android list avd


Il quale porta alla visualizzazione di alcune informazioni del seguente tipo:

Available Android Virtual Devices:

Name: AVD_for_Android_Wear_Round_by_Google
Device: wear_round (Google)
Path: ~/.android/avd/AVD_for_Android_Wear_Round_by_Google.avd
Target: Android 4.4W (API level 20)
Tag/ABI: android-wear/armeabi-v7a
Skin: AndroidWearRound
Sdcard: 100M
Snapshot: no


Una possibile causa di errore dovuta al fatto che in queste informazioni non vi
il nome che avevamo dato in precedenza al nostro AVD. Lanciando il seguente
comando

emulator64-arm -avd RoundWear


si otterrebbe quindi il messaggio derrore

emulator: ERROR: unknown virtual device name: 'RoundWear'
emulator: could not find virtual device named 'RoundWear'


NOTA
Questa probabilmente la causa del problema relativo al lancio dellemulatore dallAVD
Manager, che si spera venga risolto rapidamente.

Il comando da lanciare quello che utilizza il nome ottenuto dal precedente


comando di elenco AVD ovvero:

emulator64-arm -avd AVD_for_Android_Wear_Round_by_Google


Questo porta alla visualizzazione della schermata rappresentata nella Figura 7.23
ovvero lemulazione di un dispositivo Wear con display circolare.

Figura 7.23 Emulazione di un dispositivo Wear con display circolare.

Lo stesso procedimento potr essere seguito per un dispositivo Wear con display
rettangolare o quadrato.
NOTA
alquanto divertente constatare come, catturando lo screenshot del dispositivo Wear con display
circolare, si ottenga comunque unimmagine quadrata. In pratica quello che abbiamo lanciato non
altro che quello a display quadrato con una maschera rotonda sopra.

Osservando con attenzione la precedente immagine si nota come nella parte


superiore vi sia limmagine di una nuvoletta tagliata, che sta a indicare che il
dispositivo non accoppiato ad alcuno smartphone. Nel nostro caso abbiamo
collegato il dispositivo reale alla macchina di sviluppo attraverso un cavetto USB, per
cui vorremmo fare in modo di utilizzare il dispositivo Wear emulato. Per farlo serve
lapplicazione Android Wear, che possibile scaricare dallo Store; dopo aver
collegato lo smartphone alla macchina di sviluppo attraverso il cavetto USB, occorre
eseguire il seguente comando:

adb -d forward tcp:5601 tcp:5601


A questo punto lanciamo lapplicazione Android Wear sullo smartphone e
selezioniamo lopzione in alto a destra, come indicato nella Figura 7.24 che ci
permette di eseguire il Pair con lemulatore.

Figura 7.24 Eseguiamo il Pair con lemulatore.

A questo punto la precedente icona con la nuvoletta tagliata dovrebbe scomparire.


Per testare la connessione possibile selezionare la funzione che permette di inviare
delle Card di esempio dallo smartphone. Se scegliamo, per esempio, linvio della Card
di Flight Status otteniamo quanto rappresentato nella Figura 7.25.

Figura 7.25 Esempio di invio di una Card di test allemulatore dallo smartphone.
NOTA

Si pu notare come la Card non sia proprio stata pensata per questo tipo di display, anche alla
luce della nota precedente. In seguito vedremo come gestire entrambi i tipi di display.

bene infine precisare come questo procedimento debba essere seguito ogni volta
che il dispositivo smartphone viene connesso alla macchina di sviluppo via cavo
USB.
Il secondo dispositivo Wear che utilizzeremo nei nostri esempi invece reale e ha un
display quadrato. In questo caso il Pairing con lo smartphone avviene in modo
classico. Anche questo dispositivo si pu comunque connettere via cavo USB
attraverso unapposita basetta e quindi diventa visibile attraverso una normale
modalit di debug. Unico accorgimento riguarda il fatto che non riusciamo a
rimanere collegati a entrambi i dispositivi, ma questo era prevedibile.
A questo punto abbiamo tutti gli strumenti necessari per sviluppare la nostra
applicazione Wear, che quindi vogliamo definire nel nostro progetto. Come primo
passo faremo in modo di creare una semplicissima HelloWorld, la quale ci permetter
di descrivere il processo di creazione dellapplicazione e della sua installazione sul
dispositivo Wear. Se ricordiamo quanto fatto nel Capitolo 1 (e precisamente la Figura
1.6), in corrispondenza della creazione del progetto vi era la possibilit di creare fin
da subito la parte Wear; cosa che non abbiamo fatto, ma che dovremo fare ora.
Selezioniamo lopzione File > New > Module ottenendo quanto rappresentato nella
Figura 7.26, che ci permette di scegliere tra unapplicazione classica, per TV per
Android Wear o per Google Glasses.

Figura 7.26 Creazione di un nuovo modulo.

Selezioniamo la terza opzione, relativa ad Android Wear e selezioniamo il pulsante


Next, che ci porta alla schermata rappresentata nella Figura 7.27.
Scegliamo il nome dellapplicazione Wear e quindi lAPI Level, che al momento il
solo disponibile, ovvero 20. di fondamentale importanza che il nuovo modulo Wear
abbia lo stesso package dellapplicazione principale. Un package diverso non
permetterebbe infatti alle due applicazioni di comunicare con i meccanismi di
sincronizzazione e scambio di messaggi che descriveremo di seguito.

Figura 7.27 Inseriamo il nome del modulo e selezioniamo lAPI Level corrispondente.

A questo punto il Wizard ci offre la possibilit di aggiungere una prima Activity per
il dispositivo mobile, cosa che facciamo selezionando la seconda opzione,
rappresentata nella Figura 7.28.
Come possiamo vedere nellultima immagine, una Activity per dispositivo Wear in
grado di gestire sia lo schermo quadrato sia quello rotondo. Come avvenga risulta

chiaro dalla schermata che vediamo nella Figura 7.29, cui si arriva attraverso la
selezione del pulsante Next.
Notiamo come ci venga chiesto il nome della classe che descriver la nostra
Activity e quindi tre informazioni di layout. La prima riguarda il nome del file di
layout che la nostra Activity andr a utilizzare. Gli altri due sono i nomi dei due
documenti di layout che verranno automaticamente selezionati nel caso di dispositivo
con schermo circolare o rettangolare. Finalmente abbiamo la possibilit di
selezionare il pulsante Finish, che porta alla creazione del nuovo modulo allinterno
del progetto. Dopo una prima operazione di build si ottiene la struttura a directory
rappresentata nella Figura 7.30, nella quale notiamo la presenza del nuovo modulo.

Figura 7.28 Creiamo una prima Activity per lapplicazione Wear.

Figura 7.29 Inseriamo le informazioni sullActivity da creare.

Figura 7.30 Il nuovo modulo Wear stato aggiunto alla struttura del progetto.

Prima di lanciare lapplicazione sul dispositivo Wear reale e sul nostro emulatore
con display circolare andiamo a vedere che cosa stato effettivamente creato.
Innanzitutto notiamo come il nuovo modulo sia dotato di un file di configurazione
build.gradle, il quale contiene la definizione delle seguenti dipendenze:

dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.google.android.support:wearable:1.0.0
compile com.google.android.gms:play-services-wearable:6.5.87
}


A parte lutilizzo dei Google Play services nellultima versione disponibile,
notiamo la presenza di una libreria di supporto che si chiama wearable. Si tratta di una
libreria non ancora ufficiale, che definisce una serie di componenti che possibile
utilizzare nella realizzazione delle applicazioni Wear, semplificandone di molto lo
sviluppo. Un primo utilizzo di queste librerie riguarda proprio la gestione di layout
diversi, per dispositivi con forma del display diversa. Se andiamo a vedere il
documento di layout principale contenuto nel file activity_main.xml, notiamo come
questo sia definito nel seguente modo:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WatchViewStub

xmlns:android=http://schemas.android.com/apk/res/android
xmlns:app=http://schemas.android.com/apk/res-auto
xmlns:tools=http://schemas.android.com/tools
android:id=@+id/watch_view_stub
android:layout_width=match_parent
android:layout_height=match_parent
app:rectLayout="@layout/rect_activity_main"
app:roundLayout="@layout/round_activity_main"

tools:context=.MainActivity
tools:deviceIds=wear/>

La root del documento XML , infatti, un elemento di tipo WatchViewStub, il quale
descritto da unomonima classe del package android.support.wearable.view che fa parte di
quelli definiti nella precedente libreria di supporto per il Wearable. Si tratta, in breve, di
un componente che riconosce il tipo di display Wearable del dispositivo utilizzando
quindi il layout definito attraverso altrettanti attributi associati al namespace app ovvero

e roundLayout, che nel nostro caso fanno riferimenti agli altri documenti di

rectLayout

layout. Il documento relativo al display rettangolare il seguente:



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools=http://schemas.android.com/tools
android:layout_width=match_parent
android:layout_height=match_parent
android:orientation=vertical
tools:context=.MainActivity
tools:deviceIds=wear_square>
<TextView
android:id=@+id/text
android:layout_width=wrap_content
android:layout_height=wrap_content
android:text=@string/hello_square />
</LinearLayout>


Mentre quello relativo al display rotondo il seguente:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools=http://schemas.android.com/tools
android:layout_width=match_parent
android:layout_height=match_parent
tools:context=.MainActivity
tools:deviceIds=wear_round>
<TextView
android:id=@+id/text
android:layout_width=wrap_content
android:layout_height=wrap_content

android:layout_centerHorizontal=true
android:layout_centerVertical=true
android:text=@string/hello_round />
</RelativeLayout>


Nei due listati abbiamo messo in evidenza le parti diverse. Notiamo come nel caso
del display rettangolare il layout contenitore sia un LinearLayout, mentre nel caso di
display circolare si utilizza un RelativeLayout. Questo per il semplice motivo che con
un display circolare si tende a centrare il contenuto, cosa pi semplice nel caso di un
RelativeLayout o di un FrameLayout.
Non ci resta che eseguire la nostra applicazione Wear selezionando il modulo
nel menu rappresentato nella Figura 7.31 e quindi scegliendo il

friendfencewear

dispositivo Wear di destinazione.


Dopo aver modificato le risorse di tipo String per il nostro progetto, il risultato nel
dispositivo emulato con display rotondo quello rappresentato nella Figura 7.32.

Figura 7.31 Lanciamo lapplicazione Wear.

Figura 7.32 Lapplicazione Wear nel dispositivo a schermo rotondo.

Il risultato nel dispositivo reale con schermo rettangolare invece quello


rappresentato nella Figura 7.33, dove risalta moltissimo la diversit del layout
utilizzato.

Figura 7.33 Lapplicazione Wear nel dispositivo a schermo rettangolare.

Concludiamo la descrizione di questa prima semplicissima applicazione Wear con il


codice relativo allActivity, che il seguente:

public class MainActivity extends Activity {

private TextView mTextView;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final WatchViewStub stub =
(WatchViewStub) findViewById(R.id.watch_view_stub);
stub.setOnLayoutInflatedListener(
new WatchViewStub.OnLayoutInflatedListener() {
@Override
public void onLayoutInflated(WatchViewStub stub) {
mTextView = (TextView) stub.findViewById(R.id.text);

}
});
}
}


A parte lutilizzo della classe WatchViewStub, di cui si ottiene un riferimento attraverso
il classico metodo findViewById(), balza allocchio il fatto che si tratti di una classe che
estende la stessa classe Activity che siamo abituati a utilizzare sullo smartphone. In
realt la classe Activity definita nel runtime di uno smartphone non la stessa che
stata definita nel runtime del dispositivo Wear; comunque molto importante che il
tutto sia completamente trasparente allo sviluppatore.
Nel paragrafo precedente abbiamo visto come inviare una notifica al dispositivo
Wear personalizzandola per quelle che sono le possibili azioni. Sebbene le linee guida
spingano a utilizzare notifiche standard anche nei dispositivi Wear, si potrebbe avere la
necessit di creare una Activity come la precedente, da utilizzare al posto del normal
layout di una notifica. Si tratta per di uno scenario con alcune limitazioni, che vale
solamente per notifiche inviate dalla stessa applicazione Wear e non dallapplicazione
che possiamo chiamare principale, come nel caso precedente. Per fare questo
dobbiamo seguire alcuni semplici passi, che abbiamo implementato nel nostro
progetto di Wear. Innanzitutto abbiamo modificato il layout di default creato in
precedenza, aggiungendo un Button selezionando il quale potremo lanciare la notifica,
la quale sar rappresentata da una Activity molto semplice, che abbiamo descritto
attraverso la classe NotificationActivity. Il tutto funziona nel seguente modo. Al lancio
dellapplicazione sul dispositivo Wear, visualizzeremo la schermata principale, la quale
ora conterr un Button, selezionando il quale lanceremo una Notification, che per non
verr visualizzata subito. Per osservare il risultato di questa notifica dovremo infatti
eliminare la precedente Card con uno swipe verso destra. Per lanciare questa notifica
dalla MainActivity abbiamo utilizzato il seguente codice, il quale viene eseguito in
corrispondenza della selezione del Button aggiunto al layout.

private void launchNotification() {

Intent displayIntent = new Intent(this, NotificationActivity.class);


PendingIntent displayPendingIntent = PendingIntent.getActivity(this,

0, displayIntent, 0);
// We create the Notification
final Notification.Builder builder =
new Notification.Builder(this)
.setContentTitle(this.getString(R.string
.custom_notification_title))
.setContentText(this.getString(R.string
.custom_notification_text))
.setSmallIcon(R.drawable.ic_launcher)
.extend(new Notification.WearableExtender()
.setDisplayIntent(displayPendingIntent));
final NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(0, builder.build());
}


Possiamo osservare come, essendo allinterno del dispositivo Wear, non si utilizza la
libreria di compatibilit, ma direttamente la classe Notification.Builder. Nel codice
evidenziato abbiamo mostrato lutilizzo del metodo setDisplayIntent(), il quale
contiene appunto il PendingIntent relativo al lancio della Activity di gestione della
notifica, che nel nostro caso descritta dalla classe NotificationActivity. A dire il vero,
il funzionamento, visualizzato nella Figura 7.34, non molto lineare, in quanto il
layout custom viene visualizzato dopo che la schermata principale stata
esplicitamente eliminata.

Figura 7.34 La visualizzazione della schermata principale e della schermata di notifica custom.

A questo punto possiamo dire che quello generato in modo automatico il modulo
relativo a una semplicissima applicazione Wear che utilizza una libreria di supporto
che impareremo a conoscere meglio nei prossimi paragrafi e che ci permetter di
semplificare lo sviluppo dei componenti caratteristici di questo nuovo tipo di
dispositivi. In questa fase bene sottolineare come le librerie utilizzate nei vari
contesti siano tre. Nel caso volessimo gestire solamente le notifiche, magari
personalizzandole per il dispositivo Wear, lunica libreria necessaria sarebbe quella la
cui dipendenza stata definita nel seguente modo:

compile 'com.android.support:support-v4:20.0.0'


Lutilizzo dei componenti come quello descritto dalla classe WatchViewStub prevede
invece la definizione della seguente dipendenza:


compile 'com.google.android.support:wearable:1.0.0'


Pi avanti vedremo invece modalit diverse di comunicazione tra i dispositivi Wear
e gli smartphone, le quali necessiteranno della definizione della dipendenza verso
lultima versione di Google Play services. In questo momento la definizione quindi
la seguente:

compile 'com.google.android.gms:play-services:6.1.+'


Abbiamo capito come lo sviluppo di unapplicazione Wear non si discosti molto da
quello di unapplicazione classica per smartphone, tenendo conto dei nuovi pattern di
usabilit.

La gestione dei comandi vocali


Il dispositivo Wear non dispone di una tastiera, per cui lunico modo per inviare
comandi quello di selezionare delle Action oppure utilizzare dei comandi vocali, i
quali possono essere di due tipi: quelli predefiniti dalla piattaforma e quelli che
possiamo impostare autonomamente, con la limitazione che si tratta di comandi del
tipo "Start <nome attivit>". Al primo gruppo appartengono alcuni comandi predefiniti,
tra cui "Call me a taxi", "Show step count" e altri ancora, che si possono consultare nella
documentazione ufficiale. Quello che ci interessa maggiormente invece capire
come fare in modo che una nostra Activity possa rispondere a questo tipo di comando.
Il lettore avr sicuramente capito che il tutto si tradurr nel lancio di un Intent, a cui la
nostra Activity dovr in qualche modo dichiarare di essere interessata attraverso un
elemento di tipo <IntentFilter/> nel documento di configurazione AndroidManifest.xml,
presente anche nel caso di unapplicazione Wear. Se andiamo a vedere la
documentazione relativa al comando "Call me a taxi" notiamo come il tutto si traduca
nel lancio di un Intent caratterizzato dallAction:

com.google.android.gms.actions.RESERVE_TAXI_RESERVATION

Nel caso in cui il comando vocale sia del tipo "Show step count" si avr invece il
lancio di un Intent caratterizzato da una action uguale a

vnd.google.fitness.VIEW


associata al MimeType pari a

vnd.google.fitness.data_type/com.google.step_count.cumulative


Per gestire questi comandi dobbiamo definire un IntentFilter corrispondente
nellAndroidManitest.xml. Per gestire il comando vocale di chiamata di un taxi attraverso
una nostra Activity di nome CallTaxyActivity, sar sufficiente aggiungere la seguente
definizione:

<activity android:name=".CallTaxiActivity">

<intent-filter>
<action android:name=com.google.android.gms
.actions.RESERVE_TAXI_RESERVATION />
<category android:name=android.intent.category.DEFAULT />
</intent-filter>
</activity>


A questo punto sufficiente toccare il display del dispositivo Wear oppure dire "Ok
per attivare il riconoscimento vocale e quindi pronunciare la frase "Get me a

Google"

per vedere avviata la nostra CallTaxiActivity. Nel caso in cui le precedenti frasi

taxi"

predefinite non facciano al caso nostro, possibile utilizzare unaltra modalit, che
consiste nel pronunciare la parola "Start seguita da un nome, che impostiamo sempre
nel documento di configurazione AndroidManifest.xml attraverso lattributo label
dellelemento <activity/>. Per attivare questa modalit per la CallTaxiActivity e lanciarla
a seguito della pronuncia della frase "Ok Google" seguita da "Start Taxi" sar sufficiente
aggiungere la definizione qui evidenziata:

<activity android:name=".CallTaxiActivity" android:label="taxi">

<intent-filter>
<action android:name=com.google.android.gms
.actions.RESERVE_TAXI_RESERVATION />
<category android:name=android.intent.category.DEFAULT />
</intent-filter>
<intent-filter>
<action android:name=android.intent.action.MAIN />
<category android:name=android.intent.category.LAUNCHER />
</intent-filter>
</activity>


Dovr essere unattivit che possibile lanciare come unapplicazione e questo il
motivo della presenza del corrispondente IntentFilter. In alcuni casi potremmo invece
aver bisogno semplicemente della funzione di riconoscimento vocale per linvio, per
esempio, di brevi messaggi di testo. A dimostrazione di questa funzionalit abbiamo
aggiunto alla schermata principale un altro Button, selezionando il quale avviamo il
riconoscimento vocale, per poi visualizzare la frase riconosciuta. Anche in questo
caso tutto molto semplice e consiste nel lancio di un Intent con la modalit
per poi raccogliere leventuale risultato attraverso il metodo di

startActivityForResult()

callback onActivityResult(). Per lanciare il riconoscitore vocale sufficiente eseguire


queste poche righe di codice:

private void startTalk() {

Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);


intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
startActivityForResult(intent, TALK_REQUEST_ID);
}


Notiamo la presenza dellazione associata alla costante ACTION_RECOGNIZE_SPEECH della
classe RecognizerIntent e lutilizzo del metodo startActivityForResult(). Per ricevere il

comando sufficiente implementare il seguente metodo:



@Override
protected void onActivityResult(int requestCode, int resultCode,

Intent data) {
if (TALK_REQUEST_ID == requestCode) {
if (RESULT_OK == resultCode) {
// We get a list of possible result
List<String> results = data.getStringArrayListExtra(
RecognizerIntent.EXTRA_RESULTS);
// We get the first if available
if (results != null && results.size() > 0) {
String recognizedText = results.get(0);
mTextView.setText(recognizedText);
} else {
mTextView.setText(R.string.talk_not_understood);
}
}
}
super.onActivityResult(requestCode, resultCode, data);
}


Come accade anche nel caso delle API equivalenti, per le applicazioni smartphone,
il risultato rappresentato da una serie di possibili valori ordinati per attendibilit. In
caso di successo non facciamo altro che prendere tale valore, restituito attraverso un
extra di nome RecognizerIntent.EXTRA_RESULTS, e visualizzarlo nel TextView contenuto nello
stesso layout.
Per quanto riguarda la gestione dei comandi vocali, il tutto quindi molto semplice
e si traduce, nella maggior parte dei casi, in aspetti di pura configurazione.

Componenti di unapplicazione Wear


Nel paragrafo precedente abbiamo visto come creare vere e proprie applicazioni in
grado di essere eseguite su un dispositivo Wear, accedendo ai sensori che ci mette a
disposizione o comunque dando allutente unulteriore modalit di interazione con il
proprio dispositivo. La creazione di classi che estendono la classe Activity ci ha dato
modo di capire che gli strumenti che abbiamo a disposizione non sono molto diversi
da quelli che si hanno su smartphone e tablet. Una Activity che viene eseguita sul
dispositivo Wear sar diversa da quella che viene eseguita sullo smartphone,
soprattutto per le informazioni visualizzate e le modalit con cui lutente pu
interagire. Questo si traduce, innanzitutto, nella necessit di creare dei layout
specializzati. Per semplificare il tutto, Google ci ha messo a disposizione una libreria
di supporto che abbiamo importato attraverso la definizione seguente nel file di
configurazione di Gradle del progetto e che descriveremo in questo paragrafo:

dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.google.android.support:wearable:+
compile com.google.android.gms:play-services-wearable:+
}


Prima di addentrarci nelle varie classi di questa libreria di supporto, ci vogliamo
occupare di un problema generale, ovvero quello di gestire dispositivi Wear con
display di forma diversa. Al momento esistono infatti due tipi diversi di dispositivi,
alcuni con schermo rettangolare (come il dispositivo LG) e altri con display circolare
(come il Moto360). Si ha quindi la necessit di trovare un meccanismo che permetta
di gestire entrambi i sistemi senza perdita di informazioni o problemi da parte
dellutente nellinterazione con lapplicazione Wear. Per risolvere questo problema
esistono due diversi approcci. Il primo consiste nellutilizzo della classe WatchViewStub,
che abbiamo gi utilizzato nel paragrafo precedente, che rappresenta, come dice il
nome stesso, uno Stub per il layout. Le Activity, o altri componenti che necessitano di
unoperazione di inflate, faranno riferimento a questo layout, nel quale si utilizza un
oggetto di tipo WatchViewStub che, attraverso due specifici attributi, permette di
specificare quale layout utilizzare per un dispositivo a schermo rettangolare e quale
per un dispositivo a schermo rotondo. Come esempio possiamo notare come nel

nostro progetto lattivit descritta dalla classe MainActivity utilizzi il layout contenuto
nel file activity_main.xml, il quale definisce lutilizzo dello Stub, come evidenziato nel
seguente codice, il quale utilizza di due attributi app:rectLayout e app:roundLayout per
decidere il layout da utilizzare per il display rispettivamente rettangolare e circolare:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WatchViewStub

xmlns:android=http://schemas.android.com/apk/res/android
xmlns:app=http://schemas.android.com/apk/res-auto
xmlns:tools=http://schemas.android.com/tools
android:id=@+id/watch_view_stub
android:layout_width=match_parent
android:layout_height=match_parent
app:rectLayout="@layout/rect_activity_main"
app:roundLayout="@layout/round_activity_main"

tools:context=.MainActivity
tools:deviceIds=wear/>

Un aspetto interessante da tenere in considerazione riguarda per la modalit con
cui si ottiene il riferimento alle varie View dalla classe che descrive lActivity.
Laccesso ai componenti contenuti nei layout relativi ai diversi display non pu
avvenire allo stesso modo con cui si accede allo stesso oggetto di tipo WatchViewStub.
Questo componente deve infatti prima verificare le caratteristiche del dispositivo e
poi caricare, in modo asincrono, il layout corrispondente. Questo il motivo per cui il
riferimento agli elementi dei layout specifici avviene in un metodo di callback, come
possiamo vedere nel seguente codice, che abbiamo sempre preso dal nostro
precedente esempio:

final WatchViewStub stub =

(WatchViewStub) findViewById(R.id.watch_view_stub);
stub.setOnLayoutInflatedListener(
new WatchViewStub.OnLayoutInflatedListener() {
Override

public void onLayoutInflated(WatchViewStub stub) {


mTextView = (TextView) stub.findViewById(R.id.text);
stub.findViewById(R.id.button)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Here we want to launch the custom notification
launchNotification();
}
});
stub.findViewById(R.id.talk)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Here we want to launch the custom notification
startTalk();
}
});
}
});


Dopo aver ottenuto il riferimento alloggetto di tipo WatchViewStub abbiamo utilizzato
unimplementazione dellinterfaccia WatchViewStub.OnLayoutInflatedListener per ottenere
la notifica dellavvenuto inflate del layout specifico. Da qui abbiamo utilizzato
loggetto ottenuto come parametro del metodo di callback onLayoutInflated() per
eseguire leffettivo riferimento agli oggetti dellinterfaccia.
La seconda modalit con cui possibile gestire la diversit delle forme dei display
presuppone lutilizzo del componente descritto dalla classe BoxInsetLayout. Se
pensiamo alle interfacce delle Figure 7.31 e 7.32 notiamo come la versione su display
rotondo si possa ottenere da quella per display rettangolare semplicemente centrando

il contenuto. In pratica quello che verrebbe nascosto negli angoli potrebbe essere
semplicemente spostato verso il centro del display. Come dimostrazione di questo
abbiamo creato il documento di layout di nome box_inset_layout.xml che contiene
semplicemente una TextView in un FrameLayout contenuto a sua volta in un
.

BoxInsetLayout


<android.support.wearable.view.BoxInsetLayout

xmlns:android=http://schemas.android.com/apk/res/android
xmlns:app=http://schemas.android.com/apk/res-auto
android:layout_width=match_parent
android:layout_height=match_parent
android:padding=15dp>
<FrameLayout
android:layout_width=match_parent
android:layout_height=match_parent
android:padding=5dp
app:layout_box=all>
<TextView
android:layout_width=match_parent
android:layout_height=wrap_content
android:layout_gravity=top
android:gravity=center
android:text=@string/some_long_text />
<TextView
android:layout_width=match_parent
android:layout_height=wrap_content
android:layout_gravity=bottom
android:gravity=center
android:text=@string/some_short_text />

</FrameLayout>
</android.support.wearable.view.BoxInsetLayout>


In questo caso il contenuto dellelemento BoxInsetLayout un FrameLayout al quale
vengono applicati degli inset solamente nel caso in cui venga visualizzato su un
display rotondo. Nel caso di display rettangolare queste informazioni verranno quindi
ignorate. Attraverso lattributo app:layout_box possiamo indicare in quali direzioni
applicare questo inset. Il valore all ci permette di applicarlo allinterno del FrameLayout.
Come possiamo vedere nella Figura 7.35, come se lo spazio a disposizione fosse
solamente quello rappresentato dalla circonferenza.

Figura 7.35 Utilizzo di un BoxInsetLayout nei due display.

In questi casi dobbiamo fare attenzione a non mettere troppi elementi nel display,
in quanto lo spazio disponibile nel display rotondo minore. Lasciamo al lettore
come esercizio lutilizzo di questo layout nella MainActivity.

Gestire le liste
Abbiamo visto come la selezione delle opzioni in un dispositivo Wear debba essere
molto veloce e immediata. Possiamo pensare di visualizzare una lista di opzioni tra
cui fare delle scelte. Un esempio quello rappresentato nella Figura 7.36, che
permette di selezionare una voce nel menu delle impostazioni del dispositivo.

Figura 7.36 Visualizzazione di una lista di opzioni.

Si tratta di una lista che permette anche di animare gli elementi. In questo caso, per
esempio, la voce evidenziata viene resa leggermente pi grande rispetto a quelle che
sono agli estremi, che vengono sbiadite nel colore. Il componente che ci permette di
implementare questa interfaccia si chiama WearableListView: si stratta sostanzialmente
della versione Wearable della ListView. Anche in questo caso avremo quindi a che fare
con la definizione di un layout di riga e, soprattutto, con un oggetto equivalente ad
Adapter. Il procedimento da seguire , infatti, molto simile e consiste, inizialmente,
nella creazione di un documento di layout che contenga il componente di tipo
WearableListView. Nel nostro caso si tratta del documento di layout wearable_list.xml, che
notiamo utilizzare il componente BoxInsetLayout descritto in precedenza.

<android.support.wearable.view.BoxInsetLayout

xmlns:android=http://schemas.android.com/apk/res/android
xmlns:app=http://schemas.android.com/apk/res-auto

android:layout_width=match_parent
android:layout_height=match_parent>
<FrameLayout
android:id=@+id/list_container
android:layout_width=match_parent
android:layout_height=match_parent
app:layout_box=left|bottom|right>
<android.support.wearable.view.WearableListView
android:id=@+id/wearable_list
android:layout_width=match_parent
android:layout_height=match_parent />
</FrameLayout>
</android.support.wearable.view.BoxInsetLayout>


In questo caso notiamo come il valore dellattributo app:layout sia una
composizione tra left, bottom e right, che permettono di applicare in quelle direzioni
gli inset nel caso di display rotondo. La classe che descrive la nostra attivit in questo
caso si chiama WearableListActivity e utilizza il layout appena definito. Nel proprio
metodo onCreate() si preoccuper di ottenere il riferimento alloggetto di tipo
, creare un Adapter e assegnarlo alla lista stessa. Questi passi non sono

WearableListView

per cos immediati e richiedono qualcosa di particolare. Il passo successivo consiste,


infatti, nella definizione di una classe che rappresenter gli elementi della lista e che
somiglia molto a una Custom View o, meglio, a una Compound View.
NOTA
Ricordiamo che una Compound View , di solito, una classe che estende ViewGroup e aggrega
elementi di vario tipo in un unico componente.

Nel nostro caso abbiamo deciso di seguire lesempio classico, che prevede
unimmagine a sinistra e un testo a destra. Per descrivere questo componente
abbiamo creato la classe WearListItem, che andiamo a descrivere nel dettaglio. Si tratta
di una classe che, oltre a estendere LinearLayout, implementa linterfaccia
che definisce alcune operazioni che permettono di gestire lo stato

WearableListView.Item

degli item stessi a seconda di dove sono visualizzati in quel momento. In sintesi

ciascun elemento pu subire modifiche durante lo scrolling, che dipendono da un


particolare valore che pu assumere un massimo e un minimo. Un esempio potrebbe
essere quello di un fattore di scala per le dimensioni. Quando lelemento nella parte
centrale del display, potremmo, per esempio, visualizzarlo con le dimensioni
massime, mentre quando vicino ai bordi potremmo visualizzarlo di dimensioni
minime. I valori massimi e minimi di questo fattore di scala dovranno quindi essere
forniti dalle implementazioni di queste due operazioni:

public abstract float getProximityMaxValue()
public abstract float getProximityMinValue()


La WearableListView si preoccuper poi di calcolare il valore relativo alla posizione di
ciascun item e quindi di notificarlo agli stessi attraverso questaltra operazione:

public abstract void setScalingAnimatorValue(float value)


Lapplicazione della particolare trasformazione dovr quindi avvenire
nellimplementazione di questultimo metodo dellinterfaccia WearableListView.Item.
Sempre nellesempio del fattore di scala, non dovremo fare altro che utilizzare il
valore ricevuto come parametro e applicarlo agli elementi del layout. Insieme al
precedente dovremo fornire limplementazione anche delloperazione:

public abstract float getCurrentProximityValue()


Infine, la WearableListView ci permette di sapere se il particolare Item nella parte
centrale, superiore o inferiore del display. Come possiamo vedere nella Figura 7.36
possiamo pensare che gli item visualizzati siano tre, di cui uno centrale e due esterni
nella parte superiore e inferiore del display. Attraverso limplementazione di queste
ultime due operazioni dellinterfaccia WearableListView.Item possiamo ricevere notifica
del fatto che il particolare item sta andando nella parte centrale o nelle altre due:

public abstract void onScaleDownStart()
public abstract void onScaleUpStart()

Si tratta di due metodi che potranno contenere una modifica nei parametri di
visualizzazione degli elementi, come il colore, la trasparenza o altro ancora.
Descriviamo dunque la nostra classe WearListItem, che permette di contenere il
riferimento a una ImageView e una TextView come per la visualizzazione della classica
icona con testo. Innanzitutto abbiamo lintestazione della classe e la definizione delle
variabili distanza:

public class WearListItem extends LinearLayout

implements WearableListView.Item {
/**
* The ImageView for the icon
*/
private ImageView mIcon;
/**
* The TextView for the label
*/
private TextView mName;
/**
* The Current scale value
*/
private float mScale;
// Other code
}


Notiamo come essa estenda la classe LinearLayout e implementi linterfaccia
che definisce le operazioni appena descritte. Notiamo poi la

WearableListView.Item

presenza della variabile mScale, che utilizzeremo per applicare alcune semplici
trasformazioni agli elementi della lista. Di seguito abbiamo la definizione dei
costruttori che caratterizzano le Custom View.

public WearListItem(Context context) {

super(context);
}
public WearListItem(Context context, AttributeSet attrs) {

super(context, attrs);
}
public WearListItem(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);


}


Molto pi interessante invece la parte relativa allimplementazione di un metodo
che viene richiamato al termine delloperazione di inflate, ovvero il seguente:

@Override
protected void onFinishInflate() {

super.onFinishInflate();
// We get the reference to the items into the layout
mIcon = (ImageView) findViewById(R.id.icon_item);
mName = (TextView) findViewById(R.id.text_item);
}


Vedremo successivamente come questi due componenti vengano definiti nel
documento di layout. Di seguito abbiamo poi limplementazione delle operazioni
descritte in precedenza:

@Override
public float getProximityMinValue() {

return 1.0f;
}
@Override
public float getProximityMaxValue() {

return 1.8f;
}
@Override
public float getCurrentProximityValue() {

return mScale;
}
@Override
public void setScalingAnimatorValue(float scale) {

mScale = scale;
mName.setScaleX(mScale);
mName.setScaleY(mScale);
mIcon.setScaleX(mScale);
mIcon.setScaleY(mScale);
}
@Override
public void onScaleUpStart() {

mName.setTextColor(Color.RED);
}
@Override
public void onScaleDownStart() {

mName.setTextColor(Color.GREEN);
}


Possiamo notare come i valori massimi e minimi per il fattore di scala siano
rispettivamente 1.0 e 1.8 e come la trasformazione che andiamo ad applicare sia
effettivamente una modifica della scala per i due assi X e Y. Notiamo poi come siano
stati utilizzati i metodi relativi alla posizione degli item per modificarne il colore.
Come abbiamo detto, questa classe descrive il layout di ciascun elemento della lista,
il quale va comunque definito in un documento di layout come il seguente:

<uk.co.massimocarli.friendfence.WearListItem

xmlns:android=http://schemas.android.com/apk/res/android
android:layout_width=match_parent
android:layout_height=80dp
android:gravity=center_vertical>
<ImageView
android:id=@+id/icon_item
android:layout_width=20dp
android:layout_height=20dp
android:layout_margin=16dp
android:src=@drawable/ic_launcher />

<TextView
android:id=@+id/text_item
android:layout_width=wrap_content
android:layout_height=match_parent
android:layout_marginRight=16dp
android:fontFamily=sans-serif-condensed-light
android:gravity=center_vertical|left
android:lineSpacingExtra=-4sp
android:textSize=16sp />
</uk.co.massimocarli.friendfence.WearListItem>


Si tratta di un documento molto semplice, nel quale notiamo solamente la presenza
del componente corrispondente alla classe che descrive lelemento di riga e la
definizione al suo interno dei due componenti a cui facevamo riferimento in relazione
al metodo onFinishInflate().
A questo punto abbiamo creato una classe che descrive lelemento di riga della
nostra lista e il relativo documento di layout. Il passo successivo consiste nella
definizione di un Adapter, che in questo contesto rappresentato da un oggetto di tipo
, che abbiamo descritto nella nostra classe WearListAdapter. Come

WearableListView.Adapter

sappiamo dallo sviluppo delle applicazioni classiche per smartphone, un Adapter ha la


responsabilit di accedere ai dati, o modello, e di fornire per ciascuno di esso una View
che li possa rappresentare. Sappiamo anche che il concetto di Adapter molto legato a
quello di riutilizzo delle celle al fine di una migliore ottimizzazione delle risorse.
Attraverso un numero finito e limitato di View possibile visualizzare le informazioni
relative a moltissimi dati diversi. Si tratta di View che contengono altri elementi, di cui
si ha la necessit di ottenere un riferimento attraverso unoperazione del tipo
findViewById(), la quale tuttaltro che gratis, anche se implementa sostanzialmente
una ricerca di un elemento in un albero. Se gli elementi grafici di cui ottenere un
riferimento sono molti, si ricercano delle tecniche che permettano in qualche modo di
non ripetere le operazioni di cui si conosce gi il risultato. Questo il motivo per cui
si utilizza un pattern che si chiama Holder e che consiste nella creazione di un oggetto
nel quale vengono memorizzati i riferimenti agli oggetti contenuti in una View. Questo
permette, per esempio, di ottenere il riferimento alla ImageView e alla TextView una sola

volta, riutilizzandoli nel caso in cui una stessa View venisse utilizzata per rappresentare
dati diversi. Il tutto reso ora esplicito, in quanto un WearableListView.Adapter deve
necessariamente implementare il seguente metodo, il quale restituisce un oggetto di
tipo WearableListView.ViewHolder che implementa, appunto, il pattern Holder:

public WearableListView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType)


Nel nostro caso abbiamo definito una classe interna nel seguente modo:

public static class ItemViewHolder extends WearableListView.ViewHolder {

/**
* The TextView for the Text
*/
private TextView mTextView;
/**
* The ImageView reference
*/
private ImageView mImageView;
/**
* We create all the item from the View
*
* @param itemView The View to recycle
*/
public ItemViewHolder(View itemView) {
super(itemView);
// We recycle the reference to the TextView and ImageView
mTextView = (TextView) itemView.findViewById(R.id.text_item);
mImageView = (ImageView) itemView.findViewById(R.id.icon_item);
}
}


Come dice il nome stesso, si tratta di una classe che memorizza al proprio interno
il riferimento a un insieme di View, ottenuto attraverso la chiamata del metodo
nel costruttore. Avremo quindi unistanza della classe ItemViewHolder per

findViewById()

ogni View riciclata nella gestione della ListView. Un numero finito di View porta a un
numero finito di ItemViewHolder. Nel nostro caso il metodo onCreateViewHolder()
implementato nel seguente modo, e si compone di due parti:

@Override
public WearableListView.ViewHolder onCreateViewHolder(ViewGroup viewGroup,

int viewType) {
final View inflatedViewItem = mLayoutInflater
.inflate(R.layout.wearable_list_item, null);
return new ItemViewHolder(inflatedViewItem);
}


La prima consiste nella semplice operazione di inflate del layout di riga, mentre la
seconda consiste nellutilizzo della View ottenuta per la creazione del corrispondente
descritto dalla nostra classe interna ItemViewHolder. importante notare come

Holder

questo metodo non venga richiamato per ogni elemento della lista, come nel caso del
metodo getView() dellAdapter classico su smartphone, ma solamente in corrispondenza
della necessit di una nuova View, per la rappresentazione di un nuovo dato. In
sostanza si tratta di un metodo che verr richiamato un numero finito di volte. Da
notare anche la presenza del secondo parametro, di nome viewType, il quale permette di
implementare logiche diverse nel caso di celle di tipo diverso. Nel nostro caso questo
non avviene, ma anche qui comunque possibile gestire tipi di celle diverse
attraverso loverride del seguente metodo:

public int getItemViewType(int position)


Lultimo passo nella creazione dellAdapter permette di eseguire il binding tra le View
e il modello che abbiamo passato come parametro nel costruttore.

@Override
public void onBindViewHolder(WearableListView.ViewHolder viewHolder,

int position) {
ItemViewHolder itemHolder = (ItemViewHolder) viewHolder;
itemHolder.mTextView.setText(mModel[position]);
itemHolder.mImageView.setImageResource(R.drawable.ic_launcher);
itemHolder.itemView.setTag(position);
}


Notiamo come sia stato utilizzato il ViewHolder passato come parametro per fare
accesso agli elementi da valorizzare con i dati del modello. Un ultimo accorgimento:
salvare in ciascuna View che rappresenta un elemento della lista, lindice dellelemento
del modello che in quel momento la stessa View sta rappresentando; ci sar utile nella
gestione della selezione dellelemento.
Dopo tutta questa fatica siamo finalmente giunti alla descrizione dellattivit che
gestisce il tutto e che abbiamo definito nella classe WearableListActivity. Il tutto si
sviluppa nel seguente metodo onCreate():

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.wearable_list);
final WearableListView listView =
(WearableListView) findViewById(R.id.wearable_list);
final WearListAdapter adapter = new WearListAdapter(this, MODEL);
listView.setAdapter(adapter);
listView.setClickListener(new WearableListView.ClickListener() {
@Override
public void onClick(WearableListView.ViewHolder viewHolder) {
// We get the selected position
final int selectedPosition =
(Integer) viewHolder.itemView.getTag();

// Do something
}
@Override
public void onTopEmptyRegionClick() {
// If we click outside the ist
}
});
}


Come possiamo notare, si tratta di un procedimento molto lineare. Dopo aver
definito il layout si ottiene il riferimento alloggetto di tipo WearableListView. Fatto
questo si crea unistanza dellAdapter descritto dalla nostra classe WearListAdapter, a cui
passiamo limmancabile Context e quindi il riferimento al modello, che un semplice
array di String. Il passo successivo consiste quindi nellassegnare lAdapter alla lista.
Molto interessante la modalit con cui gestiamo la selezione di un elemento della
lista. sufficiente registrare alla WearableListView unimplementazione dellinterfaccia
(senza il prefisso On), che prevede la definizione di due

WearableListView.ClickListener

operazioni. La prima ha come parametro il riferimento alloggetto


WearableListView.ViewHolder associato allelemento selezionato e non la sua posizione.
Per questo motivo avevamo salvato la posizione come Tag della View associata,
informazione a cui arriviamo con il codice evidenziato. La seconda operazione
singolare e viene richiamata qualora lutente facesse clic nella regione della lista non
occupata da un elemento, come qualora la lista fosse posizionata sul primo elemento.
Per il momento si tratta di due operazioni che lasciamo incomplete e che
completeremo nei prossimi paragrafi.
A questo punto siamo curiosi di vedere come viene visualizzata la nostra lista nel
dispositivo Wear, anche alla luce delle trasformazioni che abbiamo applicato nella
classe WearListItem. Il risultato quello rappresentato nella Figura 7.37, dove notiamo
come lelemento al centro sia effettivamente pi grande, mentre quelli nelle parti
esterne siano di colore diverso.

Figura 7.37 Il risultato della nostra WearableListActivity.

In questo paragrafo abbiamo visto come creare delle liste allinterno di


applicazioni Wear. bene sottolineare come lapproccio utilizzato segua la nuova
modalit di gestione delle liste basata sul concetto di RecycleView, che parte
fondamentale delle nuove API di Lollipop, la versione 5.0 della piattaforma Android.

Creare Card personalizzate


Allinizio di questo capitolo abbiamo dato la definizione di Card: un componente
che visualizza alcune informazioni sul display del dispositivo Wear, le quali
dovrebbero essere viste con la coda dellocchio. Un esempio di Card , per esempio,
quello utilizzato per le notifiche e prevede unicona, un testo e unimmagine di
sfondo. La libreria di supporto per i dispositivi Wear permette di gestire le Card
attraverso unistanza della classe CardFrame, che non altro che un particolare ViewGroup
che definisce un rettangolo con sfondo bianco e bordi arrotondati, che pu contenere

altri componenti a nostro piacimento. Una possibilit quella di aggiungere


direttamente la CardFrame al nostro documento di layout utilizzato da una Activity.
Esistono comunque anche altre due possibilit, che vedremo nei prossimi esempi. La
prima consiste nellutilizzo della classe CardFragment, che permette di creare Card come
quella delle notification, con immagine, titolo e descrizione. Si tratta di unulteriore
analogia tra lo sviluppo di applicazioni per smartphone e quelle per Wear. Anche in
questo caso esiste, infatti, il concetto di Fragment e di FragmentManager, anche se con scopi
diversi, visto che in questo caso non esiste, date le piccole dimensioni dei display, la
necessit di scomporre linterfaccia utente in pi parti riutilizzabili.
NOTA
Non dobbiamo comunque dimenticarci che le stesse API possono essere utilizzate per dispositivi
diversi, come una TV il cui display invece pu essere molto grande.

Unaltra modalit invece quella che prevede lutilizzo di una CardScrollView,


ovvero di una specializzazione della classe CardFragment, che aggiunge la possibilit di
gestire pi modalit di visualizzazione nel caso di Card che occupano molto spazio. In
questo caso, infatti, possibile gestire la visualizzazione in modo compresso o
espanso gestendo lo scrolling sul display.
Iniziamo con un esempio relativo allutilizzo di un CardFragment attraverso una
che abbiamo chiamato CardFragmentTest e che visualizzeremo in corrispondenza

Activity

della prima voce della lista realizzata nel paragrafo precedente. Tale attivit utilizza il
documento di layout che abbiamo definito nel file card_fragment_container.xml. Per non
dover definire altri due documenti di layout, abbiamo utilizzato un BoxInsetLayout che
contiene un FrameLayout, che utilizzeremo come anchor per il CardFragment, come
evidenziato nel seguente codice:

<android.support.wearable.view.BoxInsetLayout

xmlns:android=http://schemas.android.com/apk/res/android
xmlns:app=http://schemas.android.com/apk/res-auto
android:layout_height=match_parent
android:layout_width=match_parent>
<FrameLayout
android:id=@+id/frame_card_anchor
android:layout_width=match_parent

android:layout_height=match_parent
app:layout_box=bottom>
</FrameLayout>
</android.support.wearable.view.BoxInsetLayout>


Il codice della classe CardFragmentTest a questo punto banale, se ci si ricorda come
funzionano i Fragment nelle applicazioni classiche.

public class CardFragmentTest extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.card_fragment_container);
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
CardFragment cardFragment = CardFragment
.create(getString(R.string.card_fragment_title),
getString(R.string.card_fragment_description),
R.drawable.ic_launcher);
fragmentTransaction.add(R.id.frame_card_anchor, cardFragment);
fragmentTransaction.commit();
}
}


Dopo aver impostato il documento di layout otteniamo il riferimento al
FragmentManager, che utilizziamo per la creazione di una FragmentTransaction che
utilizziamo a sua volta per laggiunta della CardFragment di cui creiamo unistanza
attraverso il suo metodo statico di Factory create() che, notiamo, necessita di alcuni
parametri: un titolo, una descrizione e unicona. Da notare come le classi di gestione

dei Fragment non siano quelle della libreria di supporto, ma quelle definite nello stesso
runtime Wear. A questo punto non ci resta che definire lattivit nel file di
configurazione AndroidManifest.xml e collegare la sua visualizzazione attraverso la
prima voce di menu della lista. Nel primo caso la cosa immediata e prevede
semplicemente laggiunta delle seguente definizione:

<activity android:name=".CardFragmentTest"/>


Nel secondo caso abbiamo semplicemente aggiunto una voce al modello delle
possibili opzioni e quindi abbiamo gestito la corrispondente selezione attraverso il
seguente semplice metodo:

private void launchCardFragmentTest() {

final Intent intent = new Intent(this, CardFragmentTest.class);


startActivity(intent);
}


Questo, analogamente a quanto avviene nelle applicazioni per smartphone, non fa
altro che creare un Intent esplicito e lanciarlo attraverso il classico metodo
. Il risultato che otteniamo quello rappresentato nella Figura 7.38

startActivity()

Figura 7.38 Utilizzo di un CardFragment.

La lista contiene una sola voce, selezionando la quale si ha la visualizzazione della


scheda che comprende unimmagine, un titolo e una descrizione. interessante
vedere che cosa succede quando selezioniamo lopzione della lista. In questo caso si
nota come la nuova Card si sovrapponga a quella esistente, con una piccola
animazione da destra verso sinistra. Una domanda che ci facciamo riguarda
limplementazione del back: come si traduce il back dello smartphone in un
dispositivo di questo tipo. La risposta molto semplice, in quanto per tornare al
menu baster eseguire uno swipe da sinistra verso destra, per ritrovare la lista.
Qualora non volessimo gestire uno stack, sar sufficiente richiamare il metodo
finish(), come nel caso delle Activity che abbiamo imparato a gestire nelle
applicazioni per smartphone.
Come accennato in precedenza, esiste anche una seconda opzione per la creazione
di una Card, che consiste nellutilizzo della classe CardFrame direttamente nel documento
di layout oppure allinterno di un componente che si chiama CardScrollView e che
fornisce la possibilit di scorrere contenuti che non stanno interamente nel display.
Come esempio di questa situazione abbiamo creato il documento di layout
card_scroll.xml, che riportiamo di seguito:

<android.support.wearable.view.BoxInsetLayout

xmlns:android=http://schemas.android.com/apk/res/android
xmlns:app=http://schemas.android.com/apk/res-auto
android:layout_width=match_parent
android:layout_height=match_parent

android:background=@drawable/ic_launcher>
<android.support.wearable.view.CardScrollView
android:id=@+id/card_scroll_view
android:layout_width=match_parent
android:layout_height=match_parent
app:layout_box=bottom>
<android.support.wearable.view.CardFrame
android:layout_width=fill_parent
android:layout_height=wrap_content>
<LinearLayout
android:layout_width=match_parent
android:layout_height=wrap_content
android:orientation=vertical
android:paddingLeft=5dp>
<TextView
android:layout_width=match_parent
android:layout_height=wrap_content
android:fontFamily=sans-serif-light
android:text=@string/some_short_text
android:textColor=@color/black
android:textSize=20sp />
<TextView
android:layout_width=match_parent
android:layout_height=wrap_content
android:fontFamily=sans-serif-light
android:text=@string/card_fragment_long_description
android:textColor=@color/black
android:textSize=14sp />

</LinearLayout>
</android.support.wearable.view.CardFrame>
</android.support.wearable.view.CardScrollView>
</android.support.wearable.view.BoxInsetLayout>


Notiamo come nel layout sia stato utilizzato il componente CardScrollView, il quale
pu contenere un unico elemento di tipo CardFrame, che nel nostro caso contiene a sua
volta un LinearLayout con i due componenti che vediamo visualizzati nella Figura 7.39.

Figura 7.39 Esempio di utilizzo di una CardScrollView.

Attraverso lattivit descritta dalla classe CardScrollActivity abbiamo ottenuto il


riferimento alloggetto CardScrollView e quindi impostato la gravity, ovvero la posizione
del display verso la quale vogliamo visualizzare il contenuto del CardFrame.

public class CardScrollActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.card_scroll);
CardScrollView cardScrollView =
(CardScrollView) findViewById(R.id.card_scroll_view);
cardScrollView.setCardGravity(Gravity.BOTTOM);
}
}


A questo punto lasciamo al lettore la possibilit di fare altri esperimenti con queste
due tipologie di componenti. Ricordiamo infine come anche questo esempio sia stato
aggiunto come possibile voce nella lista del paragrafo precedente.

Il pattern 2D Picker
In precedenza abbiamo visto come implementare una lista che permetta la
selezione di unopzione tra alcune disponibili. Quello ottenuto nella Figura 7.37
permette per di visualizzare le varie voci in ununica Card e quindi di fare una
selezione. Tra i pattern di usabilit dei dispositivi Wear ve n uno che permette di
navigare tra un insieme di Card attraverso degli swap verticali e orizzontali, come se
fossero organizzate in una grande matrice. Questo pattern si chiama 2D Picker e viene
utilizzato, per esempio, nella visualizzazione dei risultati di una ricerca nel Web
attraverso il dispositivo Wear. Anche in questo caso la libreria di compatibilit per
ci viene in aiuto e ci mette a disposizione il componente GridViewPager, a cui

Wearable

passeremo il riferimento a uno speciale Adapter implementato attraverso la classe


e che contiene tutte le informazioni sulla griglia.

FragmentGridPagerAdapter

NOTA

Nel codice che segue non penseremo pi a gestire le due versioni di display, cosa che lasciamo
come esercizio al lettore.

Nel nostro caso implementiamo un esempio molto semplice, che permette di creare
una matrice composta da tre righe ciascuna, con un numero di colonne crescente. La
prima Page sar quindi unica, mentre quella nella seconda riga avr un altro elemento
sulla destra, il terzo due e cos via. Come primo passo non dobbiamo fare altro che
definire un layout, che nel nostro caso stato definito nel file grid_layout.xml.

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.GridViewPager

xmlns:android=http://schemas.android.com/apk/res/android
android:id=@+id/grid_pager
android:layout_width=match_parent
android:layout_height=match_parent />

Il passo successivo consiste nella creazione delladapter, una classe che estende
e che nel nostro caso molto semplice.

FragmentGridPagerAdapter


public class GridAdapter extends FragmentGridPagerAdapter {

private static final int ROW_COUNT = 3;


public GridAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getFragment(int row, int col) {
final String text = ( + row + , + col + );
CardFragment fragment =
CardFragment.create(Grid, text, R.drawable.ic_launcher);
fragment.setCardGravity(Gravity.CENTER);
fragment.setExpansionEnabled(true);
fragment.setExpansionDirection(CardFrame.EXPAND_DOWN);

fragment.setExpansionFactor(2.0f);
return fragment;
}
@Override
public int getRowCount() {
return ROW_COUNT;
}
@Override
public int getColumnCount(int rowNum) {
return rowNum + 1;
}
@Override
public ImageReference getBackground(int row, int column) {
return ImageReference.forDrawable(R.drawable.ic_launcher);
}
}


Le operazioni pi importanti sono quelle che ci permettono di definire il numero di
righe e, per ciascuna riga, il numero di colonne. Questo viene fatto attraverso i
metodi getRowCount() e quindi getColumnCount(). Nel nostro caso abbiamo cinque righe,
mentre la riga i-esima ha esattamente i + 1 colonne. Il metodo pi importante per
getFragment(), che ha come parametri di input gli indici della riga e colonna da
riempire. Nel nostro caso non facciamo altro che creare una CardFragment che visualizza
le informazioni di riga e colonna. Da notare come lo stesso Adapter ci permetta di
decidere anche limmagine di sfondo per la particolare cella, attraverso il metodo
getBackground() che restituisce un oggetto di tipo ImageReference che permette di gestire i
riferimenti alle immagini in modo ottimizzato in termini di memoria.
Anche in questo caso lActivity molto semplice e descritta dalla classe GridActivity.
Le operazioni da eseguire sono quelle classiche: si importa il layout, si ottiene il
riferimento alloggetto GridViewPager, per poi passargli il riferimento allAdapter creato
poco prima.


public class GridActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.grid_layout);
// We get the reference to the Pager
final GridViewPager pager =
(GridViewPager) findViewById(R.id.grid_pager);
// We create the Adapter
final GridAdapter gridAdapter = new GridAdapter(getFragmentManager());
// We set the Adapter to the Pager
pager.setAdapter(gridAdapter);
}
}


Il risultato di tutto questo quello rappresentato nella Figura 7.40. Le pagine
vengono organizzate in uno Stream di tre pagine verticali, con un numero di Page
associate incrementale.

Figura 7.40 Implementazione del pattern 2D Picker.

Azioni di conferma
Come abbiamo visto in precedenza, attraverso il dispositivo Wear possibile
eseguire azioni di vario tipo. Alcune permettono la semplice visualizzazione di una
Activity, come nel caso del nostro menu degli esempi. Altre implementano invece un
pattern che si chiama Automatic Confirmation Timers, che consiste nellavvio di
unanimazione, al termine della quale lazione confermata in modo automatico, ma
che pu essere interrotta dallutente stesso. Questo accade, per esempio, quando si
pronuncia un comando vocale del tipo "Call me a taxi", il quale avvia una chiamata a
un numero telefonico. Se lutente non compie alcuna azione, la chiamata viene
avviata. Se lutente interrompe lazione, il tutto si conclude l. Il completamento
dellazione, in questi casi, viene poi rappresentato attraverso unopportuna
animazione. Esistono poi delle azioni che necessitano di una conferma esplicita,

attraverso delle schermate di sistema. Altre azioni, infine, permettono il lancio del
menu delle impostazioni per la selezione di una particolare opzione.
La libreria di supporto Wearable ci permette di gestire lAutomatic Confirmation
Timers attraverso il componente DelayedConfirmationView. Come esempio di utilizzo di
questo componente realizziamo una Activity descritta dalla classe AutoConfirmActivity
che, dopo essere stata attivata attraverso la solita lista, avvia un timer e visualizza
unanimazione. Se durante questo periodo si annulla loperazione, si ritorna
allelenco delle opzioni. Nel caso in cui lazione venisse confermata, provvederemo
alla visualizzazione di un messaggio. Iniziamo, come sempre, dalla definizione del
layout nel documento auto_confirm.xml.

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.BoxInsetLayout

xmlns:android=http://schemas.android.com/apk/res/android
xmlns:app=http://schemas.android.com/apk/res-auto
android:layout_width=match_parent
android:layout_height=match_parent
android:padding=15dp>
<FrameLayout
android:layout_width=match_parent
android:layout_height=match_parent
android:padding=5dp
app:layout_box=all>
<android.support.wearable.view.DelayedConfirmationView
android:id=@+id/delayed_confirm
android:layout_width=40dp
android:layout_height=40dp
android:layout_gravity=center
android:src=@drawable/ic_launcher
app:circle_border_color=@color/green
app:circle_border_width=4dp

app:circle_radius=50dp />
</FrameLayout>
</android.support.wearable.view.BoxInsetLayout>


Notiamo come, nel codice evidenziato, vi sia la possibilit di specificare il colore e
la dimensione del cerchio che indica il trascorrere del tempo. Nel nostro caso
abbiamo utilizzato un cerchio di colore verde, per semplificarne la visualizzazione
rappresentata nella Figura 7.41.

Figura 7.41 Visualizzazione dellanimazione di Auto Confirm.

Anche in questo caso lActivity molto semplice. Dopo aver ottenuto un


riferimento allelemento di tipo DelayedConfirmationView ne registra un Listener, il quale
prevede la definizione di due operazioni. La prima, onTimerFinished(), viene richiamata
nel momento in cui il timer finisce il proprio tempo e quindi lazione deve darsi per
valida. Il secondo metodo, onTimerSelected(), viene invece selezionato nel caso in cui
lutente faccia clic sullanimazione. In ogni caso il parametro passato contiene il
riferimento alla DelayedConfirmationView selezionata.

public class AutoConfirmActivity extends Activity {

/**
* The delay to wait before automatic confirmation
*/
private static final long DELAY = 2000L;
/**

* The Reference to the Confirmation item


*/
private DelayedConfirmationView mDelayedView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.auto_confirm);
// We get the reference to the DelayedConfirmationView
mDelayedView =
(DelayedConfirmationView) findViewById(R.id.delayed_confirm);
// We register the Listener
mDelayedView.setListener(
new DelayedConfirmationView.DelayedConfirmationListener() {
@Override
public void onTimerFinished(View view) {
// Here the time is finished
final Intent boomIntent = new Intent(AutoConfirmActivity.this,
BoomActivity.class);
startActivity(boomIntent);
finish();
}
@Override
public void onTimerSelected(View view) {
mDelayedView.setListener(null);
// The timer was closed
finish();
}
});

}
@Override
protected void onStart() {
super.onStart();
// We set the timer
mDelayedView.setTotalTimeMs(DELAY);
// We start the timer
mDelayedView.start();
}
}


Nellimplementazione del metodo onStart() impostiamo il tempo massimo di attesa
prima della conferma attraverso il metodo setTotalTimeMs() e quindi avviamo
lanimazione con il metodo start(). Queste sono operazioni che nel nostro esempio
abbiamo messo in questo punto, ma che devono essere associate allavvio di
unazione da parte dellutente. Lultima osservazione riguarda limplementazione del
metodo di callback onTimerSelected(), il quale contiene la rimozione del Listener
attraverso lassegnazione di null. Questo si rende necessario in quanto, anche nel
caso di annullamento dellazione, il metodo onTimerFinished() viene comunque
richiamato al termine del tempo impostato.
In questa prima parte del nostro esempio abbiamo fatto in modo che, in caso di
scadenza del tempo a disposizione per lannullamento dellazione, si visualizzasse
una Activity con un messaggio, come indicato nella Figura 7.42.

Figura 7.42 Messaggio di completata azione.

Fortunatamente la libreria di supporto per le Wearable ci mette a disposizione


unattivit descritta dalla classe ConfirmationActivity, che ha come scopo proprio quello
di visualizzare unanimazione con lesito delloperazione. Esistono animazioni in
caso di azione eseguita con successo, azione fallita oppure unanimazione che indica
lapertura di una Activity sullo smartphone. Il tutto si gestisce attraverso degli extra
definiti attraverso opportune costanti della stessa classe ConfirmationActivity. Come
dimostrazione abbiamo implementato il metodo showStandardConfirmation(), che contiene
proprio la logica di avvio di questa attivit, ovvero:

private void showStandardConfirmation() {

Intent intent = new Intent(this, ConfirmationActivity.class);


intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,
ConfirmationActivity.SUCCESS_ANIMATION);
intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE,
getString(R.string.boom_text));
startActivity(intent);
}


Notiamo come la costante EXTRA_ANIMATION_TYPE identifichi il tipo di animazione, che
pu essere uno di questi tre:

ConfirmationActivity.SUCCESS_ANIMATION

ConfirmationActivity.FAILURE_ANIMATION
ConfirmationActivity.OPEN_ON_PHONE_ANIMATION


Infine notiamo come sia possibile anche impostare un testo che, nel nostro caso,
ancora il messaggio di boom.
NOTA
Ricordiamo che la classe ConfirmationActivity non automaticamente registrata nel file di
configurazione AndroidManifest.xml. quindi indispensabile aggiungerla alle altre dichiarazioni.

Il risultato sar quello rappresentato nella Figura 7.43 ovvero un cerchio verde
visualizzato attraverso una simpatica animazione.

Figura 7.43 Messaggio standard di completata azione.

Lasciamo al lettore il compito di scoprire le animazioni corrispondenti agli altri


due casi di operazione fallita e di apertura di unattivit nel dispositivo smartphone.

Semplificare luscita dalle applicazioni wearable


Supponiamo di aver utilizzato il 2D Picker per visualizzare un numero
relativamente elevato di Page e di voler uscire dalla nostra applicazione. Se durante la
navigazione ci siamo spostati molto verso destra, luscita dallapplicazione
presuppone che si debba ritornare indietro attraverso una serie di swap da sinistra
verso destra. In casi come questo nasce quindi la necessit di trovare un meccanismo
di uscita dallapplicazione pi veloce e immediato, che consiste in due fasi.
Innanzitutto necessario disabilitare swipe to dismiss e quindi fornire una modalit
alternativa attraverso un altro pattern che si chiama long press to dismiss. La prima
parte molto semplice e consiste nella definizione di uno style, nel quale si disabilita

appunto lo swipe to dismiss attraverso il corrispondente attributo, come nel seguente


codice:

<style name="AppTheme" parent="Theme.DeviceDefault">

<item name=android:windowSwipeToDismiss>false</item>
</style>


Il secondo passo, ovvero limplementazione del pattern long press to dismiss
anchesso molto semplice e presuppone lutilizzo di un nuovo componente, descritto
dalla classe DismissOverlayView. Si tratta, sostanzialmente, di un componente che si
mette sopra tutti gli elementi del layout della Activity, occupando tutto lo spazio
disponibile, con il compito di visualizzare lazione di uscita dallapplicazione in caso
di bisogno. Come esempio abbiamo creato il seguente documento di layout, di nome
dismiss_layout.

<android.support.wearable.view.BoxInsetLayout

xmlns:android=http://schemas.android.com/apk/res/android
xmlns:app=http://schemas.android.com/apk/res-auto
android:layout_width=match_parent
android:layout_height=match_parent
android:padding=15dp>
<FrameLayout
android:layout_width=match_parent
android:layout_height=match_parent
android:padding=5dp
app:layout_box=all>
<TextView
android:layout_width=match_parent
android:layout_height=wrap_content
android:layout_gravity=top
android:gravity=center

android:text=@string/some_long_text />
<TextView
android:layout_width=match_parent
android:layout_height=wrap_content
android:layout_gravity=bottom
android:gravity=center
android:text=@string/some_short_text />
<android.support.wearable.view.DismissOverlayView
android:id=@+id/dismiss_overlay
android:layout_width=match_parent
android:layout_height=match_parent />
</FrameLayout>
</android.support.wearable.view.BoxInsetLayout>


La parte interessante consiste nellutilizzo del componente DismissOverlayView al di
sopra di tutti gli elementi del layout. NellActivity dovremo quindi riconoscere il long
press e quindi provvedere alla visualizzazione del componente di chiusura. La classe
DismissActivity la seguente:

public class DismissActivity extends Activity {

private DismissOverlayView mDismissOverlay;


private GestureDetector mDetector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dismiss_layout);
mDismissOverlay =
(DismissOverlayView) findViewById(R.id.dismiss_overlay);
mDismissOverlay.setIntroText(R.string.long_press_dismiss);

mDismissOverlay.showIntroIfNecessary();
// We use the GestureDetector to understand if the user did long press
// Configure a gesture detector
mDetector = new GestureDetector(this,
new GestureDetector.SimpleOnGestureListener() {
public void onLongPress(MotionEvent ev) {
// We show the Dismiss Overlay
mDismissOverlay.show();
}
});
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// This is the Method where we actually capture the long pressed
return mDetector.onTouchEvent(ev) || super.onTouchEvent(ev);
}
}


Dopo aver impostato il documento di layout, abbiamo ottenuto un riferimento
alloggetto di tipo DismissOverlayView. I metodi seguenti permettono di visualizzare, alla
prima visualizzazione, un messaggio che informa lutente di questa possibilit,
ovvero di poter uscire dallapplicazione attraverso un evento di long press. Loggetto
di tipo GestureDetector ci serve proprio per intercettare levento e quindi visualizzare il
attraverso il suo metodo show(). Il risultato quello rappresentato nella

DismissOverlay

Figura 7.44. Attraverso il pulsante visualizzato sar quindi possibile uscire


dallactivity associata eseguendo di fatto un finish().

Figura 7.44 Pulsante per luscita dallactivity associata.

Tecniche di sincronizzazione
Quando abbiamo parlato di applicazioni Wear ci siamo soffermati sulla necessit di
capire quali operazioni debbano essere eseguite sullo smartphone e quali invece sul
dispositivo Wearable. Un primo criterio fa riferimento ai sensori che un dispositivo di
quel tipo pu avere per il fatto di essere, per esempio, un orologio a stretto contatto
con la pelle di chi lo indossa. Nel caso per in cui si avesse bisogno di unelevata
capacit di elaborazione, si potrebbe utilizzare il dispositivo Wear per la raccolta dei
dati, i quali potrebbero essere inviati allo smartphone, che dopo unelaborazione li
rimette a disposizione del device Wear per visualizzarli sul suo display. Quanto detto ci
porta alla necessit di un meccanismo che permetta lo scambio di dati tra lo
smartphone e il dispositivo Wear. Queste librerie fanno parte dei Google Play services
e per questo, come vedremo, necessitano di una fase di inizializzazione, come per gli
altri servizi che abbiamo imparato a usare nei capitoli precedenti.
Prima di fare questo comunque di fondamentale importanza descrivere gli oggetti
principali che andremo a gestire, primo tra tutti loggetto DataItem. In generale, quando
si ha la necessit di trasmettere un insieme di dati, bene fare uso di un pattern molto
comune in ambiente enterprise e che si chiama Transfert Object. Come spesso
avviene nel caso dei Design Pattern si tratta semplicemente di regole di buon senso,
che prevedono di memorizzare tutti i dati in un unico oggetto che incapsula, in questo
caso, logiche di serializzazione. Quando abbiamo la necessit di aggiungere un nuovo
dato, non dovremo fare altro che incapsularlo in un DataItem e inviarlo alla
destinazione, che sa come ricostruirlo. In questo modo la tecnologia utilizzata per la
trasmissione di un DataItem non dipende in alcun modo dal significato del dato in esso
contenuto. Potrebbe essere uninformazione di location, una stringa o un oggetto pi
complesso. Se esaminiamo la documentazione della classe DataItem, notiamo
innanzitutto che si tratta di unastrazione descritta da una classe astratta, che ha tra le
operazioni principali le seguenti:

public abstract DataItem setData (byte[] data)
public abstract byte[] getData ()


Quello che svolge le funzione di sender inserir il dato nella particolare
specializzazione della classe DataItem attraverso il metodo setData(), mentre il receiver
acceder allo stesso dato attraverso il metodo getData().

NOTA
Al momento esiste un limite di 100 kB nelle dimensioni del dato che possiamo impostare
attraverso il metodo setData().

Come sappiamo, molte delle informazioni che abbiamo la necessit di inviare a un


dispositivo Wearable sono immagini, che possono anche essere di dimensione superiore
ai 100 kB.
NOTA
Come regola generale, qualora avessimo bisogno di inviare unimmagine al dispositivo wearable
buona cosa farne il resize nel dispositivo smartphone e ridurne le dimensioni a quelle del
display di destinazione prima dellinvio. In questo modo il dispositivo wearable si trover un
oggetto gi pronto alla visualizzazione e che non necessita di altre operazioni che possono
risultare pesanti in termini di risorse.

Per casi come questi stata definita la classe Asset, che permette di rappresentare
delle informazioni binarie, come unimmagine, un suono o altro ancora. Il sistema si
preoccupa in modo trasparente di trasmettere questo tipo di informazioni
ottimizzando la connessione Bluetooth, utilizzando inoltre tecniche di cache che
impediscono ripetizioni inutili.
Oltre ai dati che contiene, un DataItem caratterizzato da unaltra informazione
fondamentale che altro non che un path, ovvero una stringa del tipo
che ha come unica limitazione quella di dover necessariamente

/path/subpath/other

iniziare con una slash (/). Tale informazione di fondamentale importanza, perch
identifica in modo univoco il particolare dato in invio e ricezione. Questo il motivo
della presenza nella classe DataItem della seguente operazione, che ci restituisce il path
associato, incapsulato in un oggetto di tipo Uri:

public abstract Uri getUri()


Notiamo come si tratti di un metodo get, che quindi permette di accedere
allinformazione, ma non di impostarla. Questo perch gli oggetti di tipo DataItem non
si utilizzano direttamente, ma attraverso i metodi messi a disposizione dalla classe
PutDataRequest. Un primo esempio ci permetter di inviare un semplice messaggio di
testo al dispositivo Wear, il quale crea una modifica attraverso un comando vocale per
restituire lo stesso messaggio alla precedente activity; una specie di ping pong di
messaggi. Iniziamo con la creazione dellActivity dellapplicazione principale, che
abbiamo chiamato SyncDataActivity e che contiene sostanzialmente la stessa interfaccia
dellactivity realizzata per la gestione dei messaggi di chat, senza la label per i

messaggi in ingresso, che metteremo direttamente nel campo di editing. Il layout


contiene un EditText in cui visualizzare il messaggio ricevuto, eventualmente
modificarlo e quindi inviarlo al dispositivo Wear. Per arrivare a questa funzione
abbiamo quindi aggiunto una nuova voce nel menu delle opzioni. Il passo successivo
molto importante, in quanto permette di inizializzare i Google Play services per
lutilizzo delle API di sincronizzazione dei dati con il dispositivo Wear. Non vogliamo
ripetere le cose dette nei capitoli precedenti, in quanto si tratta di qualcosa che
abbastanza ripetitiva.
NOTA
Ricordiamo che comunque possibile creare lActivity attraverso il corrispondente plug-in di
Android Studio attraverso la voce di menu New > Google > Google Play Service Activity.

La parte fondamentale quella che abbiamo messo nel metodo onCreate(), ovvero la
modalit di inizializzazione delloggetto GoogleApiClient che utilizzeremo per accedere
alle funzionalit richieste, e precisamente:

mGoogleApiClient = new GoogleApiClient.Builder(this)

.addApi(Wearable.API)
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedLister)
.build();

Abbiamo evidenziato la richiesta di utilizzo delle API associate alla costante
Wearable.API. Se tutto procede per il verso giusto avremo il riferimento a un oggetto
valido a partire dal metodo di callback onConnected(), come fatto anche

GoogleApiClient

per le altre API. Ricordiamo che comunque necessario eseguire il metodo connect()
sulloggetto GoogleApiClient in corrispondenza del metodo onStart() e il metodo
in corrispondenza del metodo onStop(). La logica di invio del messaggio

disconnect()

contenuta nel metodo syncMessage(), richiamato in corrispondenza della selezione del


pulsante di invio.
NOTA
Il metodo si chiama syncMessage() e non sendMessage(). Questo perch, a essere precisi, non si
tratta dellinvio di un messaggio, ma della sincronizzazione di uninformazione di tipo testuale. Nel
prossimo paragrafo vedremo quali sono le API per linvio del messaggio, che corrisponde quindi
a un paradigma completamente diverso.

Il metodo syncMessage() a questo punto molto semplice e segue, come spesso


accade con i Google Play services, un pattern abbastanza standard che descriviamo
con il seguente codice:

private void syncMessage(final String message) {

final PutDataRequest putDataRequest =


PutDataRequest.create(MESSAGE_PATH);
putDataRequest.setData(message.getBytes());
Wearable.DataApi.putDataItem(mGoogleApiClient, putDataRequest)
.setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
@Override
public void onResult(DataApi.DataItemResult dataItemResult) {
if (dataItemResult.getStatus().isSuccess()) {
// We get the DataItem sent
final DataItem dataItem = dataItemResult.getDataItem();
Toast.makeText(SyncDataActivity.this,
R.string.data_layer_sync_success,
Toast.LENGTH_SHORT).show();
} else {
// Error creating the message
Toast.makeText(SyncDataActivity.this,
R.string.data_layer_sync_failure,
Toast.LENGTH_SHORT).show();
}
}
});
}

Come abbiamo gi detto, la creazione di un DataItem non avviene quasi mai


direttamente, ma attraverso altri oggetti. In questo caso si utilizza la classe
PutDataRequest, la quale dispone del metodo statico di factory create() che necessita
come parametro del PATH associato al dato che stiamo per sincronizzare. importante
ripetere come questa informazione sia fondamentale per lidentificazione univoca
dellinformazione da sincronizzare. Nel nostro caso abbiamo definito la seguente
costante per semplicit:

/**
* The Path of the message we want to send
*/
private static final String MESSAGE_PATH = "/friendfence/sync";


Una volta ottenuto il riferimento alloggetto PutDataRequest utilizziamo il suo metodo
per impostare il payload, ovvero il nostro messaggio. In questa fase

setData()

importante notare come questa informazione venga passata come array di byte e
quindi in una versione, in un certo senso, serializzata.
NOTA
Vedremo tra poco come risolvere questo problema nel caso di informazioni semplici attraverso la
classe PutDataMapRequest.

A questo punto possiamo inserire il dato in una sorta di contenitore condiviso


con il dispositivo Wearable. La sincronizzazione funziona proprio in questo modo.
come se ci fosse una scatola condivisa tra pi dispositivi. Quando uno di questi
modifica questo contenitore aggiungendo o togliendo dei dati, gli altri ricevono una
notifica che indica che cosa cambiato. In questo caso questo possibile attraverso il
seguente metodo della classe DataApi a cui accediamo con la notazione Wearable.DataApi:

public abstract PendingResult<DataApi.DataItemResult>

putDataItem (GoogleApiClient client, PutDataRequest request)



Il primo parametro limmancabile oggetto GoogleApiClient, mentre il secondo
loggetto di tipo PutDataRequest, che abbiamo precedente creato e che ci ha permesso di
gestire oggetti di tipo DataItem. importante notare come si tratti di unoperazione
asincrona, il cui esito ci viene comunicato attraverso unimplementazione
dellinterfaccia ResultCallback<DataApi.DataItemResult> registrata a un oggetto di tipo

nel modo che abbiamo imparato anche in relazione

PendingResult<DataApi.DataItemResult>

ai Google Drive.
Con questo abbiamo concluso il primo passo, ovvero linserimento del dato
condiviso nel contenitore comune. Passiamo quindi sul dispositivo Wearable per vedere
come questo dato potr essere ricevuto. Si tratter, come prevedibile, di
implementare una sorta di Observer. In questo vorremmo che lattivit che visualizza il
dato sincronizzato non fosse in esecuzione al momento della sincronizzazione. Per
fare questo le DataApi ci mettono a disposizione una particolare implementazione di un
descritto dalla classe WearableListenerService, che utilizzeremo anche per gestire

Service

altre modalit di comunicazione. Si tratta di una classe che utilizzeremo sia per il
dispositivo Wearable che per lapplicazione sullo smartphone. Nel nostro caso abbiamo
implementato la classe SyncDataService eseguendo loverride del seguente metodo:

public void onDataChanged(DataEventBuffer dataEvents)


Il metodo viene richiamato nel momento in cui succede qualcosa sul contenitore
condiviso a cui abbiamo accennato in precedenza. necessario informare il sistema
della presenza di questo servizio e collegarlo in qualche modo allevento di ricezione
di una comunicazione da parte di un altro dispositivo. Per fare questo sufficiente
aggiungere al file AndroidManifest.xml la seguente definizione, nella quale abbiamo
evidenziato il nome della action:

<service android:name=".datalayer.SyncDataService">

<intent-filter>
<action
android:name=com.google.android.gms.wearable.BIND_LISTENER />
</intent-filter>
</service>


A questo punto immaginiamo che linserimento del dato da parte dellapplicazione
sullo smartphone produca lavvio del servizio sul dispositivo Wearable e quindi la
chiamata del metodo onDataChanged(). Nel nostro caso dovremo fare in modo che questo
servizio legga le informazioni ricevute e lanci la visualizzazione di unattivit che

abbiamo descritto, anche qui, attraverso la classe WearSyncDataActivity. Prima di


descrivere questa attivit dobbiamo per vedere come gestire la notifica nel servizio.

@Override
public void onDataChanged(DataEventBuffer dataEvents) {

super.onDataChanged(dataEvents);
Log.d(TAG_LOG, onDataChanged received on Wearable device);
// We get the information form the DataEvents as Freezable object
final List<DataEvent> events =
FreezableUtils.freezeIterable(dataEvents);
// We initialize the GoogleApiClient obejct
GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.build();
// We connect the GoogleApiClient
ConnectionResult connectionResult =
googleApiClient.blockingConnect(CONNECTION_TIMEOUT,
TimeUnit.SECONDS);
if (connectionResult.isSuccess()) {
// Now we have the list of received events so we manage
// all the data in it
for (DataEvent event : events) {
manageEvent(event);
}
} else {
Log.e(TAG_LOG, Error connecting to GoogleApiClient);
}
}


Innanzitutto notiamo come la prima operazione utilizzi una classe di utilit che si

chiama FreezableUtils e che permette di gestire i dati contenuti nelloggetto di tipo


ricevuto come parametro, come lista di oggetti Freezable. Ma che cos

DataEventBuffer

un oggetto Freezable? Si tratta sostanzialmente di un oggetto che pu essere reso


immutabile. Come sappiamo, il concetto di immutabilit di fondamentale
importanza quando si gestiscono tecnologie di condivisione dei dati. Nel caso di
programmazione concorrente, per esempio, fondamentale poter gestire degli oggetti
immutabili, in quanto si impedisce a un Thread di modificare il contenuto di un oggetto
utilizzato da un altro Thread, cadendo in situazioni imprevedibili e di difficile
risoluzione.
NOTA
Lo stato di un oggetto immutabile non pu essere modificato e quindi loggetto pu essere
condiviso tranquillamente tra Thread o comunque componenti diversi. In questo caso si rendono i
dati immutabili, in modo da non cadere in un loop infinito di notifiche tra un dispositivo e un altro,
una situazione molto simile a quella descritta dallantipattern Hot Potato.

Il passo successivo consiste nellinizializzazione anche sul dispositivo Wearable


delloggetto di tipo GoogleApiClient, richiedendo lutilizzo delle API associate alla
costante Wearable.API. Si tratta per di un oggetto non ancora connesso, per cui si rende
necessaria la chiamata, sullo stesso oggetto di tipo GoogleApiClient, del seguente
metodo, che abbiamo utilizzato nella versione con timeout per impedire che il Thread
corrente venga bloccato indefinitamente:

public abstract ConnectionResult blockingConnect (long timeout, TimeUnit unit)


In ogni caso il risultato un oggetto di tipo ConnectionResult, che andiamo a
consultare per conoscere lesito delloperazione di connessione. Nel caso di mancata
connessione, visualizziamo semplicemente un messaggio di log, mentre in caso di
successo procediamo, finalmente, con la lettura delle informazioni contenute
nelloggetto di tipo DataEventBuffer ricevuto come parametro del metodo onDataChanged()
del servizio. In precedenza avevamo ottenuto linsieme di eventi ricevuti come lista
di oggetti di tipo Event, che ora andiamo a gestire attraverso un metodo di utilit che
abbiamo chiamato manageEvent() e che implementa leffettiva elaborazione dei dati.
NOTA
Ricordiamo, come abbiamo detto in fase di creazione del modulo Wear, che il funzionamento di
questo meccanismo presuppone che lapplicazione sullo smartphone e quella nel dispositivo
Wear siano associate allo stesso package. In caso contrario le operazioni di sincronizzazione
verrebbero completamente ignorate, senza dare alcun errore.

Si tratta di un metodo che dovr estrarre le informazioni dallevento ricevuto e


quindi impiegarle per il lancio di unattivit che utilizzeremo per implementare la
logica di risposta attraverso comando vocale.

private void manageEvent(final DataEvent event) {

// We get the DataItem


final DataItem dataItem = event.getDataItem();
// We get the Uri for the received event
final Uri receivedUri = dataItem.getUri();
// We check if the uri is related to the data sent form the
// application on the main device
if (MESSAGE_PATH.equals(receivedUri.getPath())) {
// We get the nodeId we need for the reply
String nodeId = receivedUri.getHost();
// We read the payload for the received data
final byte[] payload = dataItem.getData();
final String receivedMessage = new String(payload);
// We launch the Activity to manage this
Intent intent = new Intent(this, WearSyncDataActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(WearSyncDataActivity.MESSAGE_EXTRA,
receivedMessage);
intent.putExtra(WearSyncDataActivity.NODE_EXTRA, nodeId);
startActivity(intent);
}
}

Innanzitutto notiamo come dallevento ricevuto come parametro si ottenga il


riferimento alloggetto di tipo DataItem di cui otteniamo il path di identificazione
attraverso il suo metodo getUri(). Si tratta del path che utilizziamo per verificare se il
dato ci interessa, in quanto attraverso questo meccanismo riceviamo tutte le notifiche
di modifica delle informazioni condivise. Il passo successivo consiste nella lettura
dellinformazione relativa al nodo, che in realt non ci servir in questa fase, ma che
sar utile nel caso di scambio di messaggi. Attraverso il metodo getData() andiamo a
leggere i dati che ottiene sotto forma di array di byte. Nel nostro caso sappiamo
trattarsi di una String, per cui non facciamo altro che ricostruirla per poi inviarla
allattivit che la dovr visualizzare nel display. In questa fase notiamo lutilizzo di
alcuni flag. Il primo, Intent.FLAG_ACTIVITY_NEW_TASK, necessario, in quanto siamo
allinterno di un Service e la visualizzazione di una Activity necessita della creazione di
un nuovo task. Il secondo, invece, Intent.FLAG_ACTIVITY_SINGLE_TOP, importante perch
quando utilizzeremo lattivit per inviare la risposta allapplicazione principale
andremo a cambiare il contenitore condiviso e quindi anche lapplicazione Wear stessa
ne ricever notifica. In quel caso abbiamo deciso di non mettere alcun filtro in fase di
ricezione, ma semplicemente di gestire il fatto che venga richiesta la visualizzazione
del dato a una Activity che gi attiva. Attraverso questo flag facciamo in modo che
se lActivity gi attiva in cima allo stack delle attivit, non ne venga creata una
nuova istanza, ma semplicemente notificato lIntent attraverso la chiamata del
metodo di callback onNewIntent(), come vedremo tra poco. In questa fase abbiamo
implementato il fatto che una modifica delle informazioni condivise provochi il
lancio di un servizio, il cui compito quello di leggere i dati e inviarli a unattivit
che ci permetter di gestirne la visualizzazione e lulteriore modifica. Questa logica
stata implementata nella classe WearSyncDataActivity del progetto Wear. In questa classe
non ci sono particolari novit rispetto a quanto realizzato per il dispositivo
smartphone nella classe SyncDataActivity. Anche qui abbiamo dovuto gestire
linizializzazione delloggetto di tipo GoogleApiClient e linvio del dato attraverso un
oggetto di tipo PutDataRequest. Lunico aspetto da notare riguarda limplementazione
del metodo onNewIntent() nel seguente modo:

@Override
protected void onNewIntent(Intent intent) {

super.onNewIntent(intent);
// We show the text if present

final String message = intent.getStringExtra(MESSAGE_EXTRA);


if (!TextUtils.isEmpty(message)) {
mMessageTextView.setText(message);
}
}


Il metodo viene richiamato al momento della modifica del dato sincronizzato da
parte dellattivit stessa. A questo punto dobbiamo ritornare sullapplicazione
principale e implementare una logica analoga di gestione dellevento relativo alla
modifica del dato. La buona notizia che il codice da realizzare perfettamente
simmetrico e quindi non ci resta che creare unimplementazione dello stesso servizio
SyncDataService fatta per lapplicazione Wearable. Andiamo quindi a creare la stessa
classe nel progetto principale, con la sola modifica che ora linterazione dovr
avvenire con lattivit descritta dalla classe SyncDataActivity. bene fare attenzione a
registrare il servizio nel documento AndroidManifest.xml di configurazione e ad
aggiungere limplementazione del metodo onNewIntent().
A questo punto siamo pronti a verificarne il funzionamento. Nella nostra
applicazione principale selezioniamo la voce Ping Pong nel menu delle opzioni,
ottenendo la schermata rappresentata nella Figura 7.45, dove notiamo la presenza di
un campo di testo nel quale andiamo a inserire del testo. Selezioniamo il pulsante Send
e osserviamo come il testo inserito venga replicato sul dispositivo wearable, come
indicato nella Figura 7.46. Nella stessa notiamo la presenza di un pulsante per la
gestione della risposta.

Figura 7.45 Visualizzazione della schermata per lapplicazione Ping Pong.

Figura 7.46 Visualizzazione del messaggio sul dispositivo wearable.

Per modificare il testo sul dispositivo Wearable, abbiamo utilizzato il riconoscitore


vocale che abbiamo imparato a usare in precedenza.
Attraverso il riconoscitore vocale modifichiamo il testo visualizzato sul display,
che viene quindi sincronizzato. Questo causa la modifica del testo sia sul dispositivo
Wearable che sul quello principale. Una cosa molto importante: se il testo non viene
modificato e quindi si prova a rimettere nel contenitore condiviso un valore uguale a
quello esistente, la notifica non viene inviata. Possiamo dire che vengono notificate
solamente le effettive modifiche e non le operazioni di inserimento del dato. Altra
osservazione riguarda il fatto che gli oggetti di tipo DataEvent che vengono ricevuti
dispongono del seguente metodo, che ci permette di capire se si tratta di un evento di
update di un dato o della sua eliminazione.

public abstract int getType()


Questo possibile attraverso le seguenti costanti:

DataEvent.TYPE_CHANGED
DataEvent.TYPE_DELETED

Utilizzo della classe PutDataMapRequest


Nellesempio precedente abbiamo visto come gestire la sincronizzazione di oggetti
tra lo smartphone e il dispositivo Wearable. Il meccanismo che abbiamo descritto
presuppone che loggetto condiviso sia serializzabile, ovvero si possa trasformare in
un array di byte. Nella programmazione Android abbiamo imparato a utilizzare
oggetti di tipo Bundle, che sono delle specie di Map che ci permettono di associare valori
di vario tipo a chiavi di tipo String. Per semplificare la sincronizzazione dei dati, le
DataApi, ci mettono a disposizione una nuova classe, che si chiama appunto
PutDataMapRequest e che permette di gestire i dati da sincronizzare allo stesso modo con
cui gestiamo le informazioni in un Bundle. Per vedere come funziona abbiamo
modificato lesempio precedente, implementando due metodi alternativi per linvio
delle informazioni dallapplicazione sia sullo smartphone, sia sul dispositivo Wearable.
Nel caso dellinvio dal dispositivo smartphone abbiamo implementato questo
secondo metodo, che il lettore potr testare semplicemente collegando la sua
chiamata alla selezione del pulsante di sincronizzazione.

private void syncMessageMap(final String message) {

if (!mGoogleApiClient.isConnected()) {
Log.d(TAG_LOG, mGoogleApiClient not connected );
return;
}
// We create the PutDataMapRequest object
final PutDataMapRequest putDataMapRequest =
PutDataMapRequest.create(MESSAGE_PATH);
// We get the DataMap

final DataMap dataMap = putDataMapRequest.getDataMap();


// We set the message as value in the map
dataMap.putString(MESSAGE_EXTRA, message);
// We get the PutDataRequest
final PutDataRequest putDataRequest =
putDataMapRequest.asPutDataRequest();
// We set the data as payload into the shared container
Wearable.DataApi.putDataItem(mGoogleApiClient, putDataRequest)
.setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
@Override
public void onResult(DataApi.DataItemResult dataItemResult) {
if (dataItemResult.getStatus().isSuccess()) {
// We get the DataItem sent
final DataItem dataItem = dataItemResult.getDataItem();
Toast.makeText(SyncDataActivity.this,
R.string.data_layer_sync_success,
Toast.LENGTH_SHORT).show();
} else {
// Error creating the message
Toast.makeText(SyncDataActivity.this,
R.string.data_layer_sync_failure,
Toast.LENGTH_SHORT).show();
}
}
});
}

Come possiamo notare, si crea innanzitutto un oggetto di tipo PutDataMapRequest


attraverso il suo metodo statico di factory create() passando, anche in questo caso,
come parametro il path associato. Fatto questo si richiama sulloggetto creato il
metodo getDataMap(), ottenendo un oggetto di tipo DataMap che ci permette di inserire
una serie di valore associandoli a chiavi diverse. Nel nostro caso abbiamo associato
linformazione relativa al messaggio alla stessa chiave che utilizziamo per
lidentificazione dellextra nellActivity. Inseriti tutti i valori che intendiamo
sincronizzare, richiamiamo il metodo asPutDataRequest(), il quale ci permette di
ritrovare un oggetto di tipo PutDataRequest e quindi ricadere nel caso descritto nel
paragrafo precedente. Quello descritto il metodo utilizzato nellActivity
dellapplicazione per smartphone, ma il lettore potr verificare come lo stesso si
possa fare nellapplicazione sul dispositivo Wearable. Oltre alla parte di inserimento del
dato, dobbiamo cambiare qualcosa anche nella parte di lettura, come possiamo vedere
nel seguente metodo, che abbiamo implementato nel servizio dellapplicazione
Wearable.
NOTA
Anche in questo caso le operazioni sono simmetriche nellapplicazione per il dispositivo wearable
e per lo smartphone.

Analogamente a quanto fatto in precedenza, il lettore potr semplicemente


sostituire le chiamate dei vari metodi per le due diverse implementazioni.

private void manageEventMap(final DataEvent event) {

// We get the DataItem


final DataItem dataItem = event.getDataItem();
// We get the Uri for the received event
final Uri receivedUri = dataItem.getUri();
// We check if the uri is related to the data sent form
// the application on the main device
if (MESSAGE_PATH.equals(receivedUri.getPath())) {
// We get the nodeId we need for the reply
String nodeId = receivedUri.getHost();
// We get the DataMapItem

final DataMapItem dataMapItem = DataMapItem.fromDataItem(dataItem);


// We read the message
final String receivedMessage = dataMapItem.getDataMap()
.getString(SyncDataActivity.MESSAGE_EXTRA);
// We launch the Activity to manage this
Intent intent = new Intent(this, SyncDataActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(SyncDataActivity.MESSAGE_EXTRA, receivedMessage);
intent.putExtra(SyncDataActivity.NODE_EXTRA, nodeId);
startActivity(intent);
}
}


Nelle istruzioni evidenziate possiamo notare come la prima cosa da fare consista
nellottenere il riferimento a un oggetto di tipo DataItem, che poi passiamo come
parametro del metodo statico fromDataItem() della classe DataMapItem per riottenere il
riferimento alla mappa con i dati inseriti in precedenza. Attraverso la stessa chiave
andiamo quindi a prendere il messaggio che poi utilizziamo in modo analogo a
quanto fatto in precedenza.
Questo metodo di gestione delle informazioni da sincronizzare sicuramente pi
semplice qualora si debbano sincronizzare diversi campi di tipi semplici ovvero
primitivi o String. Lasciamo quindi al lettore la verifica del funzionamento di questa
modalit.

Sincronizzazione di immagini
Dopo aver visto come si sincronizza uninformazione di tipo semplice, come una
String, vediamo come sincronizzare delle informazioni binarie, come possono essere
quelle che definiscono unimmagine. Per questo tipo di informazioni, le cui
dimensioni possono essere superiori ai 100 KB, stata definita la classe Asset. Oggetti
di questo tipo hanno un trattamento particolare e alcune ottimizzazioni consentono di

utilizzare al meglio la connessione Bluetooth e le risorse in genere. In questo


paragrafo vedremo come inviare dal nostro smartphone al dispositivo Wearable delle
immagini che scegliamo dalla nostra Gallery. Nellapplicazione principale abbiamo
creato la classe SyncImageActivity, la quale contiene un pulsante che permette di lanciare
un Intent per la selezione di unimmagine, che poi inviamo al dispositivo Wearable
attraverso un pulsante di Sync. Lasciamo al lettore la consultazione del codice relativo
allinizializzazione delloggetto GoogleApiClient e alla selezione di unimmagine nella
e ci concentriamo solamente sulla descrizione dei metodi di sincronizzazione

Gallery

delloggetto di tipo Bitmap, sia nel caso di un oggetto (PutDataRequest), sia nel caso di
uso di PutDataMapRequest. Innanzitutto notiamo come sia semplice trasformare un
oggetto di tipo Bitmap nellequivalente Asset.

private static Asset fromBitmap(Bitmap bitmap) {

final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();


bitmap.compress(Bitmap.CompressFormat.PNG, BEST_QUALITY, byteStream);
return Asset.createFromBytes(byteStream.toByteArray());
}


Basta trasformare il Bitmap in array di byte, da usare poi come parametro del
metodo statico di factory createFromByte() della classe Asset. Fatto questo anche i
metodi per la sincronizzazione di un Asset sono di semplice lettura. Nel caso di un
oggetto di tipo PutDataRequest si ha il seguente codice:

private void syncImage(final Bitmap bitmap) {

if (!mGoogleApiClient.isConnected()) {
Log.d(TAG_LOG, mGoogleApiClient not connected );
return;
}
// We get the Asset from the Bitmap
Asset asset = fromBitmap(bitmap);
// We create the PutDataRequest object

final PutDataRequest putDataRequest =


PutDataRequest.create(SYNC_IMAGE_PATH);
// We set the Asset into the request
putDataRequest.putAsset(IMAGE_KEY, asset);
// We set the data as payload into the shared container
Wearable.DataApi.putDataItem(mGoogleApiClient, putDataRequest)
.setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
@Override
public void onResult(DataApi.DataItemResult dataItemResult) {
if (dataItemResult.getStatus().isSuccess()) {
// We get the DataItem sent
final DataItem dataItem = dataItemResult.getDataItem();
Log.d(TAG_LOG, DataItem + dataItem + successfully sent);
Toast.makeText(SyncImageActivity.this,
R.string.data_layer_sync_success,
Toast.LENGTH_SHORT).show();
} else {
// Error creating the message
Toast.makeText(SyncImageActivity.this,
R.string.data_layer_sync_failure,
Toast.LENGTH_SHORT).show();
}
}
});
}


Lunica istruzione degna di nota quella che abbiamo evidenziato, ovvero la
chiamata del metodo putAsset() che associa loggetto di tipo Asset a una chiave di tipo

. Le altre operazioni sono esattamente le stesse dei casi precedenti. Nel caso di

String

un oggetto di tipo PutDataMapRequest il codice diventa invece il seguente:



private void syncImageMap(final Bitmap bitmap) {

if (!mGoogleApiClient.isConnected()) {
Log.d(TAG_LOG, mGoogleApiClient not connected );
return;
}
// We get the Asset from the Bitmap
Asset asset = fromBitmap(bitmap);
// We create the PutDataMapRequest object
final PutDataMapRequest putDataMapRequest =
PutDataMapRequest.create(SYNC_IMAGE_PATH);
// We get the DataMap
final DataMap dataMap = putDataMapRequest.getDataMap();
// We set the asset
dataMap.putAsset(IMAGE_KEY, asset);
// We get the PutDataRequest
final PutDataRequest putDataRequest =
putDataMapRequest.asPutDataRequest();
// We set the data as payload into the shared container
Wearable.DataApi.putDataItem(mGoogleApiClient,
putDataRequest).setResultCallback(new
ResultCallback<DataApi.DataItemResult>() {
@Override
public void onResult(DataApi.DataItemResult dataItemResult) {
if (dataItemResult.getStatus().isSuccess()) {
// We get the DataItem sent

final DataItem dataItem = dataItemResult.getDataItem();


Log.d(TAG_LOG, DataItem + dataItem + successfully sent);
Toast.makeText(SyncImageActivity.this,
R.string.data_layer_sync_success,
Toast.LENGTH_SHORT).show();
} else {
// Error creating the message
Toast.makeText(SyncImageActivity.this,
R.string.data_layer_sync_failure,
Toast.LENGTH_SHORT).show();
}
}
});
}


Nel presente codice abbiamo semplicemente evidenziato come si debba passare
attraverso un oggetto di tipo DataMap e quindi associare lAsset a una chiave.
Per quanto riguarda la visualizzazione dellimmagine nel dispositivo Wearable
abbiamo questa volta deciso di utilizzare un approccio diverso da quello del Service;
questa volta faremo in modo che levento di sincronizzazione venga ricevuto
solamente nel caso in cui lActivity sia attiva. Oltre allutilizzo della particolare
specializzazione della classe WearableListenerService, esiste infatti una seconda modalit
per ricevere le notifiche sulla disponibilit di nuovi dati, ovvero la registrazione di
unopportuna interfaccia, di nome DataApi.DataListener. Abbiamo implementato la
classe SyncImageActivity con la logica di inizializzazione delle DataApi gi vista, ma con
una cosa in pi, ovvero lesecuzione delle seguenti operazioni in corrispondenza
della connessione o disconnessione.

private final GoogleApiClient.ConnectionCallbacks mConnectionCallbacks =

new GoogleApiClient.ConnectionCallbacks() {
@Override

public void onConnected(Bundle bundle) {


Log.i(TAG_LOG, GoogleApiClient connected);
Wearable.DataApi.addListener(mGoogleApiClient,
mDataListener);
}
@Override
public void onConnectionSuspended(int i) {
Log.i(TAG_LOG, GoogleApiClient connection suspended);
retryConnecting();
Wearable.DataApi.removeListener(mGoogleApiClient,
mDataListener);
}
};


Quando il dispositivo connesso ai Google Play services registriamo una nostra
implementazione dellinterfaccia DataApi.DataListener alloggetto Wearable.DataApi per poi
deregistrarla in fase di disconnessione. Limplementazione di questa interfaccia
dipende dal tipo di oggetto che abbiamo utilizzato per linvio.

private final DataApi.DataListener mDataListener = new

DataApi.DataListener() {
@Override
public void onDataChanged(DataEventBuffer dataEvents) {
for (DataEvent event : dataEvents) {
// In case of usage of PutDataRequest
manageEvent(event);
// In case of usage of PutDataMapRequest
//manageEventMap(event);
}

}
};


Nel caso della classe PutDataRequest abbiamo implementato il metodo manageEvent()
nel seguente modo:

private void manageEvent(DataEvent event) {

// We get the DataItem


final DataItem dataItem = event.getDataItem();
// We get the Uri for the DataItem
final Uri dataUri = dataItem.getUri();
if (DataEvent.TYPE_CHANGED == event.getType() &&
SYNC_IMAGE_PATH.equals(dataUri.getPath())) {
// We get the information
final DataItemAsset dataItemAsset =
dataItem.getAssets().get(IMAGE_KEY);
// We get the File Descriptor
Wearable.DataApi.getFdForAsset(mGoogleApiClient,
dataItemAsset).setResultCallback(new
ResultCallback<DataApi.GetFdForAssetResult>() {
@Override
public void onResult(DataApi.GetFdForAssetResult
getFdForAssetResult) {
final Bitmap bitmap = BitmapFactory
.decodeStream(getFdForAssetResult.getInputStream());
mImageView.setImageBitmap(bitmap);
}
});
}


Attraverso il metodo getAssets() sul DataItem otteniamo una mappa che ci permette di
ottenere il File Descriptor dellimmagine, che poi leggiamo attraverso un InputStream
che lo stesso oggetto ci mette a disposizione. Da qui utilizziamo il BitmapFactory per
ottenere la Bitmap da visualizzare nella ImageView nel layout dellattivit.
Nel caso della classe PutDataMapRequest limplementazione leggermente diversa,
ovvero:

private void manageEventMap(DataEvent event) {

// We get the DataItem


final DataItem dataItem = event.getDataItem();
// We get the Uri for the DataItem
final Uri dataUri = dataItem.getUri();
if (DataEvent.TYPE_CHANGED == event.getType() &&
SYNC_IMAGE_PATH.equals(dataUri.getPath())) {
// We get the DataMapItem
DataMapItem dataMapItem =
DataMapItem.fromDataItem(event.getDataItem());
// We get the asset
Asset profileAsset = dataMapItem.getDataMap().getAsset(IMAGE_KEY);
// We get the File Descriptor
Wearable.DataApi.getFdForAsset(mGoogleApiClient,
profileAsset).setResultCallback(new
ResultCallback<DataApi.GetFdForAssetResult>() {
@Override
public void onResult(DataApi.GetFdForAssetResult
getFdForAssetResult) {
final Bitmap bitmap = BitmapFactory

.decodeStream(getFdForAssetResult.getInputStream());
mImageView.setImageBitmap(bitmap);
}
});
}
}


Anche in questo caso la differenza sta nel passaggio attraverso loggetto di tipo
DataMapItem, da cui accediamo allAsset attraverso la chiave utilizzata in fase di invio.
Abbiamo visto come sia semplice sincronizzare delle immagini. In questa fase non
ci siamo occupati dellottimizzazione dellimmagine sullo smartphone al fine di
ridurne le dimensioni per un utilizzo pi efficiente della connessione. Per verificare il
funzionamento di questa opzione dovremo quindi installare lapplicazione sul
dispositivo Wearable e quindi selezionare la corrispondente voce nel menu delle
opzioni che abbiamo realizzato attraverso la lista sviluppata in precedenza. Ora
lattivit SyncImageActivity pronta a ricevere limmagine da parte dellapplicazione
installata sullo smartphone, a cui abbiamo aggiunto lopzione Sync Image. A questo
punto selezioniamo unimmagine dalla nostra Gallery e facciamo clic sul pulsante di
sincronizzazione. Dopo un intervallo di tempo che dipende dalla dimensione
dellimmagine, notiamo come questa venga visualizzata nel display del dispositivo
Wearable.

Invio e ricezione di messaggi


Dopo aver visto come trasferire informazioni tra uno smartphone e un dispositivo
Wear, ci occupiamo di una funzione leggermente diversa, ovvero dellinvio e della
ricezione di messaggi. Quello che si vuole ottenere attraverso queste API , infatti, la
possibilit di inviare dei messaggi cui associare determinate operazioni nel
dispositivo remoto. Un esempio classico potrebbe essere quello che permette di
controllare il player musicale dello smartphone attraverso alcune azioni sul
dispositivo Wear. Non si tratta di operazioni di sincronizzazione di dati tra due punti
diversi, ma di messaggi unidirezionali che vengono inviati da un dispositivo e
ricevuti dallaltro. Anche in questo caso si tratta di una funzionalit che bisogna
attivare attraverso la classica modalit prevista dai Google Play services associata,
questa volta, alla classe MessageApi. Si tratta di un oggetto molto semplice, che
permette linvio di messaggi attraverso il suo metodo seguente, che notiamo definire
come parametri limmancabile oggetto GoogleApiClient seguito da un identificatore del
nodo (nodeId) che andr a ricevere il messaggio:

public abstract PendingResult<MessageApi.SendMessageResult>

sendMessage (GoogleApiClient client, String nodeId,


String path, byte[] data)

Anche qui un messaggio ha un path associato ed eventualmente dei dati che
possiamo pensare siano i parametri del messaggio stesso. Gli stessi Google Play
services ci devono mettere a disposizione un meccanismo che permetta di interrogare
la Rete sui nodi disponibili. Questo il compito delle NodeApi e che sono associate a
un oggetto omonimo che ci permetter, come vedremo successivamente, di accedere
ai nodi disponibili.
Per quanto riguarda la ricezione del messaggio, anche qui possiamo utilizzare due
meccanismi diversi. Il primo si basa sullutilizzo di unimplementazione
dellinterfaccia MessageApi.MessageListener, da registrare alloggetto MessageApi attraverso
il metodo

public abstract PendingResult<Status> addListener (GoogleApiClient client,

MessageApi.MessageListener listener)


Si tratta di uninterfaccia che definisce loperazione che viene richiamata a seguito
della ricezione del messaggio, ovvero:

public abstract void onMessageReceived(MessageEvent messageEvent)


La seconda consiste, come fatto in precedenza, nella creazione di una particolare
specializzazione della classe WearableListenerService, tramite loverride di un metodo
che ha esattamente la stessa firma di quello precedente, ovvero:

public void onMessageReceived(MessageEvent messageEvent)


Per dimostrare il funzionamento di queste API abbiamo creato unattivit descritta
dalla classe SendMessageActivity, la quale, dopo le ormai classiche operazioni di
inizializzazione, contiene solo il codice per linvio al dispositivo Wearable di
uninformazione relativa al timestamp corrente. Nel caso dellinvio di un messaggio,
dobbiamo per prima identificare le possibili destinazioni e questo possibile
attraverso le NodeApi. Per questo motivo, in corrispondenza dei metodi di callback
relativi alla gestione della connessione, eseguiamo le operazioni messe in evidenza
nel seguente codice:

private final GoogleApiClient.ConnectionCallbacks mConnectionCallbacks =

new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle bundle) {
Log.i(TAG_LOG, GoogleApiClient connected);
// We register the listener for the NodeApi
Wearable.MessageApi
.addListener(mGoogleApiClient, mMessageListener);
Wearable.NodeApi
.addListener(mGoogleApiClient, mNodeListener);

insertConnectedNodes();
}
@Override
public void onConnectionSuspended(int i) {
Log.i(TAG_LOG, GoogleApiClient connection suspended);
Wearable.MessageApi
.removeListener(mGoogleApiClient, mMessageListener);
Wearable.NodeApi
.removeListener(mGoogleApiClient, mNodeListener);
retryConnecting();
}
};


Nella parte relativa allevento di connessione registriamo innanzitutto il Listener per
la ricezione dei messaggi e quello relativo agli eventi di aggiunta o rimozione di
connessione da parte di un possibile dispositivo attraverso le NodeApi. A tale proposito
bene sottolineare come questi eventi si riferiscano allaggiunta o rimozione di nodi,
ma non diano alcuna informazione relativamente ai nodi gi collegati al momento di
avvio dellattivit. Questo il motivo della presenza del seguente metodo di utilit, il
quale ci permettere di interrogare il sistema sui nodi gi connessi da aggiungere a una
variabile distanza che abbiamo chiamato mNodeIds:

private void insertConnectedNodes() {

Wearable.NodeApi.getConnectedNodes(mGoogleApiClient)
.setResultCallback(new ResultCallback<NodeApi
.GetConnectedNodesResult>() {
@Override
public void onResult(NodeApi.GetConnectedNodesResult
getConnectedNodesResult) {
for (Node node : getConnectedNodesResult.getNodes()) {

mNodeIds.add(node.getId());
}
}
});
}


Linterfaccia NodeApi.NodeListener stata implementata nel seguente modo, per
aggiungere o rimuovere informazioni al mNodeIds, che un Set per evitare duplicazioni:

private NodeApi.NodeListener mNodeListener = new NodeApi.NodeListener() {

@Override
public void onPeerConnected(Node node) {
// We add connected nodes
mNodeIds.add(node.getId());
}
@Override
public void onPeerDisconnected(Node node) {
// We remove the nodes
mNodeIds.remove(node.getId());
}
};


Allavvio dellattivit e al termine dellavvenuta connessione dei Google Play
services abbiamo lelenco dei nodi disponibili, che andiamo a utilizzare nel metodo
sendMessage() responsabile dellinvio effettivo del messaggio.

private void sendMessage() {

if (!mGoogleApiClient.isConnected()) {
Log.d(TAG_LOG, mGoogleApiClient not connected );
return;

}
// We check if we have connected nodes
if (mNodeIds.isEmpty()) {
Toast.makeText(getApplicationContext(),
R.string.action_data_layer_no_connected_nodes,
Toast.LENGTH_SHORT).show();
return;
}
// We send the message to the Wearable device with the first id
final String nodeId = mNodeIds.toArray(new String[mNodeIds.size()])[0];
// The message to send
final byte[] dataToSend = String.valueOf(System.currentTimeMillis())
.getBytes();
// We send the data
Wearable.MessageApi.sendMessage(mGoogleApiClient, nodeId,
MESSAGE_PATH, dataToSend)
.setResultCallback(new ResultCallback<MessageApi
.SendMessageResult>() {
@Override
public void onResult(MessageApi.SendMessageResult
sendMessageResult) {
if (sendMessageResult.getStatus().isSuccess()) {
// Message sent successfully
Toast.makeText(getApplicationContext(),
R.string.data_layer_send_success,
Toast.LENGTH_SHORT).show();
} else {
// Error sending message

Toast.makeText(getApplicationContext(),
R.string.data_layer_send_failure,
Toast.LENGTH_SHORT).show();
}
}
});
}


Dopo aver controllato che loggetto di tipo GoogleApiClient sia effettivamente
connesso e che ci siano dei nodi disponibili, non facciamo altro che ottenere
linformazione da inviare come array di byte e quindi utilizzare il metodo sendMessage()
della classe Wearable.MessageApi per inviare il messaggio. Nellormai abituale modalit
asincrona andiamo quindi a verificare se linvio avvenuto con successo oppure no.
A questo punto ci trasferiamo sullapplicazione Wearable, nella quale abbiamo
semplicemente aggiunto limplementazione del metodo onMessageReceived() al servizio
esistente, descritto dalla classe SyncDataService.

@Override
public void onMessageReceived(MessageEvent messageEvent) {

super.onMessageReceived(messageEvent);
Log.d(TAG_LOG, Message received!);
if (MESSAGE_PATH.equals(messageEvent.getPath())) {
// We get the Data
final byte[] receivedData = messageEvent.getData();
final String receivedMessage = new String(receivedData);
Intent intent = new Intent(this, ShowMessageActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(ShowMessageActivity.MESSAGE_EXTRA, receivedMessage);
startActivity(intent);

}
}


Come possiamo vedere da queste poche righe di codice, non facciamo altro che
controllare che il path sia quello associato al messaggio inviato, per estrarre le
informazioni da utilizzare per lavvio dellattivit descritta dalla classe
ShowMessageActivity. Questultima una classe banale, che invitiamo il lettore a
consultare direttamente. A questo punto lanciamo lapplicazione sullo smartphone e
selezioniamo la voce di menu Send Message per osservare sul display del dispositivo
quanto rappresentato nella Figura 7.47. A questo punto diventa semplice anche
linvio, ancora simmetrico, di un messaggio dal dispositivo Wearable allapplicazione.
Come esercizio consigliamo al lettore di aggiungere un pulsante al display per
linvio, come lo stesso meccanismo, del timestamp allattivit sullo smartphone.

Figura 7.47 Linvio di un messaggio al dispositivo wearable.

Conclusioni
In questo capitolo molto impegnativo ci siamo occupati della comunicazione tra un
dispositivo Wearable e uno smartphone. Nella parte iniziale del capitolo, dopo una
descrizione dei possibili scenari di utilizzo, abbiamo descritto quello che la
piattaforma ci mette a disposizione gratis, senza alcuna riga di codice o di
configurazione. Abbiamo visto come lo smartphone invii le informazioni relative a
una qualunque notifica anche alleventuale dispositivo Wear connesso. Sempre a
proposito della gestione delle notifiche abbiamo poi visto come renderle specializzate
e fare in modo che sul dispositivo Wearable le informazioni visualizzate e le possibili
azioni siano diverse da quelle accessibili attraverso lo smartphone. Gi in questa fase
abbiamo iniziato a vedere come associare dei comandi vocali alle azioni di una
notification. Nella seconda parte del capitolo abbiamo invece iniziato a occuparci
della realizzazione di vere e proprie applicazioni in grado di essere eseguite sui
dispositivi Wearable. Dopo aver preparato lambiente di sviluppo e descritto le
modalit di deploy e di utilizzo dellemulatore, ci siamo occupati della descrizione
dei pattern principali, come il GridPager, il 2D Picker e altri ancora. In questa parte
abbiamo infatti analizzato nel dettaglio i principali componenti della libreria di
supporto wearable che Google ha messo a disposizione degli sviluppatori. Nella parte
finale ci siamo infine occupati di un altro aspetto fondamentale, ovvero delle diverse
modalit di comunicazione tra lo smartphone e il dispositivo Wearable. Abbiamo infatti
visto come provvedere alla sincronizzazione di dati e inviare e ricevere dei messaggi.
Come abbiamo detto si trattato di un capitolo molto impegnativo, che ha comunque
permesso di fare la conoscenza degli strumenti principali per la realizzazione di
applicazioni di questo tipo.

Capitolo 8

MediaRouter e Chromecast

Una delle novit dei Google Play services riguarda la possibilit di visualizzare
una serie di contenuti su dispositivi diversi, come per esempio la TV, attraverso le
Google Cast API. Si tratta di un argomento molto ampio, che vedremo solo nelle sue
parti principali. Come facile intuire, questa tecnologia si basa su unarchitettura
client/server, che prevede la definizione di un componente responsabile dellinvio
delle informazioni, detto Sender, e di un altro che le deve invece interpretare e
visualizzare sulla TV o altro dispositivo simile, che prende il nome di Receiver. Come
possiamo vedere nella Figura 8.1, il Sender tipicamente un componente definito in
unapplicazione Android.
Sebbene questa sia la nostra piattaforma bene sottolineare come siano comunque
disponibili le API per lutilizzo di questi strumenti anche su piattaforme iOS e Web.
Il Receiver invece il componente responsabile della ricezione e visualizzazione delle
informazioni sulla TV o altro dispositivo. Sebbene sia una cosa possibile, in questa
sede non ci occuperemo della realizzazione di un Receiver custom, ma supporremo di
disporre del dispositivo Chromecast, visualizzato nella Figura 8.2.
Prima di iniziare con le configurazioni e con la scrittura del codice bene vedere
come avviene la comunicazione tra il Sender e il Receiver. Quando lapplicazione Sender
viene avviata, esegue una prima operazione di inizializzazione, seguita dalla ricerca
dei vari Receiver disponibili nella stessa rete. A questo punto il particolare Receiver pu
richiedere allutente di selezionare uno dei dispositivi trovati e di iniziare la
comunicazione con essi. Una volta che un Receiver viene selezionato, il sistema si
preoccupa della sua inizializzazione e avvio. A questo punto la comunicazione
stabilita e le due parti iniziano a scambiarsi messaggi. Il ciclo termina con la
disconnessione dal Receiver.

Figura 8.1 Architettura Client/Server.

Figura 8.2 Il dispositivo Chromecast di Google.

In questo capitolo ci occuperemo della descrizione delle API che permettono di


trasmettere contenuti in un altro dispositivo, che questa volta non un device Wearable,
ma tipicamente una TV. Per fare questo inizieremo con la descrizione delle
MediaRouter API, che ci permetteranno di esaminare da subito i concetti principali di
questa tecnologia. Per fare questo tralasciamo lapplicazione FriendFence, ma creeremo
unapplicazione che ci permetter di fare qualche test con il Chromecast. Sebbene la
cosa non sia molto evidente, si tratta di API che necessitano dei servizi dei Google
Play services.

MediaRouter API
Prima di entrare nel dettaglio di un caso particolare, vogliamo descrivere il caso
generale, ovvero di quello che si pu ottenere attraverso le MediaRouter API.
Lobiettivo di questo strumento , infatti, quello di permettere alle applicazioni
Android di inviare suoni e immagini a dispositivi esterni con capacit audio e/o video
migliori. Pensiamo per esempio a una coppia di casse audio o a uno schermo molto
grande in alta risoluzione. Per fare questo esistono due diverse modalit. La prima
permette di utilizzare il dispositivo come un telecomando, attraverso il quale si
inviano al dispositivo esterno le informazioni sui media da riprodurre. Come avviene
per una televisione, attraverso il telecomando scegliamo il canale da visualizzare, ma
poi il televisore che si preoccupa di ricevere, decodificare e visualizzare il
programma. Questa modalit si chiama Remote Playback. La seconda modalit si
chiama Secondary Output e consiste nellinvio del media direttamente dal dispositivo
Android a quello esterno. Sebbene le MediaRouter API permettano di gestione
entrambe le modalit, nel nostro caso ci occuperemo maggiormente della prima, che
quella utilizzata da Chromecast, argomento di questo capitolo.
In ogni caso il meccanismo molto semplice e consiste nella configurazione del
Sender in relazione ai tipi di dispositivi cui ci si vuole connettere. Tutta linterazione
con i componenti principali definiti dalle API avviene attraverso opportune interfacce
di Callback. Una volta che un dispositivo stato individuato, serve un meccanismo che
permetta di inviare e ricevere messaggi comprensibili al dispositivo stesso; una
specie di Driver. Componenti con questa responsabilit si chiamano MediaRouter
Provider e hanno la responsabilit di gestire la comunicazione con i vari dispositivi.
bene sottolineare come si tratti di un componente che sta sul dispositivo client e
che quindi pu essere installato anche in momenti successivi, attraverso applicazioni
custom dei vari produttori. Uno dei questi MediaRouter Provider per esempio
quello che permette di comunicare con il Chromecast. Come abbiamo detto non
entreremo nel dettaglio di questo componente, ma diciamo comunque che esso
implementa un meccanismo attraverso il quale possibile ricercare dispositivi di un
particolare tipo gestendo la comunicazione con essi. Per utilizzare queste API
abbiamo bisogno di alcune classi, che vengono fornite attraverso la libreria di
compatibilit di versione superiore alla 18, insieme alla libreria specifica del
MediaRouter. Per fare questo sufficiente aggiungere la seguente definizione a quelle
che abbiamo gi dichiarato nel file build.gradle del nostro progetto e che abbiamo
chiamato molto banalmente TestMediaRouter.


dependencies {

- - compile com.android.support:appcompat-v7:20.0.0
compile com.android.support:mediarouter-v7:20.0.+
}


Il primo passo nellutilizzo di queste API consiste nel fornire allutente un
meccanismo per lavvio della connessione al dispositivo. A tale proposito esiste un
pulsante standard che pu essere attivato attraverso le classi della precedente libreria.
NOTA
Come sottolineato anche nei capitoli precedenti, lutilizzo di queste librerie di compatibilit
permette a Google una maggiore libert nella frequenza di aggiornamento. In questo caso le API
che abbiamo importato attraverso le precedenti librerie vanno a sostituire altre classi analoghe
nel package android.media dellSDK di Android. bene sottolineare come le classi delle librerie
si sostituiscano a quelle gi presenti nella piattaforma.

A tale proposito di fondamentale importanza sottolineare come questo


procedimento debba necessariamente essere utilizzato in una classe che estende
ActionBarActivity oppure una FragmentActivity, anche se lAPI Level dellapplicazione
11 (Android 3.0) o maggiore. Il modo pi semplice di aggiungere il pulsante di
attivazione della funzione di MediaRouter quello di creare unapposita voce nel menu
delle opzioni e quindi visualizzare licona nella ActionBar. Nel nostro caso abbiamo
definito la seguente voce di menu, dove abbiamo evidenziato lutilizzo della classe
MediaRouteActionProvider attraverso lattributo actionProviderClass:

<item

android:id=@+id/media_router_menu_item
android:title=@string/media_router_action
app:actionProviderClass=android.support.v7
.app.MediaRouteActionProvider
app:showAsAction=always />

Senza entrare troppo nel dettaglio di come funzioni la ActionBar, ricordiamo che un
permette di personalizzare non solo la visualizzazione delle varie action,

ActionProvider

ma anche i relativi comportamenti nel caso di selezione. In questo caso stiamo


semplicemente dicendo che la selezione del pulsante relativo al MediaRouter viene
delegato a un oggetto descritto dalla classe MediaRouteActionProvider, che si preoccuper
di gestire linizio del processo di connessione.
NOTA
In realt esistono altre due modi per avviare il processo di condivisione dei media con
MediaRouter. Il primo consiste nellutilizzo del componente MediaRouteButton, mentre il secondo
consiste nella realizzazione di un vero e proprio layout custom e implementazione di tutto il
processo che vedremo successivamente. Quello da noi utilizzato il meccanismo pi semplice e
standard.

Al termine della nostra implementazione dovremo visualizzare il pulsante in alto a


destra, come indicato nella Figura 8.3. Se nellimplementazione del lettore questo
non dovesse succedere, bene sottolineare come il pulsante venga visualizzato
solamente se nella rete locale presente un dispositivo compatibile. Nel caso in cui
non vi fossero dispositivi raggiungibili, il pulsante non verr mai visualizzato.

Figura 8.3 Il pulsante caratteristico di MediaRoute.

Nel momento in cui lutente seleziona lazione corrispondente al MediaRouter, il


si preoccupa di lanciare la ricerca dei dispositivi nella rete

MediaRouteActionProvider

locale. Per fare questo abbiamo bisogno di impostare i tipi di media di cui abbiamo
bisogno; una sorta di filtro sul tipo di dispositivo. A tale proposito si utilizza un
MediaRouteSelector, il quale ci permette di indicare, attraverso i MediaControlIntent, quali
sono le categorie di dispositivi da ricercare. Al momento esistono tre diverse
categorie, associate alle seguenti costanti:

CATEGORY_LIVE_AUDIO
CATEGORY_LIVE_VIDEO
CATEGORY_REMOTE_PLAYBACK


Le prime due si riferiscono a dispositivi cui si accede secondo la modalit Secondary
, mentre il terzo ci permette di individuare dispositivi come il Chromecast, che

Output

possiamo controllare da remoto. Le informazioni che impostiamo in questo modo

dovranno essere utilizzate al momento della selezione della corrispondente azione


nella ActionBar. Quello che dobbiamo fare consiste quindi nel creare unistanza di
utilizzando, come ormai siamo soliti fare, una corrispondente classe

MediaRouteSelector

di Builder per poi assegnare loggetto ottenuto al MediaRouteActionProvider definito in


precedenza. importante fare le cose al momento giusto, per cui inizializzeremo il
MediaRouteSelector nel metodo onCreate() e quindi lo registreremo al provider in
corrispondenza della creazione della voce di menu. Nel metodo onCreate() dovremo
quindi utilizzare il seguente codice:

mMediaRouteSelector = new MediaRouteSelector.Builder()

// These are the framework-supported intents


.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.build();

Loggetto creato viene utilizzato in corrispondenza della voce di menu attraverso
queste poche righe di codice:

MenuItem mediaRouteMenuItem = menu

.findItem(R.id.media_router_menu_item);
MediaRouteActionProvider mediaRouteActionProvider =

(MediaRouteActionProvider) MenuItemCompat.getActionProvider(
mediaRouteMenuItem);
mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);


A questo punto il MediaRouteActionProvider sa quali sono i tipi di dispositivi che deve
ricercare. Selezionando il pulsante noteremo la visualizzazione di un elenco dei
dispositivi raggiungibili nella rete locale, come indicato nella Figura 8.4.
Il passo successivo quello di gestire la connessione con il dispositivo selezionato.
A tale proposito si utilizza un oggetto di tipo MediaRouter. Si tratta di
unimplementazione del Singleton Pattern che permette di creare una sola istanza, la

quale deve essere raggiungibile da un qualunque punto dellapplicazione. Per fare in


modo che il Garbage Collector non ne reclami le risorse eliminandolo, bene legare
la sua vita a quella di un componente che vive come lintera applicazione. Questo il
motivo della seguente istruzione, che viene richiamata nel metodo onCreate() e che
lega il MediaRouter al contesto dellapplicazione stessa.

mMediaRouter = MediaRouter.getInstance(getApplicationContext());

Figura 8.4 Elenco dei dispositivi raggiungibili e compatibili.

La gestione dello stato dellinterazione con il dispositivo esterno avviene


attraverso unopportuna interfaccia di callback che viene implementata dalla classe
astratta MediaRouter.Callback.
NOTA

Il lettore si chieder forse perch questa volta si utilizzi una classe e non uninterfaccia. Il motivo
sta nel fatto che non necessario fornire implementazione di tutti questi metodi, ma solamente di
alcuni, che sono appunto quelli che sono stati definiti astratti nella classe MediaRouter.Callback e
che la rendono, di fatto, astratta.

I metodi principali che dovremo implementare saranno quelli relativi allavvenuta


connessione, disconnessione ed eventualmente alla modifica delle caratteristiche del
dispositivo di riproduzione. Pensiamo, per esempio, al caso in cui venisse modificata
la risoluzione del display oppure addirittura il display venisse rimosso. Nel nostro
caso abbiamo definito una variabile distanza nel seguente modo, delegando ad altri
metodi le operazioni da fare nei vari casi. Questo ci permetter una migliore
suddivisione del codice.

private final MediaRouter.Callback mMediaRouterCallback =

new MediaRouter.Callback() {
@Override
public void onRouteSelected(MediaRouter router,
MediaRouter.RouteInfo route) {
manageRouteSelected(router, route);
}
@Override
public void onRouteUnselected(MediaRouter router,
MediaRouter.RouteInfo route) {
manageRouteUnselected(router, route);
}
@Override
public void onRoutePresentationDisplayChanged(MediaRouter router,
MediaRouter.RouteInfo route) {
manageRoutePresentationDisplayChanged(router, route);
}
};

Innanzitutto notiamo come le tre operazioni abbiamo gli stessi parametri. Il primo
rappresentato da un oggetto di tipo MediaRouter, mentre il secondo un oggetto di
tipo RouteInfo. Il primo non altro che il riferimento alloggetto che ha generato
levento, ovvero il MediaRouter che abbiamo definito prima. Il secondo invece contiene
le informazioni specifiche del particolare dispositivo connesso, disconnesso o che ha
cambiato caratteristiche.
NOTA
Come accennato in precedenza, il lettore non si demoralizzi se il pulsante allinizio non compare
nella ActionBar. Affinch questo accada il dispositivo deve eseguire alcune operazioni di
inizializzazione connettendosi alla rete locale. Il pulsante potrebbe poi non comparire nel caso in
cui nella rete locale non fosse disponibile alcun dispositivo cui connettersi.

Prima di descrivere la nostra implementazione necessario collegare questo


Callback al MediaRouter, agganciandolo al ciclo di vita della nostra applicazione. Per
questo motivo abbiamo implementato i metodi onStart() e onStop(), nel seguente modo:

@Override
public void onStart() {

mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
super.onStart();
}
/**
* We unregister the Callback from the MediaRouter
*/
@Override
public void onStop() {

mMediaRouter.removeCallback(mMediaRouterCallback);
super.onStop();
}


NOTA
In realt le API mettono a disposizione anche la classe MediaRouteDiscoveryFragment, che si
preoccupa di gestire questo ciclo di vita allinterno di un Fragment.

A questo punto abbiamo tutto quello che ci serve per poter controllare un
dispositivo remoto, le cui informazioni sono incapsulate nelloggetto di tipo
MediaRouter.RouteInfo che otteniamo dai metodi di Callback. Si tratta di una classe che
definisce una serie di metodi che ci permettono di avere informazioni sul
corrispondente dispositivo. Il seguente metodo ci permette, per esempio, di ottenere

un insieme di oggetti IntentFilter relativi ai media che il dispositivo in grado di


supportare.

public List<IntentFilter> getControlFilters()


Si tratta, infatti, di un insieme di informazioni relative a speciali category e action
che indicano che cosa il dispositivo pu fare. Altre importanti informazioni sono
quelle fornite, per esempio, dai seguenti metodi, che forniscono rispettivamente un
identificatore unico, un nome e una descrizione del dispositivo:

public String getId()
public String getName()
public String getDescription()


Molto importanti sono anche alcuni metodi per capire se il dispositivo permette
particolari operazioni, attraverso i metodi del tipo:

public boolean supportsControlCategory(String category)
public boolean supportsControlRequest(Intent intent)

Gestire il dispositivo remoto


Una volta ottenuto il riferimento alloggetto MediaRouter.RouteInfo relativo al
dispositivo cui si connessi, provvediamo a fornire unimplementazione ai metodi
che abbiamo lasciato in sospeso in precedenza, partendo da quello relativo alla
gestione allevento di connessione. In ognuno di questi metodi importante sapere
quale tipo di dispositivo stiamo gestendo. In fase di inizializzazione abbiamo infatti
aggiunto sia la ricerca di dispositivi da controllare da remoto, sia quelli che abbiamo
chiamato Secondary Output. Abbiamo definito il seguente metodo, che non fa altro che
implementare la gestione del dispositivo esterno a seconda del suo tipo:

private void manageRouteSelected(final MediaRouter router,

final MediaRouter.RouteInfo route) {


Log.d(TAG_LOG, onRouteSelected: route= + route);

if (route.supportsControlCategory(
MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
manageRemotePlayer(route);
} else {
manageRemotePresentation(route);
}
}


Nel caso di una gestione Remote Playback il meccanismo molto semplice e
presuppone lutilizzo di un oggetto di tipo RemotePlaybackClient che utilizzeremo
attraverso lormai nota interfaccia di callback. Si tratta di un oggetto che ci permette
di eseguire le operazioni classiche di un controllo remoto di questo tipo ovvero play(),
e seek() di un particolare media, ricevendo notifiche attraverso opportune

pause()

interfacce di callback.
NOTA
Attenzione: si tratta di una classe non disponibile nella versione 18 della libreria mediarouter, ma
che stata aggiunta successivamente.

Interessante la possibilit di aggiungere media da riprodurre allinterno di una


coda, in modo da creare una specie di Playlist. Si tratta di una funzionalit che non
tutti i RemotePlaybackClient implementano, cosa che possibile sapere attraverso metodi
come il seguente:

public boolean isQueuingSupported()


La possibilit di aggiungere media alla coda legata al concetto di sessione, che
possiamo intuitivamente collegare a quello di connessione al dispositivo per la
riproduzione di un insieme di media uno di seguito allaltro. Nel nostro caso
vogliamo semplicemente avviare la riproduzione di un media di cui conosciamo lUri
e il tipo. Per fare questo utilizziamo il metodo:

public void play (Uri contentUri, String mimeType, Bundle metadata,

long positionMillis, Bundle extras,


RemotePlaybackClient.ItemActionCallback callback)


Si tratta di un metodo che permette di richiedere al dispositivo la riproduzione
immediata di uno specifico media, cancellando leventuale coda. Notiamo come il
primo parametro sia un Uri, che quindi pu fare riferimento sia a contenuti esterni sia
a contenuti interni della nostra applicazione o del nostro dispositivo. Per descrivere la
modalit di utilizzo di un RemotePlaybackClient vediamo il seguente codice:

private void manageRemotePlayer(final MediaRouter.RouteInfo route) {

// We clean the player if needed


cleanPlayer();
// Save new current route
mRoute = route;
// We create the new object for playback
mPlaybackClient = new RemotePlaybackClient(this, mRoute);
// We send a data to the remoteplayback
mPlaybackClient.play(Uri.parse(VIDEO_TEST_URL), MP4_MIME_TYPE, null,
0,
null, new RemotePlaybackClient.ItemActionCallback(){
@Override
public void onResult(Bundle data, String sessionId,
MediaSessionStatus sessionStatus,
String itemId,
MediaItemStatus itemStatus) {
showToast(Media with id + itemId +
successfully sent!);
}
@Override
public void onError(String error, int code, Bundle data) {
showToast(Player error + code + + error);
}

});
}


Innanzitutto richiamiamo un metodo di utilit che permette di rilasciare loggetto
RemotePlaybackClient nel caso questo fosse gi in uso. Si tratta di un metodo molto
semplice, che non fa altro che verificare se esiste un player attivo e quindi richiamare
su di esso il metodo release(), per poi metterne nuovamente a null il riferimento.

private void cleanPlayer() {

// If necessary we clean and release the RemotePlaybackClient object


if (mRoute != null && mPlaybackClient != null) {
mPlaybackClient.release();
mPlaybackClient = null;
}
}


Di seguito memorizziamo il riferimento alloggetto di tipo MediaRouter.RouteInfo con
le informazioni sul dispositivo corrente e quindi creiamo loggetto di tipo
RemotePlaybackClient che utilizziamo per la riproduzione di un media di cui abbiamo
specificato MimeType e URL. Notiamo come, attraverso linterfaccia
, sia poi possibile avere notifica dellesito

RemotePlaybackClient.ItemActionCallback

delloperazione di riproduzione.
Notiamo come si tratti di un meccanismo molto semplice, che ci permette di
inviare al dispositivo esterno le informazioni relative al media da riprodurre. Se
volessimo aggiungere un altro media a una coda, potremmo utilizzare il seguente
metodo:

private void addToQueue(final Uri mediaUri, final String mediaMimeType) {

if (mPlaybackClient != null && mPlaybackClient.isQueuingSupported()) {


mPlaybackClient.enqueue(mediaUri, mediaMimeType, null,
0, null, new RemotePlaybackClient.ItemActionCallback() {
@Override

public void onResult(Bundle data, String sessionId,


MediaSessionStatus sessionStatus,
String itemId,
MediaItemStatus itemStatus) {
showToast(Media with id + itemId + successfully sent!);
}
});
}
}


Dopo aver controllato se il particolare RemotePlaybackClient attivo e supporta la
gestione della coda, possiamo utilizzare il metodo enqueue() per aggiungere il media
da riprodurre successivamente. bene sottolineare come questo sia il procedimento
che utilizzeremo per linterazione con il Chromecast nella seconda parte del capitolo.
Concludiamo questa parte rimandando il lettore alla documentazione ufficiale per
quanto riguarda gli altri metodi, i quali permettono, per esempio, di mettere la
riproduzione in pausa o di andare a una particolare posizione nel media in
riproduzione. Si tratta, come visto, di un procedimento analogo a quello utilizzato
negli esempi precedenti.

Gestire i Secondary Output


Come abbiamo detto allinizio del capitolo, la modalit Secondary Output permette
la connessione a dispositivi che si occupano esclusivamente della riproduzione di un
media, senza partecipare minimamente alla fase di download e codifica. Si tratta, in
sostanza, di altoparlanti o semplici video a cui il nostro dispositivo si vuole
connettere come se fossero una sua naturale estensione. bene precisare che questa
modalit richiede delle classi che sono disponibili solamente per versioni di Android
corrispondenti a unAPI Level 17, ovvero Android 4.2 e superiori. Per questo motivo
abbiamo introdotto un controllo sulla versione nel metodo manageRemotePresentation(),
che abbiamo implementato per la gestione di questa funzionalit e che descriveremo
tra poco. Abbiamo introdotto il termine Presentation, il quale un oggetto che
rappresenta il dispositivo remoto. Creare e gestire una Presentation corrisponde a
creare una specie di interfaccia in grado di essere eseguita nel dispositivo esterno. La

cosa singolare riguarda il fatto che una Presentation non altro che una finestra di
particolare che per dispone di un proprio Context e di una propria gestione delle

Dialog

risorse. Se ci pensiamo questo corretto, in quanto la scelta delle risorse dipende


molto da quelle che sono, per esempio, le caratteristiche del display che, in questo
scenario, sono diverse da quelle del dispositivo.
La gestione di un dispositivo di questo tipo prevede la definizione di due
componenti principali. Il primo consiste in una particolare specializzazione della
classe Presentation, la quale, come abbiamo detto, una specializzazione della classe
e pu contenere un insieme di View per visualizzare informazioni o per

Dialog

linterazione con lutente. Il secondo componente ha invece responsabilit di


Controller e non fa altro che trasformare le notifiche che provengono dalloggetto
attraverso la classe di Callback, in operazioni sulla specifica Presentation. In

MediaRouter

sostanza si tratta di una classe che visualizza la Presentation quando il dispositivo si


connette e la rimuove in corrispondenza dellevento di disconnessione. Nel nostro
esempio abbiamo realizzato unimplementazione di Presentation molto semplice, la
quale utilizza un documento di layout per la visualizzazione del classico Hello World.

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public class HelloPresentation extends Presentation {

/**
* Create a HelloPresentation with its Context and Diplay reference
*
* @param outerContext The Context for this Presentation
* @param display The Display showing this Presentation
*/
public HelloPresentation(Context outerContext, Display display) {
super(outerContext, display);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We get the resources. Here not used but can be useful

// with more complicated layouts


Resources resources = getContext().getResources();
// We inflate the Hello World layout
setContentView(R.layout.hello_world_presentation);
}
}


Notiamo come si tratti di una classe che estende Presentation e che utilizza la
, in quanto ha senso solamente per API Level superiori al 17.

annotation @TargetApi

Notiamo come disponga di un costruttore che prevede due parametri. Il primo il


Context, mentre il secondo il riferimento a un oggetto di tipo Display, il quale contiene
una serie di informazioni relative al display a cui ci si connessi. Attraverso questo
oggetto possiamo sapere, per esempio, qual la risoluzione, le dimensioni, il refresh
rate fino al tipo di pixel utilizzati. Si tratta di informazioni che possono essere utili
per la personalizzazione del contenuto della Presentation.
Nel metodo onCreate() non facciamo altro che impostare il layout, il quale
descritto da un documento molto semplice:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width=match_parent
android:layout_height=match_parent>
<TextView
android:layout_width=wrap_content
android:layout_height=wrap_content
android:textAppearance=?android:attr/textAppearanceLarge
android:text=@string/hello_world
android:layout_gravity=center
android:textSize=40sp
android:id=@+id/textView
/>
</FrameLayout>


Una volta creata la particolare implementazione di Presentation non ci resta che
legarla al MediaRouter nel modo descritto dal nostro metodo manageRemotePresentation().
Anche qui si tratta di un meccanismo abbastanza standard. Nella prima parte
possiamo notare come si facciano dei controlli sullo stato dellapplicazione, come
possiamo vedere da questo codice:

// We check for the current version
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {

showToast(Option not supported!);


return;
}
// We get the reference to the selected Display
Display selectedDisplay = null;
if (route != null) {

selectedDisplay = route.getPresentationDisplay();
}
// We dismiss the previous presenter if any
if (mPresentation != null &&

mPresentation.getDisplay() != selectedDisplay) {
mPresentation.dismiss();
mPresentation = null;
}


Dopo aver controllato la versione della piattaforma, verifichiamo che esista
effettivamente un display connesso al dispositivo associato alloggetto
MediaRouter.RouteInfo selezionato. Fatto questo eseguiamo unoperazione simile a quella
fatta in precedenza per il controllo remoto, ovvero chiudiamo leventuale Presentation
visualizzata in precedenza. La parte interessante quella che si preoccupa della
visualizzazione della nostra HelloPresentation, attraverso il seguente codice:

if (mPresentation == null && selectedDisplay != null) {

// Initialize a new Presentation for the Display


mPresentation = new HelloPresentation(this, selectedDisplay);
mPresentation.setOnDismissListener(
new DialogInterface.OnDismissListener() {

@Override
public void onDismiss(DialogInterface dialog) {
if (dialog == mPresentation) {
mPresentation = null;
}
}
});
// Try to show the presentation, this might fail if the display has
// gone away in the meantime
try {
mPresentation.show();
} catch (WindowManager.InvalidDisplayException ex) {
// Couldnt show presentation display was already removed
mPresentation = null;
}
}


Dopo aver creato unistanza di HelloPresentation passando il Context e il riferimento al
, utilizziamo il suo metodo show(), il quale, notiamo, pu generare uneccezione

Display

che necessario gestire. Ricordando che una Presentation non altro che una Dialog,
possiamo utilizzare le stesse interfacce di callback per essere notificati della loro
chiusura (dismiss) e quindi ripristinare la situazione iniziale.
NOTA
Il nostro esempio molto semplice, ma permette di capire come creare e gestire una UI remota
da inviare su un dispositivo esterno come un video o un proiettore.

Gli altri metodi di callback


Concludiamo questa parte con una velocissima descrizione degli altri metodi che
abbiamo creato per la gestione delle operazioni di callback di un MediaRouter. Il primo
quello relativo alla parte di disconnessione, il quale non fa altro che ripulire gli
oggetti creati ovvero:


private void manageRouteUnselected(final MediaRouter router,

final MediaRouter.RouteInfo route) {


Log.d(TAG_LOG, onRouteSelected: route= + route);
if (route.supportsControlCategory(
MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
// We clean the player
cleanPlayer();
mRoute = null;
} else {
// We dismiss the previous presenter if any
if (mPresentation != null) {
mPresentation.dismiss();
mPresentation = null;
}
}
}


Il metodo relativo alla modifica delle caratteristiche del display analogo a quello
creato in fase di selezione e quindi pu essere sostituito da esso. Questo porta alla
modifica evidenziata nel seguente codice, che fa parte della fase di registrazione della
Callback.

private final MediaRouter.Callback mMediaRouterCallback =

new MediaRouter.Callback() {
@Override
public void onRouteSelected(MediaRouter router,
MediaRouter.RouteInfo route) {
manageRouteSelected(router, route);
}

@Override
public void onRouteUnselected(MediaRouter router,
MediaRouter.RouteInfo route) {
manageRouteUnselected(router, route);
}
@Override
public void onRoutePresentationDisplayChanged(MediaRouter router,
MediaRouter.RouteInfo route) {
manageRouteSelected(router, route);
}
};


Abbiamo visto come gestire dispositivi esterni attraverso le due modalit di Remote
e di Secondary Output. A questo punto non ci resta che passare alla gestione del

Playback

Chromecast, che cade nel primo gruppo di dispositivi.

Google Cast
Nella prima parte del capitolo abbiamo descritto i concetti fondamentali alla base
del funzionamento del MediaRouter ovvero di quella tecnologia che permette di
utilizzare un dispositivo esterno per la riproduzione di video o audio da parte di
unapplicazione Android. Abbiamo introdotto i concetti di Sender e di Receiver, che si
ripetono anche nella gestione di un tool che stato rilasciato lo scorso anno da
Google e che permette la riproduzione di audio e video sul TV di casa o comunque su
uno schermo di grandi dimensioni. Nel capitolo precedente ci siamo concentrati
solamente sullo sviluppo della parte client, mentre per la gestione del Chromecast
vedremo, anche se attraverso la creazione di esempi molto semplici, come realizzare
sia il Sender che il Receiver. Il Sender non altro che unapplicazione che permette di
gestire un media sul dispositivo remoto. Al momento esistono API non solo per
Android, ma anche per iOS e per la piattaforma Chrome. Per quanto riguarda il
Receiver esistono tre diverse alternative. La prima consiste nellutilizzo del Receiver di
default, che quello che abbiamo di fatto utilizzato nel paragrafo precedente nella
gestione delle MediaRouter API. Questo si chiama Default Media Receiver e si
presenta con un brand caratteristico di Chromecast. Una seconda possibilit quella
di riutilizzare quello di default, ma fornendo una nostra personalizzazione per il
brand. Questo il caso, per esempio, in cui si voglia inserire licona della nostra
applicazione o unimmagine o un logo di sfondo e cos via. In questo caso si parla di
Styled Media Receiver. La terza opzione consiste invece nella realizzazione di un
Receiver completamente custom, che implementi non solo alcune modifiche grafiche,
ma che permetta anche di personalizzare il tipo di messaggi che in grado di ricevere
dal Server. La scelta di quale di questi Receiver implementare dipende da vari fattori,
tra cui il tipo di media da riprodurre, la volont o meno di impostare il proprio brand
in fase di riproduzione e altri ancora. In questa seconda parte del capitolo ci
occuperemo della realizzazione del solo Receiver di tipo Styled Media Receiver. Non
eseguiremo implementazioni molto complicate, ma ci limiteremo a realizzare
componenti che ci permettano di descrivere i passi principali nello sviluppo di questo
tipo di dispositivi. Questo soprattutto alla luce del fatto che un Receiver non altro che
una particolare applicazione web, che ha poco a che fare con lo sviluppo di
applicazioni Android native. In ogni caso il primo passo da compiere la
registrazione della nostra applicazione nella console di Google. La registrazione ci
permetter di ottenere un Application ID che utilizzeremo poi nella configurazione
dei vari componenti in gioco.
NOTA

Come visto nel paragrafo precedente, lutilizzo di un Default Media Receiver non presuppone
alcun tipo di registrazione.

Proseguiamo con la registrazione della nostra applicazione. Per farlo necessario


andare al seguente indirizzo:

https://cast.google.com/publish/#/signup


Troveremo la schermata rappresentata nella Figura 8.5, in cui ci viene richiesto di
accettare i termini e le condizioni di utilizzo e di procedere al pagamento di una sorta
di tassa di registrazione che al momento di 5 dollari.

Figura 8.5 Registrazione nella console di Google Cast.

Diamo il nostro consenso e facciamo clic sul pulsante Continue to payment per
effettuare il pagamento attraverso Google Wallet. Come ultimo passo ci viene
richiesto un contatto telefonico insieme allindirizzo email da usare come riferimento.
Diamo conferma, arrivando alla schermata rappresentata nella Figura 8.6 nella quale
possiamo osservare lelenco delle nostre applicazioni e la lista dei device da
utilizzare per il test. Si tratta di un elenco che al momento vuoto e che andremo ad
alimentare tra poco.

Figura 8.6 Interfaccia della console di Google Cast.

Selezioniamo la voce Add New Application e ci viene richiesto il tipo di Receiver da


registrare, come possiamo vedere nella Figura 8.7. Come nostra implementazione del
Receiver selezioniamo la seconda opzione, ovvero quella indicata come Styled Media
che, come dice la descrizione stessa, consiste in un Receiver preconfigurato, di

Receiver

cui andiamo a specificare un foglio stile CSS custom. A differenza di quello che
andremo a creare successivamente, questo non necessita di alcun hosting.

Figura 8.7 Selezione del tipo di Receiver che andremo a implementare.

Questa nostra selezione ci porta a una schermata nella quale ci viene chiesto il
nome dellapplicazione e lURL di uno skin custom. Come nome dellapplicazione

abbiamo inserito FriendFenceSimpleCast, mentre come URL per lo skin custom abbiamo
creato un file in Google Drive e quindi utilizzato il suo indirizzo di condivisione
accessibile via HTTPS. Se andiamo a leggere la documentazione, in questo caso il
foglio stile deve poter gestire le seguenti classi CSS, le quali sono abbastanza
esplicative:

.background
.logo
.progressBar
.splash
.watermark


La classe .background ci permette di specificare limmagine da utilizzare come
sfondo. Per fare questo possiamo utilizzare la seguente definizione, dove
background.png sar limmagine da utilizzare, disponibile attraverso lo stesso
meccanismo:

.background {

background: center no-repeat url(background.png);


}


La classe .logo permette di impostare limmagine che caratterizza il Receiver che
viene quindi visualizzata nel momento in cui questo viene eseguito o quando si trova
nello stato di idle. Anche in questo caso il documento CSS contiene unentry del tipo:

.logo {

background-image: url(logo.png);
}


La classe .progressBar permette di specificare il colore della barra di caricamento.
Nel nostro caso abbiamo utilizzato questa definizione

.progressBar {

background-color: rgb(238, 255, 65);


}


La classe .splash permette di specificare limmagine di sfondo in fase di
caricamento. Si tratta di uninformazione opzionale; se non presente viene infatti
utilizzata limmagine specificata come logo. Nel nostro caso possiamo inserire la
seguente definizione:

.splash {

background-image: url(splash.png);
}


Infine un .watermark uninformazione di testo che viene visualizzata al di sopra del
media, per fornire alcune informazioni, per esempio relative al media stesso. Una
possibile definizione, in questo caso potrebbe essere la seguente:

.watermark {

background-image: url(watermark.png);
background-size: 57px 57px;
}


Per creare il nostro Receiver inseriamo queste definizioni in un documento CSS in
Google Drive, ne estraiamo lURL per la condivisione e lo inseriamo in
corrispondenza del secondo campo del form rappresentato nella Figura 8.8. Bisogna
dire che la gestione di questo file attraverso Google Drive abbastanza pesante.
Inserire il documento CSS e quindi ottenerne un URL per laccesso pubblico non
difficile. Il problema relativo allaccesso ai file delle immagini cui si fa riferimento.
Per ognuna di queste bisogna infatti ripetere lo stesso procedimento e quindi
ottenerne un URL pubblico del tipo https://www.googledrive.com/host/<id> cui fare
riferimento nel documento CSS. Purtroppo il secondo campo accetta solamente URL
che utilizzano HTTPS, per cui, in mancanza di un server pubblico, questa lunica
opzione.
Selezionando il pulsante Preview possibile avere unanteprima del risultato. Nel
nostro caso abbiamo preso delle immagini free dalla Rete e utilizzato licona
dellapplicazione FriendFence. Un esempio di risultato si ha, per esempio, nella Figura
8.9.

A parte limprobabile gusto estetico, facile notare come vi sia stata una certa
personalizzazione rispetto al Receiver di default. A questo punto lasciamo al lettore il
compito di verificare cosa succede selezionando i pulsanti che vediamo anche nella
precedente figura. Si tratta di pulsanti che ci permettono di simulare il funzionamento
del Receiver nel caso di riproduzione di unimmagine, un suono oppure un video.

Figura 8.8 Inseriamo i dati per un Styled Media Receiver.

Figura 8.9 Possibile personalizzazione del Receiver.

Tornando alla nostra interfaccia di creazione dellapplicazione non ci resta che


premere il pulsante Save per conferma, il quale ci porter a una schermata nella quale,
oltre ai complimenti, ci viene fornito lApplication Id, come visualizzato nella Figura
8.10.

Figura 8.10 Creazione dellapplicazione e dellApplication Id.

Una volta selezionato il pulsante Done, torniamo alla schermata iniziale con, questa
volta, elencata lapplicazione appena creata, come indicato nella Figura 8.11.

Figura 8.11 Elenco dei Receiver disponibili.

Per ciascuna applicazione notiamo la presenza di alcuni link sulla destra, tra cui
quello di cancellazione, editing e, soprattutto, di pubblicazione del Receiver.
Selezionando questo link si otterrebbe per un errore, dovuto al fatto che mancano
ancora delle informazioni. Selezioniamo il link Edit, che ci porta a una schermata
nella quale dobbiamo inserire altre informazioni fondamentali, tra cui il tipo di Sender
e, soprattutto, il nome del package a esso associato. Per fare questo sufficiente
cercare il campo visualizzato nella Figura 8.12 e inserirvi il nome del package
dellapplicazione che andremo a realizzare e che funger da Sender.
Dopo aver salvato la nuova impostazione, possiamo tornare alla lista di
applicazioni e procedere alla pubblicazione. In questa fase la console visualizzer un

riepilogo dei dati del Receiver e quindi, dopo la conferma, provveder alla
pubblicazione. Si tratta di unoperazione che necessita di una quindicina di minuti.
Ne approfittiamo per procedere alla registrazione di un dispositivo di cui conosciamo
il numero di serie. Per fare questo selezioniamo il pulsante Add New Device nella
schermata rappresentata nella Figura 8.6 e inseriamo i dati che ci vengono richiesti
nella successiva schermata, ovvero il Serial Number e una descrizione che ci servir
per la sua identificazione nella console.

Figura 8.12 Informazioni sul possibile Sender.


NOTA
I l Serial Number una stringa alfanumerica di 12 caratteri, scritta in caratteri piccolissimi e di
difficile lettura, posta sul retro del dispositivo.

A questo punto iniziamo a occuparci dellapplicazione client che abbiamo


realizzato, FriendFenceCast, che permetter di eseguire alcune semplici operazioni con il
dispositivo Chromecast. Come nel caso dellapplicazione del paragrafo precedente, la
realizzazione dellapplicazione Sender presuppone alcuni passi quasi automatici.
Innanzitutto dobbiamo verificare che la versione dellapplicazione sia almeno quella
corrispondente allAPI Level 9, ovvero Gingerbread. Nel nostro caso abbiamo deciso
di utilizzare addirittura un API Level pari a 16. Infatti ormai con questa versione
minima riusciamo a coprire quasi il 99% del mercato dei dispositivi. Lutilizzo delle
API per Google Cast necessita dei Google Play services e di alcune librerie di
compatibilit, che abbiamo definito nel file di configurazione di Gradle attraverso la
seguente definizione:

dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.android.support:appcompat-v7:21.0.0
compile com.android.support:mediarouter-v7:21.0.0
compile com.google.android.gms:play-services:6.5.87
}


Prima di scrivere il codice non dobbiamo dimenticarci di inserire la seguente
definizione nel documento AndroidManifest.xml.

<meta-data

android:name=com.google.android.gms.version
android:value=@integer/google_play_services_version />

Dobbiamo inoltre utilizzare il seguente tema, per non incorrere nel crash
dellapplicazione:

<style name="AppTheme" parent="Theme.AppCompat"></style>


Siamo pronti per il primo passo, che consiste nella definizione del pulsante per
lavvio della ricerca di dispositivi compatibili. In questo caso non dobbiamo fare
nulla di diverso da quanto fatto nel caso delle MediaRouter API, per cui andiamo
semplicemente a definire la seguente entry nel documento main_menu.xml che
utilizziamo come risorsa di tipo menu.

<menu xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools=http://schemas.android.com/tools
xmlns:app=http://schemas.android.com/apk/res-auto
tools:context=.MainActivity>
<item
android:id=@+id/media_route_menu_item
android:title=@string/media_route_menu_title

app:actionProviderClass=android.support.v7.app.MediaRouteActionProvider
app:showAsAction=always />
</menu>


Anche in questo caso ci ricordiamo che lattivit che conterr tutta la logica che
stiamo descrivendo dovr estendere la classe ActionBarActivity; nel nostro caso
abbiamo deciso di lasciare il nome che ci stato fornito dal plug-in di creazione
automatica di un progetto di Android Studio, ovvero MainActivity. Si tratta di
unattivit che dovr riconoscere la voce di menu appena definita, con laccortezza
della configurazione che ci permetter di selezionare solamente i dispositivi associati
allapplicazione creata attraverso la Console di Google per Cast. Si tratta di
unattivit il cui metodo onCreate() stato implementato nel seguente modo:

@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// We initialize the MediaRouter
mMediaRouter = MediaRouter.getInstance(getApplicationContext());
// We have to find devices that can launch the Receiver associated to
// our application
mMediaRouteSelector = new MediaRouteSelector.Builder()
.addControlCategory(CastMediaControlIntent
.categoryForCast(APP_ID))
.build();
}


Loggetto che ci permetter di interagire con i dispositivi esterni definito dalla
classe MediaRouter, di cui otteniamo unistanza attraverso un suo metodo statico di
factory. Il valore passato come parametro il riferimento allApplicationContext, sempre
per fare in modo che loggetto creato non venga eliminato dal Garbage Collector.
Loggetto di tipo MediaRouteSelector viene invece inizializzato attraverso un Builder

implementato dalla classe MediaRouteSelector.Builder. Nel paragrafo precedente abbiamo


visto come il metodo addControlCategory() permetta di indicare le categorie di
dispositivi che intendiamo ricercare nella rete locale. In questo caso utilizziamo il
metodo statico categoryForCast() della classe di utilit CastMediaControlIntent per ottenere
unidentificazione della categoria a partire dallApplication Id che abbiamo ottenuto
attraverso la console di Google per Cast. Loggetto di tipo MediaRouteSelector creato
viene poi utilizzato durante linizializzazione delle azioni della ActionBar attraverso il
seguente codice:

@Override
public boolean onCreateOptionsMenu(Menu menu) {

super.onCreateOptionsMenu(menu);
// We inflate the Menu
getMenuInflater().inflate(R.menu.menu_main, menu);
// We get reference to the MediaRoute menu item
MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
// We get the reference to the MediaRouteActionProvider
MediaRouteActionProvider mediaRouteActionProvider =
(MediaRouteActionProvider) MenuItemCompat
.getActionProvider(mediaRouteMenuItem);
// We set the filter for our application
mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
// Its the only voice at the moment
return true;
}


Nel codice evidenziato notiamo come si ottenga il riferimento al
MediaRouteActionProvider dalla voce di menu definita attraverso la precedente risorsa, per
poi assegnare il filtro attraverso il metodo setRouteSelector(). Ora la nostra
applicazione pronta per lanciare la ricerca dei dispositivi nei quali esiste un Receiver
compatibile con quello definito nella nostra applicazione e quindi associato
allApplication Id utilizzato in fase di inizializzazione. A questo punto, se lanciamo

lapplicazione, otteniamo la visualizzazione dellicona indicata nella Figura 8.13,


selezionando la quale si ottiene la schermata rappresentata nella Figura 8.14, che
permette di scegliere il dispositivo cui connettersi, che deve appartenere alla stessa
rete locale. Ricordiamo, infatti, che lassenza di dispositivi fa s che il pulsante non
venga visualizzato.

Figura 8.13 Licona per la ricerca dei dispositivi viene visualizzata nella ActionBar.

importante altres ricordare come non esista al momento alcun emulatore per
unapplicazione Cast. Lunico modo quello di disporre di device reali.
Prima di connetterci al dispositivo trovato, dobbiamo per aggiungere il codice che
permette di gestire la connessione. Per fare questo sufficiente creare
unimplementazione dellinterfaccia di callback MediaRouter.Callback e quindi registrarla
al MediaRouter: al momento abbiamo creato la seguente implementazione di callback,
che andremo poi ad affinare:

private final MediaRouter.Callback mMediaRouterCallback =

new MediaRouter.Callback() {
@Override
public void onRouteSelected(MediaRouter router,
MediaRouter.RouteInfo info) {
// We save the reference to the selected device
mCastDevice = CastDevice.getFromBundle(info.getExtras());
// We get the related routeId
String routeId = info.getId();
}
@Override
public void onRouteUnselected(MediaRouter router,
MediaRouter.RouteInfo info) {
// We release the reference to the device

clearCast();
// We clear the reference
mCastDevice = null;
}
};

Figura 8.14 Elenco dei dispositivi compatibili connessi alla rete locale.

Come possiamo notare, si tratta di uninterfaccia che definisce due operazioni che
vengono chiamate, rispettivamente, nel momento di connessione e in quello di
disconnessione. Al momento della connessione viene richiamato il metodo
onRouteSelected(), cui viene passato come secondo parametro il riferimento alloggetto
di tipo MediaRouter.RouteInfo che abbiamo imparato a gestire nel paragrafo precedente e
che contiene le informazioni relative al device connesso. Le Cast API dispongono

della classe CastDevice, la quale permette di rappresentare un Receiver. Per ottenere


questo oggetto non abbiamo fatto altro che utilizzare il suo metodo statico
getFromBundle(), passando come parametro il Bundle ottenuto dalloggetto di tipo
attraverso il suo metodo getExtras(). Sempre dalloggetto

MediaRouter.RouteInfo

, attraverso il metodo getId(), abbiamo ottenuto un identificatore

MediaRouter.RouteInfo

del dispositivo connesso.


NOTA
Molte delle informazioni che si ottengono dalloggetto di tipo MediaRouter.RouteInfo si possono
ottenere anche dal corrispondente oggetto di tipo CastDevice.

Possiamo notare come loggetto di tipo CastDevice venga memorizzato in una


variabile distanza, in quanto ci sar utile per implementare le logiche di
comunicazione. Loggetto di tipo MediaRouter.Callback deve essere registrato e
deregistrato in accordo con il ciclo di vita dellActivity che lha definito. Per questo
motivo abbiamo implementato i seguenti due metodi:

@Override
protected void onStart() {

super.onStart();
mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
}
@Override
protected void onStop() {

mMediaRouter.removeCallback(mMediaRouterCallback);
super.onStop();
}


Possiamo notare come loggetto di callback venga registrato in corrispondenza del
metodo onStart() e deregistrato in corrispondenza del metodo onStop(). Unalternativa
poteva essere quella di registrare il callback nel metodo onResume() e quindi
deregistrarlo in corrispondenza del metodo onPause(). In ogni caso notiamo come la
registrazione avvenga attraverso il seguente metodo:

public void addCallback (MediaRouteSelector selector,

MediaRouter.Callback callback, int flags)


Il primo parametro loggetto di tipo MediaRouteSelector creato nel metodo onCreate().
Il secondo parametro il riferimento allimplementazione dellinterfaccia di callback,
infine il terzo un flag che permette di specificare i criteri da utilizzare per il
filtraggio dei dispositivi. Il flag che abbiamo utilizzato nel nostro caso, per esempio,
rappresentato dalla seguente costante, la quale permette di attivare la ricerca passiva
dei dispositivi nel caso di applicazione in foreground:

MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY


La costante seguente, invece, permette di ricevere le notifiche anche per dispositivi
(route) che non cadrebbero tra quelli specificati attraverso loggetto MediaRouteSelector
di filtro.

MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS


possibile utilizzare altri flag, per i quali rimandiamo alla documentazione
ufficiale. Nel metodo onStop() provvediamo quindi alla deregistrazione, al fine di
ottimizzare le risorse disponibili. Come verifica intermedia invitiamo il lettore a
connettersi a uno dei dispositivi selezionati, osservando i valori ottenuti attraverso
opportuni messaggi da visualizzare nel log o attraverso semplici Toast. Una cosa da
notare riguarda il fatto che in corrispondenza dellavvenuta connessione, il pulsante
di connessione assume un aspetto diverso, come indicato nella Figura 8.15. Ora il
rettangolo , infatti, riempito di bianco.

Figura 8.15 Il dispositivo connesso.

Per disconnettersi sufficiente selezionare ancora licona, ottenendo la schermata


rappresentata nella Figura 8.16, che possibile utilizzare per la disconnessione. Dopo
la disconnessione il tutto ritorna come indicato nella Figura 8.13.
A questo punto abbiamo visto come ottenere una connessione verso un dispositivo
che abbiamo scelto tra quelli disponibili e compatibili sulla stessa rete. Il passo

successivo consiste nel lanciare il Receiver in modo da iniziare la connessione con la


nostra applicazione, che funge quindi da Sender. in questa fase che entrano in gioco i
Google Play services. Nel nostro caso abbiamo implementato questa logica nel
seguente metodo:

private void launchReceiver(final CastDevice castDevice) {

// We initialize the Builder o create the Cast connection


Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
.builder(castDevice, castListener);
// We initialize the GoogleApiClient object
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(mConnectionCallback)
.addOnConnectionFailedListener(mConnectionFailedListener)
.build();
// We connect
mGoogleApiClient.connect();
}

Figura 8.16 Modalit di disconnessione dal dispositivo Chromecast.

Come possiamo notare, le azioni da svolgere sono sostanzialmente tre. La prima


consiste nella creazione di un oggetto di tipo Cast.CastOptions.Builder, il quale vuole
come parametri il riferimento alloggetto CastDevice e unimplementazione
dellinterfaccia Cast.Listener, la quale definisce alcuni metodi di callback che
permettono al Sender di ricevere informazioni relativamente dal Receiver. Per capire il
significato di questa interfaccia sufficiente osservare la nostra implementazione,
ovvero:

private final Cast.Listener castListener = new Cast.Listener() {

@Override
public void onApplicationStatusChanged() {
if (mGoogleApiClient != null) {

// We get the new status


final String newStatus = Cast.CastApi
.getApplicationStatus(mGoogleApiClient);
Log.d(TAG_LOG, onApplicationStatusChanged: new Status
+ newStatus);
}
}
@Override
public void onVolumeChanged() {
if (mGoogleApiClient != null) {
// We get the information related to the volume
final double newVolume = Cast.CastApi
.getVolume(mGoogleApiClient);
Log.d(TAG_LOG, onVolumeChanged: new value + newVolume);
}
}
@Override
public void onApplicationDisconnected(int errorCode) {
// We close the application
clearCast();
}
};


Possiamo, per esempio, ricevere notifica della variazione di stato del Receiver
oppure una variazione del valore del volume. Infine riceviamo notifica della
disconnessione del Receiver. In questo caso non facciamo altro che visualizzare dei
messaggi di log, ma i Sender potrebbero modificare alcuni elementi dellinterfaccia
utente o prendere altre decisioni in base ai valori ricevuti.

Il secondo passo consiste nellinizializzazione delloggetto GoogleApiClient di


interazione con i Google Play services. Il codice per fare questo ci ormai familiare
e, nel caso specifico, caratterizzato solamente dallutilizzo del metodo addApi() al
quale passiamo come primo parametro la costante Cast.API e come secondo il builder
creato in precedenza. Da notare, in questo caso, la solita presenza delle
implementazioni delle interfacce GoogleApiClient.ConnectionCallbacks e
, rispettivamente per lavvenuta connessione o

GoogleApiClient.OnConnectionFailedListener

per il verificarsi di un errore. Nel caso di errori non facciamo altro che liberare le
risorse ed eseguire il reset del tutto:

private final GoogleApiClient.OnConnectionFailedListener

mConnectionFailedListener =
new GoogleApiClient.OnConnectionFailedListener() {
@Override
public void onConnectionFailed(ConnectionResult
connectionResult) {
Log.w(TAG_LOG, Connection failed + connectionResult);
// In this case we just release resources
clearCast();
}
};


Molto pi interessante invece la gestione dellevento di avvenuta connessione,
nel quale ci preoccupiamo di avviare finalmente il Receiver che abbiamo realizzato in
precedenza. Il codice per realizzare questa funzionalit il seguente, che abbiamo
scomposto in due parti per semplificarne la visualizzazione. La prima
semplicemente limplementazione dellinterfaccia GoogleApiClient.ConnectionCallbacks, la
quale resetta il tutto nel caso di connessione sospesa e richiama invece il metodo
manageConnectedState() nel caso di connessione stabilita.

private final GoogleApiClient.ConnectionCallbacks mConnectionCallback =

new GoogleApiClient.ConnectionCallbacks() {

@Override
public void onConnected(Bundle bundle) {
manageConnectedState(bundle);
}
@Override
public void onConnectionSuspended(int cause) {
// We clear the CastDevice info
clearCast();
}
};


NOTA
Prima di proseguire bene precisare come da parte nostra la gestione degli errori sia stata
semplificata, senza trattare eventuali tentativi di riconnessione.

La seconda parte rappresentata dal metodo manageConnectedState(), che abbiamo


implementato nel seguente modo:

private void manageConnectedState(final Bundle data) {

Cast.CastApi.launchApplication(mGoogleApiClient, APP_ID, false)


.setResultCallback(new
ResultCallback<Cast.ApplicationConnectionResult>() {
@Override
public void onResult(Cast.ApplicationConnectionResult
result) {
if (result.getStatus().isSuccess()) {
Log.d(TAG_LOG, Successfully launched);
mApplicationStarted = true;
// The session Id
mSessionId = result.getSessionId();
} else {

Log.d(TAG_LOG, Launch failed!);


mApplicationStarted = false;
}
}
});
}


Attraverso il metodo launchApplication() delloggetto Cast.CastApi , infatti, possibile
lanciare lapplicazione corrispondente al Receiver, la quale identificata
dallApplication Id che passiamo come secondo parametro. Come per altre operazioni
asincrone, anche in questo caso il valore restituito un oggetto a cui possiamo
registrare un ResultCallback, il quale ci fornir un oggetto di tipo
che consulteremo per conoscere lesito delloperazione

Cast.ApplicationConnectionResult

e per accedere ad alcune informazioni, tra cui un identificatore di sessione. Si tratta


dellinformazione che utilizzeremo successivamente proprio per chiudere
lapplicazione del Receiver appena eseguita. La variabile distanza mApplicationStarted ci
permetter invece di sapere se il Receiver stato effettivamente eseguito, in modo da
poterlo chiudere.
Abbiamo fatto un ulteriore passo avanti, connettendoci al dispositivo Chromecast e
lanciando in esso un Receiver custom che abbiamo configurato in precedenza e
registrato nella console di Google per Cast. Se lanciamo lapplicazione e ci
connettiamo al dispositivo Chromecast, otteniamo quanto rappresentato nelle Figure
8.17 e 8.18. Notiamo come la personalizzazione fatta in precedenza si traduca
effettivamente in un Receiver di stile personalizzato.

Figura 8.17 La schermata splash visualizzata su un televisore Samsung da 40 pollici.

Figura 8.18 La schermata idle visualizzata su un televisore Samsung da 40 pollici.

A questo punto la nostra applicazione Sender connessa al Receiver che la stessa ha


mandato in esecuzione e i due componenti sono pronti allo scambio di messaggi
attraverso un canale di comunicazione, che sar largomento del prossimo paragrafo.
La conclusione di questo paragrafo invece dedicata alla descrizione del metodo
clearCast(), che si occupa della chiusura del Sender attraverso il seguente codice:


private void clearCast() {

// If the GoogleApiClient is null we skip the operation


if (mGoogleApiClient == null) {
Log.w(TAG_LOG, GoogleApiClient not correctly initialized!);
return;
}
// We stop the application only if started
if (mApplicationStarted) {
if (mGoogleApiClient.isConnected() ||
mGoogleApiClient.isConnecting()) {
// We stop the application given its sessionId
Cast.CastApi.stopApplication(mGoogleApiClient, mSessionId);
mGoogleApiClient.disconnect();
}
mApplicationStarted = false;
}
// We reset all the objects
mGoogleApiClient = null;
mCastDevice = null;
mSessionId = null;
}


Nel caso in cui il Receiver sia effettivamente stato eseguito con successo, non
faremo altro che utilizzare il metodo stopApplication() della classe Cast.CastApi per
fermarne lesecuzione. Notiamo come si tratti di unoperazione per cui necessario
conoscere lidentificatore della sessione avviata. Infine procediamo alla
disconnessione delloggetto GoogleApiClient e quindi al reset di tutte le variabili di
stato.

Gestione dei canali di comunicazione


Come accennato nel paragrafo precedente, una volta che il Sender e il Receiver sono
stati avviati con successo, necessario fare in modo che si possano scambiare
messaggi. Il meccanismo attraverso il quale il Sender invia messaggi al Receiver si
chiama channel ed sostanzialmente uno strumento attraverso il quale possibile
mandare e ricevere messaggi di tipo testuale. Si tratter di messaggi che
implementano una sorta di protocollo di alto livello (del tipo HTTP per intenderci).
Ciascun channel caratterizzato da un namespace, il quale deve necessariamente iniziare
con il prefisso:

urn:x-cast:


Nel nostro caso abbiamo definito il seguente namespace

urn:x-cast:uk.co.massimocarli.friendfence


Sebbene non sia il nostro caso, il Sender ha la facolt di creare il numero di channel
che ritiene opportuno, a patto che ciascuno di essi sia caratterizzato da un namespace
univoco. La gestione dei messaggi che passano allinterno di un channel avviene,
anche in questo caso, attraverso limplementazione di unopportuna interfaccia, che
in questo caso si chiama Cast.MessageReceivedCallback e che contiene la definizione di
due operazioni:

private static class LogChannelCallback

implements Cast.MessageReceivedCallback {
/**
* @return The namespace for this MessageReceivedCallback
*/
public String getNamespace() {
return urn:x-cast:uk.co.massimocarli.friendfence;
}

@Override
public void onMessageReceived(CastDevice castDevice, String namespace,
String message) {
Log.d(TAG_LOG, onMessageReceived from + castDevice +
with namespace + namespace
+ message: + message);
}
}


In questo caso non abbiamo creato solamente unimplementazione dellinterfaccia,
ma una vera e propria classe interna statica. Questo perch lutilizzo del riferimento
di tipo interfaccia non ci avrebbe permesso di richiamare il metodo getNamespace().
Come possiamo notare, nel caso di ricezione di un messaggio, si avrebbe la chiamata
del metodo onMessageReceived() con le informazioni relative al dispositivo che ha
mandato il messaggio, il namespace del channel e quindi il contenuto del messaggio
stesso. Per creare il channel non dobbiamo fare altro che aggiungere il seguente codice
in corrispondenza dellavvenuta connessione del Receiver e quindi sempre nel metodo
:

manageConnectedState()


mLogChannelCallback = new LogChannelCallback();
try {

Cast.CastApi.setMessageReceivedCallbacks(mGoogleApiClient,
mLogChannelCallback.getNamespace(),
mLogChannelCallback);
} catch (IOException e) {
Log.e(TAG_LOG, Creation channel error!, e);
}


Innanzitutto creiamo unistanza della classe Cast.CastApi appena definita e quindi
richiamiamo il metodo seguente, al quale passiamo il riferimento allimmancabile
oggetto GoogleApiClient, insieme al namespace del canale che intendiamo creare, seguito
dallimplementazione dellinterfaccia appena creata.


public abstract void setMessageReceivedCallbacks (GoogleApiClient client,

String namespace, Cast.MessageReceivedCallback callbacks)



Se tutto funziona per il meglio, siamo pronti a inviare dei messaggi
allapplicazione Receiver che abbiamo installato e avviato.
NOTA
Il nostro Receiver uno Styled Media Receiver che in grado di gestire messaggi predefiniti,
come vedremo tra poco, ma nel caso di un Receiver custom possibile implementare un
protocollo personalizzato e fare in modo che il Sender e il Receiver parlino una lingua propria,
sempre attraverso il meccanismo di comunicazione del channel.

Per testare linvio di messaggi al Receiver abbiamo inizialmente aggiunto una


e un pulsante che permette linvio del testo inserito, attraverso la chiamata del

EditText

seguente metodo:

private void sendCustomMessage(final String message) {

if (mGoogleApiClient == null || mLogChannelCallback == null) {


Log.e(TAG_LOG, Initialization failed!);
return;
}
try {
Cast.CastApi.sendMessage(mGoogleApiClient,
mLogChannelCallback.getNamespace(), message)
.setResultCallback(
new ResultCallback<Status>() {
@Override
public void onResult(Status result) {
if (result.isSuccess()) {
// Success
} else {
// Error

}
}
});
} catch (Exception e) {
Log.e(TAG_LOG, Error sending message, e);
Toast.makeText(this, R.string.channel_send_message_error,
Toast.LENGTH_SHORT).show();
}
}


Come possiamo vedere, in questo caso il metodo da richiamare sulloggetto
Cast.CastApi il seguente:

public abstract PendingResult<Status> sendMessage (GoogleApiClient client,

String namespace, String message)



I parametri sono gli stessi del caso precedente, ma questa volta il significato del
messaggio relativo al messaggio da inviare e non a quello ricevuto. Nel nostro caso
i messaggi inviati al Receiver vengono ignorati, in quanto non seguono alcun
protocollo tra quelli conosciuti allo stesso.
Fortunatamente esistono per dei messaggi che il Receiver gi in grado di
comprendere e che possono essere inviati su canali anchessi gi definiti. Uno di
questi si chiama Media Channel e permette linvio di messaggi che consentano la
riproduzione di media come immagini, video o suoni. Per gestire questo canale a cui
stato associato il namespace

urn:x-cast:com.google.cast.media


sufficiente creare un oggetto di tipo RemoteMediaPlayer e registrare a esso una serie
di implementazioni di alcune interfacce di callback per ricevere le notifiche sul suo
stato. Analogamente a quello che abbiamo fatto con la nostra classe LogChannelCallback,

in corrispondenza dellavvenuto avvio del Receiver, creiamo loggetto RemoteMediaPlayer


e apriamo il corrispondente canale. Il codice simile al precedente, ovvero:

// We create the RemoteMediaPLayer
mRemoteMediaPlayer = new RemoteMediaPlayer();
try {

Cast.CastApi.setMessageReceivedCallbacks(mGoogleApiClient,
mRemoteMediaPlayer.getNamespace(),
mRemoteMediaPlayer);
} catch (IOException e) {

Log.e(TAG_LOG, mRemoteMediaPlayer channel error!, e);


}


A questo punto per necessario fare in modo che lo stato del RemoteMediaPlayer che
abbiamo definito nel nostro Sender venga sincronizzato con quello del Receiver. Per
questo motivo abbiamo la necessit di eseguire il seguente codice, che abbiamo, per
comodit, definito nel nostro metodo initRemoteMediaPlayer():

// We have to synch the state
mRemoteMediaPlayer.requestStatus(mGoogleApiClient)

.setResultCallback(

new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {

@Override
public void onResult(RemoteMediaPlayer.MediaChannelResult
result) {
if (!result.getStatus().isSuccess()) {
Log.e(TAG_LOG, Failed to request status.);
}
}
});


Nella modalit ormai nota abbiamo richiamato il metodo requestStatus() e
sincronizzato i due componenti sul Sender e sul Receiver. A questo punto vogliamo

inviare dei comandi che permettano la riproduzione e lo stop di un video. Per questo
motivo abbiamo aggiunto due semplici pulsanti Play e Stop, i quali permettono
linvio dei corrispondenti messaggi al Receiver. Nel caso dellinvio del messaggio
play abbiamo implementato il metodo sendVideo(), che si compone di due parti.
Nella prima creiamo gli oggetti che ci permettono di descrivere il media che andremo
a riprodurre. Attraverso un oggetto di tipo MediaMetadata indichiamo che si tratta di un
video, mentre attraverso loggetto di tipo MediaInfo impostiamo, attraverso il
corrispondente Builder, alcune informazioni come lindirizzo (url) e il mime type, un
titolo e indichiamo che si tratta di un contenuto accessibile via streaming.

MediaMetadata mediaMetadata =

new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
mediaMetadata.putString(MediaMetadata.KEY_TITLE, "FriendFence Video");
MediaInfo mediaInfo = new MediaInfo.Builder(VIDEO_URL)

.setContentType(MP4_MEDIA_TYPE)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(mediaMetadata)
.build();

La seconda parte del metodo consiste nel vero e proprio invio del comando di
riproduzione del video, attraverso le seguenti istruzioni:

mRemoteMediaPlayer.load(mGoogleApiClient, mediaInfo, true)

.setResultCallback(new ResultCallback<RemoteMediaPlayer
.MediaChannelResult>() {
@Override
public void onResult(RemoteMediaPlayer.MediaChannelResult result) {
if (result.getStatus().isSuccess()) {
// OK
} else {
// ERROR
}

}
});


Per inviare il messaggio di riproduzione del video abbiamo utilizzato, con la solita
modalit asincrona, il seguente metodo:

public PendingResult<RemoteMediaPlayer.MediaChannelResult>

load (GoogleApiClient apiClient, MediaInfo mediaInfo, boolean autoplay)



In realt esistono diversi overload di questo metodo, e invitiamo il lettore a
consultarli nella documentazione ufficiale. Nel nostro caso abbiamo utilizzato quello
il cui parametro di tipo boolean indica che il media dovr essere riprodotto
immediatamente.
Per fermare la riproduzione del video abbiamo implementato il metodo pauseVideo()
che non fa altro che richiamare, con una modalit analoga, il seguente metodo della
classe RemoteMediaPlayer.

private void pauseVideo() {

mRemoteMediaPlayer.pause(mGoogleApiClient).setResultCallback(
new ResultCallback<RemoteMediaPlayer.MediaChannelResult>() {
@Override
public void onResult(RemoteMediaPlayer.MediaChannelResult result) {
if (result.getStatus().isSuccess()) {
// OK
} else {
// ERROR
}
}
});
}

Prima di procedere con la verifica della nostra applicazione bene, per


completezza, dire che loggetto di tipo RemoteMediaPlayer assume determinati stati che
dipendono da vari fattori relativi al Sender e al Receiver. Per questo motivo, in molti
casi, opportuno registrare a esso delle implementazioni di alcune interfacce di
callback. Attraverso unimplementazione dellinterfaccia
infatti possibile ricevere informazioni sullo

RemoteMediaPlayer.OnStatusUpdatedListener

stato corrente di questo oggetto e agire di conseguenza, per esempio abilitando o


disabilitando i pulsanti di riproduzione che abbiamo aggiunto alla nostra interfaccia.
Attraverso unimplementazione dellinterfaccia di callback
invece possibile ricevere le notifiche in

RemoteMediaPlayer.OnMetadataUpdatedListener

relazione ai media che vengono inviati al Receiver attraverso loggetto RemoteMediaPlayer.


Lultimissima cosa riguarda a questo punto la chiusura dei canali che abbiamo
aperto con il Receiver. sufficiente utilizzare le seguenti poche righe di codice, sia nel
caso del channel custom che in quello relativo al RemoteMediaPlayer.

if (mLogChannelCallback != null) {

Cast.CastApi.removeMessageReceivedCallbacks(mGoogleApiClient,
mLogChannelCallback.getNamespace());
mLogChannelCallback = null;
}
if (mRemoteMediaPlayer != null) {

Cast.CastApi.removeMessageReceivedCallbacks(mGoogleApiClient,
mRemoteMediaPlayer.getNamespace());
mRemoteMediaPlayer = null;
}


Siamo finalmente giunti alla verifica del funzionamento della nostra semplicissima
applicazione. Inseriamo lindirizzo di un video a piacimento come valore della
costante VIDEO_URL e, dopo la fase di inizializzazione, selezioniamo il pulsante Play per
vedere il video riprodotto sulla nostra TV nel Receiver da noi personalizzato.

Conclusioni
In questo capitolo abbiamo proseguito la descrizione di come programmare
dispositivi diversi dagli smartphone o dai tablet. Qui ci siamo occupati di dispositivi
esterni per la riproduzione di media.
Nella prima parte abbiamo visto come utilizzare le MediaRouter API per la
gestione di due tipi diversi di device che abbiamo classificato in Remote Playback e
Secondary Output. Attraverso la realizzazione di una semplice applicazione abbiamo
visto come aggiungere il pulsante alla ActionBar e connetterci a dispositivi dei due tipi.
Nella seconda parte del capitolo ci siamo invece occupati della programmazione di
un device piccolo, ma molto potente come il Chromecast. Dopo aver creato una
personalizzazione grafica di un Receiver abbiamo visto come pubblicarlo attraverso la
Console di Google per Cast e come connettersi a esso da una semplice applicazione.
In questa occasione ci siamo occupati anche della descrizione di un canale di
comunicazione e di come Sender e Receiver lo utilizzino per lo scambio di messaggi.
Abbiamo terminato il capitolo con un esempio di utilizzo di un channel predefinito,
descritto dalla classe RemoteMediaPlayer. Come possiamo vedere Android si sta
spingendo anche su dispositivi diversi dallo smartphone e sembra avviarsi verso
quello che prima era un obiettivo di Java ovvero di creare applicazioni che potessero
essere realizzate una sola volta ed eseguite in dispositivi diversi. Non ancora
proprio cos, ma la strada quella.

Capitolo 9

Google Fit

Una delle principali novit della versione 5.0 di Android (Lollipop) annunciata al
Google I/O del 2014, si chiama Google Fit. Si tratta di un insieme di API che
permette di utilizzare un intero ecosistema per la raccolta e gestione di dati relativi
allo stato di forma di una persona e che vedremo in dettaglio in questo capitolo, in
quanto sono parte integrante dei Google Play services 6.5 e quindi sono state rese
accessibili dai dispositivi Android dalla versione 2.3 in poi, ovvero quelli con API
Level maggiore o uguale a 9. Da quanto detto in precedenza, Google Fit permette di
creare applicazioni che gestiscono informazioni relative allo stato di forma di un
individuo. Questa descrizione porta con se innumerevoli problematiche. La prima,
non indifferente, riguarda gli aspetti di privacy. Google raccomanda fortemente che
lutente venga sempre informato di quali siano i dati raccolti e, soprattutto, di cosa si
intende fare con queste informazioni. Non possibile, per esempio, raccogliere dati
con Google Fit e poi non utilizzarli a questo scopo. A ogni utente deve essere data la
possibilit di cancellare tutti i dati raccolti. fondamentale, infine, che i dati raccolti
riguardino aspetti legati al fitness e non alla salute, che parte fondamentale della
riservatezza di una persona. Esiste quindi una serie di regole, che non elenchiamo per
motivi di spazio, che lo sviluppatore deve necessariamente consultare prima di
realizzare una qualunque applicazione che utilizzi questi strumenti. Abbiamo parlato
di Google Fit come ecosistema, il quale composto delle parti rappresentate nella
Figura 9.1.

Figura 9.1 Architettura di Google Fit.

Innanzitutto notiamo come esista un Database che si chiama, appunto, Google


Fitness Store, il quale conterr tutti i dati raccolti in relazione a una determinata
persona. Si tratta di un servizio accessibile attraverso la Rete (un servizio cloud) che
ci permetter di condividere informazioni tra pi applicazioni su pi device. Lo
scopo, infatti, quello di raccogliere tutte le informazioni possibili attraverso tutti i
dispositivi possibili, anche wearable. Laccesso, sia in lettura che scrittura, alle
informazioni contenute in questo database in cloud avviene attraverso la Google Fit
API, una serie di servizi REST cui possibile accedere da diverse piattaforme; non
solo Android, ma anche iOS e Web. Un database ha senso se vengono raccolti dei dati.
A tale scopo stato definito un Sensor Framework la cui responsabilit quella di
mettere a disposizione dello sviluppatore una serie di strumenti che permettono non
solo la raccolta dai dati, attraverso opportuni sensori, ma anche i meccanismi che
permettano di memorizzarli e gestirli nel Google Fitness Store. Attraverso la versione
per Android possiamo infatti creare unapplicazione per smartphone e utilizzare i
sensori del telefono per raccogliere informazioni sul movimento. Se poi disponiamo
di un dispositivo wearable, possiamo ottenere informazioni anche relative al battito
cardiaco o alla pressione o altre ancora. Potremmo anche creare una semplice
applicazione Web e quindi utilizzare il browser per inserire manualmente altri dati.

Laspetto importante di questa architettura la possibilit di raccogliere dati in modi


diversi e con pi strumenti diversi in luoghi diversi. Ovviamente maggiore il
numero di informazioni e maggiore laccuratezza dei risultati cui sar possibile
arrivare.
Come accennato in precedenza, il rispetto della privacy di fondamentale
importanza, per cui le Google Fit implementano anche un meccanismo che permette
la richiesta di specifici permessi allutente di cui si vogliono raccogliere
informazioni. Come vedremo quando creeremo la nostra semplice applicazione di
prova, avremo la possibilit di definire diversi livelli di autorizzazione, che si
trasformeranno in altrettanti messaggi di richiesta di consenso allutente. Questi
permessi sono stati definiti attraverso Scope di OAuth come anche visto nel caso di
Google Plus. In questo caso si tratter di uno Scope relativo ai diritti di scrittura e
lettura, specificatamente in relazione ad aspetti legati allattivit dellutente, alla sua
posizione o al suo corpo. Ciascun permesso ci permetter di accedere a un particolare
sottoinsieme delle informazioni memorizzate nel Google Fitness Store.
Per comprendere le Google Fit API, e in particolare il Sensor Framework,
necessario dare alcune definizioni che ritroveremo durante lo sviluppo della semplice
applicazione che inizieremo a creare dal prossimo paragrafo. Abbiamo visto che
Google Fit si basa sulla raccolta di informazioni che possono essere di vario tipo e
ottenute attraverso vari dispositivi hardware o software. Un componente di questo
tipo si chiama Data Source ed caratterizzato da un nome e, soprattutto, dal tipo di dati
che in grado di raccogliere. Un particolare Data Source, per esempio, il sensore che
permette il conteggio dei passi o quello che permette di raccogliere informazioni sul
battito cardiaco. Oltre a nome e tipo di dati che il sensore in grado di raccogliere, un
Data Source pu contenere anche altre metainformazioni, come per esempio il
costruttore, laccuratezza, o altre che dipendono dalla specifica tipologia. Un
particolare tipo di dati viene rappresentato da un Data Type. Affinch tutte le
applicazioni possano comunicare tra loro serve un meccanismo comune che si presti
a diversi tipi di dato. Questo prevede che ciascun tipo di dato abbia un nome e una
lista ordinata di valori che ne rappresentano le dimensioni. Il numero di dimensioni
dipende dal tipo di dato che si intende rappresentare. Il peso o laltezza sono esempi
di dati che possono essere rappresentati attraverso ununica dimensione. La location
necessita invece di informazioni relative a latitudine, longitudine e altitudine. Altre
informazioni che richiedono dimensioni multiple sono, per esempio, quelle che
contengono un valore minimo, massimo e medio di una particolare misura. Possiamo
pensare a un Data Type come un modo per rappresentare i metadati di una misura;

passo fondamentale per poter gestire dati senza conoscerne a priori la struttura.
Abbiamo visto che i Data Source hanno la responsabilit di raccogliere dati
caratterizzati da un particolare Data Type. A questo punto serve un modo per
rappresentare i dati. Per questo esiste il concetto di Data Point, che rappresenta un
particolare dato descritto da un Data Type e fornito da una particolare Data Source con
laggiunta di una fondamentale informazione, ovvero il timestamp in cui la misura
stata acquisita. Alcune di queste informazioni potrebbero anche fare riferimento a un
intervallo temporale e non a un unico istante. In ogni caso un Data Point contiene le
informazioni che utilizzeremo per linterazione con il Google Fitness Store.
Utilizzeremo un Data Point sia per inserire nuovi dati, sia per accedere a dati o
statistiche esistenti. Nel caso di informazioni multiple viene poi definito un Data Set,
sostanzialmente un insieme di Data Point che fanno riferimento a un intervallo
temporale. Si utilizzano i Data Set, per esempio, come risultato di operazioni di query
sul Google Fitness Store. Lultimo concetto molto importante quello di Session,
ovvero di periodo durante il quale lutente genera informazioni da inviare allo Store
iniziando, per esempio, una corsa o altro tipo di attivit fisica.
Come ultima cosa prima di iniziare lo sviluppo della nostra applicazione facciamo
una veloce panoramica sulle API che fanno parte delle Google Fit e che vedremo in
dettaglio nei prossimi paragrafi. Si tratta in particolare di:
Sensors API
Recording API
History API
Sessions API
Bluetooth Low Energy API
Le Sensor API, come facile intuire, ci permetteranno di accedere a tutti i
meccanismi hardware e software in grado di fornire misure di qualcosa, ovvero di
quelli che abbiamo indicato come Data Point e che contengono informazioni descritte
attraverso le dimensioni definite in un opportuno Data Type. Si tratta di informazioni
che renderemo persistenti attraverso le Recording API, che renderanno il tutto pi
trasparente allo sviluppatore. I dati si raccolgono per eseguire delle statistiche che
riguardano determinati periodi. Questo possibile attraverso le History API. Si tratta
sostanzialmente di strumenti che permettono la gestione di grosse quantit di
informazioni. Come abbiamo visto, una Session associata a un intervallo temporale,
durante il quale lutente esegue lattivit fisica e che permette al sistema di

raccogliere informazioni. Laggiunta ai dati raccolti di metadati relativi a una


particolare sessione possibile attraverso le Session API. Infine Google Fit dispone
gi di tutti gli strumenti che permettono lacquisizione di informazioni da dispositivi
connessi in pair via Bluetooth, come quelli Wearable. Qualora si rendesse necessario il
collegamento con altri dispositivi, possibile utilizzare le Bluetooth Low Energy
API. Attraverso queste API possibile gestire tutti i classici passi di interazione con
dispositivi di questo tipo; dalla ricerca fino al pairing e allo scambio di dati.

Un semplice progetto con Google Fit


Come descritto nellintroduzione, Google Fit permette la raccolta di diversi tipi di
informazioni, che possono essere puntuali oppure relativi a un particolare periodo di
tempo. In entrambi i casi questi dati vengono rappresentati attraverso particolari Data
, ai quali associata ununica informazione temporale nel primo caso e un

Point

intervallo nel secondo. indiscusso che una particolare applicazione sia


caratterizzata dallinsieme di dati che la stessa in grado di gestire o vuole gestire.
Molte applicazioni dovranno necessariamente gestire dati che sono gi stati previsti
dalla piattaforma. Stiamo parlando, per esempio, della frequenza del battito cardiaco,
del numero di passi, di informazioni di location e altre ancora. In questo caso si parla
di tipi di dati pubblici e a questi sono gi stati associati degli identificatori che sono
accessibili attraverso particolari costanti della classe DataType. Alcune di queste sono,
per esempio:

DataType.TYPE_ACTIVITY_SAMPLE
DataType.TYPE_CALORIES_EXPENDED
DataType.TYPE_HEART_RATE_BPM
DataType.TYPE_WEIGHT


Esse rappresentano dati puntuali e hanno un prefisso del tipo TYPE_. Altre hanno
invece un prefisso AGGREGATE_ e descrivono informazioni di summary, come le seguenti:

DataType.AGGREGATE_ACTIVITY_SUMMARY
DataType.AGGREGATE_HEART_RATE_SUMMARY
DataType.AGGREGATE_WEIGHT_SUMMARY


Se osserviamo nel dettaglio i valori di queste informazioni, notiamo la presenza del
namespace com.google. La piattaforma ci permette comunque di creare un tipo di dati
specifico della nostra applicazione, ovvero un Custom Data Type. A dire il vero
abbastanza difficile creare questo tipo di informazioni, le quali spesso richiedono
apposite DataSource. Forse pi semplice nel caso di dati aggregati e quindi di
summary piuttosto che per dati puntuali. In questo caso opportuno verificare che il
dato non sia gi stato definito tra quelli pubblici precedenti e quindi non ne
rappresenti una duplicazione. Deve essere uninformazione chiara, con dimensioni
ben precise e, soprattutto, dovr essere associata a un namespace legato al package
dellapplicazione che la definisce. bene sottolineare che si tratta comunque di

informazioni private dellapplicazione, che quindi non possono essere condivise con
altre, in quanto sarebbe necessario definire un altro tipo di dato, che viene indicato
come Shareable. Come detto abbastanza difficile definire tipi custom e spesso quelli
pubblici sono pi che sufficienti. Nel nostro caso abbiamo deciso di creare
unapplicazione molto semplice, per la quale linformazione custom poteva
benissimo essere esclusa, in quanto aggiunta solamente come dimostrazione. Nella
nostra applicazione vogliamo infatti tracciare la distanza percorsa, attraverso un tipo
di dato associato alla costante

DataType.TYPE_LOCATION_SAMPLE


e allet dellutente, in giorni, come dato custom. Per questultimo creeremo il
seguente namespace, legato al package dellapplicazione:

uk.co.massimocarli.myfitapplication.age.minutes


Prima di iniziare il progetto diamo un cenno agli Shareable Data Type, che sono
particolari tipi di dati che le diverse applicazioni di diversi vendor possono
condividere con le altre. In questo caso serve una moderazione, la quale viene fatta
da Google che ne valuta leffettiva convenienza. Deve essere quindi uninformazione
che non rappresenti una duplicazione di quelle gi esistenti, che sia ben definita e,
soprattutto, che sia di effettivo utilizzo per le altre applicazioni. In questo caso si pu
inviare una richiesta a Google scrivendo una mail allindirizzo google-fit e fornendo tutte le informazioni necessarie. Qualora questo nuovo

shareable@google.com

tipo di dato venisse approvato, verrebbe aggiunto alla lista di tipi condivisi
accessibile attraverso la documentazione ufficiale. Se diamo unocchiata ai dati
condivisi in questo momento, notiamo la presenza di informazioni fornite dalle
applicazioni delle principali aziende che trattano materiale sportivo, come Nike e
Adidas.
NOTA
Laccesso a queste informazioni prevede laccettazione di condizioni che dipendono dal tipo di
dato e dallazienda che le fornisce. Nella nostra applicazione non faremo uso di questo tipo di
informazioni, per le quali si rimanda alla documentazione ufficiale.

Siamo pronti a creare il nostro progetto con Android Studio nel modo ormai
classico. Nel nostro caso abbiamo creato un semplice progetto Android con una sola

senza lutilizzo di Fragment, al fine di mantenere il tutto il pi semplice

Activity

possibile e quindi concentrarci sulle Google Fit API. Come accade spesso nella
gestione di API di questo tipo, il primo passo consiste nellattivazione del servizio
nella console di Google, come fatto anche nel caso di altre API come Google Maps e
Google Drive. Torniamo quindi allindirizzo https://code.google.com/apis/console/
selezioniamo APIs & auth sul menu a sinistra ed eseguiamo una ricerca delle Fitness
API che abiliteremo selezionando il pulsante ON sulla destra, come indicato nella
Figura 9.2. Attenzione: al momento sono API con quota, che quindi permettono la
chiamata di un numero limitato di richieste al giorno.

Figura 9.2 Attivazione delle Google Fit.

Per laccesso a queste API serve una chiave da associare al package della nostra
applicazione, che ora uk.co.massimocarli.myfitapplication, per la quale non possiamo
utilizzare quella ottenuta in precedenza per FriendFence. Per fare questo selezioniamo
la voce Credentials nel menu a sinistra e poi il pulsante New Client Id; quindi inseriamo
le informazioni richieste nella scheda che compare. Il procedimento esattamente lo
stesso descritto nel Capitolo 3 e prevede linserimento del certificato SHA1 ottenuto
attraverso lesecuzione del seguente comando nella cartella .android nella home del
particolare utente.

keytool -list -v -keystore debug.keystore -alias androiddebugkey storepass

android -keypass android



Una volta creato il Client Id non ci resta che procedere allormai classica
configurazione dei Google Play services allinterno della nostra applicazione. Primo
passo laggiunta della dipendenza nel file di configurazione build.gradle:

dependencies {

compile fileTree(dir: libs, include: [*.jar])


compile com.android.support:appcompat-v7:21.0.3
compile com.android.support:support-v4:21.0.3
compile com.google.android.gms:play-services-base:6.5.87
compile com.google.android.gms:play-services-fitness:6.5.87
}


NOTA
sempre bene eseguire una verifica della disponibilit di nuove versioni attraverso lSDK
Manager.

Aggiungiamo quindi nel file di configurazione AndroidManifest.xml la seguente


definizione, che imposta la versione corrente dei Google Play services, facendo
attenzione a inserirla nellelemento <application/>.

<meta-data android:name="com.google.android.gms.version"

android:value=@integer/google_play_services_version />

A questo punto avviamo il processo di inizializzazione delloggetto GoogleApiClient,
che questa volta segue un meccanismo leggermente diverso da quello implementato
per altri servizi e considera un nuovo stato, che quello di autorizzazione pending.
Questo riguarda il fatto che la richiesta di utilizzo delle Google Fit API richiede una
richiesta esplicita di autorizzazione allutente che prevede la visualizzazione di una
schermata che metterebbe la nostra Activity in uno stato di pausa. Per evitare che il
ritorno allo stato attivo della nostra Activity provochi linvio di una nuova richiesta,
stata aggiunta la variabile authInProgress, il cui valore deve essere gestito anche nel
caso di rotazioni.
NOTA
Avevamo gi incontrato questo problema in precedenza. Avevamo ovviato semplicemente
impedendo la rotazione del dispositivo e spostando linizializzazione dei Google Play services nel
metodo di callback onStart().

Il metodo onCreate() quindi il seguente e risulta sicuramente familiare, anche se


ora le API di cui si richiede lutilizzo sono quelle associate alla costante Fitness.API.
Notiamo anche lutilizzo di uno Scope che permette di richiedere le informazioni
relative alla location.


@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// We test if the authInProgress is true to avoid duplication
if (savedInstanceState != null) {
authInProgress = savedInstanceState.getBoolean(AUTH_PENDING);
}
// We initialize GoogleApiClient object
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Fitness.API)
.addScope(new Scope(Scopes.FITNESS_LOCATION_READ_WRITE))
.addConnectionCallbacks(mConnectionCallbacks)
.addOnConnectionFailedListener(mOnConnectionFailedListener)
.build();
}
@Override
protected void onSaveInstanceState(Bundle outState) {

super.onSaveInstanceState(outState);
outState.putBoolean(AUTH_PENDING, authInProgress);
}


Per la gestione della rotazione del dispositivo abbiamo anche descritto
limplementazione del metodo onSaveInstanceState(), che permette di salvare lo stato
della variabile authInProgress di cui leggiamo il valore nel metodo onCreate(). Anche in
questo caso abbiamo dovuto implementare due interfacce di callback per gli eventuali
errori e leffettiva connessione. Nel primo caso limplementazione la seguente:

private final GoogleApiClient.OnConnectionFailedListener

mOnConnectionFailedListener
= new GoogleApiClient.OnConnectionFailedListener() {

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
if (!connectionResult.hasResolution()) {
// If a solution is avail