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 available we show the related Dialog
GooglePlayServicesUtil
.getErrorDialog(connectionResult.getErrorCode(),
MainActivity.this, 0).show();
return;
}
// We manage the error only if the authorization is not pending
if (!authInProgress) {
try {
authInProgress = true;
connectionResult
.startResolutionForResult(MainActivity.this,
REQUEST_OAUTH);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG_LOG, Error managing Google Play Service, e);
}
}
}
};


Come abbiamo visto anche nei capitoli precedenti, nel caso in cui le informazioni
relative allerrore contengano anche la relativa soluzione, non facciamo altro che
visualizzare la corrispondente finestra di dialogo. Si tratta, di solito, dellassenza
della versione corrente di Google Play services e quindi del meccanismo che rinvia al
Play Store per linstallazione. Notiamo come nella seconda parte venga effettuato un
test sulla variabile authInProgress, per verificare se lautorizzazione pending.
Linterfaccia di gestione della connessione al momento molto semplice e non fa

altro che visualizzare un log di errore nel caso di sconnessione e richiamare il nostro
metodo goWithFitnessApi(), che implementeremo successivamente.

private final GoogleApiClient.ConnectionCallbacks mConnectionCallbacks =

new GoogleApiClient.ConnectionCallbacks() {
@Override
public void onConnected(Bundle bundle) {
// The Fitness API are connected so we can go ahead
goWithFitnessApi();
}
@Override
public void onConnectionSuspended(int cause) {
// Connection has been suspended so we show an error message
Log.w(TAG_LOG, Connection Suspended!);
}
};


Il ciclo di vita delloggetto di tipo GoogleApiClient creato nel metodo onCreate() deve
essere collegato anche a quello dellActivity attraverso le seguenti implementazioni
dei metodi onStart() e onStop().

@Override
protected void onStart() {

super.onStart();
// Connect to the Fitness API
mGoogleApiClient.connect();
}
@Override
protected void onStop() {

super.onStop();
if (mGoogleApiClient.isConnected()) {
mGoogleApiClient.disconnect();

}
}


Infine necessario gestire i callback relativi alla gestione dei Google Play services
in caso di errore, attraverso la seguente implementazione del metodo di callback
onActivityResult().

@Override
protected void onActivityResult(int requestCode, int resultCode,

Intent data) {
if (requestCode == REQUEST_OAUTH) {
authInProgress = false;
if (resultCode == RESULT_OK) {
if (!mClient.isConnecting() && !mClient.isConnected()) {
mClient.connect();
}
}
}
}


A questo punto le Fitness API sono inizializzate e siamo pronti a raccogliere le
informazioni di nostro interesse. Ma prima proviamo ad avviare la nostra
applicazione, ottenendo inizialmente la classica schermata per la selezione (se
presente) dellaccount da utilizzare e la visualizzazione della schermata di richiesta di
consensi, i quali dipenderanno dagli Scope utilizzati in fase di creazione delloggetto
.

GoogleApiClient

La gestione dei sensori


Le Google Fit API si basano su informazioni relative allo stato fisico di un utente,
le quali vengono poi memorizzate in un repository unico, per essere poi elaborate e
utilizzate da un insieme, pi o meno ampio, di applicazioni. Queste informazioni
devono essere raccolte attraverso vari sensori che le stesse Google Fit API ci
permettono di individuare. fondamentale sottolineare per che questi dati dovranno
essere utilizzati solo per la visualizzazione nel display e non dovremo preoccuparci
della loro persistenza nello store. Un conto accedere ai diversi sensori e visualizzare
le informazioni che ci mettono a disposizione e un altro decidere quali dati rendere
persistenti in modo ottimizzato e trasparente per lo sviluppatore, cosa che, come
vedremo, sar possibile attraverso le Recording API.

Figura 9.3 Richiesta del consenso allutente.

In questo paragrafo ci occupiamo della prima parte e quindi dellindividuazione


dei sensori a disposizione. Una volta connessi ai Google Play services abbiamo la

possibilit di utilizzare loggetto Fitness.SensorsApi, il quale ci mette a disposizione


diversi metodi, tra cui il seguente, il quale permette di eseguire una ricerca dei
DataSource, che determinano quali informazioni memorizzare.

public abstract PendingResult<DataSourcesResult>

findDataSources (GoogleApiClient client, DataSourcesRequest request)



Notiamo come si incapsulano in un oggetto di tipo DataSourcesRequest le informazioni
relative al tipo di sensori cui siamo interessati e inoltre come la modalit di ricezione
della risposta sia asincrona attraverso il meccanismo del PendingResult. Per creare un
oggetto di tipo DataSourcesRequest si utilizza anche in questo caso unimplementazione
del Builder Pattern descritta dalla classe DataSourcesRequest.Builder. Nel nostro esempio
abbiamo semplicemente aggiunto al menu delle opzioni la voce Search DataSource, che
richiama il seguente metodo:

private void searchDataSource() {

final MainActivity mainActivity = mActivityRef.get();


if (mainActivity == null) {
Log.w(TAG_LOG, Lost MainActivity reference!!);
return;
}
// We create the request for the Distance Delta as a raw type
final DataSourcesRequest request = new DataSourcesRequest.Builder()
.setDataTypes(DataType.TYPE_LOCATION_SAMPLE)
.setDataSourceTypes(DataSource.TYPE_RAW)
.build();
// We start the request
Fitness.SensorsApi.findDataSources(mainActivity.getGoogleApiClient(),request)
.setResultCallback(new ResultCallback<DataSourcesResult>() {
@Override

public void onResult(DataSourcesResult dataSourcesResult) {


if (dataSourcesResult.getStatus().isSuccess()) {
final List<DataSource> dataSources =
dataSourcesResult.getDataSources();
// We update the model
mModel.clear();
mModel.addAll(dataSources);
mAdapter.notifyDataSetChanged();
} else {
// Error. We show a Toast
Log.e(TAG_LOG, Error accessing DataSource);
}
}
});
}


Nella prima parte del metodo abbiamo utilizzato il Builder per la creazione della
richiesta di tipo DataSourcesRequest. importante notare come una richiesta debba
necessariamente avere sia linformazione relativa al tipo di dato che il DataSource dovr
supportare, sia quella relativa al tipo di DataSource, ovvero se fa riferimento a un tipo di
dato RAW oppure a un tipo di dato derivato. A questo punto sufficiente passare la
richiesta creata al metodo findDataSources() per ottenere, se disponibili, lelenco dei
sensori in grado di soddisfare la nostra richiesta. Nella solita modalit asincrona
otteniamo il risultato in una List di oggetti di tipo DataSource, che utilizziamo per
alimentare un ArrayAdapter che ci permette di visualizzare il tutto in una lista.
NOTA
Per dividere le varie funzionalit abbiamo inserito tutta la logica di gestione delle DataSource in
un Fragment descritto dalla classe DataSourceFragment.

Se osserviamo lAdapter, notiamo la presenza delle seguenti righe di codice, che ci


permettono di visualizzare le informazioni relative alla marca e modello del sensore e
lidentificatore della misura (Data Point) che esso in grado di fornire.


final DataSource dataSource = getItem(position);
holder.deviceTextView.setText(dataSource.getDevice()

.getManufacturer() + + dataSource.getDevice().getModel());
holder.typeTextView.setText(dataSource.getDataType().getName());


In queste poche righe notiamo come attraverso il metodo getDevice() sia possibile
ottenere le informazioni del sensore, incapsulate in un oggetto di tipo Device che
utilizzeremo successivamente per connetterci al dispositivo stesso. Se eseguiamo
lapplicazione nel device a nostra disposizione, otteniamo quanto rappresentato
nella Figura 9.4, dove notiamo la presenza di un unico sensore di Location che
appunto dato dal Nexus 5 stesso.

Figura 9.4 Il sensore per il dato di Location.

Lasciamo al lettore il compito di verificare cosa succede qualora si dovessero


ricercare altri sensori per altri tipi di dati, per esempio per il battito cardiaco. A tale
proposito bene osservare come la ricerca avvenga non solo sui sensori dello
smartphone, ma anche su tutti i dispositivi a esso connessi.
Una volta individuati i sensori disponibili possibile registrare per loro un Listener,
in modo da ricevere le informazioni da essi fornite attraverso unimplementazione
dellinterfaccia OnDataPointListener. Nel nostro caso abbiamo implementato le
operazioni di registrazione e deregistrazione attraverso opzioni contestuali, senza
tenere conto, per semplicit, del fatto che linterfaccia potesse essere gi registrata.
Abbiamo quindi creato le corrispondenti due voci nel menu contestuale alle quali
abbiamo associato la chiamata di altrettanti metodi, che descriviamo di seguito. La
parte di registrazione contenuta nel metodo subscribeDataSourceAt(). Dopo il controllo

sulla presenza del riferimento alla MainActivity ci preoccupiamo della creazione di un


oggetto di tipo SensorRequest attraverso il corrispondente Builder.

// We access the model to get the selected DataSource
final DataSource dataSource = mModel.get(position);
// We create the SensorRequest using its Builder
final SensorRequest sensorRequest = new SensorRequest.Builder()

.setDataSource(dataSource)
.setDataType(DataType.TYPE_LOCATION_SAMPLE)
.setSamplingRate(SAMPLING_RATE, TimeUnit.SECONDS)
.setFastestRate(SAMPLING_RATE, TimeUnit.SECONDS)
.setTimeout(TIMEOUT, TimeUnit.SECONDS)
.build();

Come il lettore potr notare, otteniamo come prima cosa il riferimento alloggetto
DataSource a cui ci vogliamo registrare, per utilizzarlo poi come parametro del metodo
, che ci permette di specificare la sorgente degli eventi cui siamo

setDataSource()

interessati. Oltre a questo, dobbiamo specificare il tipo di dati che vogliamo ricevere
attraverso il metodo setDataType(). Notiamo, infine, come impostare alcuni criteri
relativi alla frequenza di campionamento (lettura dal sensore) del dato, alla minima
frequenza con cui questa informazione pu arrivare e infine un timeout nel caso in
cui questa informazione non dovesse essere disponibile. Si tratta di informazioni che
somigliano molto a quelle che abbiamo utilizzato nel Capitolo 2 in relazione alla
gestione della Location attraverso la creazione di una LocationRequest. Una volta creato
loggetto di tipo SensorRequest, non ci resta che inviarlo al sistema attraverso il metodo
delloggetto Fitness.SensorsApi.

add()


public abstract PendingResult<Status>

add(GoogleApiClient client, SensorRequest request,


OnDataPointListener listener)

Oltre allimmancabile parametro di tipo GoogleApiClient, notiamo la presenza della
richiesta appena creata e del riferimento allimplementazione dellinterfaccia

. Sempre in analogia a quanto abbiamo fatto nel caso della Location,

OnDataPointListener

il lettore potr verificare come sia disponibile anche un altro overload del metodo
add() e precisamente il seguente, il quale contiene come ultimo parametro un oggetto
di tipo PendingIntent, che contiene le informazioni relative allIntent che viene lanciato
in corrispondenza di ciascun dato:

public abstract PendingResult<Status>

add(GoogleApiClient client, SensorRequest request, PendingIntent intent)



Anche in questo caso esiste la possibilit di gestire le informazioni raccolte in un
componente diverso, come pu essere il Service che abbiamo creato nel Capitolo 2. La
registrazione della nostra implementazione di OnDataPointListener viene quindi fatta
attraverso le seguenti poche righe di codice:

Fitness.SensorsApi.add(mainActivity.getGoogleApiClient(),

sensorRequest, mOnDataPointListener)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
Log.i(TAG, Listener subscription successful!);
} else {
Log.i(TAG, Listener subscription error!);
}
}
});


Prima di occuparci della nostra implementazione di OnDataPointListener, vediamo
come eseguire loperazione inversa, ovvero annullare la sottoscrizione.
NOTA
Per motivi di spazio abbiamo eliminato il codice relativo alla visualizzazione di messaggi
attraverso un Toast, che si pu invece trovare nel codice completo.

Si tratta di legare la chiamata del metodo unsubscribeDataSourceAt() alla selezione


della corrispondente voce del menu contestuale. A parte il controllo sullesistenza del
riferimento alla MainActivity, non facciamo altro che eseguire il seguente codice:

Fitness.SensorsApi.remove(

mainActivity.getGoogleApiClient(),
mOnDataPointListener)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
Log.i(TAG, Listener removed successful!);
} else {
Log.i(TAG, Listener removal error!);
}
}
});


In questo caso il metodo da richiamare il seguente:

public abstract PendingResult<Status>

remove(GoogleApiClient client, OnDataPointListener listener)



Nel caso della modalit che prevede lutilizzo del PendingIntent il metodo da usare
sar il seguente:

public abstract PendingResult<Status>

remove(GoogleApiClient client, PendingIntent pendingIntent)


per il quale valgono le stesse considerazioni fatte nel Capitolo 2 in relazione al


meccanismo che permette di sapere se due PendingIntent sono equivalenti oppure no.
A questo punto non ci resta che descrivere la nostra implementazione
dellinterfaccia OnDataPointListener, che molto semplice in quanto permette di
visualizzare le informazioni ricevute nella parte inferiore della schermata.

private final OnDataPointListener mOnDataPointListener =

new OnDataPointListener() {
@Override
public void onDataPoint(DataPoint dataPoint) {
// We get the result as a DataPoint. We get all the Field
// for the data printing its value
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(Sensor: )
.append(dataPoint.getDataSource().getDevice()
.getManufacturer()).append(\n);
stringBuilder.append(time: )
.append(dataPoint.getTimestampNanos()).append(\n);
final StringBuilder stringBuilder = new StringBuilder();
for (Field field : dataPoint.getDataType().getFields()) {
// We get the specific field as Value object
Value value = dataPoint.getValue(field);
// We print the current value data
stringBuilder.append(field.getName())
.append( = ).append(value).append(\n);
Log.i(TAG, DataPoint received: + field.getName()
+ = + value);
}
mLocationTextView.setText(stringBuilder.toString());

}
};


A parte lutilizzo di un StringBuilder per la creazione della String da visualizzare
nella TextView, notiamo come linterfaccia OnDataPointListener preveda la definizione del
metodo onDataPoint(), il quale ha come parametro un oggetto di tipo DataPoint che
incapsula le informazioni che il sensore ha messo a disposizione. Lo stesso oggetto ci
mette poi a disposizione il metodo getFields(), che ci permette di ottenere il
riferimento al particolare campo con relativo valore. Nel nostro caso il risultato
ottenuto quello rappresentato nella Figura 9.5, dove notiamo la visualizzazione di
alcune delle informazioni incapsulate nelloggetto DataPoint.

Figura 9.5 Informazioni contenute nel DataPoint relativo a Location.

Come accennato in precedenza, le Google Fit API sono aperte allutilizzo di una
vasta gamma di sensori, con i quali il dispositivo comunica attraverso Bluetooth. In
particolare sono supportati i dispositivi che supportano il BLE (Bluetooth Low
Energy) e in particolare il profilo GATT (Generic Attribute Profile). In questo caso il
primo passo consiste nelloperazione di ricerca dei dispositivi accessibili cui
connettersi. Per fare questo possibile utilizzare loggetto Fitness.BleApi e in
particolare il metodo seguente, che riceve come parametro un oggetto di tipo
StartBleScanRequest il quale, anche in questo caso, incapsula le informazioni relative al
tipo di dispositivi che intendiamo ricercare:

public abstract PendingResult<Status> startBleScan(GoogleApiClient client,

StartBleScanRequest request)

Per provare questa funzionalit abbiamo aggiunto una nuova voce al menu delle
opzioni, associandola allesecuzione del metodo startBleScan() che descriviamo nelle
sue varie parti. Dopo il solito controllo sullesistenza del riferimento allActivity
contenitore del fragment, abbiamo definito unimplementazione dellinterfaccia
la quale permette di definire due metodi di callback. Il primo viene

BleScanCallback

richiamato qualora venga effettivamente trovato un dispositivo, mentre il secondo


viene richiamato in corrispondenza della terminazione del processo di scanning. Nel
nostro caso abbiamo il seguente codice:

final BleScanCallback bleScanCallback = new BleScanCallback() {

@Override
public void onDeviceFound(final BleDevice device) {
// Here we get the BleDevice that we want to claim as DataSource
Fitness.BleApi.claimBleDevice(mainActivity.getGoogleApiClient()
,device) .setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
Log.d(TAG_LOG, device +
successfully claimed!);
} else {
Log.d(TAG_LOG, device + claiming error!);
}
}
});
}
@Override
public void onScanStopped() {
// The Scan process stopped
Log.d(TAG_LOG, BLE Scanning stopped!);

}
};


importante sottolineare come quando un dispositivo BLE viene trovato si renda
comunque necessaria unoperazione che si chiama claiming, attraverso la quale il
dispositivo viene inserito nellelenco delle possibili Data Source raggiungibili
attraverso il meccanismo visto in precedenza. Questo il motivo dellutilizzo del
metodo claimBleDevice() accessibile sempre attraverso loggetto Fitness.BleApi. Notiamo
anche come il particolare dispositivo venga rappresentato da unistanza della classe
BleDevice. Una volta che il dispositivo viene registrato come possibile DataSource,
possibile eseguire loperazione inversa attraverso il metodo unclaimBleDevice().
Il passo successivo consiste nella creazione della richiesta, ovvero di unistanza
della classe StartBleScanRequest attraverso lormai classico Builder. Nel nostro caso il
codice molto semplice e permette di creare la richiesta per un dispositivo in grado
di fornire le informazioni relative a un conteggio passi.

final StartBleScanRequest bleScanRequest =

new StartBleScanRequest.Builder()
.setDataTypes(DataType.TYPE_STEP_COUNT_CUMULATIVE)
.setBleScanCallback(bleScanCallback)
.build();

Notiamo come il riferimento allinterfaccia di callback venga passato nella
richiesta attraverso il metodo setBleScanCallback(). Infine non ci resta che lanciare la
richiesta di scanning attraverso il seguente codice:

Fitness.BleApi.startBleScan(mainActivity.getGoogleApiClient(),

bleScanRequest)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {

// Everything is ok so we want to connect


// to the device
} else {
// We check if the request failed because
// of the disabled Bluetooth
final int statusCode = status.getStatusCode();
if (FitnessStatusCodes.DISABLED_BLUETOOTH == statusCode) {
// In this case the Bluetooth is disabled so
// we have to enable it
try {
status.startResolutionForResult(mainActivity,
REQUEST_BLUETOOTH);
} catch (IntentSender.SendIntentException e) {
e.printStackTrace();
Log.e(TAG_LOG, Error enabling BLE, e);
}
}
}
}
});


importante sottolineare come in questo caso esiste anche la gestione di un errore
particolare, ovvero quello relativo al fatto che il Bluetooth non sia abilitato nel
momento della scansione. In questo caso il procedimento molto simile a quello di
gestione degli errori in fase di inizializzazione delloggetto GoogleApiClient e prevede
linvio di un Intent che lo stesso framework ci mette a disposizione attraverso il
metodo startResolutionForResult(). La gestione di questo errore viene poi completata
attraverso limplementazione del metodo onActivityResult().
NOTA

A proposito di questo bene ricordare come il metodo onActivityResult() richiamato dal


framework sia quello dellActivity e non quello del Fragment. Questo fatto ci ha costretto a
delegare esplicitamente la chiamata dello stesso metodo nellActivity a quello nel Fragment
corrente, come fatto anche nellapplicazione FriendFence.

A questo punto la nostra applicazione di prova in grado di connettersi a un


insieme di sensori che forniscono informazioni incapsulate in oggetti di tipo DataPoint.
Come ultima considerazione al riguardo ricordiamo come i dati ottenuti possano
essere utilizzati per la pura visualizzazione. Qualora avessimo bisogno di raccogliere
dei dati da rendere persistenti nello store di Google Fit, esistono altre classi, che sono
argomenti del prossimo paragrafo.
Come abbiamo visto in questo paragrafo, Google Fit permette di gestire sensori
compatibili con BLE in modo quasi automatico, attraverso il processo visto in
precedenza. Nel caso in cui si disponesse di altri tipi di dispositivi non BLE, Google
Fit mette a disposizione un ulteriore meccanismo, che permette di utilizzare il sensore
come se fosse un sensore BLE. A tale proposito sufficiente incapsulare la logica di
interazione con il sensore in una particolare specializzazione della classe
FitnessSensorService. Si tratta, come dice il nome, di una specializzazione della classe
che si occupa sostanzialmente di informare il sistema delle informazioni che il

Service

sensore in grado di raccogliere e quindi di permettere la registrazione e


deregistrazione. Per ulteriori dettagli rimandiamo alla documentazione ufficiale.

Data Point e sessioni


Nel paragrafo precedente abbiamo visto come verificare quali siano i sensori a
disposizione delle Google Fit, eventualmente aggiungendone altri compatibili o meno
con il profilo GATT di BLE. Abbiamo poi ricordato come le informazioni ottenute dai
sensori non vengano salvate automaticamente, ma possono essere utilizzate
solamente per dare un feedback allutente che ha iniziato unattivit. Per avviare la
registrazione di informazioni di un certo Data Type ottenuto da un particolare Data Source
si utilizzano invece le Recording API, che sono argomento della prima parte di
questo paragrafo. Si tratta di strumenti molto semplici, che permettono di richiedere
alle Google Fit il salvataggio delle informazioni raccolte, in modo completamente
trasparente al programmatore; dovremo semplicemente occuparci di iniziare e
terminare la fase di registrazione richiamando alcuni metodi messi a disposizione
dalloggetto Fitness.RecordingApi. Per richiedere il salvataggio di alcune informazioni
direttamente nel Google Fitness Store sufficiente utilizzare il seguente metodo, il
quale ha come secondo parametro un oggetto di tipo Data Type che indica il tipo di dati
cui siamo interessati:

public abstract PendingResult<Status> subscribe(GoogleApiClient client,

DataType dataType)

Qualora volessimo invece richiedere la memorizzazione delle informazioni
associate a una particolare Data Source, possiamo utilizzare questaltro overloading,
che prevede come secondo parametro un oggetto di tipo DataSource:

public abstract PendingResult<Status> subscribe(GoogleApiClient client,

DataSource dataSource)

Una volta che la sottoscrizione avvenuta con successo, confidiamo che le
corrispondenti informazioni verranno salvate in modo ottimizzato nel Google Fitness
Store, cui potremo accedere utilizzando le History API che vedremo nel prossimo
paragrafo. bene sottolineare come la gestione della connessione e della
disponibilit del sensore avvenga in modo completamente trasparente.
NOTA

Pu sembrare strano che queste informazioni non siano disponibili allapplicazione se non
accedendo al Google Fitness Store attraverso le History API. Questo fra i motivi della presenza
delle Sensor API per laccesso ai dati dei sensori. Il tutto avviene in modo ottimizzato.

Questo significa che, in caso di assenza di connessione, il sistema si preoccuper di


memorizzare le informazioni per inviarle quando la connessione sar nuovamente
disponibile. Lo stesso vale nel caso in cui il sensore non fosse attivo per un certo
tempo: si preoccuper in modo automatico del ripristino della memorizzazione dei
dati qualora il sensore divenisse nuovamente disponibile. La sottoscrizione persiste
fino al momento in cui non la rimuoviamo in modo esplicito richiamando uno dei
seguenti due metodi, i quali fanno riferimento sempre al caso in cui in fase di
registrazione sia stato utilizzato un Data Source o un Data Type:

public abstract PendingResult<Status> unsubscribe (GoogleApiClient client,

DataSource dataSource)
public abstract PendingResult<Status> unsubscribe (GoogleApiClient client,

DataType dataType)

In realt in questo caso esiste un terzo metodo, che prevede come parametro un
oggetto di tipo Subscription, creato in fase di sottoscrizione o di ricerca che ne
incapsula le informazioni, ovvero Data Type o Data Source.

public abstract PendingResult<Status> unsubscribe (GoogleApiClient client,

Subscription subscription)

Il fatto che la sottoscrizione sia persistente rende necessari altri metodi, che ci
permettono di verificare quali siano, in ogni momento, le sottoscrizioni attive. In
questo caso i due overloading permettono di specificare o meno il particolare Data
come filtro delle sottoscrizioni da restituire.

Type


public abstract PendingResult<ListSubscriptionsResult>

listSubscriptions (GoogleApiClient client, DataType dataType)


public abstract PendingResult<ListSubscriptionsResult>

listSubscriptions (GoogleApiClient client)


In entrambi i casi interessante notare come si ottenga un oggetto di tipo


ListSubscriptionsResult, in grado di fornire in modo asincrono le informazioni relative
alle sottoscrizioni cui possiamo accedere attraverso uno dei seguenti metodi:

public List<Subscription> getSubscriptions()
public List<Subscription> getSubscriptions(DataType dataType)


A questo punto non ci resta che utilizzare queste API nella nostra applicazione di
test. A tale proposito abbiamo creato un nuovo Fragment descritto dalla classe
, nella quale visualizziamo lelenco delle sottoscrizioni attive e diamo la

SessionFragment

possibilit di crearne di nuove e di eliminarle. Osservando il codice della classe


SessionFragment notiamo come sia stato creato un Adapter che gestisce oggetti di tipo
che incapsulano le informazioni relative agli insiemi di dati che

Subscription

intendiamo memorizzare. A questo proposito bene sottolineare come una


Subscription contenga solamente le informazioni utilizzate in fase di creazione. Questo
significa che se stato utilizzato uno specifico Data Type, linformazione relativa alla
non sar disponibile e viceversa. La nostra applicazione volutamente

Data Source

semplice e permette di vedere come elencare le sottoscrizioni presenti, come crearne


di nuove e come eliminarle.
NOTA
Al momento questa applicazione permette di creare una sola sottoscrizione, ma il passaggio
verso la possibilit di crearne di diverse breve; lo lasciamo al lettore come esercizio.

Allavvio della nostra applicazione, dopo la creazione dei componenti di rito,


richiamiamo il nostro metodo di utilit listSubscriptions(), che visualizza le
sottoscrizioni attive.

private void listSubscriptions() {

// We check the presence of the Activity


final MainActivity mainActivity = mActivityRef.get();
if (mainActivity == null) {
Log.w(TAG_LOG, Lost MainActivity reference!!);
return;
}

Fitness.RecordingApi
.listSubscriptions(mainActivity.getGoogleApiClient(),
DataType.TYPE_LOCATION_SAMPLE)
.setResultCallback(new
ResultCallback<ListSubscriptionsResult>(){
@Override
public void onResult(ListSubscriptionsResult
listSubscriptionsResult) {
// we update the model with the new subscriptions list
mModel.clear();
mModel.addAll(listSubscriptionsResult
.getSubscriptions());
mAdapter.notifyDataSetChanged();
}
});
}


Notiamo come la modalit di accesso alle informazioni sia quella ormai consueta e
come le informazioni ottenute siano state utilizzate per la visualizzazione dei dati
allinterno della lista. Al primo avvio lapplicazione non visualizzer quindi alcun
elemento. Selezioniamo lopzione di menu che abbiamo chiamato New Subscription per
lanciare il seguente metodo:

private void createSubscription() {

final MainActivity mainActivity = mActivityRef.get();


if (mainActivity == null) {
Log.w(TAG_LOG, Lost MainActivity reference!!);
return;
}
// We create a subscription for the Location data

Fitness.RecordingApi.subscribe(mainActivity.getGoogleApiClient(),
DataType.TYPE_LOCATION_SAMPLE)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
final int statusCode = status.getStatusCode();
if (statusCode == FitnessStatusCodes.SUCCESS_ALREADY_SUBSCRIBED) {
// The subscription was already present
showToast(R.string.subscription_already_present);
} else {
// Subscription created with success
showToast(R.string.subscription_created_successfully);
// We update the data in the list
listSubscriptions();
}
} else {
// Subscription creation failed
showToast(R.string.subscription_creation_error);
}
}
});
}


Nel nostro esempio abbiamo creato una sottoscrizione alle informazioni di
location, che da questo momento vengono tracciate in modo automatico dal sistema e
quindi inviate al Google Fitness Store. In questo caso notiamo come loperazione
possa essere eseguita con successo, anche se con un codice, associato alla costante
FitnessStatusCodes.SUCCESS_ALREADY_SUBSCRIBED, che ci permette di sapere come la stessa
sottoscrizione fosse gi stata creata in precedenza. Nel caso di una sottoscrizione

creata con successo, non facciamo altro che riaggiornare lelenco delle sottoscrizioni
attive. Per eliminare la sottoscrizione abbiamo creato una voce del menu contestuale,
che non fa altro che richiamare il seguente metodo, che risulta a questo punto di
semplice comprensione:

private void removeSubscription(final int subcriptionPos) {

final MainActivity mainActivity = mActivityRef.get();


if (mainActivity == null) {
Log.w(TAG_LOG, Lost MainActivity reference!!);
return;
}
// We get the Subscription to remove
final Subscription toRemove = mModel.get(subcriptionPos);
Fitness.RecordingApi.unsubscribe(mainActivity.getGoogleApiClient(),
toRemove)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
showToast(R.string.subscription_removed_successfully);
// We update the list
listSubscriptions();
} else {
showToast(R.string.subscription_removed_error);
}
}
});
}

Attraverso le API descritte in precedenza abbiamo visto come salvare le


informazioni ottenute dai diversi Data Source nel Google Fitness Store che poi
consulteremo per la fase di visualizzazione. Ma prima ci occupiamo delle Session, che
permettono di contestualizzare nel tempo i dati raccolti. I dati di questa tipologia di
applicazioni fanno infatti riferimento a un periodo durante il quale lutente inizia
unattivit fisica. Esiste quindi un istante iniziale e uno finale e i dati raccolti devono
essere studiati nel loro insieme. A tale proposito le API a nostra disposizione ci
permettono di eseguire alcune operazioni fondamentali, ovvero di definire quando
inizia e quando finisce una Session. In questo caso loggetto con cui avremo a che fare
sar quello identificato da Fitness.SessionsApi, il quale definisce due metodi:

public abstract PendingResult<Status>

startSession (GoogleApiClient client, Session session)


public abstract PendingResult<SessionStopResult>

stopSession (GoogleApiClient client, String identifier)



Come possiamo notare, il metodo per lavvio di una sessione ha come secondo
parametro un oggetto di tipo Session, creato attraverso la corrispondente classe Builder.
In particolare possibile specificare alcune informazioni, tra cui un nome, un
identificatore, una descrizione insieme a un istante di inizio e, se disponibile, di fine.
Se inseriamo solamente listante di inizio, la sessione ancora in corso.
NOTA
La data di fine sessione potrebbe non avere senso nel momento di creazione. La cosa risulta
per chiara se pensiamo, come vedremo successivamente, che una sessione pu essere creata
anche in una fase successiva e quindi utilizzata per raggruppare dati esistenti.

Insieme a quelle descritte in precedenza, la classe Session.Builder ci mette a


disposizione il seguente metodo, il quale ci permette di risolvere un problema
abbastanza comune in questo tipo di applicazioni e che riguarda leffettiva attivit di
un utente.

public Session.Builder setActivity(String activity)


Supponiamo infatti di iniziare una corsa e di avviare una sessione. Immaginiamo di
fare attivit fisica per unora, durante la quale la corsa ha occupato solamente 40
minuti. In questo caso non possiamo dire di aver corso per unora, anche se la

sessione ha quella durata; serve un meccanismo che ci permetta di indicare che cosa
stavano facendo durante quel tempo. Questo il significato dellinformazione che
impostiamo attraverso il precedente metodo, il cui parametro uno dei possibili
valori rappresentati da altrettante costanti della classe FitnessActivities. I valori che
possiamo utilizzare sono moltissimi e molto dettagliati. Nel caso della bicicletta, per
esempio, possiamo utilizzare uno dei seguenti:

FitnessActivities.BIKING
FitnessActivities.BIKING_HAND
FitnessActivities.BIKING_MOUNTAIN
FitnessActivities.BIKING_ROAD
FitnessActivities.BIKING_SPINNING
FitnessActivities.BIKING_STATIONARY
FitnessActivities.BIKING_UTILITY


Ecco altri possibili valori:

FitnessActivities.BOXING
FitnessActivities.DANCING
FitnessActivities.MARTIAL_ARTS


Si tratta di informazioni che ritroveremo come dato di tipo

DataType.TYPE_ACTIVITY_SEGMENT


allinterno del Google Fitness Store. Questa informazione sar utile per capire ci
che lutente ha svolto nel corso di una sessione. A tale proposito abbiamo aggiunto
altre due opzioni per la creazione e lo stop della sessione. Nel primo caso il metodo
da eseguire il seguente:

private void startSession() {

final MainActivity mainActivity = mActivityRef.get();


if (mainActivity == null) {
Log.w(TAG_LOG, Lost MainActivity reference!!);
return;
}
// We create the Session object

Session session = new Session.Builder()


.setName(SESSION_NAME)
.setIdentifier(SESSION_ID)
.setDescription(A testing session)
.setStartTime(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setActivity(FitnessActivities.WALKING)
.build();
// We use the SessionApi to create the session
Fitness.SessionsApi.startSession(mainActivity.getGoogleApiClient(),
session)
.setResultCallback(new ResultCallback<Status>() {
@Override
public void onResult(Status status) {
if (status.isSuccess()) {
showToast(R.string.session_start_success);
} else {
showToast(R.string.session_start_failed);
}
}
});
}


Il metodo si compone di due parti. Nella prima viene creato un oggetto di tipo
Session attraverso il corrispondente Builder, mentre nella seconda questo oggetto viene
utilizzato per leffettivo avvio. Notiamo come le informazioni utilizzare per la
creazione della sessione siano quelle descritte in precedenza, ovvero un nome, un
identificatore, una descrizione, ma soprattutto un istante di inizio e uninformazione
sul tipo di attivit a cui la sessione fa riferimento. Attraverso il metodo startSession()
delloggetto Fitness.SessionsApi la sessione viene quindi avviata. Per interrompere la
sessione non dobbiamo fare altro che eseguire il seguente codice, nel quale notiamo

come sia stato utilizzato lo stesso identificatore di sessione, che quindi


linformazione da tenere in considerazione:

private void stopSession() {

final MainActivity mainActivity = mActivityRef.get();


if (mainActivity == null) {
Log.w(TAG_LOG, Lost MainActivity reference!!);
return;
}
// We stop the session using the identifier
Fitness.SessionsApi.stopSession(mainActivity.getGoogleApiClient(),
SESSION_ID)
.setResultCallback(new ResultCallback<SessionStopResult>() {
@Override
public void onResult(SessionStopResult sessionStopResult) {
final Status status = sessionStopResult.getStatus();
if (status.isSuccess()) {
showToast(R.string.session_stop_success);
} else {
showToast(R.string.session_stop_failed);
}
}
});
}


Come accennato in precedenza, le Session API ci permettono anche di inserire dei
dati che potremmo aver raccolto direttamente da un sensore in una fase precedente o
attraverso altri meccanismi. In questo caso, che non testeremo nella nostra
applicazione, il procedimento consiste nel creare la sessione con la stessa modalit
utilizzata in precedenza e poi nel creare un oggetto di tipo SessionInsertRequest dotato

di apposito Builder. Si tratta di un oggetto che permette di associare un oggetto Session


a un altro di tipo DataSet che abbiamo gi visto allinizio del capitolo e che contiene i
dati da inserire nel Google Fitness Store. Vista la ripetitivit delle operazioni,
rimandiamo il lettore alla documentazione ufficiale.

Accesso alle informazioni registrate


Nei paragrafi precedenti abbiamo visto come gestire la persistenza delle
informazioni raccolte dai vari sensori nel Google Fitness Store, ma ci siamo dovuti in
un certo senso fidare in quanto non abbiamo visto alcun dato. Le Google Fit API ci
permettono di accedere alle informazioni raccolte secondo diverse modalit. La pi
semplice quella che permette di visualizzare le informazioni raccolte in unaltra
applicazione, come quella Google Fit ufficiale di Google facilmente individuabile
dallicona in Figura 9.6.

Figura 9.6 Icona caratteristica di Google Fit.

Per visualizzare le informazioni della nostra applicazione allinterno di unaltra


compatibile sufficiente utilizzare la classe SessionsApi.ViewIntentBuilder, la quale ci
permette di creare lIntent da lanciare. Si tratta in realt di una classe molto semplice,
che permette di specificare un identificatore di sessione e il package delleventuale
applicazione preferita. Nel nostro caso abbiamo associato il metodo showSessionData()
a unaltra voce nel menu delle opzioni, che si chiama "Show Session Data". Loggetto
necessita di una Session e non del corrispondente

SessionsApi.ViewIntentBuilder

identificatore, per cui nella prima parte abbiamo utilizzato anche il seguente metodo
della classe SessionApi, la quale prevede di creare un SessionReadRequest che ci permette
di impostare i criteri di ricerca della Session.

public abstract PendingResult<SessionReadResult>

readSession (GoogleApiClient client, SessionReadRequest request)


Nel nostro caso abbiamo fatto in modo di ricercare tutte le sessioni create
nellultima giornata attraverso il seguente codice:

// The time interval
final long endTime = System.currentTimeMillis();
final long startTime = endTime ONE_DAY;
// We create the request
final SessionReadRequest sessionReadRequest =

new SessionReadRequest.Builder()
.setSessionId(SESSION_ID)
.setSessionName(SESSION_NAME)
.setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS)
.read(DataType.TYPE_LOCATION_SAMPLE)
.build();

Il passo successivo consiste nellutilizzare loggetto Fitness.SessionsApi per laccesso
alle corrispondenti sessioni attraverso il seguente codice:

Fitness.SessionsApi.readSession(mainActivity.getGoogleApiClient(),

sessionReadRequest)
.setResultCallback(new ResultCallback<SessionReadResult>() {
@Override
public void onResult(SessionReadResult sessionReadResult) {
final Status status = sessionReadResult.getStatus();
if (status.isSuccess()) {
// We get the session object
final List<Session> sessions =
sessionReadResult.getSessions();
if (sessions != null && sessions.size() > 0) {
final Session session = sessions.get(0);
Intent intent = new SessionsApi

.ViewIntentBuilder(mainActivity)
.setSession(session)
.build();
startActivity(intent);
}
}
}
});


Nelle parti di codice evidenziate notiamo innanzitutto lutilizzo del metodo
readSession() e lutilizzo delle informazioni ottenute come parametro delloggetto di
tipo SessionApi.ViewIntentBuilder che ci permette di ottenere lIntent da lanciare per
visualizzare le informazioni nelle applicazioni compatibili con Google Fit.
Attraverso questo oggetto possiamo visualizzare i nostri dati in unaltra
applicazione. quindi lecito chiedersi come fare in modo che altre applicazioni
visualizzino le proprie informazioni allinterno della nostra. Per farlo sar sufficiente
associare alla nostra Activity un IntentFilter del seguente tipo:

<activity android:name=".MyShowDataActivity" android:exported="true">

<intent-filter>
<action android:name=vnd.google.fitness.VIEW />
<data android:mimeType=vnd.google.fitness.session/boxing />
<data android:mimeType=vnd.google.fitness.session/walking />
</intent-filter>
</activity>


importante che non venga utilizzata alcuna category; la action dovr essere di
tipo VIEW, mentre sar necessario specificare il tipo di dati supportati attraverso
altrettanti mime type, come nel frammento di codice riportato sopra.
Sempre in relazione allinteroperabilit con le altre applicazioni interessante
sapere che ogni applicazione che avvia una Session lancia un intent di Broadcast che

altre applicazioni possono intercettare per poter utilizzare le stesse informazioni.


Questo Intent caratterizzato da una action che corrisponde al valore

com.google.android.gms.fitness.session_start


e da un mime type che corrisponde al tipo di attivit associato. La Session creata
disponibile attraverso un opportuno EXTRA. Qualora la nostra applicazione fosse
interessata a gestire questo evento sar sufficiente registrare un BroadcastReceiver nel
seguente modo:

<receiver android:name=".ManageStartSessionBroadcastReceiver" >

<intent-filter>
<action android:name=com.google.android.gms.fitness.session_start />
<data android:mimeType=vnd.google.fitness.activity_type/walking />
</intent-filter>
</receiver>


Lo stesso vale per levento di fine sessione per il quale lIntent caratterizzato dal
seguente valore di action:

com.google.android.gms.fitness.session_end


A questo punto ci manca una parte fondamentale, che riguarda il modo in cui la
nostra applicazione accede e utilizza i propri dati attraverso le History API, le quali
sono accessibili tramite loggetto Fitness.HistoryApi attraverso il quale possiamo
eseguire le operazioni di lettura dei dati inseriti e di inserimento in batch di
informazioni eventualmente ottenute da altre applicazioni. Per quanto riguarda la
lettura dei dati, il meccanismo ormai il solito: creare una richiesta che, in questo
caso, rappresentata da un oggetto di tipo DataReadRequest e poi inviare tale richiesta
attraverso un opportuno metodo della classe Fitness.HistoryApi. Nella nostra
applicazione abbiamo implementato unaltra voce di menu che ci permette di
accedere alle informazioni inserite e di visualizzarle attraverso un messaggio di Log.
In particolare vogliamo visualizzare le informazioni che abbiamo raccolto nellultima

settimana. Nella prima parte del metodo dumpHistoryData() abbiamo definito lintervallo
di riferimento:

// We want to define the interval of the last month
final Calendar nowCal = Calendar.getInstance();
final Date nowDate = nowCal.getTime();
long endTime = nowCal.getTimeInMillis();
nowCal.add(Calendar.WEEK_OF_YEAR, -1);
long startTime = nowCal.getTimeInMillis();
Log.i(TAG_LOG, "Start time: " + DATE_FORMAT.format(nowDate));
Log.i(TAG_LOG, "End time: " + DATE_FORMAT.format(nowCal.getTime()));


Abbiamo poi utilizzato tale intervallo per creare la nostra richiesta attraverso il
seguente codice:

DataReadRequest readRequest = new DataReadRequest.Builder()

.read(DataType.TYPE_LOCATION_SAMPLE)
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.enableServerQueries()
.setLimit(5)
.build();

Come abbiamo detto, si tratta di un oggetto di tipo DataReadRequest, che viene creato
attraverso il solito Builder. Tramite il metodo read() impostiamo le informazioni che
intendiamo leggere. Nel nostro caso abbiamo utilizzato il seguente overload, che
prevede come parametro lidentificatore del tipo di dato:

public DataReadRequest.Builder read(DataType dataType)


Ma avremmo potuto utilizzare anche questaltro, che prevede invece il tipo di Data
:

Source


public DataReadRequest.Builder read(DataSource dataSource)

Possiamo richiamare questo metodo anche pi volte in chaining, aggiungendo tutti


i tipi di informazioni che intendiamo ottenere. Attraverso il metodo setTimeRange()
dobbiamo specificare lintervallo di riferimento che, nel nostro caso, lultima
settimana. Attraverso il seguente metodo facciamo in modo che il sistema richiami il
server nel caso in cui le informazioni nello store locale non fossero sufficienti a
soddisfare la nostra richiesta:

public DataReadRequest.Builder enableServerQueries()


Infine utilizziamo il seguente metodo per limitare a 5 il numero di risultati:

public DataReadRequest.Builder setLimit(int limit)


Il passo successivo quello dellinvio della richiesta attraverso il seguente metodo
della classe Fitness.HistoryApi:

public abstract PendingResult<DataReadResult> readData(GoogleApiClient client,

DataReadRequest request)

Lo gestiamo nella maniera ormai consueta attraverso le seguenti righe di codice:

Fitness.HistoryApi.readData(mainActivity.getGoogleApiClient(),

readRequest)
.setResultCallback(new ResultCallback<DataReadResult>() {

@Override
public void onResult(DataReadResult dataReadResult) {
final Status status = dataReadResult.getStatus();
if (status.isSuccess()) {
final List<DataSet> dataSets =
dataReadResult.getDataSets();
for (DataSet data : dataSets) {

log(data);
}
} else {
Log.e(TAG_LOG, Error reading DataSet);
showToast(R.string.data_set_error);
}
}
});


Qualora la richiesta abbia successo otterremo un oggetto di tipo DataReadResult a cui
chiederemo linsieme di DataSet attraverso il metodo getDataSets() evidenziato nel
precedente codice. A questo punto non facciamo altro che iterare su tutti i DataSet
ottenuti e richiamare per ognuno di essi il seguente metodo di log:

private void log(final DataSet dataSet) {

for (DataPoint dp : dataSet.getDataPoints()) {


Log.i(TAG_LOG, Data point:);
Log.i(TAG_LOG, \tType: + dp.getDataType().getName());
Log.i(TAG_LOG, \tStart: +
DATE_FORMAT.format(dp.getStartTime(TimeUnit.MILLISECONDS)));
Log.i(TAG_LOG, \tEnd: +
DATE_FORMAT.format(dp.getEndTime(TimeUnit.MILLISECONDS)));
for (Field field : dp.getDataType().getFields()) {
Log.i(TAG_LOG, \tField: + field.getName() +
Value: + dp.getValue(field));
}
}
}

Lanciando lapplicazione otterremo quindi una serie di informazioni del seguente


tipo:

Data point:
Type: com.google.location.sample
Start: 11 11 2014 19:50:017
End: 11 11 2014 19:50:017
Field: latitude Value: 51.452595
Field: longitude Value: -0.2130989
Field: accuracy Value: 20.0
Field: altitude Value: unset


Un aspetto interessante di queste API riguarda la possibilit di aggregare i dati. Per
fare questo si utilizza una serie di metodi aggregate() in fase di creazione della
, i quali permettono di specificare il tipo di informazione e la modalit

DataReadRequest

di aggregazione. possibile aggregare a seconda del Data Type attraverso il seguente


metodo:

public DataReadRequest.Builder aggregate(DataType inputDataType,

DataType outputDataType)

Oppure attraverso la Data Source:

public DataReadRequest.Builder aggregate (DataSource dataSource,

DataType outputDataType)

Oltre a questo possibile specificare linformazione che equivale al Group By di
una normale query SQL. Per questo esistono alcuni metodi bucketByXXX() i quali
permettono laggregazione per sessione:

public DataReadRequest.Builder bucketBySession(int minDuration,

TimeUnit timeUnit)

Oppure per tipo di attivit:

public DataReadRequest.Builder bucketByActivityType (int minDuration, TimeUnit

timeUnit, DataSource activityDataSource)



O pi semplicemente per intervalli temporali, attraverso il seguente metodo:

public DataReadRequest.Builder bucketByTime(int duration, TimeUnit timeUnit)


Le History API ci permettono poi di accedere in modo piuttosto semplice sia ai
dati puntuali sia a quelli aggregati. Oltre al metodo che permette la lettura delle
informazioni, loggetto Fitness.HistoryApi ci permette anche di inserire o cancellare
informazioni attraverso i metodi:

public abstract PendingResult<Status> insertData (GoogleApiClient client,

DataSet dataSet)
public abstract PendingResult<Status> deleteData (GoogleApiClient client,

DataDeleteRequest request)

Per quanto riguarda linserimento, il procedimento sempre molto semplice e
consiste nel creare un oggetto di tipo DataSource attraverso il corrispondente Builder, il
quale permette di specificare il tipo di dati che intendiamo inserire e a quale
intervallo temporale fa riferimento. Loggetto DataSource creato ci permette di creare
un oggetto di tipo DataSet attraverso un suo metodo di factory. Loggetto DataSet si
utilizza a sua volta per la creazione di oggetti di tipo DataPoint, che contengono i dati
veri e propri. Una volta creato il DataSet si utilizza nella modalit ormai nota il metodo
riportato sopra. Per descrivere con qualche riga di codice quanto detto

insertData()

potremmo definire un DataSource attraverso le seguenti istruzioni, che nel nostro caso
descrivono linformazione relativa al peso:

DataSource dataSource = new DataSource.Builder()

.setAppPackageName(this)
.setDataType(DataType.TYPE_WEIGHT)
.setName(My Weight)
.setType(DataSource.TYPE_RAW)

.build();

Notiamo come vi sia un nome, un tipo e lindicazione di come linformazione sar
poi rappresentata da un unico valore. Dalla DataSource poi possibile ottenere un
attraverso il metodo di factory create():

DataSet


DataSet dataSet = DataSet.create(dataSource);


Il passo successivo consiste nellinserire i dati veri e propri attraverso degli oggetti
di tipo DataPoint:

final DataPoint newDataPoint = dataSet.createDataPoint();
newData.setTimeInterval(start, end, TimeUnit.MILLISECONDS)

.setIntValues(myWeight);
dataSet.add(newDataPoint);


Una volta inserito un certo numero di DataPoint nelloggetto DataSet possiamo
richiamare il metodo insertData() nel seguente modo:

Fitness.HistoryApi.insertData(mGoogleApiClient, dataSet)


Poi possiamo gestire il successo o ol fallimento delloperazione attraverso lormai
classica implementazione dellinterfaccia di callback PendingResult<Status>.
Concludiamo largomento con loperazione che permette di cancellare le
informazioni dal Google Fitness Store. Anche in questo caso dobbiamo prima creare
una richiesta, che questa volta rappresentata da un oggetto di tipo DataDeleteRequest e
poi utilizzare il corrispondente metodo della classe Fitness.HistoryApi. Per cancellare i
dati a cui abbiamo acceduto in precedenza dovremmo quindi creare la seguente
richiesta:

DataDeleteRequest request = new DataDeleteRequest.Builder()

.setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS)

.addDataType(DataType.TYPE_LOCATION_SAMPLE)
.build();

E poi richiamare il seguente metodo e gestire il tutto nel modo solito:

Fitness.HistoryApi.deleteData(mGoogleApiClient, request)

Conclusioni
In questo capitolo abbiamo esaminato una delle funzionalit annunciate al Google
I/O del 2014 e che, inizialmente parte di Android 5.0 Lollipop, sono state da poco
accorpate ai Google Play services 6.5 ovvero le Google Fitness API. Come abbiamo
visto si tratta di una piattaforma che permette di raccogliere una serie di dati in un
repository comune, chiamato Google Fitness Store al fine di eseguire su questi delle
statistiche. Si tratta di informazioni relative allattivit fisica degli utenti, per i quali
la privacy assume unimportanza notevole. Per questo motivo, nella prima parte del
capitolo, dopo una descrizione dei concetti fondamentali, ci siamo occupati del
processo di autorizzazione. Abbiamo poi visto come gestire i sensori attraverso le
Sensor API oppure attraverso il meccanismo di scanning offerto dalle API per la
gestione dei dispositivi BLE. Abbiamo visto come utilizzare le Recording API per il
salvataggio delle informazioni raccolte in una sessione o singolarmente. Ci siamo poi
dedicati allutilizzo delle History API, per laccesso ai dati attraverso la nostra
applicazione o altre applicazioni in grado di comprendere la stessa base dati. Sebbene
si tratti di un argomento che meriterebbe molto pi spazio, in questo capitolo
abbiamo visto gli strumenti che permettono di realizzare applicazioni interessanti.
Come ultima annotazione bene dire che si tratta di API in continua modifica.
Alcune delle classi che abbiamo utilizzato non sono infatti documentate, mentre altre
che vengono nominate nella documentazione ufficiale non esistono pi. La stessa
applicazione ufficiale di Google Fit ha qualche problema, che speriamo venga risolto
in breve tempo.

Capitolo 10

Google Game

Nel precedente capitolo abbiamo visto come Google metta a disposizione una vera
e propria piattaforma per la raccolta, memorizzazione e consultazione di informazioni
relative al nostro stato di forma. In questo capitolo ci occupiamo di un altro ambiente,
dedicato questa volta ai giochi. Anche in questo caso si tratta di API in continua
evoluzione, per cui non faremo altro che realizzare unapplicazione, inizialmente
vuota, che arricchiremo di volta in volta con le funzionalit che la piattaforma
Google Game ci mette a disposizione. bene sottolineare fin da subito che non ci
concentreremo sul gioco in s (argomento che richiederebbe un libro intero), ma
solamente sugli aspetti puramente legati a Google Game.

Creazione del progetto e configurazione


Il primo passo consiste nella creazione di un progetto che abbiamo chiamato MyGame
e che inizialmente, come detto, sar vuoto. Per fare questo utilizziamo la relativa
opzione di Android Studio, con lunica accortezza di scegliere (Figura 10.1) per
quello che riguarda la prima Activity lopzione che prevede linizializzazione di una
classe che contiene gi la logica di inizializzazione dei Google Play services e quindi
delle funzionalit legate alla gestione dei giochi.

Figura 10.1 Inizializzazione automatica dei Google Play services per Google Game.

La classe creata, di nome MainActivity, non contiene un layout, che quindi


impostiamo attraverso la corrispondente risorsa main_layout.xml. La parte interessante
riguarda comunque linizializzazione delloggetto GoogleApiClient, che avviene nel
metodo di callback onStart() attraverso le seguenti righe di codice, che ci sono ormai
pi che famigliari.

protected void onStart() {

super.onStart();
if (mGoogleApiClient == null) {
mGoogleApiClient = new GoogleApiClient.Builder(this)

.addApi(Games.API)
.addApi(Plus.API)
.addScope(Games.SCOPE_GAMES)
.addScope(Plus.SCOPE_PLUS_LOGIN)

.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
mGoogleApiClient.connect();
}


NOTA
Anche in questo caso ci dobbiamo ricordare di definire la dipendenza nel file di configurazione
gradle utilizzando la libreria identificata da com.google.android.gms:play-services-games:6.5.87.

Attraverso il solito Builder abbiamo richiesto lutilizzo delle Game API e,


inizialmente, laccesso alle funzionalit previste dallo scope associato alla costante
Games.SCOPE_GAMES. importante sottolineare come sia comunque necessario gestire il
login degli utenti; solo quelli loggati possono infatti utilizzare questi servizi. Prima
di procedere con le operazioni di registrazione dellapplicazione nella Play Console
di Google ricordiamo di aggiungere al nostro progetto una libreria che fornisce
alcune classi di utilit che useremo successivamente. Non dobbiamo fare altro che
andare nel seguente repository e scaricare la libreria BaseGameUtils.
NOTA
Chi non ha esperienza con il tool git pu anche semplicemente scaricare lo zip del progetto e
copiare la cartella libraries nel nostro, ricordando di aggiungere la libreria al file di
configurazione settings.gradle.

A questo punto siamo pronti per creare il progetto nella console di Google; si tratta
della stessa console che si utilizza per la pubblicazione delle applicazioni sul Play
Store. Selezioniamo licona con il joystick sulla sinistra, come evidenziato nella
Figura 10.2.

Figura 10.2 Iniziamo la configurazione delle Google Game per il nostro gioco.

Dopo aver accettato una serie di contratti con Google, selezioniamo il pulsante
Configura servizi di giochi di Google Play, il quale ci porta alla schermata rappresentata
nella Figura 10.3, nella quale inseriamo il nome del nostro gioco e la categoria di
appartenenza.
In questa fase importante selezionare il tab rappresentato in figura, relativo al
fatto che al momento non si sta utilizzando alcuna API. Selezionando il pulsante
Continua si arriva a una nuova schermata con diverse opzioni. La prima di queste ci
permette di inserire tutte le meta-informazioni relative al nostro gioco. Nel nostro
caso non arriveremo a pubblicare lapplicazione, per cui avremo bisogno solamente
dellinformazione relativa al nome.
NOTA
bene sottolineare come la documentazione ufficiale metta a disposizione degli asset (immagini
e icone) che possono essere utilizzate in fase di studio.

Figura 10.3 Inizializzazione del gioco nella Play Console.

Un aspetto forse non ovvio riguarda il fatto che quello che abbiamo creato
attraverso la console non riguarda la sola applicazione Android che realizzeremo, ma
anche altre applicazioni realizzate con altre tecnologie, come iOS, Web o con altra
piattaforma. Il passo successivo consiste infatti nel collegare la nostra specifica
applicazione Android a quella generale creata attraverso la console. Selezioniamo
quindi la voce Applicazioni collegate nel menu a sinistra, ottenendo uninterfaccia
nella quale selezioniamo licona relativa ad Android, arrivando allinterfaccia
rappresentata nella Figura 10.4, la quale contiene gi delle impostazioni molto
importanti.

Figura 10.4 Collegamento dellapplicazione Android al gioco appena creato.

Innanzitutto notiamo come sia necessario impostare il nome del package


dellapplicazione insieme ad altre impostazioni che riguardano la modalit di gioco
multiplayer oltre a un accorgimento antipirateria. Le impostazioni relative al
multiplayer ci permettono di specificare se si tratta di unapplicazione che permette il
gioco a turno; il caso, per esempio, di una partita di dama o scacchi. La seconda
riguarda invece la possibilit di giocare in tempo reale, come potrebbe essere il caso
di un arcade o sparatutto, nel quale si pu giocare insieme. Lopzione antipirateria ci
permette di implementare un meccanismo che permette di accertarci che chi utilizza
il gioco lo abbia effettivamente acquistato o comunque disponga della licenza per
lutilizzo. In caso contrario, ogni utilizzo dei servizi di Google porta a generare un
errore. bene sottolineare come si tratti comunque di un controllo che viene fatto
solo qualora lapplicazione sia stata scaricata dal Play Store ed effettivo solamente
per le applicazioni Android (non iOS o Web) anche non a pagamento. Nel nostro
caso decidiamo di abilitare questa funzione, portando il corrispondente pulsante nello
stato Attivo.
Una volta salvate le impostazioni, la console ci porta a una schermata nella quale
ci viene richiesto di autenticare la nostra applicazione attraverso la generazione delle
opportune chiavi, come possiamo vedere nella Figura 10.5. Non facciamo altro che
selezionare il pulsante Autorizza la tua applicazione ora per arrivare alla schermata

rappresentata nella Figura 10.6, nella quale inseriamo alcune informazioni relative
alla nostra applicazione; si tratta di quello che lutente vedr in corrispondenza del
nostro gioco. Selezionando il pulsante Continua arriviamo finalmente a una schermata
che abbiamo visto anche in precedenza, nella quale inseriremo il package
dellapplicazione e la firma SHA1 ottenuta nel modo descritto nei capitoli precedenti.

Figura 10.5 Ci viene richiesto di autorizzare lapplicazione.

Figura 10.6 Le informazioni di branding del nostro gioco.

Per ottenere la firma SHA1 dobbiamo eseguire il comando keytool, utilizzando il


certificato che utilizziamo per lo sviluppo oppure per lapplicazione in produzione a
seconda che si voglia fare dei test oppure pubblicare effettivamente lapplicazione.
Nel caso del certificato di debug dovremo eseguire il seguente comando:

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


e otterremo la firma SHA1, che andiamo a inserire nel corrispondente campo come
indicato nella Figura 10.7.

Figura 10.7 Inseriamo la firma SHA1 e il package dellapplicazione associata.

Terminate queste operazioni, non ci resta che selezionare il pulsante Create client,
che ci porta alla schermata finale contenente un codice che dovremmo annotare,
perch sar quello che utilizzeremo nel codice della nostra applicazione per la sua
identificazione. Il codice, che abbiamo parzialmente coperto per ovvie ragioni,
quello evidenziato nella Figura 10.8.

Figura 10.8 Abbiamo ottenuto lID associato alla nostra applicazione Android.

Selezionando nuovamente la voce del menu a sinistra relativa alle applicazioni


collegate notiamo, nella Figura 10.9, come ora tutte le informazioni necessarie alla
pubblicazione siano attive.

Figura 10.9 Il gioco pronto alla pubblicazione.

La nostra applicazione, in questo caso, sarebbe pronta alla pubblicazione, ma al


momento completamente vuota. Non ci resta che metterci al lavoro descrivendo i
concetti fondamentali delle Game API e come queste si possano utilizzare e
implementare nella nostra applicazione.

Gestione login e obiettivi


Al momento della creazione dellapplicazione abbiamo deciso di utilizzare il
corrispondente wizard di Android Studio, il quale ha creato in modo automatico il
codice di inizializzazione delloggetto di tipo GoogleApiClient. Per la gestione di
tutte le funzionalit che andremo a descrivere questo non sufficiente, in quanto si
richiede anche che il giocatore abbia eseguito il login. Abbiamo gi visto la gestione
del login con Google+ nel Capitolo 5, per cui invitiamo il lettore a consultare il
codice della nostra classe MainActivity con la relativa logica di login. Nei nostri
esempi, per semplicit, faremo in modo di eseguire il login automaticamente
allavvio dellapplicazione. Prima di proseguire, dobbiamo comunque fare alcune
configurazioni, ovvero inserire le informazioni relative allId del gioco che abbiamo
creato in precedenza. Creiamo quindi inizialmente un file che chiamiamo ids.xml nelle
risorse di tipo values e inseriamo un valore per lid della nostra applicazione:

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

<string name=app_id>############</string>
</resources>


Nello stesso file inseriremo anche altre informazioni di configurazione. Per il
momento definiamo solamente la risorsa di nome app_id che utilizzeremo nel file di
configurazione AndroidManifest.xml attraverso il seguente elemento nellelemento
principale di nome <application/>.

<meta-data android:name="com.google.android.gms.games.APP_ID"

android:value=@string/app_id />

Con queste configurazioni siamo pronti ad avviare la nostra applicazione, che dopo
la visualizzazione di una schermata splash relativa a Google Play ci offre la
possibilit di scegliere uno degli account del nostro telefono o di crearne uno nuovo,
come possiamo vedere nella Figura 10.10.

Figura 10.10 Selezione dellaccount per il gioco.

Una volta selezionato laccount si ha la visualizzazione della schermata che


permette di dare il proprio consenso allutilizzo di queste API. Dato questo consenso
abbiamo finalmente eseguito il login e possiamo iniziare i nostri esperimenti.
Fatto questo ci occupiamo degli achievement, che in italiano sono stati tradotti
come obiettivi. Sappiamo che lobiettivo della maggior parte dei giochi quello di
vincere contro un altro giocatore, contro il computer oppure semplicemente cercare
di battere se stessi superando un record. Le Gaming API ci permettono di specificare
una serie di obiettivi e di implementare una logica che permetta a ciascun giocatore
di raggiungerli. A seconda del tipo di gioco, ogni obiettivo pu sbloccare nuove
funzionalit o modalit di gioco. Creeremo questi obiettivi attraverso la console di
Google Play; ciascuno di essi sar caratterizzato da alcune propriet, che vediamo
subito. La pi importante un ID che viene generato automaticamente dalla console
e che identifica in modo univoco lobiettivo, il quale dispone anche di un nome e di
una descrizione. Per la visualizzazione, a ognuno di essi pu essere associata

unimmagine. Unultima importante informazione rappresentata da un numero, che


indica lordine che il particolare obiettivo ha rispetto agli altri. Ogni obiettivo poi
caratterizzato da uno stato, che pu essere nascosto (hidden), visibile (revealed) o
sbloccato (unlocked). Nel primo caso di tratta di obiettivi che vengono solo accennati
al giocatore, senza dare alcuna informazione specifica. Talvolta rivelare lobiettivo
potrebbe in qualche modo rovinare il divertimento al giocatore. Il secondo caso
quello di un obiettivo che il giocatore vede, ma che non ha ancora raggiunto. Nel
terzo caso si tratta semplicemente di un obiettivo che stato raggiunto e quindi
sbloccato. Alcuni di questi obiettivi si possono raggiungere in modo graduale, per
esempio raccogliendo dei punti o giocando per molto tempo. In questo caso si parla
di obiettivi incrementali, i quali vengono gestiti in modo particolare. possibile,
infatti, vedere in ogni momento a che punto si nellobiettivo, avendo quindi un
feedback visuale di quanto manca. importante sottolineare come si tratti di
obiettivi che non si possano riportare allo stato iniziale, anche dopo diverse partite.
Questo significa che possibile implementare un obiettivo incrementale relativo al
tempo giocato, ma non al fatto che si vinca un numero consecutivo di partite
riportando il contatore a zero nel caso di sconfitta. Ogni obiettivo di questo tipo
caratterizzato dal numero di passi che necessario fare per raggiungerlo. Attraverso
le Game API le diverse applicazioni dovranno comunicare quanti passi sono stati fatti
e lo sblocco avverr in modo automatico al raggiungimento della quota impostata.
bene sottolineare come questo funzionamento valga anche qualora lobiettivo sia
nascosto, per cui bene fare attenzione in fase di sviluppo dellapplicazione.
Un altro concetto legato agli obiettivi quello dei punti a essi associati. infatti
possibile specificare per ciascuno di essi il numero di punti che il giocatore guadagna
qualora gli stessi vengano sbloccati. In questo caso esistono dei vincoli legati al fatto
che ogni obiettivo non possa avere pi di 200 punti e che il totale di tutti gli obiettivi
non possa superare i 1000 punti. Inoltre i punti del giocatore devono essere multipli
di 5. A questi sono poi collegati quelli chiamati punti esperienza (XP), che il
giocatore guadagna per ciascuno dei punti associati agli obiettivi raggiunti; in
particolare 100 punti per ogni punto legato a un obiettivo. I punti esperienza vengono
gestiti in modo automatico dalle Game API, le quali si preoccupano anche dellinvio
di una notifica allapplicazione di Game non appena si raggiunge un particolare
livello.
Non ci resta quindi che iniziare con la creazione dei nostri obiettivi attraverso la
console di Google Play. Il procedimento molto semplice e consiste nel selezionare
la corrispondente voce nel menu a sinistra, come indicato nella Figura 10.11.
NOTA

bene ricordare che le applicazioni pubblicate devono necessariamente avere almeno cinque
obiettivi; questo vincolo non valido nel caso dei test.

In italiano la voce si chiama Risultati e ci porta a una schermata nella quale


selezioniamo il pulsante per laggiunta di un obiettivo, il quale ci porta a sua volta a
una schermata nella quale possiamo inserire tutte le informazioni a cui abbiamo
accennato in precedenza.
A questo punto possibile inserire un nome e una descrizione, insieme a unicona,
seguita dalla possibilit di indicare se si tratta di un obiettivo incrementale o meno.
La Figura 10.12 illustra la possibilit di inserire le informazioni relative allo stato
iniziale e quindi al numero di punti e ordine. Nel nostro caso abbiamo deciso di
creare cinque obiettivi molto semplici, di nome Obiettivo#<N>, senza icona. Ovviamente
il lettore pu inserire i propri obiettivi con relative informazioni e immagini. Al
termine della configurazione otteniamo il risultato rappresentato nella Figura 10.13,
con i cinque obiettivi, ciascuno non incrementale di 100 punti.

Figura 10.11 Aggiunta di obiettivi al gioco.

Figura 10.12 Inserimento dei dati relativi a un obiettivo.

Nella Figura 10.12 notiamo come sia presente anche lidentificatore di ciascun
obiettivo; si tratta delle informazioni che utilizzeremo nella nostra applicazione per il
corrispondente sblocco.
A questo punto siamo pronti alla gestione di questi obiettivi nellapplicazione
client, attraverso alcuni semplici metodi della classe Games.Achievements e in particolare i
metodi:

public abstract void unlock(GoogleApiClient apiClient, String id)
public abstract
PendingResult<Achievements.UpdateAchievementResult> unlockImmediate(GoogleApiClient
apiClient, String id)


Si tratta di due metodi che si differenziamo per il momento in cui avviene lo
sblocco dellobiettivo. Il primo metodo si utilizza qualora non si sia interessati
immediatamente allesito della operazione; il secondo metodo restituisce un oggetto
di tipo PendingResult che abbiamo imparato a gestire nei capitoli precedenti.

Figura 10.13 Elenco degli obiettivi creati.

Qualora lobiettivo sia incrementale, i metodi da utilizzare sono invece i seguenti e


si differenziano per lo stesso motivo di immediatezza del risultato:

public abstract void increment(GoogleApiClient apiClient, String id, int numSteps)
public abstract PendingResult<Achievements.UpdateAchievementResult>

incrementImmediate(GoogleApiClient apiClient, String id, int numSteps)



Come possiamo notare, si tratta di metodi che hanno come parametro
lidentificatore dellobiettivo e il numero di passi che il giocatore ha completato.
Prima di esaminare il nostro esempio, facciamo notare come, nelle due versioni,
siano presenti anche dei metodi per la visualizzazione di un obiettivo prima nascosto
e precisamente i metodi:

public abstract void reveal(GoogleApiClient apiClient, String id)
public abstract PendingResult<Achievements.UpdateAchievementResult>

revealImmediate(GoogleApiClient apiClient, String id)



Una volta raggiunti gli obiettivi, le Game API ci permettono anche di visualizzarli
attraverso unapposita interfaccia. Sar sufficiente utilizzare il seguente metodo:

public abstract Intent getAchievementsIntent (GoogleApiClient apiClient)


il quale ci fornir lIntent da lanciare per la visualizzazione della schermata degli
obiettivi.
Andiamo quindi alla nostra applicazione client, nella quale abbiamo incapsulato la
logica degli obiettivi in un Fragment di nome AchievementsFragment che descriveremo
nelle sue parti principali. Si tratta di un Fragment che visualizziamo nella nostra
applicazione selezionando la corrispondente voce nel menu delle opzioni. Ma prima
specifichiamo gli identificatori degli obiettivi definiti nel file ids.xml, nel quale
avevamo gi specificato lid della nostra applicazione. Si tratta di risorse di tipo String
associate a identificatori che iniziano per achievement_ come i seguenti:

<!-- Achievements resources-->
<string name="achievement_1">CgkIhYSL4rYKEAIQAg</string>
<string name="achievement_2">CgkIhYSL4rYKEAIQAw</string>
<string name="achievement_3">CgkIhYSL4rYKEAIQBA</string>
<string name="achievement_4">CgkIhYSL4rYKEAIQBQ</string>
<string name="achievement_5">CgkIhYSL4rYKEAIQBg</string>


Il nostro Fragment quindi molto semplice, in quanto contiene solamente una serie
di Button per lo sblocco di altrettanti obiettivi, pi uno per la visualizzazione dello
stato corrente. Quando lanciamo lapplicazione e selezioniamo lopzione relativa agli
Achievements, otteniamo la schermata rappresentata nella Figura 10.14.

Figura 10.14 Elenco degli obiettivi creati.

Ovviamente, allinizio non vi alcun obiettivo sbloccato, per cui proviamo a


selezionare il pulsante per la visualizzazione degli obiettivi raggiunti, ottenendo
quanto rappresentato nella Figura 10.15.

Figura 10.15 Visualizzazione degli obiettivi raggiunti; al momento nessuno.

Per la visualizzazione di questa schermata completamente gestita dalle Game API,


abbiamo semplicemente eseguito il seguente codice:

/**
* The Request Id for the Achievements
*/
private static final int REQUEST_ACHIEVEMENTS = 37;
fragmentLayout.findViewById(R.id.show_achievements)

.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final Intent intent = Games.Achievements
.getAchievementsIntent(getGoogleApiClient());
startActivityForResult(intent, REQUEST_ACHIEVEMENTS);

}
});


Ottenuto lIntent attraverso il metodo getAchievementsIntent() non abbiamo fatto altro
che lanciarlo nella modalit startActivityForResult() utilizzando la classica costante per
il valore relativo alla RequestId. Il lettore potr provare a selezionare uno degli obiettivi
della lista, ottenendo la visualizzazione di una piccola finestra di dialogo con
informazioni aggiuntive. Premiamo Back e torniamo alla schermata precedente, dove
selezioniamo il pulsante relativo al primo obiettivo, a cui abbiamo associato il
seguente codice:

fragmentLayout.findViewById(R.id.unblock_achievement_1)

.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// We unlock the first achievement
Games.Achievements.unlockImmediate(getGoogleApiClient(),
getString(R.string.achievement_1))
.setResultCallback(new
ResultCallback<Achievements.UpdateAchievementResult>() {
@Override
public void onResult(Achievements
.UpdateAchievementResult updateAchievementResult) {
showToast(Achievements 1 unlocked!);
}
});
}
});


Si tratta semplicemente della chiamata del metodo unlockImmediate() con la relativa
gestione del valore restituito, di tipo ResultCallback, nel quale visualizziamo un
messaggio con un Toast.

NOTA
Il lettore potr notare come questo e i prossimi Fragment estendano la classe BaseGameFragment
che, oltre a permettere laccesso alloggetto di tipo GoogleApiClient gestito dallActivity
contenitore, contiene alcuni metodi di utilit come quello usato per la visualizzazione del
messaggio di Toast.

Selezioniamo quindi il primo pulsante sbloccando di fatto il primo obiettivo e


torniamo nella schermata riassuntiva ottenendo questa volta il risultato rappresentato
nella Figura 10.16, nella quale possiamo vedere come lobiettivo sia stato sbloccato,
pi uninformazione che ci dice che si tratta del primo di cinque obiettivi. Notiamo
come vi sia anche linformazione relativa alla data in cui lo sblocco avvenuto. Nel
secondo obiettivo abbiamo invece utilizzato la modalit non immediata, ovvero la
seguente:

fragmentLayout.findViewById(R.id.unblock_achievement_2)

.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// We unlock the second
Games.Achievements.unlock(getGoogleApiClient(),
getString(R.string.achievement_2));
}
});


Selezionando il secondo pulsante procediamo allo sblocco anche del secondo
obiettivo.

Figura 10.16 Primo obiettivo raggiunto.

In questo caso facciamo anche notare la visualizzazione di una simpatica


animazione nella parte superiore del display, che mostra il nuovo obiettivo, come
indicato nella Figura 10.17.

Figura 10.17 Visualizzazione di unanimazione in corrispondenza dello sblocco dellobiettivo.

Abbiamo visto come funziona lo sblocco di un obiettivo e la visualizzazione della


schermata riassuntiva. Proviamo ora ad aggiungere allelenco un obiettivo di tipo
incrementale. Torniamo nella console e aggiungiamo un nuovo obiettivo nel modo
gi visto, impostando questa volta le informazioni relative al numero di passi
necessari al raggiungimento, come indicato nella Figura 10.18.

Figura 10.18 Creazione di un obiettivo incrementale.

Confermiamo ottenendo un nuovo id e quindi aggiungiamolo a quelli gi presenti


nella nostra applicazione. Aggiungiamo un pulsante a cui associamo il seguente
codice, il quale non fa altro che incrementare di 1 i passi raggiunti nel corrispondente
obiettivo:

fragmentLayout.findViewById(R.id.increment_achievement)

.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// We increment 1 step
Games.Achievements.increment(getGoogleApiClient(),

getString(R.string.achievement_incremental), 1);
}
});


Lanciamo poi lapplicazione e selezioniamo per tre volte il pulsante relativo
allobiettivo incrementale e andiamo quindi alla schermata riassuntiva, ottenendo
quanto rappresentato nella Figura 10.19.
In questo caso lo sblocco dellobiettivo necessita il raggiungimento di tutti i 10
passi impostati, dopo i quali si ha ancora la visualizzazione del messaggio
rappresentato nella Figura 10.20
Mentre nella schermata riassuntiva lobiettivo incrementale verr visualizzato
come tutti gli altri raggiunti in precedenza.

Leaderboard
Nel paragrafo precedente abbiamo visto come sia semplice gestire degli obiettivi a
ciascuno dei quali possibile associare dei punti. Una funzionalit correlata permette
di confrontare i punti dei vari giocatori al fine di stilare una classifica, che si chiama,
appunto, leaderboard. Come vedremo successivamente, utilizzeremo la console per
la creazione di alcune classifiche (il significato di leaderboard in italiano) ottenendo
una serie di identificatori. Ogni volta che un giocatore termina la propria partita, sar
possibile linvio dei punti che ha totalizzato a una delle classifiche create. Il sistema
si preoccupa di confrontare in modo automatico il punteggio del giocatore con quello
degli altri, su base assoluta, giornaliera e settimanale, inviando una notifica in caso di
record. Le Game API ci mettono a disposizione degli strumenti per la consultazione
dei punteggi in base a diversi criteri, che dipendono anche dal tipo di classifica.

Figura 10.19 Visualizzazione dellobiettivo incrementale.

Figura 10.20 Obiettivo incrementale raggiunto.

Ogni gioco pu infatti avere fino a un massimo di settanta classifiche, che possono,
per esempio, essere associate a particolari livelli o stati di un gioco. bene
sottolineare come esistano comunque due tipi diversi di classifiche. La prima detta
social e consiste sostanzialmente nel considerare solamente gli utenti che
appartengono a una particolare cerchia che uno o pi giocatori hanno creato o cui
hanno deciso di appartenere. Esistono poi le classifiche pubbliche, che contengono
tutti i giocatori che hanno deciso di condividere pubblicamente i propri punteggi. Le
Game API ci permettono di accedere a tutte le informazioni relative alle varie
classifiche, ma, nella maggior parte dei casi, comunque pi semplice utilizzare gli
strumenti predefiniti per la loro visualizzazione, in modo analogo a quanto fatto nel
caso degli obiettivi. Quello che dovremo fare semplicemente specificare a quale
tipo di classifica siamo interessati e quindi lanciarne la visualizzazione. Anche in
questo caso si tratta di un componente che amministreremo attraverso la console di
Google Play, specificando alcune informazioni quali il nome, una descrizione e
unicona. Oltre allordine che la classifica dovr occupare rispetto alle altre in fase di
visualizzazione esiste una configurazione molto importante che ci permette di
specificare lintervallo di possibili valori. Questa configurazione ci permette di
eliminare dalla classifica quei punteggi che sono palesemente contraffatti e inviati in
qualche modo al sistema. Per quello che riguarda lordinamento dei punteggi, il
sistema ci permette di gestire due casi, a seconda che sia meglio avere un valore alto
o basso. Nel caso di punti, il valore pi alto sar ovviamente il migliore, ma nel caso
dei tempi vale il contrario: il giocatore pi bravo quello, per esempio, che ha
percorso un tragitto nel minor tempo possibile. Nel caso delle classifiche si dovr
quindi fare attenzione anche a problematiche diverse, legate alla
internazionalizzazione (I18N), come la formattazione dei dati o la semplice
traduzione dei termini.
Anche in questo caso passiamo alla pratica e creiamo una classifica selezionando
la voce Classifica nel menu a sinistra della console e quindi il pulsante Aggiungi

, arrivando alla schermata rappresentata nella Figura 10.21 nella quale

Classifica

notiamo la presenza dei parametri di configurazione descritti in precedenza.

Figura 10.21 Creazione di una classifica (leaderboard).

Nel nostro caso abbiamo inserito un nome The Best, il tipo di valore Numerico con 0
decimali, un valore massimo di 1.000.000 specificando che il punteggio migliore sar
il pi alto.

Figura 10.22 La lista di classifiche create in console.

Selezionando quindi il pulsante Salva, arriviamo alla schermata riassuntiva

rappresentata nella Figura 10.22, nella quale notiamo la presenza dellidentificatore


come avvenuto anche per gli obiettivi. Anche in questo caso linterazione con le
classifiche avviene attraverso appositi metodi, accessibili attraverso un oggetto che
questa volta di tipo Games.Leaderboards il quale ci permette di eseguire due operazioni.
La prima permette linvio del punteggio di un giocatore a una specifica classifica. Per
farlo sufficiente utilizzare il metodo seguente, disponibile anche nella versione
immediate che abbiamo visto nel paragrafo precedente e che non riportiamo pi per
motivi di spazio:

public abstract void submitScore(GoogleApiClient apiClient,

String leaderboardId, long score)



Notiamo come i parametri necessari siano semplicemente lidentificatore della
classifica e il punteggio ottenuto. bene comunque ricordare come il riferimento
allutente sia gestito automaticamente, in quanto si tratta di API cui possibile
accedere solamente se si eseguito il login. La seconda funzionalit consiste nella
semplice visualizzazione della classifica attraverso il seguente metodo, il quale
restituisce un Intent che dovremo poi lanciare attraverso il metodo
:

startActivityForResult()


public abstract Intent getLeaderboardIntent(GoogleApiClient apiClient,

String leaderboardId)

In questo caso si avr la visualizzazione della classifica per lidentificatore
specificato; qualora si volessero visualizzare tutte le classifiche, il metodo da
utilizzare il seguente, che ha come unico parametro limmancabile riferimento
alloggetto GoogleApiClient.

public abstract Intent getAllLeaderboardsIntent(GoogleApiClient apiClient)


Il lettore potr notare come esistano molti altri metodi per laccesso alle
informazioni delle classifiche in modo pi o meno granulare. Non ci dilungheremo
molto su ciascuno di questi, ma descriviamo brevemente il nostro esempio che
contenuto nella classe LeaderBoardFragment che ci permette di inviare dei punteggi alla

nostra classifica e di visualizzarne il riassunto attraverso i precedenti metodi. Anche


in questo caso si tratta di una funzionalit accessibile attraverso unopportuna voce
nel menu delle opzioni. Ma prima bene definire le varie classifiche nel file delle
risorse ids.xml, utilizzando questa volta delle chiavi che iniziano per leaderboard_ come
nel seguente caso:

<!-- Leaderboards resources-->
<string name="leaderboard_thebest">CgkIhYSL4rYKEAIQCA</string>


Nel nostro esempio abbiamo definito una sola leaderboard, ma lasciamo al lettore
come esercizio la creazione di altre classifiche, da visualizzare nella nostra
applicazione di esempio. In questo caso la nostra interfaccia ancora molto scarna e
contiene una semplice SeekBar per la definizione del numero di punti da aggiungere
alla classifica e tre pulsanti per linserimento dei punti stessi e la visualizzazione
della classifica nelle due modalit, come possiamo vedere nella Figura 10.23.

Figura 10.23 La nostra UI per il test delle leaderboard.

Dopo lavvio, proviamo a visualizzare la nostra classifica selezionando il


corrispondente pulsante (il secondo). Otteniamo la schermata rappresentata nella
Figura 10.24, la quale visualizza un messaggio che ci dice che nessuno ha giocato al
nostro gioco.
Nella stessa interfaccia possiamo notare come nellangolo superiore destro vi sia
un commutatore che ci permette di passare dalla classifica social a quella pubblica.
Nella parte inferiore compare invece un pulsante che ci permette di inviare un
messaggio ai nostri amici, invitandoli a giocare con noi al nostro gioco. Il lettore

potr verificare come la selezione di questo pulsante ci permetta di inviare un


qualunque tipo di messaggio in relazione a quelle che sono le applicazioni di
messaggistica installate nel nostro dispositivo, ma tutti contenenti il riferimento al
nostro gioco sul Play Store. A questo punto torniamo nella nostra applicazione,
selezioniamo un po di punti e premiamo il primo pulsante. Se tutto andato per il
verso giusto si dovrebbe avere la visualizzazione di un messaggio di Toast di
successo, che diventa di errore in caso contrario. Se torniamo nella nostra classifica
troviamo quanto rappresentato nella Figura 10.25, nella quale si ha la visualizzazione
del primo classificato, che in questo momento anche lunico che ha giocato.
In questa schermata possiamo notare diversi particolari. Nellangolo superiore
sinistro notiamo la presenza di un menu a tendina che ci permette di selezionare il
tipo di classifica in base a un diverso intervallo temporale, come indicato nella Figura
10.26. Nellangolo superiore destro notiamo la presenza di una label che indica la
posizione dellutente corrente. Il lettore potr verificare come i punti dellutente siano
effettivamente quelli assegnati nel passo precedente.

Figura 10.24 La classifica quando vuota.

Figura 10.25 La classifica con il numero di punti del primo classificato.

Figura 10.26 possibile modificare lintervallo temporale di riferimento.

Osservando la nostra classe LeaderBoardFragment il codice per farlo molto semplice.


Nel caso dellassegnazione dei punti allutente abbiamo semplicemente utilizzato le
seguenti righe di codice, associate alla selezione del pulsante di aggiunta:

addPointsButton.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
Games.Leaderboards.submitScoreImmediate(getGoogleApiClient(),
getString(R.string.leaderboard_thebest), mPointsToAdd)

.setResultCallback( new
ResultCallback<Leaderboards.SubmitScoreResult>() {
@Override
public void onResult(Leaderboards.SubmitScoreResult
submitScoreResult) {
final Status status = submitScoreResult.getStatus();
if (status.isSuccess()) {
showToast(Points added with success);
} else {
showToast(Error adding points +
status.getStatusMessage());
}
}
});
}
});


Per la visualizzazione della classifica il codice invece il seguente, che dovrebbe
essere a questo punto ormai molto intuitivo:

fragmentLayout.findViewById(R.id.show_the_best_leaderboard_button)

.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final Intent showTheBestIntent = Games.Leaderboards
.getLeaderboardIntent(getGoogleApiClient(),
getString(R.string.leaderboard_thebest));
startActivityForResult(showTheBestIntent,
REQUEST_THE_BEST_LEADERBOARD);
}
});


A questo punto ci chiediamo come possa essere la classifica nel caso di pi utenti.
Come esperimento disinstalliamo lapplicazione (non abbiamo infatti implementato
loperazione di logout) e installiamola nuovamente scegliendo un altro utente in fase
iniziale. Utilizzando la stessa interfaccia diamo anche a questo utente dei punti, in
modo che lo portino in cima alla classifica e quindi selezioniamo il pulsante per la
visualizzazione, ottenendo quanto rappresentato nella Figura 10.27, la quale
visualizza il solo nuovo utente e non entrambi. Questo per il semplice motivo che si
tratta di una classifica nella modalit social e i due contatti non sono nella stessa
cerchia di Google+. Se selezioniamo la modalit All attraverso il corrispondente
commutatore in alto a destra, otteniamo quanto rappresentato nella Figura 10.28
ovvero la presenza di entrambi gli utenti. Lasciamo al lettore la dimostrazione di cosa
succeda nel caso in cui i due utenti fossero nella stessa cerchia di Google+.

Figura 10.27 La classifica social nel caso di utenti che non sono nella stessa cerchia.

Non ci resta che selezionare lultimo pulsante, relativo a tutte le classifiche, per
ottenere quanto rappresentato nella Figura 10.29, che altro non che un menu
introduttivo prima di entrare in una delle precedenti classifiche.

Figura 10.28 La classifica globale.

Figura 10.29 Menu introduttivo verso tutte le classifiche disponibili.

In questo caso il codice che abbiamo eseguito ancora molto semplice:



fragmentLayout.findViewById(R.id.show_all_leaderboards_button)

.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final Intent showAllIntent = Games.Leaderboards
.getAllLeaderboardsIntent(getGoogleApiClient());
startActivityForResult(showAllIntent, REQUEST_ALL_LEADERBOARDS);
}
});

Realtime multiplayer
Nei paragrafi precedenti abbiamo visto alcuni aspetti legati al gioco in generale,
mentre ora vogliamo descrivere gli strumenti che le Game API ci mettono a
disposizione per il gioco vero e proprio. In particolare vogliamo descrivere le API a
disposizione per la modalit di gioco in realtime, che consiste nel permettere a pi
utenti di giocare contemporaneamente allo stesso gioco. In questi casi il primo
concetto quello di Room ovvero di luogo nel quale i vari giocatori si trovano per
iniziare una sfida. In questo caso le modalit di definizione del compagno o
avversario possono essere diverse. Gli altri giocatori possono, per esempio, essere
scelti casualmente oppure possibile inviare degli inviti.
NOTA
bene precisare come in questo paragrafo faremo riferimento a un altro giocatore, ma il tutto
pu essere applicato anche a un numero qualsiasi di giocatori, per giochi pi social.

Le API per la gestione del multiplayer sono abbastanza complicate e


richiederebbero molto pi spazio, per cui in questo paragrafo descriveremo solamente
gli scenari di utilizzo pi comuni nella gestione dellentit Room che rappresenta il
luogo in cui i giocatori si ritrovano per iniziare un gioco. Si tratta di uno strumento
molto efficiente, in quanto linizializzazione di una Room consiste nella creazione di
un rete di collegamenti tra tutti i giocatori, utilizzando tecniche di peer-to-peer, senza
alcun accesso diretto al server. Il tutto inizia con unazione esplicita dellutente che
ha eseguito il login nellapplicazione.

Modalit Quick Start


Iniziamo con lo scenario pi semplice, che non prevede la creazione di una Room:
si intende richiedere al sistema di trovare un altro giocatore con cui iniziare una sfida;
in questo caso si parla di modalit quick start. Come per le altre funzionalit abbiamo
implementato un Fragment attraverso la classe RealtimeMultiplayerFragment, il quale
contiene alcuni pulsanti che ci permettono di eseguire varie azioni. Anche questa
funzione sar accessibile attraverso la corrispondente voce nel menu delle opzioni.
Tramite la modalit Quick Start possibile invitare fino a tre giocatori al nostro
gioco. Per fare questo, abbiamo associato alla selezione del pulsante il seguente
codice:

// We initialize the mask for the user
final int exclusiveBitMask = 0;
// We create the Bundle with the auto match criteria
Bundle autoCriteria = RoomConfig

.createAutoMatchCriteria(MIN_AUTOMATCH_PLAYERS,
MAX_AUTOMATCH_PLAYERS, 0);
// We create the Room configuration using the previous bundle.
// We register the RoomUpdateListener implementation
RoomConfig.Builder roomConfigBuilder =

RoomConfig.builder(mRoomUpdateListener);
// We set the auto criteria
roomConfigBuilder.setAutoMatchCriteria(autoCriteria);
// We register the listener for the different status for the Room
roomConfigBuilder.setRoomStatusUpdateListener(mRoomStatusUpdateListener);
// We register the listener for the different messages sent
roomConfigBuilder.setMessageReceivedListener(mMessageReceivedListener);
// We create the room
Games.RealTimeMultiplayer.create(getGoogleApiClient(),

roomConfigBuilder.build());

Come possiamo notare, la classe che ci permette di gestire il tutto si chiama
RoomConfig e dispone di un Builder per linizializzazione delle caratteristiche della
Room che intendiamo creare. Attraverso il metodo factory seguente, creeremo un
oggetto Bundle con le configurazioni relative allauto-match, ovvero alla possibilit di
trovare in modo automatico e casuale dei giocatori.

public static Bundle createAutoMatchCriteria (int minAutoMatchPlayers,

int maxAutoMatchPlayers, long exclusiveBitMask)



Il numero minimo e massimo di giocatori dato dai primi due parametri,
ricordando che il massimo non pu superare il numero 3. Il terzo parametro ci
permette di indicare alcuni filtri legati al particolare gioco. possibile, per esempio,
assegnare dei ruoli ai giocatori e in base a quelli applicare dei filtri oppure utilizzare i
punti dei giocatori e altro ancora. Nel nostro caso non utilizziamo questo criterio, per
cui mettiamo il corrispondente valore a 0. Loggetto di tipo Bundle cos ottenuto deve
essere impostato nel Builder attraverso il seguente metodo:

public RoomConfig.Builder setAutoMatchCriteria(Bundle autoMatchCriteria)


Come spesso avviene in questi casi, si ha la necessit di definire
limplementazione di alcune interfacce di callback per la notifica dei vari stati che un
oggetto pu assumere. In questo caso la prima implementazione che registriamo

attraverso il metodo seguente, quella dellinterfaccia RoomStatusUpdateListener, la quale


definisce una serie di metodi di callback che ci permettono di conoscere lo stato della
Room di cui si chiede la creazione.

public RoomConfig.Builder setRoomStatusUpdateListener(RoomStatusUpdateListener listener)


Osservando la documentazione, si tratta di un elevato numero di operazioni, che
riguardano diversi aspetti della Room, quali lo stato si connessione in generale oltre a
quello P2P (peer to peer). Nella nostra implementazione abbiamo semplicemente
visualizzato dei messaggi Toast oltre a gestire il codice di stato attraverso il seguente
metodo di utilit:

private boolean statusCodeManaged(final int statusCode) {

switch (statusCode) {
case GamesStatusCodes.STATUS_OK:
return false;
case GamesStatusCodes.STATUS_CLIENT_RECONNECT_REQUIRED:
showToast(Reconnect is required);
return true;
case GamesStatusCodes.STATUS_REAL_TIME_CONNECTION_FAILED:
showToast(Connection failed);
return true;
case GamesStatusCodes.STATUS_MULTIPLAYER_DISABLED:
showToast(The Multiplayer status is disabled);
return true;
case GamesStatusCodes.STATUS_INTERNAL_ERROR:
showToast(Internal error);
return true;
}
return false;


Nellutilizzo di API di questo tipo, la gestione degli errori molto importante. Con
lo stesso criterio abbiamo quindi registrato unimplementazione dellinterfaccia
RealTimeMessageReceivedListener attraverso il metodo:

public RoomConfig.Builder setMessageReceivedListener

(RealTimeMessageReceivedListener listener)

Questa permette di ricevere delle notifiche in relazione agli eventuali messaggi di
realtime ricevuti. Lultima istruzione consiste nella creazione del RoomConfig
attraverso il corrispondente metodo di build e quindi il suo utilizzo come parametro
del seguente metodo delloggetto Games.RealTimeMultiplayer, che ci permette di accedere
alle funzionalit multiplayer

public abstract void create(GoogleApiClient apiClient, RoomConfig config)


Non ci resta che vedere che cosa succede avviando lapplicazione e selezionando il
pulsante di Quick Start. Come previsto, si ha la generazione di un errore e la
visualizzazione del seguente messaggio di Toast che possiamo vedere anche nel Log
dellapplicazione.

I/BaseGameFragment(14485): The Multiplayer status is disabled


Come altre configurazioni anche quella relativa al Multiplayer Realtime deve
essere attivata nella console di Google Play, come abbiamo visto allinizio del
capitolo. Torniamo quindi nella console di amministrazione e attiviamo il
corrispondente pulsante come indicato nella Figura 10.30.

Figura 10.30 Attivazione del multiplayer.

Il pulsante si trova nella configurazione dellapplicazione e non in quella del gioco


in generale. Si tratta poi di una configurazione che impiega un po di tempo per la sua
effettiva attivazione.
Dopo aver salvato la nuova impostazione, torniamo nella nostra applicazione e
riproviamo a selezionare il pulsante relativo al Quick Start, il quale, questa volta,
dovrebbe avvenire con successo con la visualizzazione di un messaggio di log del
seguente tipo

I/BaseGameFragment(14485): onRoomCreated
RoomEntity{RoomId=ChoKCQiFhIvitgoQAhABGAAg____________ARD4vf3uy4H1ocwB,
CreatorId=p_CPi9_e7LgfWhzAEQAQ, CreationTimestamp=1417868373049, RoomStatus=1, Description=null,
Variant=0, AutoMatchCriteria=Bundle[{max_automatch_players=2, min_automatch_players=1,
exclusive_bit_mask=0}], Participants=[ParticipantEntity{ParticipantId=p_CPi9_e7LgfWhzAEQAQ,
Player=PlayerEntity{PlayerId=110772780011997621617, DisplayName=Massimo Carli,
IconImageUri=null, IconImageUrl=null, HiResImageUri=null, HiResImageUrl=null,
RetrievedTimestamp=1417868366497, Title=Rookie,
LevelInfo=com.google.android.gms.games.PlayerLevelInfo@1e1b36}, Status=2, ClientAddress=null,
ConnectedToRoom=false, DisplayName=Massimo Carli, IconImage=null, IconImageUrl=null,
HiResImage=null, HiResImageUrl=null, Capabilities=0, Result=null}],
AutoMatchWaitEstimateSeconds=-1}


In questo ci sono diverse informazioni relative alla Room creata. Nel codice
evidenziato notiamo la presenza di alcune informazioni relative ai partecipanti, che
possiamo ottenere dalla room attraverso il seguente codice:

List<String> participantsIds = room.getParticipantIds();
if (participantsIds != null) {

for (String participantId : participantsIds) {


final String participantName =
room.getParticipant(participantId).getDisplayName();
Log.i(BaseGameFragment.TAG, Participant: +

participantName);
}
}


In questo caso lunico partecipante lutente che ha creato la room. Il passo
successivo quello di mettersi in attesa del numero di giocatori sufficienti per
iniziare il gioco. Per fare questo le Game API ci mettono a disposizione la
corrispondente UI, che possiamo lanciare con lIntent che otteniamo attraverso il
seguente metodo delloggetto Games.RealTimeMultiplayer:

public abstract Intent getWaitingRoomIntent (GoogleApiClient apiClient,

Room room, int minParticipantsToStart)



Si tratta di un metodo che permette di visualizzare la Waiting Room, che permette
di ricevere informazioni sui giocatori, mano a mano che accettano di partecipare al
nostro gioco. Come avvenuto nei casi precedenti, si tratta di un Intent che dobbiamo
lanciare attraverso la modalit startActivityForResult(), per cui abbiamo definito una
costante come request id e soprattutto abbiamo gestito il risultato attraverso
unopportuna implementazione del metodo onActivityResult(). La logica per il lancio di
questo Intent nel seguente metodo di utilit:

private void launchWaitingRoom(final Room room) {

Intent waitingIntent = Games.RealTimeMultiplayer


.getWaitingRoomIntent(getGoogleApiClient(),
room, WAITING_ROOM_MIN_PLAYERS);
startActivityForResult(waitingIntent, WAITING_ROOM_REQUEST_ID);
}


Come possiamo notare, si tratta di un metodo delloggetto Games.RealTimeMultiplayer,
che vuole come primo parametro limmancabile oggetto GoogleApiClient e come
secondo il riferimento alla Room che abbiamo appena creato. Il terzo parametro
molto importante, in quanto permette di specificare il numero minimo di giocatori
che servono per iniziare la partita, compreso quello che ha iniziato la ricerca. Prima

di vedere come gestire i valori restituiti nel metodo onActivityResult() vediamo di


simulare il comportamento di due giocatori che vogliono trovare un avversario
secondo questa modalit.
NOTA
Per collaudare questo meccanismo si consiglia di utilizzare due dispositivi reali che utilizzano due
account abilitati al test attraverso la console di Google Play.

Nel nostro caso disponiamo di due dispositivi che hanno acconsentito allutilizzo
del gioco attraverso due account diversi. Lanciamo lapplicazione nel primo
dispositivo e, nel menu delle opzioni, selezioniamo la voce relativa al multiplayer
realtime. Otteniamo una schermata che contiene il pulsante Quick Start. Facciamo lo
stesso nel secondo dispositivo, ottenendo la stessa schermata. Per vedere che cosa
succede, ci aiutiamo anche con una serie di messaggi di Log che abbiamo inserito
tramite alcuni metodi di utilit privati. Dopo la selezione del pulsante nel primo
dispositivo otteniamo la schermata rappresentata nella Figura 10.31.

Figura 10.31 Il primo player ha creato la room ed in attesa di un avversario.

Come possiamo notare, ci sono tre posti disponibili, che sono quelli che abbiamo
specificato in precedenza in corrispondenza della creazione della room. Prima di
proseguire, possiamo notare come siano stati visualizzati i seguenti messaggi di log:

onRoomCreated
onRoomCreated Room Id: <room id>
Participant: Massimo Carli


Questo significa che abbiamo ricevuto la notifica della creazione della Room di cui
otteniamo un identificatore e il cui unico partecipante lutente che lha creata. La
schermata che abbiamo visualizzato con lIntent permette anche di gestire il fatto che
entro un determinato periodo non si presenti alcun giocatore. In quel caso si ottiene
infatti il messaggio rappresentato nella Figura 10.32 che ci chiede se attendere ancora
oppure uscire.

Figura 10.32 Dopo un periodo di inattivit ci viene chiesto se proseguire oppure no.

Ora prendiamo il secondo dispositivo e selezioniamo la stessa opzione. A questo


punto, nel primo dispositivo, precedentemente in attesa, si ha la visualizzazione della
schermata rappresentata nella Figura 10.33 dove possiamo notare come non sia
presente alcun nome, ma solamente un identificativo.

Figura 10.33 Il secondo giocatore arriva nella waiting room.

Si tratta, infatti, di giocatori che non appartengono alle nostre cerchie e di cui
bisogna quindi garantire lanonimato. A questo punto molto interessante osservare
il log per vedere che cosa succede e quali metodi di callback vengono richiamati. Si
tratta di un log, relativo al primo dispositivo, nel quale lidentificativo della room
stato sostituito con <room id> e quelli dei partecipanti con <part id n> per motivi di
spazio. Il primo evento quello relativo alla notifica che un altro utente ha eseguito il
join e quindi si ha la chiamata del metodo onPeerJoined() dellinterfaccia
. Il parametro che viene passato lidentificatore della room

RoomStatusUpdateListener

(sempre per il primo player). infatti bene precisare che sebbene i due giocatori si
siano trovati essi non condividono la stessa room e quindi gli id sono diversi; ognuno
ha infatti creato la propria room nel momento in cui ha scelto il pulsante Quick Start.
Notiamo poi come oltre allidentificativo della room vi sia la visualizzazione di un
nome per il giocatore arrivato. importante sottolineare come il nome non rivela

lidentit del giocatore; si tratta infatti di un giocatore anonimo che non abbiamo
invitato perch appartenente a una delle nostre cerchie.

I/BaseGameFragment( 1190): onPeerJoined
I/BaseGameFragment( 1190): onPeerJoined Room Id: <room Id 1>
Participant: Player 4031


Gli eventi successivi sono sempre legati alla gestione del P2P e ci vengono
notificati attraverso altrettanti metodi della stessa interfaccia RoomStatusUpdateListener.

onRoomConnecting
onRoomConnected Room Id: <room Id 1>
onP2PConnected <part Id 1>
participantId: <part Id 1>
onPeerLeft Room Id:<room Id 1>
onPeersConnected
onPeersConnected Room Id:<room Id 1>
Participant: Player 4031
onRoomConnected
onRoomConnected Room Id:<room Id 1>


Quello che ci interessa maggiormente riguarda il fatto che ora abbiamo due player
che sono connessi tra loro e che si possono ora scambiare dei messaggi che
dipendono dal particolare gioco. Attraverso i metodi di callback abbiamo infatti le
informazioni relative alla room e agli id dei partecipanti, che utilizzeremo
successivamente per linvio dei messaggi. Ma dove sono, esattamente, queste
informazioni? In realt dobbiamo ricordarci che abbiamo avviato lActivity per il
waiting attraverso il metodo startActivityForResult(), per la quale dobbiamo fornire
limplementazione del metodo onActivityResult(). Se tutto va per il verso giusto
otteniamo infatti le informazioni sulla room attraverso il seguente codice:

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

super.onActivityResult(requestCode, resultCode, data);


if (requestCode == WAITING_ROOM_REQUEST_ID) {
if (resultCode == Activity.RESULT_OK) {
// We get the current room
mRoom = data.getParcelableExtra(Multiplayer.EXTRA_ROOM);
printRoomData(onActivityResult , mRoom);

printRoomParticipants(mRoom);
}
}
}


In questo caso notiamo, nel codice evidenziato, come il riferimento alla Room si
ottenga dallIntent ricevuto come risultato attraverso un extra di tipo Parcelable,
accessibile attraverso la costante Multiplayer.EXTRA_ROOM. Attraverso il messaggio di Log,
il lettore potr verificare come ora i partecipanti della room creata siano
effettivamente due come possiamo vedere qui:

I/BaseGameFragment( 9277): onActivityResult Room Id:<room Id 1>
I/BaseGameFragment( 9277): Participant: Player 5554
I/BaseGameFragment( 9277): Participant: Massimo Carli

Modalit a invito
Il secondo scenario che vogliamo esaminare prevede che un giocatore possa
invitare altri utenti al gioco. In questo caso il flusso leggermente diverso, in quanto
dovremo inizialmente lanciare una UI che ci permetta di scegliere le persone da
invitare e quindi, quando otteniamo un feedback, creare la room per la gestione del
gioco. Nel nostro caso abbiamo aggiunto alla nostra interfaccia un secondo pulsante,
a cui abbiamo collegato il seguente codice:

Intent intent = Games.RealTimeMultiplayer

.getSelectOpponentsIntent(getGoogleApiClient(),
MIN_INVITE_PLAYERS, MAX_INVITE_PLAYERS);
startActivityForResult(intent, INVITE_ROOM_REQUEST_ID);


Questa volta abbiamo utilizzato il metodo getSelectOpponentsIntent(), il quale ci
permette di ottenere lIntent per lanciare linterfaccia che ci permette di invitare un
numero di utenti di cui abbiamo specificato il numero minimo e massimo. Lanciando
lapplicazione e selezionando il pulsante associato a questo codice otteniamo la
schermata rappresentata nella Figura 10.34, nella quale notiamo come vi sia la

possibilit di selezionare alcuni giocatori presenti o meno nelle proprie cerchie.


Notiamo anche la possibilit di abilitare linvito agli utenti anche in base alla loro
location; opzione al momento disabilitata.
Anche in questo caso lIntent viene lanciato attraverso il metodo
, per cui il passo successivo consiste nel gestirne il risultato nel

startActivityForResult()

metodo onActivityResult(). In questo caso linformazione ottenuta relativa agli utenti


invitati, a cui possiamo accedere attraverso un extra associato alla costante

Games.EXTRA_PLAYER_IDS


Nella Figura 10.34 notiamo la presenza dellopzione di auto-pick, che consiste
nella possibilit di aggiungere ai giocatori invitati anche un numero di giocatori
casuali, come nel caso precedente. Qualora decidessimo di aggiungere un giocatore
in auto-pick e il secondo nella lista, otterremmo quanto rappresentato nella Figura
10.35.

Figura 10.34 La schermata per invitare altri giocatori.

Figura 10.35 Invito diretto di un giocatore e di uno in auto-pick.

Notiamo come gli utenti invitati siano rappresentati nella parte superiore della
schermata, attraverso la quale possibile anche annullare linvito. A seguito della
selezione di uno o pi contatti, notiamo come nella parte inferiore compaia un
pulsante Play, attraverso il quale possiamo procedere allinvito vero e proprio. Le
informazioni relative alla modalit auto-pick vengono comunicate attraverso
altrettante costanti ovvero

Multiplayer.EXTRA_MIN_AUTOMATCH_PLAYERS
Multiplayer.EXTRA_MAX_AUTOMATCH_PLAYERS


Per poter utilizzare le informazioni relative agli inviti dobbiamo implementare la
seguente logica nel metodo onActivityResult(), in corrispondenza del requestId

utilizzato in fase di lancio, che abbiamo definito attraverso la costante


INVITE_ROOM_REQUEST_ID che riportiamo solo per la parte di interesse:

if (requestCode == INVITE_ROOM_REQUEST_ID) {

if (resultCode == Activity.RESULT_OK) {
// We get the invited players
final ArrayList<String> invitedPlayers = data
.getStringArrayListExtra(Games.EXTRA_PLAYER_IDS);
// We get information related to the auto match player
Bundle autoMatchCriteria = null;
int minAutoMatchPlayers = data.getIntExtra(Multiplayer.
EXTRA_MIN_AUTOMATCH_PLAYERS, 0);
int maxAutoMatchPlayers = data.getIntExtra(Multiplayer.
EXTRA_MAX_AUTOMATCH_PLAYERS, 0);
if (minAutoMatchPlayers > 0) {
autoMatchCriteria = RoomConfig.createAutoMatchCriteria(
minAutoMatchPlayers, maxAutoMatchPlayers, 0);
} else {
autoMatchCriteria = null;
}
// We create now the Room with the participants to invite
RoomConfig.Builder roomConfigBuilder = RoomConfig
.builder(mRoomUpdateListener)
.setRoomStatusUpdateListener(mRoomStatusUpdateListener)
.setMessageReceivedListener(mMessageReceivedListener);
// We add the participants
roomConfigBuilder.addPlayersToInvite(invitedPlayers);
// We set the auto match criteria if any
if (autoMatchCriteria != null) {

roomConfigBuilder
.setAutoMatchCriteria(autoMatchCriteria);
}
// We create the room
Games.RealTimeMultiplayer.create(getGoogleApiClient(),
roomConfigBuilder.build());
// Here we have to keep the screen on because if the
// screen go off during the handshake the game ends
getActivity().getWindow().addFlags(WindowManager
.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}


Dopo aver verificato che loperazione sia stata effettuata con successo attraverso il
controllo del resultCode, andiamo a leggere il valore nel Bundle associato alla chiave
che sappiamo contenere un ArrayList con gli identificativi degli

Games.EXTRA_PLAYER_IDS

utenti che abbiamo invitato. Di seguito andiamo poi a leggere le informazioni relative
agli eventuali giocatori che intendiamo invitare secondo la modalit auto-pick.
Ottenute queste informazioni siamo pronti per creare la Room nella stessa modalit
utilizzata in precedenza. Lunica differenza consiste nellutilizzo del metodo

public RoomConfig.Builder addPlayersToInvite(ArrayList<String> playerIds)


per laggiunta delle informazioni relative agli utenti da invitare oltre a quelle
eventualmente impostate per gli utenti in auto-pick attraverso il metodo:

public RoomConfig.Builder setAutoMatchCriteria(Bundle autoMatchCriteria)


Dopo la creazione della Room nella stessa modalit vista in precedenza, notiamo
lutilizzo della seguente istruzione, che ci permette di tenere acceso il display durante

la fase di handshake, per evitare la disconnessione degli utenti durante questa delicata
fase.

getActivity().getWindow().addFlags(WindowManager

.LayoutParams.FLAG_KEEP_SCREEN_ON);

A questo punto, dopo le selezioni fatte in Figura 10.35, premiamo il pulsante Play e
quindi eseguiamo il precedente codice, il quale ci porta alla visualizzazione della
schermata rappresentata nella Figura 10.36. Questo perch la creazione della Room
ha causato la chiamata del nostro metodo di callback onRoomCreated() che, come prima,
ha causato la visualizzazione della schermata di attesa, rappresentata nella Figura
10.36.
A questo punto interessante vedere che cosa succede nelle applicazioni degli
utenti invitati. Intanto diciamo che il posto associato alla modalit auto-pick potr
essere riempito da un utente che decide di selezionare lopzione Quick Start. Quello
riservato al nostro contatto deve invece essere gestito in modo esplicito da
questultimo. Se andiamo a osservare il nostro secondo dispositivo di test, notiamo
che linvio dellinvito ha causato la visualizzazione della notifica rappresentata nella
Figura 10.37.
Si tratta di una notifica che non viene gestita da noi, ma direttamente
dallapplicazione di Gaming di Google. Selezionando questa notifica veniamo portati
alla schermata rappresentata nella Figura 10.38, la quale ci permette di accettare o
rifiutare linvito.
A questo punto lutente invitato ha la possibilit di accettare o rifiutare linvito,
selezionando la corrispondente opzione. Purtroppo la selezione dellopzione di
accettazione richiede del lavoro da parte nostra; dobbiamo infatti preparare la nostra
applicazione allinvio dellinformazione di accettazione al sistema di gaming. Per
fare questo dobbiamo tornare alla nostra Activity principale e precisamente nella
classe MainActivity. Quando lutente che ha ricevuto linvito seleziona lopzione di
accettazione, il sistema non fa altro che lanciare la nostra applicazione mettendole a
disposizione alcune informazioni che diventano disponibili al momento della
connessione al sistema. Per questo motivo il punto di ingresso nella gestione degli
inviti ricevuti proprio nel metodo onConnect() della nostra MainActivity, ovvero nel
metodo che viene richiamato a seguito della connessione avvenuta con successo in
fase di avvio.

Figura 10.36 Schermata di attesa delle risposte agli inviti.

Figura 10.37 La notifica relativo allinvio.

Figura 10.38 Viene chiesto di accettare o rifiutare linvito.

Nel nostro caso il metodo onConnect() diventa il seguente:



public void onConnected(Bundle connectionHint) {

Log.i(TAG, GoogleApiClient connected);


// Here we have to check if we have received an invite
if (connectionHint != null) {
// we check if we have received an invite
Invitation invitation = connectionHint
.getParcelable(Multiplayer.EXTRA_INVITATION);
if (invitation != null) {
// We create the multiplayer Fragment
final RealtimeMultiplayerFragment realtimeMultiplayerFragment =
RealtimeMultiplayerFragment.newInstance(invitation);
// We add the Fragment
getFragmentManager().beginTransaction()
.replace(R.id.anchor_point,

realtimeMultiplayerFragment).commit();
}
}
}


Utilizzando il parametro connectionHint, di tipo Bundle, andiamo a vedere se esiste una
informazione associata allextra Multiplayer.EXTRA_INVITATION, la quale, se presente, di
tipo Invitation. Se presente significa che la nostra applicazione stata avviata proprio
a seguito della ricezione di un invito, che deve quindi essere gestito. Le specifiche
richiedono che la ricezione dellinvito porti lapplicazione direttamente alla
schermata che ne permette la gestione, che nel nostro caso quella descritta dalla
classe RealtimeMultiplayerFragment. Per questo motivo abbiamo creato un nuovo metodo
factory che prevede appunto come parametro loggetto Invitation ricevuto. Il metodo
factory del Fragment molto semplice e non fa altro che memorizzare lInvitation
ottenuta allinterno di un Bundle che viene poi gestito nel metodo onActivityCreated() nel
seguente modo:

public static RealtimeMultiplayerFragment newInstance(final Invitation invitation) {

RealtimeMultiplayerFragment fragment =
new RealtimeMultiplayerFragment();
Bundle args = new Bundle();
args.putParcelable(INVITATION_ARG_KEY, invitation);
fragment.setArguments(args);
return fragment;
}
public void onActivityCreated(Bundle savedInstanceState) {

super.onActivityCreated(savedInstanceState);
final Invitation invitation = getArguments()
.getParcelable(INVITATION_ARG_KEY);
if (invitation != null) {
RoomConfig.Builder roomConfigBuilder =
RoomConfig.builder(mRoomUpdateListener)

.setRoomStatusUpdateListener(mRoomStatusUpdateListener)
.setMessageReceivedListener(mMessageReceivedListener);
roomConfigBuilder.setInvitationIdToAccept(invitation
.getInvitationId());
Games.RealTimeMultiplayer.join(getGoogleApiClient(),
roomConfigBuilder.build());
getActivity().getWindow().addFlags(WindowManager
.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}


Notiamo come, dopo aver ricevuto leventuale Invitation, non si faccia altro che
creare la Room nella modalit ormai solita, con lunica differenza che ora si specifica
lidentificatore dellinvito attraverso il metodo:

public RoomConfig.Builder setInvitationIdToAccept(String invitationId)


Infine si utilizza il seguente metodo della classe Games.RealTimeMultiplayer per linvio
dellaccettazione al sistema.

public abstract void join(GoogleApiClient apiClient, RoomConfig config)


A questo punto viene creata la Room dal lato dellinvitato e quindi si arriva alla
stessa situazione del caso precedente, ovvero un player connesso al gioco attraverso
una particolare Room. Tornando al dispositivo invitante, notiamo come ora la
schermata sia quella rappresentata nella Figura 10.39, nella quale notiamo come
lutente invitato in modo esplicito abbia effettivamente accettato.
Oltre a questo notiamo come nella parte inferiore sia ora presente un pulsante Start
. Questo significa che stato raggiunto il numero minimo di giocatori per linizio

Now

della partita e questa responsabilit nelle mani dellutente che ha iniziato la sfida.
Nel caso in cui anche il terzo player invitato fosse disponibile, si verificherebbe un

caso simile a quello del paragrafo precedente, con la chiusura di questa schermata e
linizio automatico della partita.
A differenza dellaccettazione di un invito, il rifiuto viene gestito in modo
automatico dal sistema attraverso la visualizzazione di un messaggio come quanto
rappresentato nella Figura 10.40.
Per quello che riguarda limplementazione di tutti i metodi di callback delle varie
interfacce, rimandiamo alla documentazione ufficiale, in quanto si tratta di un aspetto
delle API piuttosto ripetitivo e che non si discosta di molto da quanto descritto finora.
Concludiamo questo paragrafo con qualche informazione aggiuntiva sulle diverse
possibilit disponibili. La prima consiste nella possibilit di visualizzare la schermata
degli inviti attraverso il seguente metodo, questa volta delloggetto Games.Invitations:

public abstract Intent getInvitationInboxIntent(GoogleApiClient apiClient)


Questo restituisce, al solito, un Intent che possiamo lanciare nella modalit
e che non fa altro che visualizzare la schermata rappresentata

startActivityForResult()

nella Figura 10.38.

Figura 10.39 Lutente invitato ha accettato.

Figura 10.40 Rifiuto dellinvito.

Anche in questo caso abbiamo aggiunto un pulsante che permette lesecuzione del
seguente codice per la visualizzazione della schermata di inbox per gli inviti.

fragmentLayout.findViewById(R.id.realtime_invite_inbox)

.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent inviteInboxIntent = Games.Invitations
.getInvitationInboxIntent(getGoogleApiClient());
startActivityForResult(inviteInboxIntent,
INVITE_INBOX_REQUEST_ID);
}
});


In questo caso la gestione della risposta ottenuta attraverso limplementazione del
metodo onActivityResult() la seguente, molto simile a quella che abbiamo
implementato nel caso della ricezione dellinvito direttamente dalla notifica.

if (requestCode == INVITE_INBOX_REQUEST_ID) {

if (resultCode == Activity.RESULT_OK) {
// get the selected invitation
Bundle extras = data.getExtras();
Invitation invitation =
extras.getParcelable(Multiplayer.EXTRA_INVITATION);
RoomConfig.Builder roomConfigBuilder =
RoomConfig.builder(mRoomUpdateListener)
.setRoomStatusUpdateListener(mRoomStatusUpdateListener)
.setMessageReceivedListener(mMessageReceivedListener);
roomConfigBuilder.setInvitationIdToAccept(
invitation.getInvitationId());
Games.RealTimeMultiplayer.join(getGoogleApiClient(),
roomConfigBuilder.build());
getActivity().getWindow()

.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}


Ottenuta leventuale Invitation non facciamo quindi altro che creare la Room e
quindi provvedere alla sua accettazione.

Figura 10.41 La Inbox per gli inviti.

Un aspetto interessante di questa schermata riguarda la possibilit di diventare


visibili per giocare con giocatori che sono nella nostra stessa zona geografica. Nella
parte superiore della Figura 10.41 possiamo infatti notare come sia presente un
pulsante che ci permette di essere visibili ai giocatori che intendono giocare con noi e
sono nelle vicinanze. Si tratta di quei giocatori che sarebbero visibili nel caso di
ricerche come quelle indicate in corrispondenza della Figura 10.35.
Un altro aspetto che vogliamo descrivere relativo al caso in cui si ricevano degli
inviti proprio mentre si sta giocando. In questo caso la risposta molto semplice e
consiste nella semplice implementazione di un interfaccia OnInvitationReceivedListener,
la quale definisce le seguenti due operazioni, richiamate rispettivamente in caso di
ricezione o annullamento dellinvito:

public abstract void onInvitationReceived(Invitation invitation)


public abstract void onInvitationRemoved(String invitationId)


A questo punto lasciamo al lettore come esercizio limplementazione di questa
interfaccia e la gestione dellinvito attraverso lo stesso codice utilizzato nel caso di
inviti dalla Inbox o dalla notifica.
Un ultimissimo aspetto riguarda la gestione degli errori e delle possibili
disconnessioni dalle varie Room. In questo caso il procedimento da seguire quello
che abbiamo utilizzato nel seguente metodo di callback:

@Override
public void onDisconnectedFromRoom(Room room) {

showToast(onDisconnectedFromRoom );
printRoomData(onDisconnectedFromRoom , room);
Games.RealTimeMultiplayer.leave(getGoogleApiClient(),
null, room.getRoomId());
getActivity().getWindow().clearFlags(WindowManager
.LayoutParams.FLAG_KEEP_SCREEN_ON);
}


Si tratta del caso in cui il giocatore venga disconnesso da una room. In questo caso,
oltre al rilascio dello schermo, utilizziamo il metodo seguente per labbandono
esplicito della room:

public abstract void leave(GoogleApiClient apiClient,

RoomUpdateListener listener, String roomId)



Per limplementazione di tutti gli altri metodi di callback, come detto piuttosto
ripetitivi, rimandiamo alla documentazione ufficiale.

Scambio di messaggi
Nei paragrafi precedenti abbiamo visto come mettere in comunicazione due utenti
che intendono giocare nella modalit realtime multiplayer. In questo paragrafo

vediamo invece come questi giocatori possono effettivamente comunicare tra loro
attraverso messaggi. Ovviamente le Game API ci mettono a disposizione gli
strumenti per linvio di dati, ma il protocollo delle informazioni inviate e ricevute
dovr dipendere dal particolare gioco.
NOTA
Potremmo dire che le Game API ci forniscono il protocollo di trasporto, ma non quello a livello
applicativo, che dipende, infatti, dalla particolare applicazione o gioco.

A seconda della natura delle informazioni che vogliamo trasmettere, abbiamo la


possibilit di utilizzare una modalit reliable (affidabile) o una modalit inaffidabile
(unreliable) nel senso che non garantisce la ricezione dellinformazione.
NOTA
un po la stessa differenza che si ha nel Web nellutilizzo di un protocollo basato su TCP, che
garantisce la ricezione del messaggio, o UDP, che non garantisce la ricezione dei dati ed quindi
pi adatta a informazioni che variano molto velocemente o allo streaming audio/video.

Nel caso della connessione di tipo reliable, il sistema garantisce che i messaggi
vengano recapitati integri e mantenendo lo stesso ordine di invio. Come vedremo, la
notifica della ricezione di questo tipo di messaggi avviene attraverso opportuni
metodi di callback. Questa la modalit che si pu utilizzare qualora il tempo di
ricezione dellinformazione non sia di fondamentale importanza oppure nel caso di
file relativamente grossi. Nel caso di connessioni di tipo unreliable il sistema non
garantisce la ricezione del messaggio n la conservazione dellordine, ma ne
garantisce comunque lintegrit. A differenza dei messaggi reliable, il tempo di
latenza molto basso, per cui sono messaggi che vanno bene nel caso di informazioni
per le quali il tempo importante. Esistono dei limiti nelle dimensioni dei messaggi:
nel primo caso di 1400 byte, mentre nel secondo leggermente inferiore: 1168
byte.
Nel caso di applicazioni Android esiste poi una terza modalit, che permette una
gestione pi fine delle comunicazioni; si tratta delle comunicazioni su Socket. In
questa modalit i due punti vengono messi in comunicazione attraverso un Socket e le
informazioni vengono inviate e ricevute attraverso i corrispondenti Stream; le
informazioni, sotto forma di byte, immesse sullo stream nel punto A vengono poi
lette dallo stream di input nel punto B. Si tratta di una modalit che pu essere usata
in alternativa a quella unreliable, nel caso si avesse bisogno di un tuning pi fine
delle informazioni. Gli svantaggi sono legati al fatto di dover necessariamente gestire
lapertura e, soprattutto, la chiusura degli Stream non pi utilizzati.
Non ci resta che descrivere la nostra applicazione e vedere come inviare e ricevere
messaggi tra partecipanti a un gioco che si sono incontrati attraverso uno dei

meccanismi descritti in precedenza. Sebbene le informazioni inviate e ricevute


dipendano dal tipo di applicazione, nel nostro caso abbiamo implementato una sorta
di chat. Attraverso una EditText, tramite la selezione di un pulsante (in modalit
reliable o unreliable) inseriamo del testo che inviamo a tutti gli utenti collegati. In
modalit reliable abbiamo utilizzato il seguente metodo della classe
Games.RealTimeMultiplayer:

public abstract int sendReliableMessage (GoogleApiClient apiClient,

RealTimeMultiplayer.ReliableMessageSentCallback listener,
byte[] messageData, String roomId, String recipientParticipantId)

Come possiamo notare, i parametri sono relativi a unimplementazione
dellinterfaccia di callback per conoscere lesito dellinvio, il messaggio da inviare
come array di byte, un identificatore della room a cui gli utenti sono connessi e
lidentificatore del partecipante. Secondo questa modalit non infatti possibile
inviare i messaggio a tutti gli altri utenti, per cui dovremo necessariamente
impiegare un ciclo fra i partecipanti. Il codice che abbiamo collegato alla selezione
del primo pulsante il seguente:

private void sendReliableMessage(final String messageToSend) {

if (mRoom != null) {
// We get the List of participants
final List<Participant> participants = mRoom.getParticipants();
// We get the message as array of bytes
byte[] message = messageToSend.getBytes();
// We send the message to all the participants but me
final String myId = mRoom.getParticipantId(Games.Players
.getCurrentPlayerId(getGoogleApiClient()));
for (Participant p : participants) {
// If the participants is not me
final String participantId = p.getParticipantId();

if (!myId.equals(participantId)) {
// We send the message in reliable way
Games.RealTimeMultiplayer
.sendReliableMessage(getGoogleApiClient(),
mReliableMessageSentCallback,
message, mRoom.getRoomId(), participantId);
}
}
} else {
showToast(No room connected);
}
}


In questo frammento di codice abbiamo evidenziato il codice utilizzato per
ottenere lid del giocatore corrente, ovvero quello che in questo momento ha eseguito
il login e sta eseguendo questo codice, oltre alla chiamata del precedente metodo per
linvio effettivo del messaggio nella modalit reliable. La nostra implementazione
dellinterfaccia di callback molto semplice e permette semplicemente di
visualizzare un messaggio di log. Abbiamo poi passato il messaggio come un array di
byte, lidentificatore della eventuale Room e lid del partecipante di destinazione. Nel
codice del metodo di callback dellinterfaccia ReliableMessageSentCallback notiamo come
sia possibile gestire diversi valori dello status code.

@Override
public void onRealTimeMessageSent(final int statusCode, final int tokenId,

final String recipientParticipantId) {


// We manage the different type of status
String logMessage = null;
switch (statusCode) {
case
GamesStatusCodes.STATUS_REAL_TIME_MESSAGE_SEND_FAILED:
logMessage = Message sending failed to player

+ recipientParticipantId;
break;
case GamesStatusCodes.STATUS_REAL_TIME_ROOM_NOT_JOINED:
logMessage = Player + recipientParticipantId
+ has not joined the room;
break;
case GamesStatusCodes.STATUS_OK:
default:
logMessage = Message send successfully to
+ recipientParticipantId + with token + tokenId;
}
showToast(logMessage);
Log.i(BaseGameFragment.TAG, logMessage);
}


Per quello che riguarda la gestione dellinvio del messaggio nella modalit
unreliable si hanno diverse possibilit, legate ad altrettanti metodi della classe
Games.RealTimeMultiplayer. Per inviare un messaggio unreliable a uno specifico utente
potremmo utilizzare il seguente metodo:

public abstract int sendUnreliableMessage (GoogleApiClient apiClient,

byte[] messageData, String roomId, String recipientParticipantId)



Nel caso di pi partecipanti esiste il seguente overload della stessa classe:

public abstract int sendUnreliableMessage (GoogleApiClient apiClient,

byte[] messageData, String roomId, List<String> recipientParticipantIds)



Nel nostro caso abbiamo invece utilizzato il seguente metodo, che ci permette di
implementare la logica del caso reliable ovvero linvio del messaggio a tutti gli

altri. In questo caso esiste infatti il seguente metodo:



public abstract int sendUnreliableMessageToOthers (GoogleApiClient apiClient,

byte[] messageData, String roomId)



Labbiamo utilizzato nel nostro metodo, collegandolo alla selezione del relativo
Button.

private void sendUnreliableMessage(final String messageToSend) {

if (mRoom != null) {
// We get the message as array of bytes
byte[] message = messageToSend.getBytes();
// Send the message to all the other players
Games.RealTimeMultiplayer
.sendUnreliableMessageToOthers(getGoogleApiClient(), message,
mRoom.getRoomId());
} else {
showToast(No room connected);
}
}


Come possiamo notare, le uniche informazioni che servono sono quelle relative al
messaggio e allidentificatore della Room.
A questo punto la gestione della ricezione del messaggio banale, in quanto si ha
la chiamata del metodo di callback dellinterfaccia RealTimeMessageReceivedListener, che
abbiamo implementato nel seguente modo:

private final RealTimeMessageReceivedListener mMessageReceivedListener =

new RealTimeMessageReceivedListener() {
@Override

public void onRealTimeMessageReceived(RealTimeMessage realTimeMessage) {


// We define if the message is reliable or not
final boolean isReliable = realTimeMessage.isReliable();
// We get the data
final String message = new String(realTimeMessage.getMessageData());
// The sender participant
final String participantId = realTimeMessage
.getSenderParticipantId();
if (isReliable) {
showToast(Receiver message: + message
+ as reliable message from: + participantId);
} else {
showToast(Receiver message: + message
+ as unreliable message from: + participantId);
}
}
};


Non ci resta che eseguire un test, creando una Room attraverso una delle modalit
di invito viste in precedenza e provando a inviare messaggi nelle due modalit. Nel
caso dellinvio reliable il risultato rappresentato nella Figura 10.42.

Figura 10.42 Ricezione di un messaggio nella modalit reliable.

Per concludere questo paragrafo diamo qualche indicazione sulluso della modalit
Socket in alternativa a quella unreliable. Queste due modalit non possono coesistere,
per cui non possibile utilizzare una modalit unreliable quando attiva la modalit
Socket. Lattivazione di questa modalit avviene in fase di creazione della Room
attraverso il Builder della classe RoomConfig. Il metodo da richiamare il seguente:

public RoomConfig.Builder setSocketCommunicationEnabled(boolean enableSockets)


Per stabilire una connessione con un particolare partecipante possibile utilizzare
il seguente metodo, che restituisce un oggetto di tipo RealTimeSocket:

public abstract RealTimeSocket getSocketForParticipant(GoogleApiClient

apiClient, String roomId, String participantId)



Da questo poi possibile ottenere gli stream di input e output attraverso i
corrispondenti metodi getInputStream() e getOutputStream() analogamente a quanto

avviene nella gestione dei Socket classici. A questo punto la connessione stabilita
per cui possibile gestire linvio e la ricezione dei dati. Come detto, questa modalit
permette una gestione pi fine dei dati, al prezzo di un maggiore sforzo in fase di
sviluppo; infatti necessario gestire tutto in background, preoccupandosi di chiudere
gli stream dopo luso.

Gioco a turni
Unaltra modalit di gioco che le Game API ci mettono a disposizione quella a
turni: un certo numero di partecipanti partecipa a uno stesso gioco modificandone lo
stato uno alla volta. I vari partecipanti non devono essere necessariamente collegati
nello stesso momento e vengono invitati secondo modalit che somigliano molto a
quelle viste nel paragrafo precedente. A differenza della modalit in realtime, in
questo caso vengono definite delle informazioni relative al Game, che vengono
quindi condivise tra i vari giocatori, i quali le modificano a turno ricevendo poi
notifiche relative alle modifiche fatte da altri giocatori. Si tratta di informazioni che
vengono gestite dal server di Google e che possono essere al massimo di 128 KB. Un
concetto molto importante, in questo caso, quello di Match, che pu essere in uno
dei seguenti stati che vengono gestiti dal server di Google e che necessario
consultare per ogni azione dei giocatori.

ACTIVE
AUTO_MATCHING
COMPLETECANCELED
EXPIRED


Quando nello stato ACTIVE il gioco attivo e quindi i vari giocatori possono, a
turno, eseguire le proprie azioni. Nello stato AUTO_MATCHING il gioco non pu procedere e
deve necessariamente attendere che altri giocatori si aggiungano al gioco. Lobiettivo
di ogni gioco quello di vincere. Nel caso di giochi a turno vi quasi sempre uno
stato che indica la fine del gioco e quindi la determinazione di un vincitore. Quando
questo avviene il gioco nello stato COMPLETE. In questo caso il sistema si preoccupa di
inviare ai vari partecipanti delle notifiche in relazione alla conclusione del gioco e
quindi alla determinazione del vincitore, se disponibile. Qualora invece il giocatore
che ha creato il gioco decidesse di annullarlo, il match viene messo nello stato
CANCELED. Anche in questo caso il sistema si preoccupa, in dipendenza del tipo di gioco,
di inviare le opportune notifiche ai vari partecipanti. Infine un Match viene messo
nello stato EXPIRED quando dopo due settimane nessuno degli invitati ha accettato
linvito e quindi di fatto il gioco viene cancellato.
Per capire il ciclo di vita di un Match proviamo a descrivere lo svolgersi di una
possibile partita secondo questa modalit. Innanzitutto vi deve essere un utente che
inizia il Match attraverso le diverse modalit di invito che abbiamo visto nel
paragrafo precedente. Quando il gioco inizia dobbiamo verificare se esistono le

corrispondenti informazioni. Nel caso queste informazioni fossero null, sar


responsabilit di questo utente provvedere alla loro inizializzazione. A questo punto il
giocatore corrente potr fare la propria mossa, dopo la quale dovr eseguire due
operazioni fondamentali: aggiornare lo stato del gioco e decidere lutente successivo.
Per fare questo ci sono diverse possibilit, che dipendono dal particolare gioco. Le
API a disposizione ci permettono di fare in modo che la mossa successiva venga fatta
dallo stesso giocatore, da un altro che abbiamo invitato in precedenza o addirittura da
un giocatore nella modalit auto-pick. In questultimo caso il gioco viene messo nello
stato AUTO_MATCHING fino a che il giocatore successivo non si rende disponibile. Questi
passi dovranno poi ripetersi fino al termine della partita.
Un esempio di utilizzo di questa modalit descritto nella classe
TurnMultiplayerGameFragment, cui arriviamo attraverso la corrispondente voce nel menu
delle opzioni, come fatto in precedenza. Prima di procedere dobbiamo ricordarci di
attivare la modalit a turni nella console di Google Play, come visualizzato nella
Figura 10.43.

Figura 10.43 Attivazione della modalit a turni.

Anche in questo caso abbiamo implementato lo scenario pi frequente, che


prevede lutilizzo della UI messa a disposizione dalle Game API per la raccolta dei
giocatori. Per fare questo abbiamo aggiunto un Button, selezionando il quale
eseguiamo il seguente codice:

Intent inviteIntent = Games.TurnBasedMultiplayer

.getSelectOpponentsIntent(getGoogleApiClient(), MIN_INVITE_PLAYERS,
MAX_INVITE_PLAYERS, true);
startActivityForResult(inviteIntent, TURN_INVITE_REQUEST_ID);

Come possiamo notare, loggetto da utilizzare Games.TurnBasedMultiplayer, il quale ci


mette a disposizione il metodo getSelectOpponentsIntent() che prevede come parametri il
numero minimo e massimo di giocatori e un boolean che ci permette di indicare se
abilitata o meno lopzione di auto-match. Anche in questo caso si tratta di un metodo
che ci restituisce un Intent, da lanciare secondo la modalit startActivityForResult() di
cui gestiremo il risultato nel metodo di callback onActivityResult(). Ci non si
differenzia di molto da quanto fatto nel caso di gioco realtime descritto nel paragrafo
precedente.

// We get the invited players
final ArrayList<String> invitedPlayers =

data.getStringArrayListExtra(Games.EXTRA_PLAYER_IDS);
// We get information related to the auto match player
Bundle autoMatchCriteria = null;
int minAutoMatchPlayers = data

.getIntExtra(Multiplayer.EXTRA_MIN_AUTOMATCH_PLAYERS, 0);
int maxAutoMatchPlayers = data

.getIntExtra(Multiplayer.EXTRA_MAX_AUTOMATCH_PLAYERS, 0);
if (minAutoMatchPlayers > 0) {

autoMatchCriteria = RoomConfig.createAutoMatchCriteria(
minAutoMatchPlayers, maxAutoMatchPlayers, 0);
} else {

autoMatchCriteria = null;
}
TurnBasedMatchConfig turnBasedMatchConfig = TurnBasedMatchConfig.builder()

.addInvitedPlayers(invitedPlayers)
.setAutoMatchCriteria(autoMatchCriteria)
.build();
Games.TurnBasedMultiplayer.createMatch(getGoogleApiClient(),

turnBasedMatchConfig)
.setResultCallback(mMatchInitiatedCallback);
getActivity().getWindow().addFlags(WindowManager

.LayoutParams.FLAG_KEEP_SCREEN_ON);

Come possiamo vedere dal precedente codice, anche in questo caso andiamo a
leggere le informazioni relative agli identificatori dei partecipanti ed eventualmente
quelle relative alla modalit auto-match. Laspetto diverso riguarda il fatto che ora
andiamo a creare un oggetto di tipo TurnBasedMatchConfig che rappresenta appunto
loggetto che incapsula tutte le informazioni relative al gioco a turni che vogliamo
creare. Si tratta delloggetto che utilizziamo come parametro del seguente metodo,
che ci permette di creare il Match:

public abstract PendingResult<TurnBasedMultiplayer.InitiateMatchResult>

createMatch(GoogleApiClient apiClient, TurnBasedMatchConfig config)



Come possiamo notare, si tratta di un metodo che restituisce un oggetto di tipo
ResultCallback che abbiamo imparato pi volte a gestire. In questo caso abbiamo creato
una sua implementazione come variabile distanza, in modo da semplificare la
leggibilit del codice. Se ora lanciamo la nostra applicazione, verr visualizzata la
stessa interfaccia rappresentata nella Figura 10.34, attraverso la quale arriveremo ad
avere il numero di giocatori richiesti per lavvio della partita.
Tutta la gestione del match implementata nel nostro ResultCallback, che restituisce
un oggetto di tipo TurnBasedMultiplayer.InitiateMatchResult che abbiamo gestito nel
seguente modo:

private final ResultCallback<TurnBasedMultiplayer.InitiateMatchResult>

mMatchInitiatedCallback =
new ResultCallback<TurnBasedMultiplayer.InitiateMatchResult>() {
@Override
public void onResult(TurnBasedMultiplayer.InitiateMatchResult
initiateMatchResult) {
if (initiateMatchResult.getStatus().isSuccess()) {
// We get the information related to the created Match
final TurnBasedMatch match =
initiateMatchResult.getMatch();
// We check if we already have data. If not we have to

// create the data for the match


byte[] matchData = match.getData();
if (matchData == null) {
// we have to init the Match
initMatch(match);
} else {
// In this case the Match already exists
// so we have to make our move
showToast(Make your move);
}
} else {
showToast(Error inviting players);
}
}
};


Dopo aver verificato lo status del risultato, abbiamo ottenuto loggetto
TurnBasedMatch creato in precedenza, attraverso il quale otteniamo i dati associati al
match attraverso il metodo getData(). Qualora la partita fosse appena iniziata, tali
informazioni saranno null, per cui dovremo provvedere alla prima inizializzazione nel
nostro metodo di utilit initMatch(). Qualora la partita fosse gi iniziata e quindi vi
fossero gi delle informazioni, visualizziamo semplicemente un messaggio che indica
il fatto che lutente pu svolgere la propria mossa attraverso gli strumenti offerti dal
gioco, che, nel nostro caso, sono rappresentati da un semplice pulsante collegato al
metodo makeMove(), che vedremo pi avanti. Per quello che riguarda lo stato del gioco,
di solito si crea una classe che ne incapsula tutte le informazioni. Nel nostro caso
abbiamo creato la classe TurnMatchState, che ha come unica informazione la propriet
, di tipo String.

state

NOTA
Ovviamente si tratta di una classe che nel caso specifico potrebbe anche essere evitata, ma che
abbiamo creato come dimostrazione di un pattern molto comune.

Quale sia lutente che esegue la prima mossa dipende dal tipo di gioco. Potrebbe
essere un giocatore scelto casualmente oppure, come nel nostro caso, il giocatore che
ha iniziato la partita. bene sottolineare come la notifica ai giocatori invitati non
avvenga fino al loro turno di gioco. Nel nostro esempio lutente che ha creato la
partita quello che ha il pallino del gioco, che quindi deve fare la prima mossa.
NOTA
In caso contrario dovremo comunque implementare il codice per decidere quale debba essere il
giocatore successivo.

Descriviamo limplementazione del metodo initMatch(), il quale dovr inizializzare


loggetto TurnMatchState e decidere chi debba essere il prossimo utente a giocare.
Abbiamo creato il seguente metodo:

private void initMatch(final TurnBasedMatch match) {

// We create the MatchState


mMatchState = TurnMatchState.get(TurnMatchState.STATE_0);
// The Other player
String otherPlayerId = getOtherPlayerId(match);
Games.TurnBasedMultiplayer.takeTurn(getGoogleApiClient(),
match.getMatchId(),
mMatchState.asByteArray(), otherPlayerId).setResultCallback(
new ResultCallback<TurnBasedMultiplayer.UpdateMatchResult>() {
@Override
public void onResult(TurnBasedMultiplayer.UpdateMatchResult result) {
if (result.getStatus().isSuccess()) {
// We get the new Match state
mMatch = result.getMatch();
if (mMatch != null) {
mMatchState = TurnMatchState
.get(mMatch.getData());
}
showToast(Turn OK);

}
}
});
}


Per partire inizializziamo loggetto TurnMatchState e poi utilizziamo il seguente
metodo della classe Games.TurnBasedMultiplayer per decidere quale debba essere il player
successivo.

public abstract PendingResult<TurnBasedMultiplayer.UpdateMatchResult> takeTurn
(GoogleApiClient apiClient, String matchId,

byte[] matchData, String pendingParticipantId)



Il metodo necessita, come parametri, dellidentificatore del match, dei dati che il
giocatore ha modificato nel proprio turno e che vuole condividere con gli altri, e
quindi dellidentificativo del partecipante a cui si vuole passare la palla. Nel nostro
caso abbiamo creato il metodo di utilit getOtherPlayerId(), che ci permette di ottenere
il primo partecipante diverso da quello corrente. Ovviamente, in un gioco reale,
questo metodo, o equivalente, conterr la logica che permette di decidere quale debba
essere il giocatore successivo. Importante notare come il riferimento alloggetto di
tipo TurnBasedMatch venga aggiornato in caso di successo; in questo caso esso contiene
infatti le informazioni relative allo stato del match.
Se ora lanciamo la nostra applicazione e invitiamo un utente, si avr la creazione
del match, linizializzazione del corrispondente stato e il passaggio del turno
allutente invitato. Questultimo evento permette linvio della notifica, con
conseguente invito rappresentato nella Figura 10.44.

Figura 10.44 Ricezione della notifica per il proprio primo turno.

Qualora la notifica fosse relativa ai turni successivi, la schermata sarebbe


leggermente diversa, come indicato nella Figura 10.45.

Figura 10.45 Indicazione del proprio turno.

A questo punto lutente pu decidere di accettare o rifiutare la sfida. Nel primo


caso dovremo gestire le informazioni dellinvito in modo analogo a quanto fatto nel
caso del multiplayer realtime. Andiamo quindi nell Activity principale descritta dalla

classe MainActivity e precisamente nel metodo onConnected() aggiungendo le seguenti


righe di codice:

// We check if the game is turn based
TurnBasedMatch turnBasedMatch = connectionHint

.getParcelable(Multiplayer.EXTRA_TURN_BASED_MATCH);
if (turnBasedMatch != null) {

// We create the turn Fragment


final TurnMultiplayerGameFragment turnMultiplayerFragment =
TurnMultiplayerGameFragment.newInstance(turnBasedMatch);
// We add the Fragment
getFragmentManager().beginTransaction()
.replace(R.id.anchor_point, turnMultiplayerFragment).commit();
return;
}


Loggetto ricevuto attraverso lextra di tipo Parcelable associato alla costante
di tipo TurnBasedMatch e contiene le informazioni

Multiplayer.EXTRA_TURN_BASED_MATCH

relative al gioco a turni corrispondente allinvito. Come fatto nel caso del
multiplayer, abbiamo anche qui creato un nuovo metodo factory per il fragment,
passando il riferimento alloggetto relativo al Match che viene poi utilizzato nel
metodo onActivityCreated() nel seguente modo:

public void onActivityCreated(Bundle savedInstanceState) {

super.onActivityCreated(savedInstanceState);
// We check if we have an invitation. If yes we create
// the related room and accept it
final TurnBasedMatch match = getArguments()
.getParcelable(INVITATION_ARG_KEY);
if (match != null) {
mMatch = match;

mMatchState = TurnMatchState.get(match.getData());
// We get the information related to the turn match
showToast(Make your move);
}
}


Verifichiamo infatti la presenza delloggetto contenuto nel Bundle degli argomenti
del Fragment e, nel caso, salviamo i riferimenti al TurnBasedMatch e alle informazioni in
esso contenute relative allo stato del gioco. A questo punto anche il secondo
giocatore riceve le informazioni relative alla partita ed in grado di eseguire la
propria mossa. Nella nostra applicazione abbiamo fatto in modo che ciascun client
potesse eseguire le azioni del proprio turno semplicemente selezionando il Button
Send Move, alla cui selezione abbiamo associato il contenuto del metodo makeMove()
che ci accingiamo a descrivere nelle parti principali. Dopo un controllo sulla reale
presenza di un Match, utilizziamo la seguente riga di codice per sapere se possiamo
effettivamente eseguire il nostro turno oppure se il turno di qualche altro utente:

final boolean isMyTurn = match.getTurnStatus() ==

TurnBasedMatch.MATCH_TURN_STATUS_MY_TURN;

Qualora non fosse il turno del giocatore corrente, non faremo altro che visualizzare
il corrispondente messaggio di toast. Se invece si tratta effettivamente del turno del
giocatore corrente, andiamo a calcolare il nuovo stato del Match attraverso le
seguenti righe:

// We change the status of the game
TurnMatchState currentState = TurnMatchState.get(match.getData());
// We go to the next state
currentState.nextState();


Attraverso il metodo getData() otteniamo lo stato come array di byte, che
utilizziamo per linizializzazione delloggetto TurnMatchState. Nella classe TurnMatchState
abbiamo infatti implementato una specie di macchina che pu assumere quattro stati
differenti prima di terminare. Ciascun giocatore non fa altro che passare dallo stato
corrente a quello successivo, fino allo stato finale, che provocher la fine del gioco.

Per questo motivo il passaggio successivo consiste nel verificare se il gioco finito
oppure no. Qualora il gioco proseguisse non facciamo altro che richiamare
nuovamente il metodo takeTurn() per la notifica al sistema del nuovo stato, restituendo
quindi il controllo agli altri giocatori.

String otherPlayerId = getOtherPlayerId(match);
Games.TurnBasedMultiplayer.takeTurn(getGoogleApiClient(),

match.getMatchId(), currentState.asByteArray(),
otherPlayerId).setResultCallback(
new ResultCallback<TurnBasedMultiplayer.UpdateMatchResult>() {
@Override
public void onResult(TurnBasedMultiplayer.UpdateMatchResult result) {
if (result.getStatus().isSuccess()) {
mMatch = result.getMatch();
if (mMatch != null) {
mMatchState = TurnMatchState.get(mMatch.getData());
}
showToast(Turn OK);
} else {
showToast(Error making turn +
result.getStatus().getStatusMessage());
}
}
});


La modalit seguita la stessa del caso della prima mossa. Al termine del gioco il
controllo passa al nostro metodo di utilit manageEndGame(), che descriveremo di seguito.
Prima di invitare il lettore a verificare leffettivo ping pong delle mosse,
dobbiamo implementare un meccanismo che ci permetta di ricevere le notifiche
relative al fatto che un altro utente abbia eseguito la propria mossa. Anche in questo

caso esiste uninterfaccia, OnTurnBasedMatchUpdateReceivedListener, che abbiamo


implementato in modo molto semplice:

private OnTurnBasedMatchUpdateReceivedListener mMatchUpdateReceivedListener =

new OnTurnBasedMatchUpdateReceivedListener() {
@Override
public void onTurnBasedMatchReceived(TurnBasedMatch turnBasedMatch) {
mMatch = turnBasedMatch;
showToast(Make your move);
}
@Override
public void onTurnBasedMatchRemoved(String s) {
Log.i(TAG, onTurnBasedMatchRemoved);
}
};


Da notare come si provveda allaggiornamento del riferimento alloggetto relativo
al Match. Per registrare questa implementazione dobbiamo utilizzare il seguente
metodo definito dalla classe Games.TurnBasedMultiplayer, che abbiamo utilizzato nel
metodo onCreateView() del Fragment.

public abstract void registerMatchUpdateListener (GoogleApiClient apiClient,

OnTurnBasedMatchUpdateReceivedListener listener)

A questo punto siamo pronti per simulare il comportamento di un match a turni.
Quello che ci manca la gestione della fine della partita, che nel nostro caso stata
implementata attraverso il seguente metodo:

private void manageEndGame(final TurnBasedMatch match) {

Games.TurnBasedMultiplayer.finishMatch(getGoogleApiClient(),
match.getMatchId())

.setResultCallback(new
ResultCallback<TurnBasedMultiplayer.UpdateMatchResult>() {
@Override
public void onResult(TurnBasedMultiplayer
.UpdateMatchResult updateMatchResult) {
if (updateMatchResult.getStatus().isSuccess()) {
showToast(Match finished successfully!);
} else {
showToast(Error closing match!);
}
}
}
});


Qui abbiamo evidenziato lutilizzo del seguente metodo, che permette di notificare
a tutti i giocatori che il match terminato.

public abstract PendingResult<TurnBasedMultiplayer.UpdateMatchResult>

finishMatch (GoogleApiClient apiClient, String matchId)



In questo paragrafo abbiamo visto un caso tipico di gestione di un gioco a turni
attraverso lutilizzo delle Game API. Esse ci mettono a disposizione moltissimi
strumenti per un controllo pi fine delle modalit di gioco, che richiederebbero un
testo a parte e per le quali rimandiamo alla documentazione ufficiale.

Game Gift
Uninteressante funzionalit messa a disposizione dalle Game API permette linvio
di quelli di gift (regali) e wish (desideri) agli utenti che condividono con noi il gioco.
I gift sono contenuti che un utente intende regalare ad altri utenti del gioco. I wish
sono invece delle cose che lutente richiede agli altri per il completamento di un
obiettivo o semplicemente per progredire nel gioco. Come vedremo, anche in questo
caso le Game API ci mettono a disposizione la UI e una serie di strumenti per linvio
e la ricezione di questo tipo di elementi. Prima di descrivere il codice scritto al
riguardo vediamo velocemente quali sono le propriet di questi nuovi oggetti, che
andremo a specificare in fase di creazione. Innanzitutto dovremo indicare il tipo,
ovvero gift o wish e quindi un payload con le informazioni che intendiamo inviare
agli altri utenti. Si tratta di un array di byte la cui dimensione reperibile attraverso
un opportuno metodo getMaxPayloadSize(). Oltre ad altre informazioni, come unicona,
un nome e una descrizione completa, vi sono altre informazioni temporali come la
durata di validit, il tempo di creazione e di scadenza. Infine sar importante definire
lo stato, che inizialmente sar RECIPIENT_STATUS_PENDING.
Per dimostrare come gestire questo tipo di informazioni abbiamo quindi creato la
classe GiftWishFragment, accessibile ancora attraverso la corrispondente voce nel menu
delle opzioni. Anche in questo caso abbiamo creato dei Button a cui abbiamo associato
le varie funzionalit da collaudare. La prima di queste quella associata alla funzione
di creazione di un Gift, attraverso il seguente codice:

Intent giftIntent = Games.Requests.getSendIntent(getGoogleApiClient(),

GameRequest.TYPE_GIFT,MyGift.getBytes(),
GIFT_WISH_LIFETIME_DAYS,
icon, Gift Example);
startActivityForResult(giftIntent, GIFT_REQUEST_ID);


Per accedere a queste funzionalit si utilizza loggetto Games.Requests e precisamente
il suo metodo:

public abstract Intent getSendIntent (GoogleApiClient apiClient,

int type, byte[] payload, int requestLifetimeDays,

Bitmap icon, String description)



Questo restituisce lIntent da lanciare nella modalit startActivityForResult() per la
creazione di un Gift. Notiamo come siano presenti come parametri le informazioni
cui avevamo accennato in precedenza; tra queste assume fondamentale importanza
linformazione associata al Gift, che dovr essere elaborata in modo opportuno in
ricezione. Nel nostro caso la selezione del pulsante di creazione di un Gift porterebbe
alla visualizzazione della schermata rappresentata nella Figura 10.46, nella quale
notiamo la possibilit di scegliere lutente cui inviare il regalo descritto nella parte
inferiore. Confermando la selezione degli utenti di destinazione si ha linvio vero e
proprio dellinformazione agli utenti selezionati. Prima di vedere come questa
informazione viene ricevuta e gestita, facciamo notare come sia possibile
implementare il metodo di callback onActivityResult() nel modo descritto nel seguente
codice, attraverso il quale possiamo avere indicazione sulleffettivo esito dellinvio:

if (requestCode == GIFT_REQUEST_ID) {

switch (resultCode) {
case Activity.RESULT_OK:
showToast(Gift successfully sent!);
break;
case GamesActivityResultCodes.RESULT_SEND_REQUEST_FAILED:
showToast(Error sending Gift);
break;
}
}

Figura 10.46 Creazione di un regalo.

Una volta che il regalo stato inviato, il sistema si occupa di generare una notifica
nel dispositivo di destinazione, selezionando la quale si ottiene quanto rappresentato
nella Figura 10.47. Attraverso questa interfaccia lutente potr quindi accettare o
rifiutare il regalo. Nel caso di rifiuto il sistema si preoccuper di gestire la sua
eliminazione in modo automatico. Laccettazione del regalo dovr invece essere
gestita in maniera esplicita, ma in un modo analogo a quelli visti in precedenza per il
multiplayer.
Anche in questo caso andiamo a modificare il metodo onConnected() della classe
attraverso il seguente codice:

MainActivity


ArrayList<GameRequest> gameRequests =

Games.Requests.getGameRequestsFromBundle(connectionHint);
if (gameRequests != null) {

// We create the turn Fragment


final GiftWishFragment giftWishFragment =
GiftWishFragment.newInstance(gameRequests);
// We add the Fragment
getFragmentManager().beginTransaction()
.replace(R.id.anchor_point, giftWishFragment).commit();
}

Figura 10.47 Schermata di ricezione del Gift.

Attraverso il metodo getGameRequestsFromBundle() delloggetto Games.Requests andiamo a


vedere se esiste linformazione relativa a un Gift (oppure Wish) nel parametro
connectionHint. Qualora fosse disponibile non facciamo altro che istanziare il nostro

attraverso il corrispondente metodo statico di factory. Anche in questo

GiftWishFragment

caso la logica di gestione delloggetto viene fatta nel metodo onActivityCreated() nel
seguente modo. Innanzitutto andiamo a verificare leffettiva presenza delloggetto
ricevuto attraverso la notifica.

final ArrayList<GameRequest> gameRequests =

getArguments().getParcelableArrayList(GIFT_WISH_ARG_KEY);

Nel caso questo oggetto fosse disponibile, abbiamo bisogno di creare una struttura
dati temporanea che ci permetta di gestire non solo le GameRequest, ma anche i
corrispondenti identificatori che ci serviranno per laccettazione. Per questo motivo
utilizziamo sia una List che una Map nel seguente modo:

// We get the id for the gift requests
ArrayList<String> requestIds = new ArrayList<String>();
// We use a Map for mapping the id for the request to the request itself
final HashMap<String, GameRequest> gameRequestMap =

new HashMap<String, GameRequest>();


// We initialize the map
for (GameRequest request : gameRequests) {

String requestId = request.getRequestId();


requestIds.add(requestId);
gameRequestMap.put(requestId, request);
}


Per provvedere allaccettazione delle richieste possibile utilizzare il seguente
metodo della classe Games.Requests, che necessita appunto degli identificatori che
abbiamo appena ottenuto:

public abstract PendingResult<Requests.UpdateRequestsResult> acceptRequests

(GoogleApiClient apiClient, List<String> requestIds)



Nel nostro esempio provvederemo alla sola accettazione, ma ovviamente qualora si
volesse rifiutare baster richiamare il metodo:


public abstract PendingResult<Requests.UpdateRequestsResult> dismissRequest

(GoogleApiClient apiClient, String requestId)



Oltre a queste versioni che permettono la gestione di pi richieste ne esistono
altrettanti che invece permettono laccettazione o il rifiuto di ununica richiesta. Nel
nostro caso abbiamo utilizzato il seguente il seguente codice:

Games.Requests.acceptRequests(getGoogleApiClient(), requestIds)

.setResultCallback(new
ResultCallback<Requests.UpdateRequestsResult>() {
@Override
public void onResult(Requests.UpdateRequestsResult result) {
// WE MANAGE ALL THE RESPONSES
}
});


La gestione delle risposte nellimplementazione delloggetto ResultCallback invece
la seguente e permette di verificare per ogni risposta ricevuta il corrispondente tipo,
delegandone poi la gestione ad altrettanti metodi di utilit che non fanno altro che
estrarre linformazione associata e, nel nostro caso, visualizzarla attraverso un
messaggio di Toast.

for (String requestId : result.getRequestIds()) {

if (!gameRequestMap.containsKey(requestId)
|| result.getRequestOutcome(requestId)
!= Requests.REQUEST_UPDATE_OUTCOME_SUCCESS) {
continue;
}
final int reqType = gameRequestMap.get(requestId).getType();
switch (reqType) {

case GameRequest.TYPE_GIFT:
// Process the game gifts request
manageGift(gameRequestMap.get(requestId));
break;
case GameRequest.TYPE_WISH:
// Process the wish request
manageWish(gameRequestMap.get(requestId));
break;
}
}


I metodi di elaborazione nel nostro esempio sono molto semplici, ma in un
contesto reale dovranno contenere la logica di elaborazione delle informazioni
ricevute. Potremo infatti utilizzare queste informazioni per visualizzare uninterfaccia
particolare allutente, per sbloccare un obiettivo o altro ancora.

private void manageGift(final GameRequest giftRequest) {

// We read the info into the gift


final String gift = new String(giftRequest.getData());
showToast(Received Gift: + gift);
}
private void manageWish(final GameRequest wishRequest) {

// We read the info into the wish


final String wish = new String(wishRequest.getData());
showToast(Received Wish: + wish);
}


Nella descrizione fatta finora abbiamo sempre parlato di Gift, ma lo stesso vale
esattamente per i Wish. Selezionando il secondo Button il lettore potr verificare come
lo stesso meccanismo valga per la gestione dei Wish.

Gestione degli eventi e missioni


Unaltra funzionalit molto interessante quella che permette di raccogliere
informazioni relative al comportamento del giocatore e quindi definire, sulla base dei
dati raccolti, le missioni. Possiamo, per esempio, avere indicazioni sul livello del
giocatore o sulla sua frequenza di gioco, per proporgli missioni a cui associare poi dei
premi. Anche in questo caso si tratta di oggetti che vanno configurati attraverso la
Play Console selezionando la voce Eventi nel menu a sinistra, arrivando alla
schermata rappresentata nella Figura 10.48, nella quale possiamo notare come le
informazioni da specificare siano relative al nome, alla formattazione e al tipo del
valore associato, alleventuale icona fino al classico ordine. Notiamo poi come sia
possibile anche decidere se rendere subito visibile levento oppure no. Nel nostro
caso abbiamo semplicemente aggiunto tre eventi del tipo Event#, di tipo numerico.
Una volta completato il processo di definizione degli eventi, veniamo riportati alla
schermata rappresentata nella Figura 10.49, nella quale possiamo vedere gli eventi
con i relativi identificatori che la console ha associato a ciascuno di essi.
A questo punto la nostra applicazione dovr, in qualche modo, associare dei
punteggi ai nostri eventi, attraverso alcuni metodi delloggetto Games.Events. Per vedere
allopera questo meccanismo, abbiamo definito la classe EventQuestFragment, anchessa
accessibile attraverso la corrispondente voce del menu delle opzioni.

Figura 10.48 Inserimento di un evento.

Figura 10.49 Elenco degli eventi creati.

Nel nostro caso abbiamo implementato uninterfaccia simile a quella creata nel
caso delle leaderboard, che ci permette di definire la quantit di punti da associare a
un particolare evento e quindi inviarlo al server. Per farlo abbiamo utilizzato il
seguente metodo della classe Games.Events:

public abstract void increment(GoogleApiClient apiClient,

String eventId, int incrementAmount)



In particolare abbiamo associato alla selezione di ciascun Button il seguente codice
il quale riferito al primo evento, ma che viene replicato anche per gli altri due:

final String event1Id = getString(R.string.event_1);
Games.Events.increment(getGoogleApiClient(), event1Id, 1);


Una volta definiti i possibili eventi possiamo passare alla definizione delle
missioni sempre attraverso linterfaccia messa a disposizione della Console Google
Play. Selezioniamo quindi la voce Missioni in alto a sinistra e poi selezioniamo il
pulsante Aggiungi Missione, il quale ci porta alla schermata rappresentata nella Figura
10.50 nella quale notiamo la possibilit di specificare varie informazioni tra cui, oltre
alle solite icone e descrizioni, il numero di punti in un particolare evento. inoltre
possibile riferire il tutto a un determinato periodo temporale e caricare un file che
rappresenta leventuale premio. Questultimo pu essere un file di tipo qualunque,
ma di dimensioni non superiori ai 4 KB e dovr contenere tutte le informazioni utili
alla descrizione del premio.

Figura 10.50 Creazione di una missione.

Una volta creata la missione dobbiamo fare in modo che questa sia visibile
nellapplicazione. A tale scopo le Game API ci mettono a disposizione una UI che
possibile visualizzare attraverso le seguenti, poche, righe di codice, che abbiamo
associato alla selezione di un altro Button, di label Show Mission:

int[] selectors = new int[]{Quests.SELECT_OPEN};
Intent questsIntent = Games.Quests

.getQuestsIntent(getGoogleApiClient(), selectors);
startActivityForResult(questsIntent, 0);


Attraverso il metodo getQuestsIntent() otteniamo infatti lIntent da lanciare per la
visualizzazione della schermata riassuntiva delle missioni attive. Notiamo come sia
possibile filtrare le missioni in base al relativo stato.
NOTA
bene precisare che per poter essere collaudata, una missione deve essere attiva nel momento
in cui si sta eseguendo la nostra applicazione. bene, quindi, controllare in fase di creazione che
la data corrente sia compresa tra quelle specificate.

Nel nostro caso possiamo selezionare il pulsante e ottenere la schermata


rappresentata nella Figura 10.51, nella quale notiamo la visualizzazione della nostra
missione con lo stato corrente del giocatore.

Figura 10.51 Visualizzazione dello stato della missione.

Anche in questo caso le Game API ci mettono a disposizione diversi metodi per la
gestione di questa funzionalit, che invitiamo lutente a consultare nella
documentazione ufficiale. Lultimo aspetto che vediamo quello relativo
allinterfaccia QuestUpdateListener, che possibile implementare per ricevere notifica
del raggiungimento o meno di una missione. Una volta creata limplementazione di
questa interfaccia possibile registrarla attraverso la chiamata del seguente metodo
della classe Quests:

public abstract void registerQuestUpdateListener (GoogleApiClient apiClient,

QuestUpdateListener listener)

Questa istruzione va eseguita in corrispondenza della connessione e quindi sempre
nel metodo onConnected() della MainActivity; infatti in questa fase che il sistema

verifica la presenza delle missioni e del relativo stato.



Games.Quests.registerQuestUpdateListener(mGoogleApiClient, new QuestUpdateListener() {

@Override
public void onQuestCompleted(Quest quest) {
// We get the reward
Games.Quests.claim(mGoogleApiClient, quest.getQuestId(),
quest.getCurrentMilestone().getMilestoneId());
byte[] rewardData = quest.getCurrentMilestone()
.getCompletionRewardData();
Toast.makeText(MainActivity.this, Reward:
+ new String(rewardData), Toast.LENGTH_SHORT).show();
}
});


Come possiamo vedere, si tratta di uninterfaccia che definisce come unica
operazione quella di nome onQuestCompleted() che ci fornisce direttamente il riferimento
al Quest che utilizziamo poi per richiedere il premio attraverso il seguente metodo:

public abstract PendingResult<Quests.ClaimMilestoneResult> claim

(GoogleApiClient apiClient, String questId, String milestoneId)



A questo punto il tipo del premio e la modalit con cui viene utilizzato
dipenderanno dalla particolare applicazione.

Salvataggio dei progressi


Lultima funzionalit di cui ci occupiamo in questo capitolo riguarda la possibilit
di rendere persistenti le informazioni relative allo stato del gioco, in modo da poterlo
ripristinare successivamente, non solo sullo stesso dispositivo, ma anche in altri
dispositivi anche in ambienti diversi. Attraverso questi strumenti possiamo salvare i
nostri progressi e riprendere il gioco in un altro momento esattamente nello stesso
punto senza la necessit di ricominciare. Un aspetto importante di questa funzionalit
riguarda il forte legame con le API che abbiamo visto in relazione a Google Drive, e
in particolare quella che prende il nome di App Folder. Questo aspetto ha due
importanti conseguenze. La prima riguarda il fatto che la quantit di dati salvati va a
incidere sulleventuale Quota (quantit di spazio disponibile) di Google Drive,
mentre per quello che riguarda le Game API non esiste alcun vincolo del genere. La
seconda riguarda invece il fatto che i dati vengono salvati in una area privata
dellutente per quella specifica applicazione e quindi si pu considerare, in un certo
senso, privata. Un vantaggio nellutilizzo di Google Drive consiste invece nella
possibilit di accedere a queste informazioni anche nella modalit offline, sempre che
i dati siano stati in precedenza sincronizzati con il server; si tratta comunque di
unoperazione del tutto trasparente allo sviluppatore.
Le informazioni che salviamo sono tipicamente composte di due parti. La prima
rappresentata dai dati veri e propri, ovvero quelli che rappresentano lo stato del gioco
e che verranno quindi utilizzati per il suo ripristino. La seconda invece data da una
sorta di metadati che le Game API utilizzano per presentare allutente i vari
salvataggi attraverso uninterfaccia standard, che queste ci mettono a disposizione.
Per ciascun salvataggio dovremo specificare un nome e una descrizione. Il nome
quello utilizzato dallo sviluppatore per il salvataggio, mentre la descrizione quella
che viene effettivamente mostrata allutente. Seguono poi due informazioni temporali
relative al momento del salvataggio oltre a unindicazione del tempo di gioco;
questultima serve al giocatore per avere unidea del momento di salvataggio. Infine
possibile fornire unimmagine che verr mostrata al giocatore e che dovr essere
descrittiva del momento salvato. Nel caso di un gioco a tappe potrebbe, per esempio,
descrivere la tappa in corso.
Anche in questultimo caso abbiamo creato un Fragment attraverso la classe
, accessibile dalla classica voce del menu delle opzioni. Prima di

SaveStateFragment

descriverne il contenuto, si rende necessaria unaggiunta in fase di inizializzazione,


legata proprio alla necessit di utilizzare le funzioni di Google Drive.

Allinizializzazione delloggetto GoogleApiClient occorre aggiungere le righe di codice


evidenziate di seguito:

mGoogleApiClient = new GoogleApiClient.Builder(this)

.addApi(Games.API)
.addApi(Plus.API)
.addApi(Drive.API)

.addScope(Games.SCOPE_GAMES)
.addScope(Plus.SCOPE_PLUS_LOGIN)
.addScope(Drive.SCOPE_APPFOLDER)

.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();

Oltre allabilitazione delle API di Google Drive, dobbiamo infatti anche
aggiungere lo scope relativo allutilizzo della App Folder. bene inoltre ricordarsi di
attivare le API nella console di Google generando una chiave pubblica di accesso.
Per farlo sufficiente andare allindirizzo https://console.developers.google.com e seguire
le stesse istruzioni descritte nel capitolo dedicato a Google Drive.
Un ultimo passo di configurazione consiste nellattivare la funzionalit di
salvataggio nella console di Google Play. Per farlo selezioniamo la voce Dettagli del
nel menu a sinistra. Poi selezioniamo lopzione Partite Salvate, come indicato

Gioco

nella Figura 10.52 ricordandoci di salvare le impostazioni con lapposito pulsante.

Figura 10.52 Attivazione dellopzione di salvataggio.

Quella del salvataggio dello stato del gioco una funzionalit che spesso
accessibile al giocatore attraverso il menu delle opzioni. Nel nostro caso abbiamo
semplicemente aggiunto dei Button che ci permettono di simulare lo stesso

comportamento. Linterfaccia che permette allutente di salvare lo stato del gioco,


ripristinare lo stato salvato in precedenza o semplicemente vedere gli stati gi salvati
ci viene fornita direttamente dalle Game API ed accessibile attraverso il seguente
codice, che abbiamo legato alla selezione del primo Button di label Show Savings.

// We launch the saved state UI
final String label = getString(R.string.save_state_show_savings);
Intent savedGamesIntent = Games.Snapshots

.getSelectSnapshotIntent(getGoogleApiClient(),
label, true, true, NUM_SAVINGS_TO_SHOW);
startActivityForResult(savedGamesIntent, SAVINGS_REQUEST_ID);


Come possiamo notare abbiamo semplicemente utilizzato il seguente metodo della
classe Games.Snapshots il quale prevede di specificare alcuni parametri molto importanti.

public abstract Intent getSelectSnapshotIntent (GoogleApiClient apiClient,

String title, boolean allowAddButton,


boolean allowDelete, int maxSnapshots)

A parte il solito oggetto GoogleApiClient, notiamo la presenza del titolo della
schermata, ma soprattutto di due booleani che ci permettono di specificare se dare la
possibilit di salvare (allowAddButton) o di cancellare (allowDelete) lo stato del gioco o di
fare entrambe le cose. Infine possibile specificare il numero massimo di salvataggi
da visualizzare, attraverso il parametro maxSnapshots. Nel nostro caso abbiamo scelto di
visualizzare al massimo cinque salvataggi e di permettere sia laggiunta di nuovi
salvataggi, sia la cancellazione di quelli esistenti. Come ormai consuetudine si tratta
di un metodo che restituisce un Intent, che dovremo poi lanciare nella modalit
e quindi gestirne i valori restituiti nel metodi di callback

startActivityForResult()

. Lanciando lapplicazione e selezionando il primo Button viene

onAcivityResult()

visualizzata la schermata rappresentata nella Figura 10.53.


Inizialmente non riporta alcun salvataggio e quindi non ne permette il caricamento,
ma solamente la creazione. Se selezioniamo il pulsante di creazione otterremo la
chiamata del metodo onActivityResult(), che abbiamo implementato nel seguente
modo:


public void onActivityResult(int requestCode, int resultCode, Intent data) {

super.onActivityResult(requestCode, resultCode, data);


if (requestCode == SAVINGS_REQUEST_ID && data != null) {
if (data.hasExtra(Snapshots.EXTRA_SNAPSHOT_METADATA)) {
// In this case we have pressed the load so we have
// the Snapshot object with the data
SnapshotMetadata snapshotMetadata = (SnapshotMetadata)
data.getParcelableExtra(Snapshots
.EXTRA_SNAPSHOT_METADATA);
// We get the name for this Snapshot
mCurrentSavedGame = snapshotMetadata.getUniqueName();
// We load the data from the SnapShot
loadSnapshotData();
} else if (data.hasExtra(Snapshots.EXTRA_SNAPSHOT_NEW)) {
// We create the name of the saving for using the timestamp.
// We could use a random value
mCurrentSavedGame = snapshotTemp- +
System.currentTimeMillis();
// We save the data
saveSnapshotData(null);
}
}
}

Figura 10.53 Schermata di salvataggio.

Per creare un nuovo salvataggio, che in questo contesto si chiama Snapshot, lIntent
restituito conterr un extra associato alla costante Snapshots.EXTRA_SNAPSHOT_NEW. Nel
nostro caso creiamo un nome univoco utilizzando il timestamp corrente e
richiamiamo il nostro metodo di utilit saveSnapshotData() per la gestione del
salvataggio. Qualora avessimo invece selezionato uno dei salvataggi, avremmo
ottenuto il corrispondente oggetto SnapshotMetadata attraverso sempre un extra, questa
volta di nome Snapshots.EXTRA_SNAPSHOT_METADATA. Attraverso il suo metodo getUniqueName()
ne otteniamo il nome univoco, per poi implementare la logica di lettura nel nostro
metodo di utilit loadSnapshotData(). Da notare come non vi sia alcun controllo sul
.

resultCode

Il primo passo da compiere consiste nella creazione dello Snapshot, attraverso il


seguente metodo della classe Games.Snapshots:


public abstract PendingResult<Snapshots.OpenSnapshotResult> open

(GoogleApiClient apiClient, String fileName, boolean createIfNotFound)



Notiamo come i parametri richiesti siano il nome del file associato ai dati che si
vogliono salvare e un booleano che ci permette di indicare se lo Snapshot debba
essere creato qualora questo non fosse disponibile; nel nostro caso sar ovviamente
true. importante che la chiamata di questo metodo avvenga allinterno di un thread
diverso da quello della UI, per cui spesso si utilizza un AsyncTask, che a partire dal
nome del file restituisce un oggetto di tipo Snapshots.OpenSnapshotResult che incapsula le
informazioni relative allo Snapshots creato. Il contenuto del metodo saveSnapshotData()
sar quindi il seguente:

private void saveSnapshotData(final SnapshotMetadata metadata) {

new AsyncTask<String, Void, Snapshots.OpenSnapshotResult>() {


@Override
protected Snapshots.OpenSnapshotResult doInBackground(String
params) {
// Here we get the Snapshot using the open() method
if (metadata == null) {
return Games.Snapshots.open(getGoogleApiClient(),
mCurrentSavedGame, true).await();
} else {
return Games.Snapshots.open(getGoogleApiClient(), metadata)
.await();
}
}
@Override
protected void onPostExecute(Snapshots.OpenSnapshotResult
openSnapshotResult) {

super.onPostExecute(openSnapshotResult);
// Here e have to manage the result that could contain
// some error
int status = openSnapshotResult.getStatus().getStatusCode();
switch (status) {
case GamesStatusCodes.STATUS_OK:
case GamesStatusCodes.STATUS_SNAPSHOT_CONTENTS_UNAVAILABLE:
// Everything is ok so we persist the data
persist(openSnapshotResult.getSnapshot());
break;
case GamesStatusCodes.STATUS_SNAPSHOT_CONFLICT:
manageConflict(openSnapshotResult);
break;
}
}
}.execute();
}


Innanzitutto notiamo come il nostro metodo abbia un parametro di tipo
SnapshotMetadata, che pu essere, come nel caso di creazione dei dati, un valore null.
Ricordiamo che si tratta delloggetto che incapsula i metadati del salvataggio. Tutta la
logica definita allinterno di un AsyncTask, il cui metodo doInBackground() contiene la
chiamata delloverload del metodo open() corrispondente al caso in cui i metadati
siano disponibile oppure no. A tale proposito notiamo come venga utilizzata la
modalit sincrona, in quanto gi in esecuzione in background perch nel metodo
doInBackground() di un AsyncTask. Il risultato di questa chiamata quindi un oggetto di
tipo Snapshots.OpenSnapshotResult di cui andiamo a verificare lo status nel metodo
. Qualora tutto vada per il verso giusto, non facciamo altro che

onPostExecute()

richiamare il metodo persist() che rende di fatto persistenti i dati. Si potrebbe invece
verificare un conflitto quando i dati vengono modificati contemporaneamente da

dispositivi diversi. In quel caso abbiamo incapsulato la logica di gestione dei conflitti
nel nostro metodo di utilit manageConflict(). Il nostro metodo persist() il seguente:

private void persist(final Snapshot snapshot) {

// We save the data into the opened Snapshot


final String savedData = data- + System.currentTimeMillis();
snapshot.writeBytes(savedData.getBytes());
// We get the image
final Bitmap coverImage = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_launcher);
// Save the snapshot.
SnapshotMetadataChange metadataChange =
new SnapshotMetadataChange.Builder()
.setCoverImage(coverImage)
.setDescription(Persisted data at : +
Calendar.getInstance().getTime())
.build();
Games.Snapshots.commitAndClose(getGoogleApiClient(), snapshot,
metadataChange);
showToast(Persisted data + savedData);
Log.i(BaseGameFragment.TAG, Snapshot : + snapshot);
}


Lo Snapshot stato aperto in precedenza attraverso il metodo open() e ora pronto
per la scrittura dei dati, attraverso il seguente metodo:

public abstract boolean writeBytes(byte[] bytes);


Per quello che riguarda laggiornamento dei suoi metadata abbiamo creato
unistanza della classe SnapshotMetadataChange attraverso la sua classe di Builder. Tramite

questo oggetto possiamo impostare le informazioni relative alla descrizione e cover


image. Infine possiamo rendere effettivo il tutto attraverso il seguente metodo:

public abstract PendingResult<Snapshots.CommitSnapshotResult>

commitAndClose (GoogleApiClient apiClient, Snapshot snapshot,


SnapshotMetadataChange metadataChange)

stato introdotto un meccanismo al fine di ottimizzare laccesso al sistema di
persistenza attraverso ununica chiamata in corrispondenza del commit.
A questo punto non ci resta che provare a creare uno Snapshot e a creare il primo
salvataggio con dati al momento di test. Se tutto andato per il verso giusto
assisteremo alla visualizzazione di un messaggio di Toast e saremo in grado di
visualizzare nuovamente la schermata dei salvataggi al fine di verificarne lavventura
registrazione. Nel nostro caso otteniamo quanto rappresentato nella Figura 10.54,
dove notiamo essere presente anche la possibilit di cancellare leventuale dato.

Figura 10.54 Informazioni di salvataggio presenti nellelenco.

A questo punto non ci resta che selezionare lo stesso dato e accedere alle
informazioni salvate in precedenza. Selezionando lunico dato si ha nuovamente la
chiamata del metodo onActivityResult(), ma questa volta le informazioni relative ai
metadata sono disponibili e ci servono per ottenere il nome del file che andremo a
leggere, sempre in modalit asincrona, attraverso la seguente implementazione:

private void loadSnapshotData() {

new AsyncTask<Void, Void, String>() {


@Override
protected String doInBackground(Void params) {
// We open the Snapshot using its name
Snapshots.OpenSnapshotResult result =

Games.Snapshots.open(getGoogleApiClient(),
mCurrentSavedGame, true).await();
// We get the status of the reading
int status = result.getStatus().getStatusCode();
if (status == GamesStatusCodes.STATUS_OK) {
// In this case the reading was ok so we get the
// Snapshot object
Snapshot snapshot = result.getSnapshot();
// We read the data from it
final byte[] data = snapshot.readFully();
final String strData = new String(data);
return strData;
}
return null;
}
@Override
protected void onPostExecute(String result) {
if (result != null) {
showToast(Restored data: + result);
} else {
showToast(Error reading data: );
}
}
}.execute();
}


In questo caso lAsyncTask restituisce una String che abbiamo precedentemente
salvato. Per farlo abbiamo utilizzato il metodo open(), abbiamo ottenuto loggetto
Snapshot e quindi abbiamo estratto da esso i dati attraverso il suo metodo:


public abstract byte[] readFully();


A questo punto, se selezioniamo uno dei salvataggi dellelenco noteremo come
venga effettivamente letta linformazione precedentemente salvata. Concludiamo
questultimo paragrafo descrivendo la gestione di un eventuale conflitto nei dati
salvati e ripristinati. Intanto diciamo che ci accorgiamo del conflitto attraverso lo
status delloggetto di tipo Snapshots.OpenSnapshotResult, ottenuto a seguito dellapertura
di uno Snapshot. In questo caso lo stesso oggetto contiene anche le informazioni
relative allaltra versione di Snapshot che ha dato problemi e questo possibile
attraverso il seguente metodo:

public abstract Snapshot getConflictingSnapshot()


Sar responsabilit dellapplicazione esaminare le informazioni contenute nei due
oggetti, eleggendo quello da considerare valido. A questo punto, per impostare lo
Snapshot da considerare valido, risolvendo di fatto il conflitto, baster utilizzare il
seguente metodo:

public abstract PendingResult<Snapshots.OpenSnapshotResult> resolveConflict

(GoogleApiClient apiClient, String conflictId, Snapshot snapshot)



O, in alternativa, loverload:

public abstract PendingResult<Snapshots.OpenSnapshotResult> resolveConflict

(GoogleApiClient apiClient, String conflictId, String snapshotId,


SnapshotMetadataChange metadataChange, SnapshotContents
snapshotContents)

Nel nostro caso abbiamo implementato questa logica nel seguente modo:

private void manageConflict(final Snapshots.OpenSnapshotResult result) {

// We get the first Snapshot

Snapshot snapshot = result.getSnapshot();


// And the conflicting one
Snapshot conflictSnapshot = result.getConflictingSnapshot();
// We decide which one to keep as resolving one. To do that we
// use the information related to the last modified date
Snapshot resolvedSnapshot = snapshot;
if (snapshot.getMetadata().getLastModifiedTimestamp() <
conflictSnapshot.getMetadata().getLastModifiedTimestamp()) {
resolvedSnapshot = conflictSnapshot;
}
// We resolve the conflict
Snapshots.OpenSnapshotResult resolveResult =
Games.Snapshots.resolveConflict(
getGoogleApiClient(), result.getConflictId(), resolvedSnapshot)
.await();
final int status = resolveResult.getStatus().getStatusCode();
if (status == GamesStatusCodes.STATUS_OK) {
showToast(Conflict fixed);
} else {
showToast(Cannot fix the conflict);
}
}


Si utilizza la data di ultima modifica per determinare lultimo Snapshot creato e
quindi quello probabilmente pi aggiornato. Si tratta di una regola che dipende dalla
logica del corrispondente gioco e dalla natura dei dati salvati.

Conclusioni
In questo lungo capitolo abbiamo descritto lutilizzo delle Game API e abbiamo
visto come ci permettano di aggiungere funzionalit molto potenti alla nostra
applicazione. Nella prima parte abbiamo visto come sia possibile creare e gestire gli
obiettivi, che permettono al giocatore di sbloccare nuove funzionalit o livelli.
Abbiamo poi esaminato le logiche di raccolta dei punti e abbiamo visto come sia
semplice visualizzare delle graduatorie (leaderboard) secondo diversi criteri.
La parte pi complessa quella relativa ai servizi che permettono a pi utenti di
incontrarsi per poter giocare. Abbiamo visto come implementare una logica di un
gioco realtime, nel quale pi utenti giocano contemporaneamente allo stesso gioco.
Abbiamo infatti visto come sia possibile invitare altri giocatori oppure utilizzare la
funzione quick start per trovare in modo casuale dei giocatori interessati. In questa
fase abbiamo anche visto come, una volta stabilita la rete di connessioni P2P tra i
giocatori, sia possibile inviare o ricevere messaggi con informazioni legate al
particolare gioco. Sempre nel contesto di un gioco tra pi utenti abbiamo poi visto
come configurare e gestire un gioco a turni, secondo il quale un giocatore inizia la
partita, esegue la propria mossa e quindi definisce un meccanismo che permette di
definire il giocatore con il turno successivo. Abbiamo concluso il capitolo trattando
argomenti pi semplici, come quelli relativi ai Gift e Wish e alla gestione degli eventi,
per capire quali sono i comportamenti dei giocatori e rendere il gioco sempre pi
interessante. Si trattato di un capitolo molto impegnativo, che ci ha comunque
permesso di vedere quali sono i concetti principali legati agli aspetti social di un
gioco e soprattutto come poterli implementare attraverso le Game API.

Capitolo 11

Pagamenti con In-app Billing

Nel precedente capitolo abbiamo descritto le Game API, ovvero gli strumenti che
ci hanno permesso di gestire alcuni aspetti comuni nella realizzazione di giochi, quali
la leaderboard, gli obiettivi e altro ancora. Se prendiamo, appunto, gli obiettivi,
notiamo come questi possano essere sbloccati attraverso criteri legati al gioco vero e
proprio, come i punti o il tempo giocato. Se per osserviamo i giochi che si scaricano
dallo store possiamo notare come esista un altro modo, ovvero quello di comprarli.
Lacquisto di una qualunque cosa, virtuale o meno, attraverso unapplicazione
qualcosa tuttaltro che banale. Per questo motivo i Google Play services ci mettono a
disposizione le In-app Billing, che sono API che ci permettono di gestire lacquisto di
un bene virtuale attraverso la nostra applicazione. Come vedremo, queste API ci
permetteranno lacquisto una tantum oppure in abbonamento (subscription).
Questultima modalit quella che si utilizza nel caso di un abbonamento a una
rivista il cui PDF pu essere scaricato, per esempio, mensilmente. Come facile
intuire, lacquisto di un bene attraverso unapplicazione un qualcosa di molto
delicato, in quanto implica una transazione di denaro. Per questo motivo le API ci
permetteranno di interagire con il sistema attraverso interfacce, completamente
realizzate da Google, che rendono trasparenti le operazioni che richiedono un
adeguato livello di sicurezza. Per accedere a questa funzionalit non serve altro che
unapplicazione sullo store; il meccanismo di billing lo stesso utilizzato da Google
Wallet.
NOTA
Anche Google Wallet dispone di API per la gestione dellInstant Buy, al momento disponibili
solamente per il mercato americano; questo il motivo per cui non vengono trattate in questo
testo.

In questo capitolo descriveremo le API che permettono di gestire lacquisto di un


bene nella modalit classica e in quella a subscription. Per farlo ci aiutiamo con
unapplicazione di esempio.

Concetti base e predisposizione ambiente


Prima di descrivere la nostra applicazione di esempio intendiamo sottolineare
alcuni concetti fondamentali legati soprattutto a fattori di sicurezza. Innanzitutto
bene precisare come le applicazioni non interagiranno mai direttamente con i server
di billing di Google, ma il tutto avverr attraverso lapplicazione di Google Play
services installata nei vari dispositivi. in pratica lo stesso meccanismo impiegato,
per esempio, per le informazioni di location, le quali vengono gestite in modo
centralizzato. In questo caso non si tratta solamente di informazioni, ma soprattutto di
connessione sicura e affidabile con il server che gestisce le transazioni. La versione
delle API disponibile al momento la 3, compatibile con tutte le versioni di Android
successive alla 2.2.
Lentit al centro di queste API si chiama In App Product, e rappresenta il
prodotto, o i prodotti, che intendiamo vendere attraverso la nostra applicazione.
importante sottolineare come si tratti solamente di beni virtuali, come immagini,
video, musica, giornali oppure altre informazioni, come quelle che permettono lo
sblocco di una funzionalit oppure il passaggio di un livello di un gioco. Proprio per
la diversit dei beni che si possono comprare, queste API non si preoccupano del
recapito del bene, ma solamente della conferma o meno dellavvenuto acquisto. Se la
nostra applicazione vende musica, sar nostra responsabilit fare in modo che
lutente possa accedervi dopo lacquisto. Come accennato, esistono comunque diversi
tipi di beni, che si differenziano per il modo con cui vengono contabilizzati. Sia per i
beni normali, sia per le subscription si utilizza la console di Google Play, la stessa
utilizzata per la pubblicazione della nostra applicazione. Attraverso questa console
andiamo infatti a creare il nostro listino e successivamente a gestire i vari acquisti.
Come vedremo, nel nostro caso utilizzeremo lo stesso strumento per la creazione di
account di test o per la gestione degli ordini veri e propri.
Andiamo alla Google Play Console, dove andremo a registrare la nostra
applicazione, che abbiamo chiamato MyStore. Si tratta, al momento, di unapplicazione
semplicissima, creata attraverso il wizard di Android Studio selezionando lopzione
che crea una Activity, al momento vuota. Lapplicazione deve necessariamente
dichiarare lutilizzo del seguente permesso, relativo proprio allutilizzo delle API di
In-app Billing:

<uses-permission android:name="com.android.vending.BILLING" />

Selezioniamo il pulsante per laggiunta di una nuova applicazione e inseriamo le


informazioni richieste nella schermata rappresentata nella Figura 11.1.

Figura 11.1 Creazione di una nuova applicazione.

A questo punto selezioniamo il pulsante Carica APK e carichiamo il file della nostra
applicazione, che possiamo trovare nella cartella app del nostro progetto. Ma prima
notiamo che la console ci mostra la schermata rappresentata nella Figura 11.2, nella
quale abbiamo selezionato la scheda relativa alle versioni alpha.

Figura 11.2 Caricamento dellAPK dellapplicazione per alpha test.

Ricordiamo che questa una possibilit fornita dalla Google Play Console, la
quale permette di fare in modo che lapplicazione venga data in versione alpha e poi
beta a un numero di utenti limitato, come test prima della pubblicazione in
produzione. Andiamo quindi a caricare il nostro APK selezionando il corrispondente
pulsante. Una volta caricato lAPK si ottiene quanto rappresentato nella Figura 11.3,
ovvero lelenco delle diverse versioni della nostra applicazione in versione alpha, con
la possibilit di promuovere tale versione in beta e quindi in produzione.
NOTA
Nel nostro caso, non mostrato in figura, abbiamo anche avuto un alert per la mancanza delle
informazioni relative al nostro indirizzo fisico nel profilo. Si tratta di una restrizione aggiunta di
recente nei termini di utilizzo del servizio.

Inserite le informazioni mancanti andiamo a verificare quali siano gli utenti di test,
selezionando il link Gestisci elenco tester nella colonna Alpha tester sulla destra; si
aprir una schermata nella quale si richiede linserimento di un gruppo di Google
oppure di una cerchia di Google+ dei tester della nostra applicazione.

Figura 11.3 LAPK stato caricato per i tester alpha.

Al momento non disponiamo di alcun gruppo, per cui andiamo in Google+ e


creiamo una community privata. Selezionando la voce Community sulla sinistra
selezioniamo il pulsante Crea Community, ottenendo la schermata rappresentata nella
Figura 11.4, che ci permette di scegliere tra una community limitata (privata) o
pubblica. Selezioniamo la prima voce e a premere il pulsante Avanti per giungere a
unaltra schermata, simile alla precedente, nella quale ci viene richiesto se inserire
automaticamente tutti i membri di massimocarli.it oppure se inserire solamente

quelli invitati. Questa volta selezioniamo la seconda opzione, alla quale diamo il
nome MyStoreAlphaTester e, come indicato nella Figura 11.5, indichiamo la volont di
mantenerla nascosta.

Figura 11.4 Creazione di una community limitata.

Confermata la creazione della community attraverso il relativo pulsante veniamo


portati alla schermata rappresentata nella Figura 11.6, nella quale possiamo creare e
quindi inviare i diversi inviti per la nostra community di alpha tester.

Figura 11.5 Creazione di una Community privata a invito, nascosta.

Figura 11.6 Interfaccia per inviare inviti nella community di alpha tester.

La selezione del pulsante Invia conclude questa fase, che avr come conseguenza
linvio degli inviti e leventuale accettazione da parte degli invitati. Possiamo tornare
alla nostra Console di Google Play e inserire il nome della nostra community nel
campo rappresentato nella Figura 11.7, relativo al gruppo o community di alpha
tester. importante notare come il riferimento alla community creata non sia altro
che lURL della community stessa e non il nome. Si tratta di uninterfaccia che ci
permette di aggiungere pi gruppi o cerchie, che nel nostro caso limitiamo a quella
appena creata.
A questo punto probabile che lapplicazione non sia ancora pronta alla
pubblicazione, in quanto necessario fornire tutte le informazioni relative agli
screenshot, alle icone e descrizioni varie. Una volta completata questa parte, notiamo
come si renda disponibile il pulsante che compare in alto a destra nella Figura 11.8.
A questo punto lapplicazione disponibile in versione alpha e potr essere
installata solamente dagli utenti che abbiamo inserito nella nostra cerchia di Google+.
Lapplicazione non visibile nello store pubblico, ma potr essere scaricata dai tester
attraverso un link che sar nostra cura inviare loro.

Figura 11.7 Definizione del gruppo di alpha tester.

Figura 11.8 La versione alpha pronta per la pubblicazione.

Per ottenere questo link non dobbiamo fare altro che tornare nella schermata
relativa ai tester, selezionando il corrispondente link che porta alla visualizzazione
della schermata rappresentata nella Figura 11.9, nella quale notiamo la presenza del
link dellapplicazione da condividere con i tester.

Figura 11.9 Il link disponibile per linvio ai tester.

Attraverso Google+ sar nostra cura andare a condividere il precedente link. A


questo punto interessante osservare che cosa succede agli utenti che lo ricevono e
che lo selezionano. Il risultato la visualizzazione della schermata rappresentata
nella Figura 11.10, che invita lutente a diventare un tester per lapplicazione MyStore.

Figura 11.10 Linvito ricevuto dai tester.

Selezionando il pulsante di invito possibile accettare il ruolo di tester per


lapplicazione e scaricarla dal Play Store. infatti attraverso il Play Store che
avviene il controllo sul particolare utente. Anche se il link venisse inviato a un
account diverso, questo non riuscirebbe a scaricare lapplicazione, cosa possibile
solamente attraverso il Play Store.
NOTA
bene sottolineare come, anche nel caso della versione alpha, la pubblicazione dellapplicazione
non immediata e potrebbe richiedere qualche ora. Questo per tranquillizzare il lettore:
probabile che, inizialmente, il link per laccesso allapplicazione nello store porti a una pagina di
errore.

Creazione del catalogo


A questo punto abbiamo creato unapplicazione che al momento non fa
assolutamente nulla, ma che ci ha permesso di creare un APK che abbiamo
pubblicato limitatamente a una cerchia di alpha tester. Si tratta di un passo necessario
per labilitazione della voce Prodotti in-app nel menu a sinistra della console di Google
Play, selezionando la quale arriviamo alla schermata rappresentata nella Figura 11.11.
NOTA
probabile che il lettore debba attivare un account come merchant. Questo passo obbligatorio
al fine di poter iniziare a inserire gli elementi nel nostro catalogo. Per farlo sufficiente inserire
alcune informazioni sul tipo di merce venduta e accettare le condizioni di Google.

Ricordiamo che labilitazione di questa funzione necessita anche della presenza del
precedente permission nel file AndroidManifest.xml dellAPK pubblicato in precedenza.

Figura 11.11 Siamo pronti a inserire i prodotti da vendere.

Prima di cominciare a inserire qualche prodotto bene sottolineare come ogni


catalogo sia associato a un APK. Diverse applicazioni associate quindi a diversi APK
hanno quindi diversi cataloghi, che sono, da questo punto di vista, indipendenti.
Selezioniamo il pulsante per laggiunta di un nuovo prodotto e otteniamo la
schermata rappresentata nella Figura 11.12, nella quale ci vengono richieste due
informazioni. La prima un identificatore del prodotto, ovvero una String che deve
avere certe regole, come quella di avere solamente lettere minuscole, numeri e
underscore (_). molto importante sottolineare che si tratta di uninformazione unica
per lapplicazione e che non pu essere pi modificata, una volta che il prodotto
stato definito.

Figura 11.12 Inserimento del primo prodotto gestito.

La seconda informazione implicita nella scheda che abbiamo selezionato


allinizio: il prodotto gestito. Si tratta di un prodotto le cui transazioni vengono
gestite completamente da Google Play. Un prodotto non gestito invece quello la cui
transazione si svolge esternamente e non pu essere richiesta a Google Play. Il terzo
tipo quello degli abbonamenti (subscription) che si adatta allacquisto di giornali o
simili. Selezionando il pulsante Continua arriviamo a una schermata che ci permette di
inserire altre informazioni relative al nostro prodotto e in particolare un titolo e una
descrizione, le quali possono essere internazionalizzate in diverse lingue.
Linformazione pi importante ovviamente il prezzo, che nel nostro caso viene
specificato in GBP (Great Britain Pound) e che deve essere compreso tra 0,50 e
150,0 . Un aspetto interessante riguarda la conversione di questo prezzo in altre
valute. In questa fase abbiamo due possibilit: inserire il prezzo in pound (o euro) o
selezionare lopzione di conversione automatica. La seconda consiste nella
definizione manuale dei prezzi per ognuna delle valute presenti. A questo punto
possiamo salvare le informazioni relative al nostro prodotto il quale non comunque
ancora attivo. Per procedere allattivazione dovremo selezionare il corrispondente
valore nel menu a tendina in alto a destra. Nella Figura 11.13 tale menu nello stato
Non attivo.

Figura 11.13 Informazioni di un prodotto.

Allo stesso modo proviamo a creare un prodotto non gestito, selezionando la


seconda scheda come indicato nella Figura 11.14. Una volta selezionato il pulsante
Continua, le informazioni richieste sono le stesse del caso precedente.
Come terzo prodotto andiamo invece a creare una subscription, ovvero un
abbonamento. Selezioniamo la terza scheda e inseriamo ancora lidentificativo del
prodotto, come indicato nella Figura 11.15. In questo caso dobbiamo sempre inserire
una descrizione internazionalizzata del prodotto, ma abbiamo pi opzioni per quello
che riguarda la definizione del prezzo. possibile infatti specificare un periodo di
fatturazione secondo diversi criteri. Possiamo usare automaticamente un criterio
mensile o annuale. La terza opzione, stagionale, permette invece di definire una
validit specificando la data di inizio e di fine, come possiamo vedere nella Figura
11.16. Nel nostro caso abbiamo deciso di utilizzare un periodo di fatturazione
mensile. Notiamo anche come esista la possibilit di creare un periodo di prova
gratuito, che deve durare almeno 7 giorni, trascorsi i quali la fatturazione avverr in
modo automatico.
Prima di passare al passo successivo dobbiamo selezionare la voce Servizi e API
nel menu a sinistra nella Google Play Console e cercare la chiave relativa alla

licenza. Si tratta di una chiave pubblica RSA con codifica Base64 che utilizzeremo
nel paragrafo successivo nella creazione degli account di test.

Figura 11.14 Creazione di un prodotto non gestito.

Figura 11.15 Creazione di un abbonamento.

Figura 11.16 Definizione del periodo di fatturazione.

Si tratta della chiave che abbiamo visualizzato, appositamente troncata, in Figura


11.17 e che dovremo annotare, in quanto ci sar utile successivamente.

Figura 11.17 Licenza di fatturazione per la nostra applicazione.

I servizi per la gestione degli acquisti


Dopo aver configurato il nostro catalogo e aver preso nota della nostra licenza
vediamo quali sono le operazioni che le In-app Billing API ci mettono a disposizione
per la gestione degli acquisti. Come abbiamo accennato in precedenza, tutta
linterazione con il sistema di Google avviene attraverso lapplicazione dei Google
Play services installati nel nostro dispositivo. Essendo unaltra applicazione, servir
quindi un meccanismo di IPC (InterProcess Communication) che ci permetta di
interagire con essa che, ricordiamo, viene eseguita in un processo diverso. Per farlo si
potrebbe utilizzare un meccanismo basato sullutilizzo degli Intent oppure, come in
questo caso, laccesso a un oggetto attraverso uninterfaccia remota. In Android
queste interfacce vengono descritte da file che utilizzano il linguaggio AIDL
(Android Interface Definition Language), molto simile al C. Attraverso questo
linguaggio si definiscono solamente le interfacce di comunicazione, le quali vengono
poi utilizzate per la generazione del codice del client e del server. Nel nostro caso il
server gi implementato nellapplicazione dei GPs, per cui questo file ci sar utile
per vedere quali sono i servizi cui accedere. Nel nostro caso Google ci mette a
disposizione direttamente il file IInAppBillingService.aidl, che possiamo iniziare a
esaminare per quanto riguarda i metodi che andremo a chiamare e che possiamo
trovare nella cartella:

<Android SDK>/extras/google/play_billing


Innanzitutto possiamo notare come questo file inizi con le seguenti dichiarazioni,
che somigliano molto a quelle di un sorgente Java.

package com.android.vending.billing;
import android.os.Bundle;


La prima definisce questo file come appartenente al package
, mentre la seconda ci permette di utilizzare un oggetto di

com.android.vending.billing

tipo Bundle nel resto del file. Di seguito notiamo la definizione dellinterfaccia
, ma anche un commento che ci dice che tutte le operazioni definite

IInAppBillingService

restituiscono un valore numerico che descrive lesito delloperazione.


interface IInAppBillingService {

// Code
}


In particolare per ciascuna operazione potremmo ottenere i seguenti valori,
corrispondenti ad altrettante costanti.

RESULT_OK = 0 success
RESULT_USER_CANCELED = 1 user pressed back or canceled a dialog
RESULT_BILLING_UNAVAILABLE = 3 this billing API version is not supported
RESULT_ITEM_UNAVAILABLE = 4 requested SKU is not available for purchase
RESULT_DEVELOPER_ERROR = 5 invalid arguments provided to the API
RESULT_ERROR = 6 Fatal error during the API action
RESULT_ITEM_ALREADY_OWNED = 7 Failure to purchase since item is already owned
RESULT_ITEM_NOT_OWNED = 8 Failure to consume since item is not owned


La prima operazione ci permette di verificare se lapplicazione supporta la
funzionalit di billing in base a una versione specificata. Notiamo come gli altri
parametri siano lidentificatore della specifica applicazione, ovvero il package, e
quindi il tipo di prodotto che si vorrebbe acquistare. Lultimo parametro pu infatti
assumere il valore inapp oppure subs, rispettivamente per i prodotti normali e quelli
in abbonamento.

int isBillingSupported(int apiVersion, String packageName, String type);


Si tratta di unoperazione che restituir RESULT_OK in caso di successo oppure il
codice derrore. La seconda operazione molto interessante e ha la seguente firma, la
quale permette di ottenere le informazioni relative a un insieme di prodotti di cui si
specificano gli ID detti anche SKU:

Bundle getSkuDetails(int apiVersion, String packageName, String type,

in Bundle skusBundle);

Anche in questo caso dobbiamo specificare come primo parametro la versione
delle API che si sta utilizzando (la 3 nel nostro caso), il nome del package della
nostra applicazione, il tipo di prodotto che intendiamo visualizzare e un Bundle che
contiene gli SKU dei prodotti cui siamo interessati allinterno di una ArrayList di String

associata alla chiave ITEM_ID_LIST. In caso di successo questa operazione restituisce un


che contiene le informazioni associate a due chiavi distinte. Attraverso la

Bundle

chiave RESPONSE_CODE possibile accedere allinformazione sullesito della chiamata,


ovvero uno dei valori descritti in precedenza. La seconda chiave DETAILS_LIST ci
permette invece di accedere alle informazioni relative ai prodotti richiesti. In questo
caso il valore associato ancora una ArrayList di String, che contiene le informazioni
relative ai prodotti come documento JSON, ovvero del tipo:

{

productId : <requested sku>,


type : inapp,
price : $5.00,
title : Example Title,
description : This is an example description
}


Unoperazione di fondamentale importanza quella che ci permette di ottenere un
Intent che andremo a lanciare per avviare lacquisto di un prodotto. Per farlo le API
definiscono la seguente operazione:

Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,

String developerPayload);

Come possiamo notare, i primi due parametri sono ancora la versione delle API
che stiamo utilizzando e il nome del package dellapplicazione. Il terzo parametro
lidentificativo del prodotto che intendiamo acquistare, di cui specifichiamo il tipo
attraverso il parametro type. Il quarto parametro uninformazione, opzionale, che ha
una funzione analoga al requestCode quando si lancia un Intent attraverso il metodo
. In quel caso lo stesso requestCode viene restituito come

startActivityForResult()

parametro del metodo onActivityResult(), mentre in questo caso il processo


leggermente pi complesso. Intanto diciamo che il parametro restituito da questa
operazione ancora un oggetto di tipo Bundle che contiene le informazioni associate a
due chiavi. La prima ancora quella associata alla chiave RESPONSE_CODE e contiene il

codice relativo allesito delloperazione. La seconda associata alla chiave BUY_INTENT


e contiene lIntent che dovremo lanciare per avviare il processo di pagamento. In
questo caso dobbiamo per fare attenzione, poich si tratta di un PendingIntent, che
dovremo lanciare attraverso il seguente metodo della classe Activity:

public void startIntentSenderForResult (IntentSender intent, int requestCode,

Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags)



A parte il nome, ha anche una serie di parametri diversi rispetto al classico
startActivityForResult(). Il primo un IntentSender che possiamo ottenere dal
attraverso il metodo getIntentSender().

PendingIntent

NOTA
Anche qui bene chiarire, come fatto nel Capitolo 4, la differenza tra un IntentSender e un
PendingIntent. In entrambi i casi la documentazione ufficiale dice che si tratta di un oggetto che

incapsula le informazioni relative a un Intent e alla sua destinazione. Un IntentSender si ottiene


per solamente a partire da un PendingIntent e possiamo pensarlo come un modo per
incapsulare le informazioni relative al tipo di componente che pu lanciare lIntent stesso.
Possiamo comunque utilizzare indifferentemente luno o laltro.

Il parametro requestCode, se rappresentato da un valore positivo, ha lo stesso


significato che ha nel caso del metodo startActivityForResult() e viene quindi
riproposto come valore dellomonimo parametro nel metodo onActivityResult(). Il
parametro fillInIntent, anchesso opzionale, ci permette di impostare gli eventuali
parametri aggiuntivi secondo il meccanismo descritto dal metodo fillIn() della classe
. Il parametro flagsMask ci permette di specificare di quali campi dellIntent

Intent

originale eseguire loverriding attraverso lIntent passato come parametro fillInIntent.


Possiamo, per esempio, ridefinire uno o pi campi corrispondenti alle costanti del
tipo:

Intent.FILL_IN_ACTION
Intent.FILL_IN_DATA
Intent.FILL_IN_CATEGORIES
Intent.FILL_IN_PACKAGE
Intent.FILL_IN_COMPONENT
Intent.FILL_IN_SOURCE_BOUNDS
Intent.FILL_IN_SELECTOR
Intent.FILL_IN_CLIP_DATA

Per i dettagli rimandiamo alla documentazione ufficiale. Attraverso il parametro


flagsValues andiamo a specificare i valori corrispondenti ai precedenti flag, mentre
lultimo parametro, extraFlags, sempre 0, in quanto non ancora utilizzato. Il lancio
del nostro PendingIntent attraverso questo metodo porter, al termine del processo, alla
chiamata del metodo di callback onActivityResult() con un resultCode corrispondente
allo stato OK o CANCELED. Nel primo caso loperazione avvenuta con successo e le
informazioni sono contenute nei dati incapsulati nellIntent ricevuto come terzo
parametro, il quale contiene alcune informazioni associate ad altre tre chiavi. La
prima ancora RESPONSE_CODE che in questo caso contiene il codice di successo. La
seconda INAPP_PURCHASE_DATA e ci permette di accedere alle informazioni relative alla
transazione sotto forma di stringa JSON del tipo:

{

orderId:129997xxxxxx54705758.1371079406387615,
packageName:co.uk.massimocarli.mystore,
productId:<requested sku>,
purchaseTime:1345678900000,
purchaseToken : 122333444455555,
developerPayload:<previous dev payload>
}


In questo documento notiamo la presenza delle informazioni relative alla nostra
applicazione e allidentificativo o sku del prodotto acquistato. Uninformazione
molto importante quella relativa allorderId, che ci permetter di fare riferimento
allacquisto in momenti successivi. Altra informazione importante quella cui
abbiamo accennato in precedenza, corrispondente al parametro developerPayload
delloperazione dellinterfaccia AIDL remote. Lo stesso valore specificato in
corrispondenza della chiamata della operazione getBuyIntent() lo restituiamo qui e
possiamo quindi utilizzarlo come verifica al fine di aumentare la sicurezza della
transazione. Infine abbiamo linformazione associata alla propriet purchaseToken, la
quale contiene un token che viene generato a partire dalle informazioni dellordine e
dellutente che ha effettuato lacquisto. La terza chiave nel Bundle dellIntent di
risposta del metodo onActivityResult() INAPP_DATA_SIGNATURE e ci permette di accedere a

una String che contiene i dati di acquisto firmati con la chiave privata dello
sviluppatore. Si tratta ancora di uninformazione che pu essere utilizzata al fine di
una maggiore sicurezza della transazione.
A questo punto abbiamo esaminato le operazioni che ci permettono di vedere quali
sono i prodotti disponibili e di provvedere allacquisto. Le prossime due operazioni ci
permettono invece di gestire gli acquisti fatti in precedenza. Attraverso la seguente
operazione, infatti possibile accedere allelenco delle informazioni relative ai
prodotti che lutente ha gi acquistato e non ancora consumato come vedremo
successivamente:

Bundle getPurchases(int apiVersion, String packageName,

String type, String continuationToken)



Oltre agli ormai soliti tre parametri relativi a versione, package dellapplicazione e
tipologia di acquisto, notiamo la presenza di un quarto parametro, di nome
continuationToken, il quale ci permette di gestire la paginazione, se necessaria. Si tratta
di un parametro che avr valore null in corrispondenza della prima chiamata. Qualora
si avesse la necessit di gestire un numero elevato di acquisti, il valore di questo
parametro per la pagina N ci verr restituito a seguito della chiamata fatta per i dati
relativi alla pagina N-1. Anche in questo caso i risultati di questa operazione sono
contenuti in un oggetto di tipo Bundle associati a delle chiavi. Oltre allesito
delloperazione, associato alla chiave RESPONSE_CODE, possiamo accedere allelenco degli
SKU dei prodotti attraverso la chiave INAPP_PURCHASE_ITEM_LIST. Qualora ci servissero
maggiori informazioni relative ai prodotti acquistati possiamo accedere ai dati
contenuti nellarray associato alla chiave INAPP_PURCHASE_DATA_LIST anche in questo caso
forniti attraverso delle String in formato JSON. Le informazioni relative alle
informazioni firmate sono disponibili attraverso la chiave INAPP_DATA_SIGNATURE_LIST.
Infine leventuale continuationToken da utilizzare per la richiesta successiva
disponibile attraverso la chiave INAPP_CONTINUATION_TOKEN. Ovviamente, qualora non vi
fossero pagine successive questa informazione sarebbe mancante.
Lultima operazione che le API ci mettono a disposizione ci permette di
consumare un prodotto, ovvero di toglierlo dai prodotti restituiti dal metodo
precedente.

int consumePurchase(int apiVersion, String packageName, String purchaseToken);


Come possiamo notare, questo metodo accetta come terzo parametro purchaseToken
linformazione ottenuta in fase di acquisto che possiamo comunque anche ottenere
attraverso la chiamata del metodo getPurchases(). In questo caso il valore restituito
direttamente il codice di successo o di errore. Un prodotto consumato pu essere
nuovamente acquistato.

Accesso allinterfaccia remota


Dopo aver descritto le possibili operazioni per la gestione degli ordini, giunto
finalmente il momento di scrivere del codice. Per farlo dobbiamo innanzitutto
configurare la nostra applicazione aggiungendo il file .aidl nel nostro progetto.
Creiamo la cartella aidl allinterno di main e quindi il package com.android.vending.billing
copiando al suo interno il file IInAppBillingService.aidl che abbiamo descritto nel
paragrafo precedente e che si trova nella sezione extras dellSDK di Android. Una
volta copiato, il file otteniamo la struttura rappresentata nella Figura 11.18.
A questo punto eseguiamo una build del progetto, la quale genera per noi in modo
automatico una serie di classi che ci permetteranno di accedere ai servizi descritti in
precedenza dallinterno della nostra applicazione (Figura 11.19). Come possiamo
vedere nel menu a tendina in alto a sinistra, questi file sono visibili solamente con la
vista Project, ma non con quella denominata Android che rappresenta il default.
A questo punto abbiamo definito linterfaccia remota che ha permesso di generare
il codice che ci permetter di interagire con i servizi di In-app Billing. Come per tutti
i servizi di questo tipo, il primo passo da compiere quello del binding. Dobbiamo
ottenere un riferimento alloggetto remoto per poterne richiamare le operazioni.

Figura 11.18 Abbiamo aggiunto al progetto il file .aidl.

Figura 11.19 Codice generato a seguito della definizione del file .aidl.

Per farlo si utilizza un meccanismo classico di Android, che abbiamo


implementato nella classe MainActivity della nostra applicazione, di cui riportiamo il
codice di interesse. La prima cosa da fare definire la variabile distanza che conterr
il riferimento al servizio remoto.

private IInAppBillingService mService;


Come possiamo notare, si tratta di una variabile di tipo IInAppBillingService ovvero
del tipo relativo alla classe generata in modo automatico a partire dal file .aidl fornito
in precedenza. A questo punto definiamo unimplementazione dellinterfaccia
ServiceConnection, la quale definisce i metodi di callback in corrispondenza della
operazione di bind e di unbind. Nel nostro caso abbiamo utilizzato il seguente codice:

private final ServiceConnection mServiceConn = new ServiceConnection() {

@Override

public void onServiceDisconnected(ComponentName name) {


mService = null;
showToast(Disconnected);
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IInAppBillingService.Stub.asInterface(service);
showToast(Service connected);
}
};


Oltre alla visualizzazione di alcuni messaggi attraverso dei Toast abbiamo messo in
evidenza listruzione che ci ha permesso di ottenere il riferimento alloggetto di tipo
IInAppBillingService a partire dalloggetto IBinder ottenuto come parametro. Si tratta
della procedura che ci permette di ottenere il riferimento al proxy che incapsula tutta
la logica di interazione con il servizio che, ricordiamo, in esecuzione in un processo
diverso ovvero quello dellapplicazione dei Google Play services.
A questo punto non ci resta che eseguire il bind del servizio attraverso le seguenti
istruzioni, che abbiamo inserito nel metodo onCreate() e che abbiamo qui evidenziate.

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// We bind the service
Intent serviceIntent = new
Intent(com.android.vending.billing.InAppBillingService.BIND);
serviceIntent.setPackage(com.android.vending);
bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
}

Come possiamo notare, abbiamo come prima cosa creato un Intent che identifica in
modo esplicito il nostro servizio. Per rafforzare il tutto abbiamo utilizzato il metodo
setPackage(), per far riferimento in modo esplicito proprio allimplementazione dei
Google Play services. Infine abbiamo richiamato il metodo bindService() passando il
riferimento alla nostra implementazione di ServiceConnection creata in precedenza.
Lultimo passo quello che ci permette di gestire la disconnessione attraverso la
seguente implementazione del metodo onDestroy():

public void onDestroy() {

super.onDestroy();
if (mService != null) {
unbindService(mServiceConn);
}
}


Questo non fa altro che richiamare il metodo unbindService() sempre passando la
nostra implementazione di ServiceConnection.
Una volta ottenuto il riferimento alloggetto di tipo IInAppBillingService, non ci resta
che utilizzarlo per richiedere inizialmente le informazioni complete dei prodotti
disponibili, ovvero quelli che abbiamo definito nei paragrafi precedenti attraverso la
Google Play Console.
NOTA
Nella nostra applicazione abbiamo definito gli SKU dei prodotti attraverso delle costanti. In
unapplicazione completa questa operazione dovr essere gestita attraverso appositi servizi, che
dipendono dalla particolare applicazione e dalla tipologia di prodotti.

Per farlo abbiamo definito una voce nel menu delle opzioni, List Products, che
abbiamo collegato al metodo di utilit requestProductList() il quale ci permette di
accedere al metodo getSkuDetails() allinterno di un AsyncTask. Tutte le operazioni che
andremo a eseguire richiedono infatti una connessione di Rete che non pu quindi
essere eseguita nel Thread principale. Qui di seguito riportiamo solamente il codice
contenuto nel metodo doInBackground(), ovvero quello che viene eseguito in
background.

@Override

protected ArrayList<String> doInBackground(Void params) {

ArrayList<String> skuList = new ArrayList<String>();


skuList.add(SKUs.MANAGED_PROD_SKU);
skuList.add(SKUs.UNMANAGED_PROD_SKU);
skuList.add(SKUs.SUBSCRIPTION_PROD_SKU);
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(Const.ITEM_ID_LIST, skuList);
try {
// We access the service and get the result Bundle
Bundle skuDetails = mService.getSkuDetails(Const.API_VERSION,
Const.PKG_NAME, Const.IN_APP_TYPE, querySkus);
// We get the result code
int responseCode = skuDetails.getInt(Const.RESPONSE_CODE);
if (responseCode == Const.RESPONSE_CODE_OK) {
// We get the result data
ArrayList<String> responseList = skuDetails
.getStringArrayList(Const.DETAILS_LIST);
// We update the Model
mModel.clear();
mModel.addAll(responseList);
return responseList;
}
} catch (RemoteException e) {
e.printStackTrace();
}
return null;
}

Il primo passo consiste nella creazione di un oggetto di tipo ArrayList<String> nel


quale inseriamo gli SKU dei prodotti di cui vogliamo ottenere le informazioni. Come
accennato in precedenza, si tratta di informazioni che ciascuna applicazione dovrebbe
gestire in modo proprio; nel nostro caso abbiamo utilizzato semplici costanti. Questo
oggetto dovr essere associato alla chiave ITEM_ID_LIST e inserito nel Bundle che poi
andiamo a utilizzare come quarto parametro del metodo getSkuDetails() che andiamo a
richiamare ottenendo un Bundle. Andiamo quindi a prendere il responseCode attraverso la
corrispondente chiave e, in caso di successo, a estrarre le informazioni relative ai
prodotti richiesti associati alla chiave DETAILS_LIST. In questo contesto le performance
non ci preoccupano pi di tanto (il parsing JSON non ottimizzato), per cui abbiamo
deciso di mantenere come modello dellAdapter proprio lo stesso ArrayList ottenuto dal
servizio. A questo punto aggiorniamo il modello e quindi visualizziamo il tutto
allinterno di una ListView.
NOTA
Per il codice corrispondente alla creazione dellAdapter rimandiamo al sorgente del progetto.

Il nostro modello contiene una lista di String, le quali contengono a loro volta i
JSON con i dati dei vari prodotti. Nel nostro codice facciamo poi in modo che la
selezione di un prodotto ne permetta lacquisto. In questo caso non abbiamo fatto
altro che agganciare la selezione di uno dei prodotti in lista alla visualizzazione di
una Dialog di conferma. Qualora lutente confermasse lacquisto, richiameremo il
metodo di utilit processPurchase(), che invece vediamo nel dettaglio.

private void processPurchase(final String productType, final String productId) {

// Here we calculate the Developer Payload


final String developerPayload = getDevPayload(productId);
// We get the PendingIntent to launch
try {
Bundle buyIntentBundle = mService.getBuyIntent(Const.API_VERSION,
Const.PKG_NAME, productId, productType, developerPayload);
// We check for the result
int responseCode = buyIntentBundle.getInt(Const.RESPONSE_CODE);
if (responseCode == Const.RESPONSE_CODE_OK) {

// We get the PendingIntent to launch


PendingIntent pendingIntent =
buyIntentBundle.getParcelable(Const.BUY_INTENT);
// We launch the purchase
startIntentSenderForResult(pendingIntent.getIntentSender(),
PURCHASE_REQUEST_ID, new Intent(), 0, 0, 0);
} else {
showErrorMessage(responseCode);
}
} catch (Exception e) {
e.printStackTrace();
showToast(Something went wrong!);
}
}


Innanzitutto notiamo come i parametri di questo metodo siano il tipo di prodotto e
la corrispondente SKU o Id. Attraverso un metodo che abbiamo chiamato
getDevPayload() generiamo una String a partire dallo SKU per poterla poi utilizzare
come developerPayload come descritto nei precedenti paragrafi.
NOTA
Nel nostro caso il developerPayload viene generato in modo molto semplice, aggiungendo un
prefisso allo SKU. Ovviamente unapplicazione reale potr implementare il proprio algoritmo in
base alle esigenze di sicurezza dellapplicazione stessa.

Ottenuta questa String non facciamo altro che utilizzarla come parametro nella
chiamata del metodo getBuyIntent() attraverso il servizio di tipo IInAppBillingService di
cui abbiamo ottenuto in precedenza un riferimento. Si tratta di un metodo che
restituisce un Bundle che contiene lesito delloperazione e, in caso di successo, il
riferimento al PendingIntent da lanciare per procedere allacquisto del prodotto con
SKU indicato. A questo punto non ci resta che lanciare il PendingIntent attraverso il
metodo startIntentSenderForResult(), come descritto nel paragrafo precedente. A questo
punto inizia il processo di acquisto, che svolgeremo nel prossimo paragrafo, e che
porta alla chiamata del metodo di callback onActivityResult() nel quale si andr a

verificare lesito delloperazione e quindi, in caso di successo, allestrazione delle


informazioni sullacquisto. La nostra implementazione la seguente:

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

if (requestCode == PURCHASE_REQUEST_ID) {
if (resultCode == RESULT_OK) {
// We get the information from the received data
int responseCode = data.getIntExtra(Const.RESPONSE_CODE, 0);
if (responseCode == Const.RESPONSE_CODE_OK) {
String purchaseData = data
.getStringExtra(Const.INAPP_PURCHASE_DATA);
String dataSignature = data
.getStringExtra(Const.INAPP_DATA_SIGNATURE);
// We get the purchase information
try {
JSONObject jsonObj = new JSONObject(purchaseData);
String sku = jsonObj.getString(Const.PRODUCT_ID);
String orderId = jsonObj.getString(Const.ORDER_ID);
showToast(Product Successfully Purchased + sku + :
+ sku + orderId: + orderId);
// IMPORTANT: Remove this for security reason
Log.i(TAG, DataSignature: + dataSignature);
} catch (JSONException e) {
showToast(Something went wrong in the purchase process);
e.printStackTrace();
}
} else {
showErrorMessage(responseCode);

}
} else {
showToast(Purchase cancelled!);
}
}
}


Come possiamo vedere, il codice non fa altro che verificare che loperazione sia
avvenuta con successo. Poi si utilizzano le costanti che abbiamo definito per reperire
le informazioni relative allacquisto. Prima di descrivere lutilizzo degli altri prodotti
bene fare alcune precisazioni sulle diverse modalit di test che, ricordiamo, sono
molto importanti in applicazioni di questo tipo.

Configurazione di test
Come possiamo facilmente intuire, il testing di applicazioni di questo tipo, che
prevedono non solo linterazione con sistemi esterni, ma soprattutto lesecuzione di
vere e proprie transazioni un aspetto di fondamentale importanza. A tale scopo
Google ci permette di utilizzare due diverse modalit. La prima ci permette di
collaudare lapplicazione nella sua completezza, anche attraverso la simulazione di
acquisti, ma poi senza procedere alladdebito sulla nostra carta di credito. La seconda
ci permette di interagire con i sistemi di Google attraverso prodotti fittizi, che non
esistono nel nostro catalogo e che quindi non possono in alcun modo essere
addebitati.
NOTA
Esiste anche una terza modalit di test, che prevede il semplice utilizzo dellapplicazione in
produzione. Questa modalit permette di eseguire lacquisto dei prodotti con conseguente
addebito sul conto di Google Wallet dellutente.

importante sottolineare come il testing non possa essere fatto attraverso un


emulatore, ma si debba necessariamente utilizzare un dispositivo reale con una
versione di Android che deve essere almeno la 1.6 (API Level 4), ma con lultima
versione dellapplicazione dei Google Play services.
La prima modalit di test che intendiamo descrivere prevede lutilizzo di prodotti
fittizi corrispondenti a identificatori predefiniti. Si tratta di valori che permettono di
dire ai sistemi di Google che il prodotto non esiste e che quindi si stanno
semplicemente facendo dei test. Si tratta di prodotti con ID o SKU predefiniti, che
porteranno alla visualizzazione di dati puramente indicativi o casuali, che quindi non
corrispondono a quelli che abbiamo inserito in precedenza nel nostro catalogo
attraverso la console di Google Play. Per questo tipo di acquisti non possiamo
specificare la modalit di pagamento e non verr effettuato alcun addebito. Al
momento esistono quattro diversi identificatori che possiamo utilizzare per il testing
di situazioni diverse e precisamente:

android.test.purchased
android.test.canceled
android.test.refunded
android.test.item_unavailable


Il primo valore, android.test.purchased, ci permette di collaudare il caso in cui
lacquisto andasse per il verso giusto. In questo caso il server ci invier delle
informazioni fasulle che possiamo comunque utilizzare per verificare che il nostro

codice sia configurato in modo corretto. Per farlo abbiamo aggiunto, come per gli
altri SKU di test, delle voci nel menu delle opzioni, che abbiamo poi collegato al
nostro metodo processPurchase(). Per effettuare questo test non necessario pubblicare
lapplicazione, ma sufficiente caricare lAPK in un dispositivo, che deve essere
reale. Lanciamo quindi la nostra applicazione e selezioniamo lopzione Successful
. Come possiamo notare (Figura 11.20) lapplicazione apre una finestra di

Purchase

dialogo con le informazioni sul prodotto acquistato. Notiamo come venga esplicitato
il fatto che si tratta di un test.

Figura 11.20 Finestra di dialogo con i dati del prodotto simulato.

Qualora selezionassimo la tendina di fianco al prezzo possiamo notare quanto


rappresentato nella Figura 11.21 ovvero la possibilit di scegliere il metodo di
pagamento o di richiedere il rimborso.

Figura 11.21 Le diverse opzioni oltre allacquisto.

Andiamo quindi a selezionare il pulsante Buy ottenendo quanto rappresentato nella


Figura 11.22, che ci indica che lacquisto avvenuto con successo, chiedendo poi
informazioni sulla modalit da seguire per la password. Possiamo impostare la
password a ogni acquisto oppure ogni 30 minuti.

Figura 11.22 Acquisto avvenuto con successo.

A questo punto il prodotto stato acquistato e la conferma si ha anche attraverso la


visualizzazione della finestra di dialogo rappresentata nella Figura 11.23.

Figura 11.23 Acquisto avvenuto con successo

interessante notare come un nuovo acquisto dello stesso prodotto porti alla
generazione di un errore corrispondente al codice RESULT_ITEM_ALREADY_OWNED, che
corrisponde appunto al caso in cui il prodotto a disposizione sia gi in nostro
possesso. Per poter riacquistare lo stesso prodotto dobbiamo prima consumarlo.
Lutilizzo del precedente SKU di test segue lo stesso flusso di un prodotto reale, con
limportante differenza di non portare a unoperazione di fatturazione. Approfittiamo

di questa cosa per descrivere loperazione che ci permette, appunto, di consumare


un prodotto per poi poterlo acquistare nuovamente. Anche in questo caso abbiamo
implementato una voce nel menu delle opzioni che si chiama Consume Test e che non fa
altro che richiamare loperazione consumePurchase() del servizio utilizzando lo SKU di
test. Prima di questo abbiamo per bisogno di conoscere linformazione relativa al
purchaseToken per ottenere la quale dobbiamo utilizzare un altro metodo, e precisamente
, per richiamare il quale abbiamo creato un metodo di utilit accessibile

getPurchases()

attraverso ancora una voce del menu delle opzioni. Anche in questo caso si tratta di
unoperazione che deve essere richiamata in background, per cui abbiamo creato
nuovamente un AsyncTask di cui descriviamo solamente la parte di interesse. Questa
volta la chiamata del metodo del servizio viene eseguita nel metodo:

@Override
protected Bundle doInBackground(Void params) {

Bundle result = null;


try {
result = mService.getPurchases(Const.API_VERSION,
Const.PKG_NAME, Const.IN_APP_TYPE, null);
} catch (RemoteException e) {
e.printStackTrace();
}
return result;
}


Lelaborazione dei risultati viene eseguita nel metodo di callback nel thread della
UI:

@Override
protected void onPostExecute(Bundle bundle) {

super.onPostExecute(bundle);
getProgressDialog().dismiss();
if (bundle != null) {
// We manage the result

int responseCode = bundle.getInt(Const.RESPONSE_CODE);


if (responseCode == Const.RESPONSE_CODE_OK) {
// We get the result data
ArrayList<String> ownedSkus = bundle.getStringArrayList(
Const.INAPP_PURCHASE_ITEM_LIST);
ArrayList<String> purchaseDataList =
bundle.getStringArrayList(Const
.INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList =
bundle.getStringArrayList(Const.
INAPP_DATA_SIGNATURE_LIST);
Log.i(TAG, INAPP_PURCHASE_ITEM_LIST: + ownedSkus);
Log.i(TAG, INAPP_PURCHASE_DATA_LIST: + purchaseDataList);
Log.i(TAG, INAPP_DATA_SIGNATURE_LIST: + signatureList);
} else {
Log.e(TAG, Error getting List + responseCode);
}
} else {
showToast(Something went wrong!);
}
}


Anche in questo caso il meccanismo di estrazione delle informazioni analogo ai
casi precedenti, anche se le informazioni cui possiamo accedere sono diverse. Quelle
che ci interessano sono in questo momento quelle relative agli acquisti fatti, che
visualizziamo, per semplicit, attraverso il log. In precedenza avevamo acquistato il
prodotto di test, per cui il risultato sar qualcosa del tipo:

INAPP_PURCHASE_ITEM_LIST: [android.test.purchased]
INAPP_PURCHASE_DATA_LIST:
[{"packageName":"uk.co.massimocarli.mystore","orderId":"transactionId.android.test.purchased","productId":
INAPP_DATA_SIGNATURE_LIST: []


Abbiamo evidenziato linformazione che ci serve per consumare lordine, ovvero il
purchaseToken. Non ci resta che annotare tale valore e utilizzarlo come parametro del
metodo launchConsume() che abbiamo ancora una volta associato a una voce del menu
delle opzioni. Anche in questo caso abbiamo dovuto implementare un AsyncTask con il
seguente metodo in background:

@Override
protected Integer doInBackground(Void params) {

Integer resultCode = null;


try {
resultCode = mService.consumePurchase(Const.API_VERSION,
Const.PKG_NAME, purchaseToken);
} catch (RemoteException e) {
e.printStackTrace();
}
return resultCode;
}


A questo punto siamo riusciti a collaudare lapplicazione sia per quello che
riguarda lacquisto che per quello che riguarda il consumo del prodotto di test. Il
lettore pu anche verificare il funzionamento del primo metodo che abbiamo
utilizzato, ovvero il metodo getSkuDetails(). In quel caso il risultato quello
rappresentato nella Figura 11.24, dove notiamo anche la presenza del prodotto di test.

Figura 11.24 Elenco delle informazioni dei vari prodotti.

Per completezza, vediamo cosa succede nel caso di test delle altre opzioni
iniziando da quella che ci permette di simulare la cancellazione di un acquisto. Se
utilizziamo lo SKU associato alla costante android.test.canceled, il sistema risponde
come se lordine fosse stato cancellato come conseguenza, per esempio di dati errati
nella modalit di pagamento o di altro tipo. Attraverso lutilizzo dello SKU
android.test.refunded possibile invece collaudare il caso in cui il sistema risponde
come se fosse stato richiesto il rimborso del prodotto. Infine possibile collaudare il
caso in cui il prodotto non sia pi disponibile, semplicemente usando come SKU il
valore android.test.item_unavailable. Si tratta di casi particolari, che invitiamo il lettore a
collaudare per esercizio, in quanto non si discostano nelle procedure gi viste.
NOTA
Il test di queste applicazioni unoperazione molto delicata, per cui invitiamo il lettore a seguire i
passi con attenzione e consultare comunque la documentazione ufficiale, che potrebbe cambiare.

Prima di descrivere brevemente laltra modalit di test, facciamo unultima


considerazione legata a un aspetto di sicurezza che implica lutilizzo della chiave che
avevamo in precedenza detto di annotare. Si tratta di una chiave che ci permette di
avere unulteriore verifica della validit del nostro ordine. Il primo passo, in questo
caso, consiste nella creazione di una variabile distanza di tipo String, che abbiamo
chiamato mSignatureBase64 e che inizializziamo nel metodo onCreate(). Nel nostro
esempio tale inizializzazione, nel metodo getSignature(), stata solo accennata, ma
ovviamente il lettore potr sostituire il nostro placeholder con il valore ottenuto dalla
console di Google Play. A tale proposito, Google consiglia anche, oltre a offuscare il

codice, di utilizzare la concatenazione di String, al fine di rendere difficile la


ricostruzione della chiave in caso di decompilazione del codice. Stiamo parlando di
una cosa del tipo:

/**
* @return The Signature Base 64
*/
private String getSignature() {

return MIIBIjANBgkqhkUzwJgnZ0ZBhzPy +
HlJCdx3An9IdcM7mAH+DS4sUmZU +
K0pOt3cczsb+WhWzq4rUoY6twqk +
Py+nRXKO0DaUyHOx9YSmCl/H1AW +
Mim//GDhmer6ABHbeEwIDAQAB;
}


La verifica sulla validit dellacquisto poi implementata allinterno di alcune
classi che Google ci fornisce come parte del progetto TrivialDrive, che possiamo
trovare sempre nella cartella extras dellSDK dove abbiamo trovato in precedenza il
file di estensione .aidl. Nel package util notiamo la presenza della classe Security, che
contiene il seguente metodo statico:

public static boolean verifyPurchase(String base64PublicKey,

String signedData, String signature) {


if (TextUtils.isEmpty(signedData)
|| TextUtils.isEmpty(base64PublicKey)
|| TextUtils.isEmpty(signature)) {
Log.e(TAG, Purchase verification failed: missing data.);
return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}

Questo ci permette di verificare che lordine sia valido. Nel nostro caso abbiamo
utilizzato questo metodo in corrispondenza della ricezione delle informazioni
dellacquisto nel metodo onActivityResult(). Abbiamo infatti semplicemente utilizzato
le seguenti righe di codice:

String purchaseData = data.getStringExtra(Const.INAPP_PURCHASE_DATA);
String dataSignature = data.getStringExtra(Const.INAPP_DATA_SIGNATURE);
// We verify the order
if (!Security.verifyPurchase(mSignatureBase64, purchaseData, purchaseData)) {

showToast(Verification ERROR!);
return;
}


Come possiamo notare utilizziamo i dati relativi ai campi INAPP_PURCHASE_DATA e
e li andiamo a verificare utilizzando la nostra firma in base 64.

INAPP_DATA_SIGNATURE

Come ultima cosa vediamo come collaudare la nostra applicazione attraverso la


creazione di un account di testing. Per farlo sufficiente andare nella console di
Google Play e selezionare i Settings nel menu a sinistra. A questo punto sulla destra
individuiamo la parte relativa al Test Licenza, nella quale possiamo inserire le email
dei nostri utenti di test, che possono essere fino a 400. Nel nostro caso non abbiamo
inserito nulla, in quanto lutente associato allapplicazione gi compreso.

Figura 11.25 Inserimento degli utenti di test.

Inviando agli utenti indicati la nostra applicazione, questi sono ora in grado di
collaudare lacquisto e le altre operazioni. Questo meccanismo molto vicino
allutilizzo vero e proprio dellapplicazione reale, per cui rimandiamo alla
documentazione ufficiale per altri dettagli

Conclusioni
In questo capitolo ci siamo occupati della descrizione delle In-app Billing API per
la gestione degli acquisti dalla nostra applicazione. Nella prima parte abbiamo visto
come creare il nostro catalogo per i diversi tipi di acquisto, mentre nella seconda
parte abbiamo visto le operazioni messe a disposizione dal servizio InAppBillingService,
di cui abbiamo integrato nel nostro progetto e utilizzato linterfaccia AIDL. Anche in
questo caso abbiamo avuto la possibilit di descrivere i concetti fondamentali e i
passi base verso lintegrazione di questa potente possibilit che ci permette di
vendere prodotti dalla nostra applicazione.

Capitolo 12

Google Analytics

Nello sviluppo di unapplicazione esistono moltissimi aspetti che non risultano


evidenti agli utenti. Se consideriamo unapplicazione che permette di leggere le
notizie di un giornale, siamo portati a pensare che le uniche informazioni scaricate
siano quelle relative alle notizie e alle eventuali immagini o video; in realt c molto
altro. Chi pubblica lapplicazione ha come obiettivo quello di fare in modo che
lutente vi acceda il maggior numero di volte, al fine di portare a una pi alta
visualizzazione dei banner da cui ottenere poi dei ricavi. Per farlo necessario che
lutente trovi lapplicazione non solo facile da usare, ma soprattutto trovi interesse
nei suoi contenuti. Ma come fa leditore a sapere quali sono questi argomenti e quale
la migliore modalit per poterli presentare? A tale scopo esistono strumenti di
tracking che permettono, appunto, di tracciare, mantenendone lanonimato, i
comportamenti degli utenti. possibile, quindi, sapere quante volte un utente avvia
lapplicazione, quali schermate visualizza, quanto dura una sessione, e molto altro
ancora. Per farlo esistono diversi strumenti, tra cui anche Google Analytics, che fa
parte delle API allinterno dei Google Play services. Ovviamente non tratteremo
aspetti statistici, ma solamente quelli relativi alla modalit di integrazione di queste
API nella nostra applicazione. Ma prima di cominciare bene precisare che queste
API possono comunque essere utilizzate anche in dispositivi che non hanno i Google
Play services semplicemente integrando una libreria fornita sempre da Google. La
versione utilizzata in questo capitolo la 4.

Installazione e Screen View


Come fatto per le altre API, anche in questo caso creiamo una semplice
applicazione che ci permetter di sperimentare le varie funzionalit. Abbiamo creato,
utilizzando il wizard di Android Studio, unapplicazione che abbiamo chiamato
AnalyticsTest e che al momento contiene un unico Fragment, rappresentato da una classe
interna alla classe MainActivity, ma che abbiamo portato fuori e descritto attraverso la
classe MainFragment, la quale utilizza un layout che la identifica. Prima di proseguire
dobbiamo per accedere alla console di Google Analytics, raggiungibile allindirizzo
http://www.google.com/analytics/, e creare un account.
Nellangolo superiore destro del menu esiste la voce Amministrazione, selezionando la
quale possibile creare un nuovo account tramite la corrispondente voce nella
tendina nella parte sinistra (Figura 12.1):

Figura 12.1 Creazione di un account in Google Analytics.

A questo punto ci viene chiesto se vogliamo tracciare un sito web oppure


unapplicazione. Selezioniamo la seconda opzione e inseriamo le informazioni
relative al nome dellaccount, dellapplicazione oltre ad altri dati relativi al fuso
orario e alla tipologia di applicazione. Dopo la conferma otteniamo lID di
monitoraggio, che dovremo poi utilizzare nella nostra applicazione per la gestione dei
vari tipi di tracker. Si tratta di un valore del tipo:


UA-XXXXXXXX-X


Nella Figura 12.2 possiamo notare anche i collegamenti alla documentazione per
Android e iOS.
Una volta ottenuto lID di monitoraggio iniziamo a configurare la nostra
applicazione iniziando dalla libreria che dobbiamo importare. Una delle novit
principali della versione 6.5 dei Google Play services la possibilit di importare
solamente le librerie che interessano, in modo da risparmiare spazio e non cadere nei
vincoli che esistono, per esempio, nel numero dei metodi di unapplicazione.

Figura 12.2 Abbiamo ottenuto lid di monitoraggio.

Nel caso di Google Analytics abbiamo solo bisogno di definire la dipendenza


messa in evidenza di seguito, ovvero quella di base.

dependencies {

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


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


Il passo successivo consiste nella definizione, se non gi fatto, dei permessi
necessari allinvio dei dati nel file di configurazione AndroidManifest.xml:

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


La prima riga permette di aprire una connessione di rete dal dispositivo verso
lesterno, mentre la seconda permette di ricevere informazioni relative allo stato della
rete, in modo da poter inviare i dati nel momento opportuno.
A questo punto dobbiamo fornire le informazioni che ci permetteranno di
inizializzare un oggetto di tipo Tracker che sar quello che utilizzeremo per
linterazione con le Analytics API. Per farlo possiamo seguire lapproccio
dichiarativo o quello imperativo ovvero utilizzare un file di configurazione oppure
definire tutto a livello di codice. Nel nostro caso abbiamo deciso di utilizzare quello
dichiarativo, definendo un documento XML che andiamo a inserire nelle
corrispondenti risorse, ovvero in /res/xml.
NOTA
Le API permettono di creare un numero qualsiasi di Tracker attraverso una delle modalit
descritte in precedenza e che vedremo in dettaglio pi avanti. Lutilizzo di Tracker diversi pu
essere utile per distinguere tra dati di e-commerce oppure di navigazione o altro ancora. Nel
nostro caso, per semplificare, abbiamo creato un unico Tracker.

Creiamo il file tracker.xml che al momento contiene solo linformazione relativa


allID di monitoraggio che abbiamo ottenuto in precedenza. Il file, al momento, il
seguente. Abbiamo evidenziato il nome della propriet di tipo String che contiene,
appunto, il codice:

<resources tools:ignore="TypographyDashes"

xmlns:tools=http://schemas.android.com/tools>
<! The Monitoring ID>
<string name=ga_trackingId>UA-XXXXXX24-1</string>
<! Frequency for sampling >
<string name=ga_sampleFrequency>90</string>
</resources>


Notiamo come venga utilizzata anche la propriet ga_sampleFrequency, che permette di
impostare lintervallo con cui i dati vengono rilevati nellapplicazione.
NOTA
Notiamo anche la presenza dellattributo ignore, associato al namespace tools, che ci permette
di rimuovere con Lint eventuali errori dovuti alla presenza dei trattini nellID di monitoraggio.

Il passo successivo consiste nel creare loggetto per il tracking, che


implementiamo attraverso un Singleton descritto dalla classe che abbiamo chiamato
Track. Vedremo successivamente come arricchire questa classe; per il momento
vediamo solamente la modalit di inizializzazione.

public final class Track {

/**
* The Singleton Instance
*/
private static Track sInstance;

/**
* The Tracker
*/
private final Tracker mTraker;

/**
* Private constructor
*
* @param context The Context
*/
private Track(final Context context) {
// We initialize the Tracker using the XML resource file
mTraker = GoogleAnalytics.getInstance(context)
.newTracker(R.xml.tracker);
}
/**
* Returns the Track singleton using the given Context for initialization
*
* @param context The Context
* @return The Track singleton
*/

public synchronized static Track get(final Context context) {


if (sInstance == null) {
sInstance = new Track(context);
}
return sInstance;
}
/**
* Returns the Track singleton that should be already initialized
*
* @return The Track singleton
*/
public synchronized static Track get() {
if (sInstance == null) {
throw new IllegalStateException(Invoke get(Context) before this!);
}
return sInstance;
}
}


Innanzitutto notiamo come vi siano due diversi metodi statici di factory che si
differenziamo per la presenza o meno del parametro di tipo Context. Il metodo
, che prevede un parametro di tipo Context, verr eseguito una sola volta

get(Context)

allinterno di una nostra specializzazione della classe Application, che abbiamo anche
definito nel file di configurazione AndroidManifest.xml attraverso lattributo name
dellelemento <application/>.

<application

android:allowBackup=true
android:icon=@drawable/ic_launcher
android:label=@string/app_name

android:name=.MainApplication
android:theme=@style/AppTheme>
- - </application>


Come possiamo vedere, abbiamo semplicemente richiamato il metodo get(Context)
per provvedere allinizializzazione delloggetto di tipo Tracker.

public class MainApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
// We initialize the Tracking
Track.get(this);
}
}


Il secondo metodo get() senza alcun parametro ci permetter di accedere allistanza
del Singleton senza necessariamente avere il riferimento a un Context, anche se in
realt questo verrebbe utilizzato solo la prima volta. Si tratta di un modo per
semplificare laccesso al Singleton che utilizzeremo per il tracking.
A questo punto dobbiamo solamente utilizzare la nostra classe per linvio della
prima informazione relativa alla schermata che abbiamo visualizzato e che
chiameremo MainFragment. Per farlo, loggetto Tracker ci mette a disposizione il seguente
metodo:

public void setScreenName (String screenName)


Questo non fa altro che impostare il valore di una propriet associata alla costante:

Fields.SCREEN_NAME


Quindi potrebbe essere impostato anche attraverso il seguente metodo, sempre
della classe Tracker:

public void set(String key, String value)


importante sottolineare come la chiamata di questi metodi non provochi linvio
della relativa informazione, ma semplicemente imposti una propriet di stato che far
parte di tutti gli eventi che successivamente verranno inviati al server di Google
Analytics. Per rimuovere il valore sar quindi sufficiente usare il valore null.
Ovviamente possibile accedere allinformazione associata a una chiave attraverso il
seguente metodo che restituisce null qualora il valore non fosse impostato:

public String get(String key)


A questo punto vogliamo inviare delle informazioni al server, in relazione alle
schermate viste. Per farlo le API definiscono la classe HitBuilders.HitBuilder che
contiene una serie di metodi comuni a diversi tipi di eventi e informazioni. Uno
specifico HitBuilder una classe che permette di creare una serie di associazioni
chiave/valore da inviare al server e che potranno successivamente essere elaborate.
NOTA
Il metodo build() di questa implementazione del Builder pattern restituisce un oggetto di tipo
Map<String,String>.

Una di queste informazioni proprio lassociazione della costante Fields.SCREEN_NAME


relativa al nome della schermata visualizzata. Nel caso specifico si utilizza una
particolare specializzazione della classe HitBuilders.HitBuilder, che si chiama
. Essa non aggiunge alcun metodo a quelli definiti nella

HitBuilders.ScreenViewBuilder

classe HitBuilders.HitBuilder, per cui utile qualora lunica informazione da inviare sia
quella relativa alla schermata vista, come nel nostro caso. Andiamo allora nel metodo
onCreate() del Fragment descritto dalla classe MainFragment e utilizziamo le seguenti righe
di codice:

@Override
public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
// We send the screen name info
final Tracker tracker = Track.get().getTracker();
tracker.setScreenName(SCREEN_NAME);
tracker.send(new HitBuilders.AppViewBuilder().build());
}


Attraverso il metodo getTracker() della nostra classe Track abbiamo ottenuto il
riferimento alloggetto Tracker, che ricordiamo essere quello che ci permette di
interagire con le Analytics API. Si tratta di un metodo che abbiamo implementato nel
seguente modo e che richiede che il Singleton sia stato in precedenza correttamente
inizializzato:

/**
* @return The Tracker reference
*/
public Tracker getTracker() {

if (mTraker == null) {
throw new IllegalStateException(Tracker not initialized!
Invoke get(Context) before this!);
}
return mTraker;
}


Di seguito abbiamo utilizzato il metodo setScreenName() per impostare il nome della
schermata e poi abbiamo utilizzato unistanza della classe HitBuilders.AppViewBuilder per
la creazione dei parametri per il seguente metodo, che si preoccupa di inviare al
server le informazioni nella modalit impostata e secondo la disponibilit delle
risorse di rete:

public void send(Map<String, String> params)

Non ci resta che avviare la nostra applicazione e andare nella console di Google
Analytics per osservare se le nostre configurazioni sono corrette. Innanzitutto
selezioniamo la voce Panoramica nel menu In Tempo reale a sinistra, la quale ci permette
di verificare, in tempo reale appunto, le esecuzioni delle nostre applicazioni in quel
preciso momento. Nel nostro caso otteniamo quanto rappresentato nella Figura 12.3:

Figura 12.3 Visualizzazione delle informazioni in tempo reale.

Nella parte sinistra viene visualizzato un valore che indica il numero di


applicazioni che sono in esecuzione in questo momento. Nel nostro caso abbiamo
solo la nostra applicazione, che abbiamo appena avviato. Nella parte inferiore
possiamo notare le informazioni relative alla versione della nostra applicazione, ma
soprattutto, nella parte destra, il nome della schermata attiva in questo momento, che
corrisponde proprio a quella che abbiamo impostato attraverso le precedenti poche
righe di codice, ovvero Main Fragment. Al di sotto di queste informazioni abbiamo
anche la possibilit di visualizzare una mappa con la distribuzione geografica delle
varie esecuzioni (Figura 12.4):

Figura 12.4 Distribuzione geografica delle applicazioni in esecuzione.

Attraverso le opzioni Localit e Schermata dello stesso menu In Tempo Reale


possibile accedere alle stesse informazioni, ma con un dettaglio maggiore, che
lasciamo sperimentare al lettore.
La nostra applicazione di esempio alquanto banale, ma in una normale
applicazione possono esistere moltissime schermate diverse, ciascuna delle quali pu
a sua volta gestire diversi Fragment. La gestione delle schermate richiede poche righe
di codice, che si possono per replicare pi volte nelle diverse classi. In questi casi le
Google Analytics ci permettono di gestire il tutto anche a livello dichiarativo nel file
di configurazione del Tracker attraverso una funzionalit di Tracking automatico che si
pu abilitare tramite la seguente dichiarazione nel file di configurazione:

<bool name="ga_autoActivityTracking">true</bool>


In questi casi anche possibile indicare quale debba essere il valore da inviare al
server in corrispondenza della particolare schermata. Nel nostro caso avremmo
potuto quindi aggiungere la seguente definizione, eliminando le istruzioni del codice
nella classe MainFragment:

<screenName name="uk.co.massimocarli.analyticstest.MainFragment">

MainFragment
</screenName>

La gestione degli eventi


Nel paragrafo precedente abbiamo visto come inviare le informazioni al server di
Google Analytics per quello che riguarda la particolare schermata attiva in quel
momento. Si tratta di uninformazione importante per conoscere le funzionalit
preferite dai nostri utenti, ma che non ci d indicazioni su quello che gli utenti fanno,
ovvero sulle azioni che essi eseguono in tali schermate. Potremmo essere interessati
al fatto che i nostri utenti condividano o meno una notizia, utilizzino il menu delle
opzioni oppure preferiscano una modalit di navigazione piuttosto che unaltra.
Sarebbe quindi molto utile disporre di uno strumento che ci permetta anche di sapere
i comportamenti pi frequenti dei nostri utenti, in modo da semplificarli e renderli,
per esempio, pi facilmente raggiungibili. Si tratta di una funzionalit molto
semplice, che utilizza lo stesso meccanismo visto in precedenza. In questo caso si
definiscono degli Eventi, caratterizzati dalle seguenti propriet:

Category
Action
Label
Value


Ciascun evento pu essere raggruppato in categorie, che ci possono dare
informazioni, per esempio, sul particolare tipo di eventi. In unapplicazione che ci
permette di acquistare dei prodotti attraverso la funzionalit di In-app Billing vista
nel Capitolo 11 potremmo, per esempio, avere eventi che riguardano lacquisto di un
prodotto oppure altri eventi di pura navigazione o altri relativi alla modifica delle
informazioni dellutente. La Category quindi semplicemente un modo per
categorizzare gli eventi che fanno riferimento a parti ben definite dellapplicazione.
La seconda propriet identifica la particolare azione che lutente sta eseguendo.
Sempre nel caso di unapplicazione che vende prodotti potremmo avere lazione BUY
oppure quella REFOUND o semplicemente la visualizzazione del dettaglio di un prodotto.
Alcune di queste azioni potrebbero corrispondere alla visualizzazione di una
particolare schermata e quindi potrebbero essere gestite anche attraverso quanto visto
nel paragrafo precedente. Lutilizzo degli eventi ci permette comunque una maggiore
contestualizzazione delle informazioni ed quindi da preferire se quello che si
intende capire appunto il comportamento dellutente. Ovviamente il solo nome
dellazione pu non essere sufficiente. Nel caso di acquisto di un prodotto sarebbe
utile anche sapere di quale prodotto si tratta. Potremmo quindi vedere qual il
prodotto che viene visualizzato pi volte senza per essere acquistato, in modo da

apportare, per esempio, le opportune modifiche o comunque capirne il motivo. A


questo scopo servono le altre informazioni relative alla Label e al corrispondente Value,
un valore di tipo long.
In questo caso la procedura di invio delle informazioni analoga a quella descritta
in precedenza, con la sola differenza che ora il Builder che andiamo a utilizzare
quello descritto dalla classe HitBuilders.EventBuilder. Si tratta di una classe che
definisce proprio i metodi setXXX() relativi alle quattro propriet definite sopra. Nella
nostra applicazione di test abbiamo semplicemente aggiunto due Button, cui abbiamo
associato linvio di due diversi eventi e precisamente quelli descritti dalle seguenti
righe di codice:

/**
* Example of an event
*/
private void sendEvent1() {

final Tracker tracker = Track.get().getTracker();


tracker.send(new HitBuilders.EventBuilder()
.setCategory(E_COMMERCE_CATEGORY)
.setAction(BUY_ACTION)
.setLabel(BOOK_LABEL)
.setValue(BOOK_VALUE)
.build());
}
/**
* Example of an event
*/
private void sendEvent2() {
final Tracker tracker = Track.get().getTracker();
tracker.send(new HitBuilders.EventBuilder()
.setCategory(E_COMMERCE_CATEGORY)
.setAction(VIEW_PRODUCT_ACTION)
.setLabel(BOOK_LABEL)
.setValue(BOOK_VALUE)
.build());
}


Abbiamo semplicemente creato due eventi associati a una stessa categoria che
abbiamo chiamato e-commerce. Il primo caratterizzato dallazione buy, mentre il
secondo dallazione view entrambi su un prodotto relativo a un libro. Si tratta quindi di
due eventi associati a una stessa categoria. Nella classe MainActivity abbiamo invece
creato un altro evento associato a una categoria che abbiamo chiamato navigation,
collegata alla pressione del tasto Back da parte dellutente quando si trova nella
schermata principale. Non ci resta che avviare la nostra applicazione e generare un

po di eventi, selezionando i due pulsanti oppure entrando e uscendo


dallapplicazione con il tasto Back.
Torniamo nella console di Google Analytics e selezioniamo la voce Eventi sempre
nel menu a sinistra, ottenendo, dopo una breve attesa, quanto rappresentato nella
Figura 12.5.

Figura 12.5 Visualizzazione degli eventi.

Nella parte inferiore notiamo la presenza delle due categorie che abbiamo definito
nel nostro codice. Selezionando una di queste otteniamo un dettaglio relativo al
particolare tipo di evento. In questa sezione si trovano informazioni relative agli
utenti attivi ovvero allunica istanza in questo momento in esecuzione. Per avere
informazioni pi generali sugli eventi generati dobbiamo andare invece nel menu
Comportamento e selezionare ancora la voce Eventi. In questo caso le informazioni non
sono per immediatamente disponibili, ma necessitano di un tempo di elaborazione
che pu essere anche di alcune ore. Anche in questo caso invitiamo il lettore a
navigare nella console di Google Analytics per visualizzare le informazioni di
interesse nella modalit e forma voluta.

Eventi temporizzati
Nel paragrafo precedente abbiamo visto come inviare informazioni relative a
particolari eventi che dipendono dal tipo di applicazione e dal tipo di dati che si
intendono acquisire sul comportamento degli utenti. Le Analytics API ci permettono
per di gestire anche gli eventi temporizzati, che portano con s le stesse
informazioni degli eventi che abbiamo visto, con in pi delle informazioni temporali,
ovvero legate al momento in cui gli eventi sono stati generati. Si tratta di eventi che
ci permettono di misurare la velocit con cui vengono eseguite determinate
operazioni, come il caricamento delle risorse o lesecuzione di un particolare task che
si vuole in qualche modo ottimizzare.
La gestione di questi eventi molto semplice e consiste nel semplice utilizzo di
una specializzazione diversa della classe HitBuilders.HitBuilder descritta questa volta
dalla classe HitBuilders.TimingBuilder. Il loro utilizzo molto simile a quello degli
eventi precedenti, con la sola differenza del particolare HitBuilders. Osservando la
documentazione ufficiale possiamo inoltre notare come non vi sia il metodo relativo
allaction.
Nel nostro caso abbiamo aggiunto un altro pulsante, cui abbiamo associato linvio
delle informazioni relative a un evento temporizzato. Abbiamo quindi utilizzato il
seguente metodo, nel quale abbiamo evidenziato lutilizzo del particolare Builder.

private void sendEvent3() {

final Tracker tracker = Track.get().getTracker();


tracker.send(new HitBuilders.TimingBuilder()
.setCategory(PERFORMANCE_CATEGORY)
.setLabel(TIME_LABEL)
.setValue(System.currentTimeMillis())
.build());
}


Unultima considerazione: dove andare a reperire le informazioni raccolte in
questo modo? La console di Google Analytics in continua evoluzione. Al momento

le informazioni relative agli eventi temporizzati si trovano alla voce App Speed nel
menu Behaviour, ma potrebbero essere spostate in futuro.

Eccezioni ed errori
Oltre alla raccolta di informazioni per lottimizzazione dellapplicazione, le
Google Analytics API ci permettono di raccogliere dati relativi alle eventuali
eccezioni sollevate nella nostra applicazione. Esistono due tipi diversi di eccezioni
(oltre agli errori) ciascuna con uno scopo ben preciso che bene riprendere in questo
contesto. Supponiamo, per esempio, che la nostra applicazione permetta lacquisto di
un prodotto virtuale attraverso lutilizzo della carta di credito. Supponiamo che al
momento del pagamento lutente sbagli la digitazione del proprio PIN. In questo caso
il sistema di pagamento potrebbe sollevare uneccezione del tipo WrongPinException.
Laspetto fondamentale riguarda il fatto che non si tratta di un errore, ma
semplicemente di un modo per notificare il fatto che si verificato uno scenario
diverso da quello normale. In questo scenario lutente ha sbagliato il PIN; si tratta
di unazione che poteva capitare. Questo significa che leccezione che viene sollevata
deve essere necessariamente gestita e quindi leccezione di tipo WrongPinException dovr
essere elaborata allinterno di un blocco try/catch. Si tratta delle eccezioni dette,
appunto, catched exception, le quali devono, prima o poi, essere catturate e gestite. Per
questo tipo di eccezioni le Analytics API mettono a disposizione unulteriore
specializzazione della classe HitBuilders.HitBuilder, che si chiama
. Essa definisce due soli metodi che riguardano una

HitBuilders.ExceptionBuilder

descrizione delleccezione e un flag che indica se si tratta di uneccezione fatal


oppure no. Per gestire questa eccezione baster utilizzare codice come il seguente,
dove abbiamo evidenziato lutilizzo del particolare Builder:

try{

// Code that generates exception


}catch(WrongPinException) {

final Tracker tracker = Track.get().getTracker();


tracker.send(new HitBuilders.ExceptionBuilder()
.setDescription(EX_DESCRIPTION)
.setFatal(true)
.build());
}

La seconda categoria di eccezioni quella che si chiama Runtime e viene


caratterizzata in Java da istanze di classi che estendono, direttamente o
indirettamente, la classe RuntimeException. Queste eccezioni non devono essere
necessariamente gestite, ovvero non si ha alcun errore di compilazione qualora i
possibili metodi sorgenti di queste eccezioni non dovessero essere allinterno di un
try/catch oppure allinterno di un metodo che a sua volta le definisce come clausola
. Il significato di queste eccezioni infatti quello dellerrore in fase di sviluppo.

throws

Se otteniamo un NullPointerException (la RuntimeException per eccellenza) significa che in


quel punto un riferimento a un oggetto che doveva essere valido invece null. Questo
significa che c un errore, per cui giusto che lapplicazione si blocchi per rivelarlo
prima possibile, permettendone la risoluzione. Non si tratta di un flusso diverso
dellesecuzione dellapplicazione, ma dellindividuazione di un bug. Per questo tipo
di eccezioni le Analytics API permettono di scegliere tra due opzioni. La prima
consiste semplicemente nel mettere la seguente definizione nel file di configurazione
del Tracker:

<bool name="ga_reportUncaughtExceptions">true</bool>


In questo caso, leventuale eccezione generata verrebbe prima gestita da Google
Analytics e poi inviata alla gestione delle eccezioni di default, che causa il crash
dellapplicazione. Unimportante osservazione riguarda il fatto che si tratta di una
configurazione che pu essere impostata per ciascuno degli eventuali Tracker, ma
che riguarda un aspetto che globale dellintera applicazione. In questo caso
limpostazione relativa allultimo Tracker inizializzato quella che ha effetto sulle
altre. Qualora decidessimo di gestire le RuntimeException in modo personalizzato, le API
ci mettono a disposizione la classe ExceptionReporter, una particolare specializzazione
dellinterfaccia Thread.UncaughtExceptionHandler di Java, la quale definisce la seguente
operazione:

public void uncaughtException(Thread t, Throwable e)


Questa viene richiamata qualora venga sollevata uneccezione Runtime, detta anche
in contrapposizione a quelle di tipo catched. In Java infatti possibile creare

uncatched

una particolare implementazione dellinterfaccia Thread.UncaughtExceptionHandler e

impostarla come Handler di gestione delle eccezioni attraverso il seguente metodo


statico della classe Thread:

public static void

setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

La nostra classe ExceptionReporter non che una particolare implementazione
dellinterfaccia Thread.UncaughtExceptionHandler che riceve leccezione generata, ne estrae
le informazioni da mandare a Google Analytics per poi rimandare leccezione
allimplementazione precedente. Questo avviene solitamente attraverso le seguenti
istruzioni:

Thread.UncaughtExceptionHandler myHandler = new ExceptionReporter(tracker,

Thread.getDefaultUncaughtExceptionHandler(), context);
Thread.setDefaultUncaughtExceptionHandler(myHandler);


Nella prima creiamo unistanza di ExceptionReporter passando il riferimento al
, alla precedente implementazione di Thread.UncaughtExceptionHandler e al Context di

Tracker

Android. Di seguito utilizziamo loggetto creato come UncaughtExceptionHandler di


default; possiamo quindi dire che si tratta di una sorta di Decorator che aggiunge
allHandler di default la possibilit di inviare i dati al server di Google Analytics.
Unultima considerazione riguarda il fatto che le informazioni relative
alleccezione da inviare a Google Analytics siano rappresentate da una String, che
impostiamo attraverso il metodo setDescription() della classe
. A tale proposito i Google Play services ci mettono a

HitBuilders.ExceptionBuilder

disposizione la classe StandardExceptionParser con il seguente metodo, che permette


appunto di estrarre le informazioni di uneccezione e di formattarle in modo
compatibile con quello che Google Analytics si aspetta.

public String getDescription (String threadName, Throwable t)


Le precedenti righe di codice in questo caso diventano:


try{

// Code that generates exception


}catch(WrongPinException) {

final Tracker tracker = Track.get().getTracker();


tracker.send(new HitBuilders.ExceptionBuilder()
.setDescription(new StandardExceptionParser(this, null)
.getDescription(Thread.currentThread().getName(), e)))
.setFatal(true)
.build());
}


Anche in questo caso le informazioni inviate al server vengono poi visualizzate
attraverso la console nella sezione Crashes and Exception nel menu Behaviour.

Gestire le sessioni
Il concetto di sessione molto importante e rappresenta il periodo durante il quale
lutente sta utilizzando la nostra applicazione. Una sessione inizia quando lutente
avvia lapplicazione e termina quando la chiude. Si tratta di uninformazione che non
di facile gestione, per il semplice motivo che in Android non esistono metodi che
vengano sempre richiamati in corrispondenza della chiusura o messa in background
dellapplicazione. Per quello che riguarda le Google Analytics, una sessione pu
essere vista come un contenitore nel quale vengono inserite le informazioni relative
alle schermate viste o agli eventi che abbiamo imparato a gestire in precedenza. Tutte
queste informazioni che distano nel tempo un periodo inferiore a 30 minuti vengono
considerate come facenti parte di una stessa sessione. Si tratta comunque di un
periodo che possibile modificare attraverso la seguente propriet di tipo integer nel
file di configurazione del Tracker:

<integer name="ga_sessionTimeout">90</integer>


Si tratta del numero di secondi trascorsi i quali la sessione viene terminata in caso
di inattivit. Una sessione pu comunque essere resettata anche a livello di codice, in
quanto la classe HitBuilders.HitBuilder definisce il seguente metodo, che permette di
inviare uninformazione al server come facente parte di una nuova sessione,
chiudendo, eventualmente, quella precedente.

public T setNewSession()


Per gli eventi precedenti avremmo potuto infatti utilizzare il seguente codice:

private void sendEvent1() {

final Tracker tracker = Track.get().getTracker();


tracker.send(new HitBuilders.EventBuilder()
.setCategory(E_COMMERCE_CATEGORY)
.setAction(BUY_ACTION)
.setLabel(BOOK_LABEL)

.setValue(BOOK_VALUE)
.setNewSession()
.build());
}

Gestione delle campagne


Un ultimo argomento relativo allutilizzo delle API di Google Analytics riguarda
la gestione delle campagne. Quando si realizza unapplicazione esistono diversi
modi per poterla pubblicizzare e per portare lutente a scaricare lapplicazione sul
proprio dispositivo oppure a eseguirla direttamente. Supponiamo, per esempio, di
creare unapplicazione per un giornale web cui possibile accedere attraverso un
normale browser di un desktop oppure attraverso il browser di un dispositivo
Android. I principali giornali dispongono di una versione del sito che si adatta al
meglio ai dispositivi mobili, ma anche di unapplicazione che permette di accedere
agli stessi contenuti in modo diverso, per esempio offline. In questo caso possibile
fare in modo che quando un utente accede al sito web del giornale gli venga proposto
di scaricare la relativa applicazione. Qualora il dispositivo fosse gi provvisto
dellapplicazione, possibile fare in modo che questa venga lanciata in automatico.
In questo contesto non ci preoccupiamo della modalit con cui tutto questo avviene;
vogliamo invece misurare quale di questi meccanismi viene utilizzato di pi, in modo
da potenziarlo o comunque trarne vantaggio.
Il primo scenario riguarda quindi il caso in cui lutente non disponga
dellapplicazione e venga quindi condotto direttamente allo Play Store per il
download. In questi casi lapplicazione Market sul dispositivo si preoccupa di
ricevere uninformazione che si chiama di REFERRER che gira a sua volta
allapplicazione dopo linstallazione attraverso un opportuno Intent di broadcast con
action corrispondente alla costante INTENT_REFERRER. Il valore passato allapplicazione
lo stesso che era stato in precedenza passato al Play Store dalla particolare campagna,
attraverso il parametro referrer. Se decidiamo di pubblicizzare la nostra applicazione
attraverso due diverse campagne pubblicitarie potremmo associare a ciascuna di
essere due diversi valori del parametro referrer, che far parte del link verso il Play
Store. Ma come facciamo a inviare questo valore al server da Google Analytics?
Trattandosi di un Intent di Broadcast, le Analytics API ci mettono a disposizione le
classi sia per il BroadcastReceiver che ricever lIntent, sia per il Service che si
preoccuper della gestione dei dati raccolti. Per farlo sufficiente aggiungere le
seguenti definizioni al file di configurazione AndroidManifest.xml:

<service android:name="com.google.android.gms.

analytics.CampaignTrackingService />
<receiver android:name="com.google.android.gms

.analytics.CampaignTrackingReceiver
android:exported=true>
<intent-filter>
<action android:name=com.android.vending.INSTALL_REFERRER />
</intent-filter>
</receiver>


Notiamo come il servizio sia descritto dalla classe CampaignTrackingService, mentre il
descritto dalla classe CampaignTrackingReceiver, entrambe del package

BroadcastReceiver

. Notiamo, infine, come lazione associata sia quella

com.google.android.gms.analytics

identificata dalla String:



com.android.vending.INSTALL_REFERRER


Come accennato in precedenza, il secondo scenario prevede il caso in cui
lapplicazione sia gi installata nel dispositivo e si voglia capire da quale altra
sorgente stata avviata in modo automatico. Secondo il nostro esempio vogliamo
capire, per esempio, attraverso quale banner lapplicazione stata avviata una volta
che lutente vi ha fatto clic. In questo caso dobbiamo distinguere la responsabilit di
capire la sorgente dellavvio e la modalit di invio dei dati al server di Google
Analytics. La responsabilit di capire quale sia stata la sorgente dellavvio
dellapplicazione dellActivity lanciata, che esaminer i dati contenuti nel
corrispondente Intent. In questi casi si utilizza uno schema oppure un host particolare,
definito allinterno di un IntentFilter per lActivity principale. Quando lutente
seleziona il link attraverso il browser viene infatti lanciato un Intent con category che
viene raccolto dalla Activity principale dellapplicazione.

CATEGORY_BROWSABLE


Spesso si tratta di Activity cui associato un IntentFilter del tipo, nel quale abbiamo
messo in evidenza il particolare scheme o il particolare host:

<intent-filter>

<action android:name=android.intent.action.VIEW/>
<category android:name=android.intent.category.DEFAULT/>
<category android:name=android.intent.category.BROWSABLE/>
<data android:host=idontexist.com android:scheme=mioscheme />
</intent-filter>


Quando lutente seleziona nel browser del dispositivo un link del seguente tipo, si
ha il lancio della Activity cui stato associato il precedente IntentFilter:

mioscheme://idontexist.com/path?param1=value1&param2=value2


I vari parametri saranno quindi raccolti dallActivity stessa attraverso lIntent
ricevuto.
Una volta raccolte queste informazioni dobbiamo in qualche modo inoltrarle al
server di Google Analytics. Per farlo sufficiente utilizzare il seguente metodo della
classe base HitBuilders.HitBuilder, che abbiamo gi incontrato in precedenza:

public T setCampaignParamsFromUrl (String utmParams)


Ma come fatto lURL che dobbiamo passare al precedente metodo? Per una
completa descrizione di tutti i parametri rimandiamo alla documentazione ufficiale.
Possiamo comunque notare come esista un parametro utm_campaign cui associamo
lidentificatore della campagna, un parametro utm_source cui associato un
identificativo della sorgente e un parametro utm_medium cui associato il tipo di mezzo
utilizzato per linvio. La documentazione ufficiale fornisce anche una form che aiuta
nella costruzione del precedente URL e che pu essere trovata al seguente indirizzo:

http://goo.gl/1N0v63


Per inviare le informazioni relative alla sorgente dellavvio dellapplicazione
quindi possibile utilizzare le seguenti poche righe di codice:

final Tracker tracker = Track.get().getTracker();


tracker.setScreenName(SCREEN_NAME);
tracker.send(new HitBuilders.AppViewBuilder()

.setCampaignParamsFromUrl(CAMPAIGN_URL)
.build());

La costante CAMPAIGN_URL contiene lURL creato con la precedente form, che pu
essere del seguente tipo, ma che dipende dalla particolare sorgente:

String CAMPAIGN_URL = "http://mystore.com/index.html?" +

utm_source=email&utm_medium=email_push&utm_campaign=sale +
&utm_content=email_variation_1;

Eventi social
Questi ultimi anni sono caratterizzati sicuramente dallavvento dei social network.
Alcune informazioni che potrebbe essere interessante tracciare riguardano
linterazione tra una nostra applicazione e i vari social network. Per questo motivo le
Analytics API ci mettono a disposizione unapposita specializzazione della classe
HitBuilders.HitBuilder descritta dalla classe HitBuilders.SocialBuilder che offre la
possibilit di impostare le seguenti informazioni attraverso i corrispondenti metodi
set:

network
action
target


Per network si intende il particolare social network, ovvero Google+, Facebook,
Twitter e cos via. La action invece la particolare azione che stata eseguita, che
quindi potrebbe essere il Mi piace di Facebook oppure il +1 di Google+ o il
semplice share. Il target infine rappresenta linformazione che stata effettivamente
gestita. Se si tratta di uno share essa contiene, per esempio, il link della risorsa
condivisa.
Linvio delle informazioni social al server banale e consiste nel semplice utilizzo
delle seguenti righe di codice:

final Tracker tracker = Track.get().getTracker();
tracker.setScreenName(SCREEN_NAME);
tracker.send(new HitBuilders.SocialBuilder()

.setNetwork(MY_SOCIAL_NETWORK)
.setAction(LIKE)
.setTarget(ANDROID_URL)
.build());

Configurazioni varie
Le Google Analytics API sono molto estese e in continua evoluzione, per cui una
trattazione completa dellargomento meriterebbe da sola un intero volume.
Concludiamo questo argomento con le informazioni che riguardano alcune delle
possibilit offerte da questo strumento. La prima riguarda la possibilit di impostare
delle grandezze custom che possono essere utili, per esempio, nel caso di integrazioni
con altri sistemi CRM esterni.
Le API che abbiamo descritto permettono non solo lacquisizione di un numero di
informazioni che pu anche essere elevato, ma anche di inviarle al server in modo
ottimizzato; si parla di modalit di dispatch. La configurazione di default prevede che
le informazioni vengano inviate a intervalli regolari, che possibile modificare
attraverso la seguente definizione nel file di configurazione del particolare Tracker:

<int name="ga_dispatchPeriod">30</int>


Si tratta di una configurazione che possibile disabilitare attraverso un valore
nullo o negativo. In questi casi per necessario utilizzare la modalit dispatch manuale
e che consiste sostanzialmente nella gestione dellinvio attraverso la seguente riga di
codice, che utilizza il metodo dispatchLocalHits() delloggetto di tipo GoogleAnalytics:

GoogleAnalytics.getInstance(getActivity().getBaseContext()).dispatchLocalHits();


Unultima configurazione riguarda la possibilit di raccogliere informazioni sulla
tipologia di utenti che eseguono la nostra applicazione, abilitando la Advertising Id
Collection attraverso il seguente metodo delloggetto di tipo Tracker:

public void enableAdvertisingIdCollection (boolean enabled)


In questo caso bene assicurarsi di accettare tutti i termini e le condizioni richieste
da Google, tra cui la necessit di informare gli utenti sul fatto che si stanno
raccogliendo informazioni, spiegandone i motivi.

Conclusioni
In questo capitolo abbiamo avuto loccasione di esaminare le Google Analytics
API ovvero quegli strumenti che ci permettono di raccogliere informazioni di vario
tipo sugli utenti che utilizzano la nostra applicazione e sui relativi comportamenti.
Dopo una descrizione dei passi di configurazione, abbiamo visto come tracciare le
schermate visitate e i diversi tipi di eventi. Abbiamo visto come inviare informazioni
relative alle azioni degli utenti, ma anche informazioni relative alla tempistica con cui
le varie operazioni vengono eseguite. Abbiamo poi visto come gestire le eccezioni.
Nella parte finale abbiamo visto come gestire le campagne, ovvero definire il mezzo
che ha portato la nostra applicazione nelle mani dellutente. Si tratta di uno strumento
molto potente e in continua evoluzione che permette, se ben utilizzato, di aumentare
il numero di utenti comprendendone il comportamento.

Capitolo 13

Google Ads

In questultimo capitolo affrontiamo un argomento molto importante, relativo alla


gestione degli Ads (Advertisement) allinterno di unapplicazione Android. Si tratta
sostanzialmente di un meccanismo che permette di vendere pubblicit attraverso
unapplicazione Android. Limportanza di questo argomento legata al fatto che
molte delle applicazioni esistenti si mantengono e sostengono proprio attraverso
questo metodo di guadagno. Laggiunta di un banner alla nostra applicazione una
procedura che pu essere allo stesso tempo molto semplice e complicata. semplice
in quanto, come vedremo, i passi da seguire sono abbastanza automatici, ma
complicata in quanto quello che vedremo solamente uno dei modi che esistono per
guadagnare con la pubblicit. Anche in questo caso una descrizione di tutti questi
meccanismi richiederebbe un libro intero, per cui ci dedicheremo alla descrizione di
come integrare dei banner attraverso AdMob. Non si tratta, comunque, di una
limitazione, in quanto gli altri meccanismi sono molto simili e si differenziano
solamente a livello di configurazione. Per farlo ci aiuteremo con il classico progetto
di test.

Configurazione e visualizzazione di un
banner
Il nostro primo obiettivo quello relativo alla configurazione di AdMob e alla
visualizzazione del nostro primo Banner allinterno di un nostro Fragment, ma lo stesso
potr essere fatto con una Activity. Come fatto nel precedente capitolo creiamo il
nostro progetto, questa volta di nome AdMobTest attraverso il Wizard di Android Studio
scegliendo, tra le diverse opzioni, quella relativa alla creazione di una Activity e di un
(Figura 13.1) tenendo poi le altre impostazioni di default.

Fragment

Figura 13.1 Opzione della Activity con un Fragment.

Una volta creato il progetto andiamo a definire la dipendenza con i Google Play
services nel file di configurazione di Gradle associato al modulo principale
dellapplicazione. A questo file andiamo ad aggiungere la dipendenza evidenziata nel
seguente codice:

dependencies {

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


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

Come possiamo notare, abbiamo definito solamente la libreria che ci interessa,


senza tutte le altre dipendenze che dovranno essere importate nel caso di utilizzo
delle corrispondenti funzionalit.
A questo punto andiamo ad aggiungere le varie definizioni nel file di
configurazione AndroidManifest.xml e precisamente quelle relative ai permessi e alla
versione dei Google Play services utilizzati. In particolare i permessi sono quelli
classici relativi allutilizzo della Rete e quindi del corrispondente stato al fine di un
uso ottimale della stessa:

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


La configurazione dei Google Play services richiede la semplice aggiunta del
seguente elemento nel modo che ormai conosciamo molto bene.

<meta-data android:name="com.google.android.gms.version"

android:value=@integer/google_play_services_version/>

Prima di proseguire dobbiamo fare unultima dichiarazione in questo file, relativa
alla Activity da utilizzare per la visualizzazione del banner, se selezionato. Si tratta di
una Activity che ci viene fornita dalla libreria e che dobbiamo definire nel seguente
modo:

<activity

android:name=com.google.android.gms.ads.AdActivity
android:configChanges=keyboard|keyboardHidden|orientation|
screenLayout|uiMode|screenSize|smallestScreenSize
android:theme=@android:style/Theme.Translucent/>

Oltre al nome della classe che descrive lActivity notiamo la presenza dellattributo
che, ricordiamo, permette di indicare quali delle modifiche nelle

android:configChanges

configurazioni del dispositivo debbano essere gestite direttamente dallattivit stessa.

Il passo successivo invece quello della creazione della View che andr a contenere
il banner da visualizzare. Per farlo esistono due diverse possibilit. La prima consiste
nellutilizzare direttamente la View del Banner inserendola nel nostro layout. La
seconda, pi consona alle linee guida di Google, prevede di incapsulare il Banner
allinterno di un Fragment. Nel nostro caso abbiamo deciso di creare un Fragment che
abbiamo descritto attraverso la classe BannerFragment con associato il layout descritto
dal file fragment_banner.xml che andiamo a esaminare in dettaglio.

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

xmlns:ads=http://schemas.android.com/apk/res-auto
android:layout_width=match_parent
android:layout_height=match_parent>
<com.google.android.gms.ads.AdView
android:id=@+id/adView
android:layout_width=match_parent
android:layout_height=wrap_content
android:layout_centerHorizontal=true
ads:adSize=BANNER
ads:adUnitId=@string/banner_ad_unit_id>
</com.google.android.gms.ads.AdView>
</RelativeLayout>


Innanzitutto notiamo come il contenitore sia un RelativeLayout che occupa tutto lo
spazio a disposizione, il quale contiene una View rappresentata da un oggetto di tipo
che viene messo a disposizione dalle librerie di Google. Si tratta di un

AdView

componente custom che definisce anche due diversi attributi associati al namespace
. Il primo di questi ci permette di specificare il

http://schemas.android.com/apk/res-auto

tipo di banner che intendiamo visualizzare. I possibili valori con le relative


dimensioni sono i seguenti, ma altri potrebbero essere aggiunti in futuro:

BANNER 320x50
LARGE_BANNER 320x100

MEDIUM_RECTANGLE 300x250
FULL_BANNER 468x60
LEADERBOARD 728x90


Alcuni si adattano meglio allutilizzo nelle applicazioni per smartphone e altri
nelle versioni per tablet. Il secondo attributo invece relativo allaccount che
possiamo creare attraverso la console messa a disposizione da AdMob stessa e che
possiamo trovare al seguente indirizzo:

https://www.google.com/admob/


Al momento labbiamo inserito in una risorsa di tipo String. Le stesse impostazioni
che possiamo fare attraverso questi attributi possono essere fatte anche a livello di
codice tramite i seguenti metodi della classe AdView:

public void setAdSize(AdSize adSize)
public void setAdUnitId(String adUnitId)


una classe che permette di rappresentare i possibili valori che abbiamo

AdSize

visto in precedenza per i tipi di banner.


Per creare uno unit id sufficiente registrarsi ad AdMob, entrare nella relativa console
e selezionare lopzione Monetize your app, come mostrato in Figura 13.2. Nel caso in cui
non si volesse seguire il procedimento che andiamo a descrivere comunque
possibile utilizzare il seguente id di test, che abbiamo definito come risorsa di tipo
String nel file di nome ads.xml:

<!-- The id for the banner for test -->
<string name="banner_ad_unit_id">ca-app-pub-3940256099942544/6300978111</string>

Figura 13.2 Creiamo uno unit id per la nostra applicazione.

Questo porta alla visualizzazione di una specie di wizard che, come primo passo, ci
richiede le informazioni relative alla nostra applicazione. possibile cercare
lapplicazione nel Play Store oppure inserire i dati manualmente, come nel nostro
caso, come possiamo vedere nella Figura 13.3. Selezionando il pulsante Add app ci
viene richiesto il tipo di banner che si intende visualizzare ovvero se un normale
Banner oppure un Interstitial, come vediamo nella Figura 13.4. Scegliamo la prima
opzione (vedremo successivamente che cos un Interstitial) e notiamo la possibilit
di impostare alcune informazioni relative al tipo di banner (testuale e/o immagine), la
frequenza di aggiornamento oltre a un nome che ci permette di identificarlo.

Figura 13.3 Inseriamo i dati relativi alla nostra applicazione.

Figura 13.4 Scegliamo il tipo di banner.

Una volta che abbiamo salvato, abbiamo finalmente ottenuto il nostro


identificatore che possiamo quindi utilizzare nella nostra applicazione per quello
specifico banner.

Figura 13.5 Abbiamo ottenuto lo unit id.

A questo punto il layout completato, per cui passiamo alla gestione del Fragment
descritto dalla classe BannerFragment, che alquanto banale come possiamo vedere nel
seguente codice:

public class BannerFragment extends Fragment {

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_banner, container, false);
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
AdView mAdView = (AdView) getView().findViewById(R.id.adView);
AdRequest adRequest = new AdRequest.Builder().build();

mAdView.loadAd(adRequest);

}
}


Il metodo onCreateView() non fa altro che eseguire linflate del layout che abbiamo
creato in precedenza, mentre la parte interessante quella contenuta nel metodo
onActivityCreated() che ricordiamo essere richiamato quando lActivity contenitore ha
concluso lesecuzione del proprio metodo onCreate() e quindi ha inizializzato il
proprio layout. Come prima cosa otteniamo un riferimento alloggetto di tipo AdView
nel layout e quindi creiamo un oggetto di tipo AdRequest che rappresenta la richiesta
che stiamo inviando al server degli Ads e da cui ci aspettiamo in risposta il particolare
banner da visualizzare. In questo caso si tratta della richiesta pi semplice, che
creiamo attraverso lutilizzo del corrispondente Builder. Per linvio della richiesta
utilizziamo il seguente metodo:

public void loadAd(AdRequest adRequest)


A questo punto non ci resta che utilizzare il nostro BannerFragment nella
MainActivity. Questo pu avvenire sia a livello di codice sia direttamente nel
documento di layout, come abbiamo fatto nel seguente documento di layout nel quale
abbiamo messo in evidenza lutilizzo del nostro Fragment accorciando il nome del
package per motivi di spazio:

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

xmlns:tools=http://schemas.android.com/tools
android:id=@+id/container
android:layout_width=match_parent
android:layout_height=match_parent
tools:context=.MyActivity
tools:ignore=MergeRootFrame>
<fragment android:name=uk.co
admobtest.MainActivity$PlaceholderFragment
android:layout_width=match_parent

android:layout_height=match_parent
android:layout_above=@+id/adFragment/>
<fragment
android:id=@+id/adFragment
android:name=uk.coadmobtest.BannerFragment
android:layout_width=match_parent
android:layout_height=wrap_content
android:layout_alignParentBottom=true/>
</RelativeLayout>


Da notare lutilizzo del nostro Fragment insieme a quello che era stato creato in
modo automatico dal wizard in fase di creazione del progetto e che descritto
attraverso una classe interna. Questo il motivo dellutilizzo del simbolo $ nel nome
della classe. La nostra MainActivity diventa quindi banale, in quanto non fa altro che
definire lutilizzo del nostro documento di layout attraverso la seguente
implementazione del metodo onCreate().

@Override
protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}


Non ci resta che lanciare la nostra applicazione, ottenendo quanto rappresentato
nella Figura 13.6, dove notiamo la presenza del Banner nella parte inferiore dello
schermo.

Figura 13.6 Visualizzazione del Banner.

Se facciamo clic sul Banner viene lanciata una schermata con la documentazione
di AdMob; questo in quanto stiamo utilizzando un id di test.
In questo paragrafo abbiamo visto come sia semplice aggiungere un Banner alla
propria applicazione. Prima di procedere con la descrizione degli Interstitial
vogliamo menzionare la presenza dellinterfaccia AdListener, che ci permette di
ricevere le notifiche sullo stato delle richieste fatte al server. Questo utile
soprattutto nella gestione degli errori, in modo da visualizzare delle informazioni
alternative oppure nascondere il banner nel caso qualcosa non andasse per il verso
giusto. Nel nostro progetto di esempio abbiamo creato una nostra implementazione
che non fa altro che visualizzare dei messaggi di log in corrispondenza di ciascun
evento. Il metodo seguente viene richiamato nel momento in cui il Banner viene
caricato con successo.

@Override
public void onAdLoaded() {

Log.d(TAG, onAdLoaded);
}


Esistono poi due metodi di callback che vengono richiamati rispettivamente
quando lutente fa clic sul banner e quando ritorna allapplicazione.

@Override
public void onAdOpened() {

Log.d(TAG, onAdOpened);
}
@Override
public void onAdClosed() {

Log.d(TAG, onAdClosed);
}


Quando lutente lascia lapplicazione il metodo richiamato il seguente:

@Override
public void onAdLeftApplication() {

Log.d(TAG, onAdLeftApplication);
}


Come accennato in precedenza, lultimo metodo riguarda la gestione degli errori
attraverso alcuni codici che possiamo vedere nella nostra implementazione:

@Override
public void onAdFailedToLoad(int errorCode) {

String message = Unknown;


switch (errorCode) {
case AdRequest.ERROR_CODE_INTERNAL_ERROR:
message = ERROR_CODE_INTERNAL_ERROR;
break;
case AdRequest.ERROR_CODE_INVALID_REQUEST:

message = ERROR_CODE_INVALID_REQUEST;
break;
case AdRequest.ERROR_CODE_NETWORK_ERROR:
message = ERROR_CODE_NETWORK_ERROR;
break;
case AdRequest.ERROR_CODE_NO_FILL:
message = ERROR_CODE_NO_FILL;
break;
}
Log.d(TAG, message);
}


Una volta creata unimplementazione dellinterfaccia AdListener di callback
dobbiamo registrarla alla relativa sorgente, che in questo caso loggetto di tipo
AdView. sufficiente utilizzare il seguente metodo, come abbiamo fatto nel nostro
esempio:
public void setAdListener(AdListener adListener)

Lasciamo al lettore la verifica della chiamata dei metodi di callback in


corrispondenza delle azioni.

Interstitial
Nel paragrafo precedente abbiamo visto come aggiungere alle schermate della
nostra applicazione un banner che nel nostro esempio di piccole dimensioni e
visualizzato nella parte inferiore dello schermo. Linterazione dellutente con questo
tipo di banner pu terminare qui, in quanto non succede nulla a meno che lutente
non selezioni il banner stesso. Un banner di tipo Interstitial invece molto diverso in
quanto occupa tutto lo schermo utilizzando contenuti HTML 5 che vengono
visualizzati immediatamente. In questo caso lutente pu solamente chiudere il
banner e tornare allapplicazione. Si tratta di banner che vengono inseriti per esempio
nelle gallery di immagini, tra livelli diversi di un gioco, allavvio dellapplicazione e
altro ancora.
La gestione di un banner di tipo Interstitial molto semplice oltre che molto simile
a quanto visto nel paragrafo precedente. Nel nostro caso abbiamo semplicemente
creato una nuova Activity che si chiama InterstitialActivity e che si compone
sostanzialmente di due parti. La prima consiste nellinizializzazione delloggetto di
tipo InterstitialAd mentre la seconda consiste nella sua visualizzazione.
Linizializzazione contenuta nel metodo onCreate(), che abbiamo implementato nel
seguente modo:

private InterstitialAd mInterstitialAd;
@Override
public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_interstitial);
mInterstitialAd = new InterstitialAd(this);
mInterstitialAd.setAdUnitId(getString(R.string.interstitial_ad_unit_id));
AdRequest adRequest = new AdRequest.Builder().build();
mInterstitialAd.loadAd(adRequest);
}


Come prima cosa creiamo unistanza della classe InterstitialAd e utilizziamo il
metodo setAdUnitId() per limpostazione dello unit id ottenuto dalla console web di
AdMob. In questo caso, infatti, loggetto non stato definito nel layout e quindi
limpostazione dellid deve avvenire da programma. Come nel caso precedente

abbiamo quindi creato una AdRequest che abbiamo inviato attraverso il metodo
loadAd(). A questo punto abbiamo inviato la richiesta del banner, il quale non ancora
visualizzato. Per farlo abbiamo creato un Button che abbiamo connesso al seguente
metodo attraverso il suo attributo onClick.

public void showInterstitial(View button) {

// We show the Interstitial if its ready


if (mInterstitialAd.isLoaded()) {
mInterstitialAd.show();
}
}


Dopo la verifica che lInterstitial sia effettivamente disponibile, provvediamo alla
sua visualizzazione attraverso la chiamata del metodo show(). Il risultato quello
rappresentato nella Figura 13.7; possiamo notare la presenza del pulsante di chiusura
in alto a sinistra

Figura 13.7 Visualizzazione di un Interstitial.

La creazione dello unit id avviene come detto in modo simile al caso precedente,
con la sola differenza relativa alla configurazione dellInterstitial (Figura 13.8).
Come possiamo notare possibile decidere quali tipi di informazioni visualizzare
ovvero se testuali, come immagini o video insieme a quello che il timeout per la
loro inizializzazione. Anche in questo caso abbiamo specificato un nome che ci
permette di identificare il tipo di banner nelle eventuali statistiche.

Figura 13.8 Le configurazioni di un Interstitial nella console AdMob.

Specializzare la AdRequest
Sia nel caso della creazione di un semplice Banner che nel caso di un Interstitial
abbiamo creato una semplice AdRequest utilizzando il corrispondente Builder. In realt
questa classe ci permette di fornire al sistema delle informazioni aggiuntive che
permettono al server di personalizzare al meglio la tipologia di banner in relazione
alle caratteristiche dellutente. La prima di queste informazioni riguarda la Location.
Attraverso le seguenti poche righe di codice infatti possibile impostare la Location
dellutente in modo da richiedere contenuti in lingua:

AdRequest request = new AdRequest.Builder()

.setLocation(location)
.build();

Il metodo utilizzato ha la seguente firma, che ci indica come il parametro sia di
tipo Location ovvero del tipo che abbiamo imparato a gestire nel Capitolo 3:

public AdRequest.Builder setLocation(Location location)


La modalit con cui questa informazione viene ottenuta indipendente dalla
gestione dei banner ed comunque di responsabilit dello sviluppatore.
Unaltra informazione relativa allutente che possibile impostare riguarda il
sesso, attraverso il seguente metodo:

public AdRequest.Builder setGender(int gender)


Ecco il suo uso:

AdRequest request = new AdRequest.Builder()

.setGender(AdRequest.GENDER_FEMALE)
.build();

poi possibile impostare la data di nascita dellutente, se disponibile attraverso


Google+ o altro social network. Per farlo possibile utilizzare il seguente metodo:

public AdRequest.Builder setBirthday(Date birthday)


Eccone limpiego:

AdRequest request = new AdRequest.Builder()

.setBirthday(new GregorianCalendar(1970, 7, 28).getTime())


.build();

Si tratta di metodi della classe AdRequest.Builder, che possono essere utilizzati nella
creazione della stessa AdRequest.
Come facile intuire, la raccolta di informazioni di questo tipo pu essere un
aspetto molto sensibile, per il quale necessario poter fornire opportuni
accorgimenti. Per quello che riguarda la gestione delle informazioni sui minori
possibile utilizzare il seguente metodo:

public AdRequest.Builder tagForChildDirectedTreatment(boolean

tagForChildDirectedTreatment)

Questo permette di abilitare il controllo sui dati come previsto dalla Childrens
Online Privacy Protection Act (COPPA), per il cui approfondimento rimandiamo alla
documentazione ufficiale.
Concludiamo invece con la descrizione di uninformazione molto importante che
riguarda il testing delle applicazioni che contengono dei banner. Con AdMob e con
altri sistemi molto facile essere banditi in quanto accusati di provocare
visualizzazioni o clic sui nostri banner al fine di ottenere profitti. Per questo motivo
quando eseguiamo la nostra applicazione che contiene dei banner possiamo osservare
come nei log siano presenti informazioni del seguente tipo

I/Ads (26882): Starting ad request.
I/Ads (26882): Use AdRequest.Builder.addTestDevice("XXXXX38BXXXX55A91XXXX0270C4EFXXX") to get
test ads on this device.


Nella parte evidenziata notiamo la presenza di una istruzione che dovremo
aggiungere alla creazione delle AdRequest per il particolare device. Nel nostro caso la
seguente istruzione ci permette di indicare al sistema che il dispositivo che sta
eseguendo lapplicazione un dispositivo di test e che quindi le eventuali
visualizzazioni o clic sui nostri banner debbano essere ignorati dal sistema.

AdRequest.Builder.addTestDevice("XXXXX38BXXXX55A91XXXX0270C4EFXXX")


un accorgimento molto importante che ricordiamo deve essere ripetuto per
ciascuno dei dispositivi di test, al fine di non vedere cancellato il proprio account
AdMob.

Conclusioni
In questo breve capitolo abbiamo visto come sia facile aggiungere dei banner alla
nostra applicazione, al fine di ottenere degli introiti. Nella prima parte abbiamo visto
come creare un normale Banner, mentre nella seconda ci siamo occupati della
descrizione degli Interstitial, ovvero di quei banner a tutto schermo realizzati con
tecnologia HTML 5. Abbiamo concluso il capitolo descrivendo le impostazioni da
utilizzare al fine di una migliore targetizzazione degli annunci.

Indice

Prefazione
A chi rivolto il testo
Organizzazione del testo
Tool e versioni
Capitolo 1 - Introduzione ai Google Play services
Che cosa sono i Google Play services?
Installazione e setup di unapplicazione
Aggiornamento dei Google Play services
Conclusioni
Capitolo 2 - La gestione della Location
Design for Change
Inizializzazione dei servizi di Location
Determiniamo la nostra posizione
Utilizzo del Geocoder
Tracciamento della nostra posizione
Activity Recognition
Conclusioni
Capitolo 3 - Mappe e Geofence
Inizializzazione delle Google Maps
Visualizzazione della posizione corrente
Personalizzazione della mappa
Visualizzazione di un percorso
I Geofence
Integrazione di StreetView

Conclusioni
Capitolo 4 - Google Drive API
Google Drive
Inizializzazione di Google Drive
Esportazione delle informazioni di una FenceSession
Importazione delle informazioni di una FenceSession
La gestione dei Folder
Eseguire la ricerca di un File
Download automatico di File pinnable
Dati privati di unapplicazione
Conclusioni
Capitolo 5 - Integrazione con Google Plus
Eseguiamo la login con Google+
Invitiamo i nostri amici
Conclusioni
Capitolo 6 - Google Cloud Messaging
Architettura base
Predisposizione del client
Implementazione del server
Ricezione dei messaggi sul client
Implementazione di una semplice chat
User Notifications
Conclusioni
Capitolo 7 - Programmare i dispositivi wearable
La gestione delle notifiche per Android Wear
Applicazioni per Wear
Componenti di unapplicazione Wear
Tecniche di sincronizzazione

Invio e ricezione di messaggi


Conclusioni
Capitolo 8 - MediaRouter e Chromecast
MediaRouter API
Google Cast
Conclusioni
Capitolo 9 - Google Fit
Un semplice progetto con Google Fit
La gestione dei sensori
Data Point e sessioni
Accesso alle informazioni registrate
Conclusioni
Capitolo 10 - Google Game
Creazione del progetto e configurazione
Gestione login e obiettivi
Leaderboard
Realtime multiplayer
Gioco a turni
Game Gift
Gestione degli eventi e missioni
Salvataggio dei progressi
Conclusioni
Capitolo 11 - Pagamenti con In-app Billing
Concetti base e predisposizione ambiente
Creazione del catalogo
I servizi per la gestione degli acquisti
Accesso allinterfaccia remota
Configurazione di test

Conclusioni
Capitolo 12 - Google Analytics
Installazione e Screen View
La gestione degli eventi
Eventi temporizzati
Eccezioni ed errori
Gestire le sessioni
Gestione delle campagne
Eventi social
Configurazioni varie
Conclusioni
Capitolo 13 - Google Ads
Configurazione e visualizzazione di un banner
Interstitial
Specializzare la AdRequest
Conclusioni

Potrebbero piacerti anche