UNIVERSITÀ DEGLI STUDI DI PARMA Dipartimento di Matematica e Informatica Corso di Laurea in Informatica Progettazione e Realizzazione di una Library per Agenti Context-Aware Design and Implementation of a Library for Context-Aware Agents Relatore: Chiar.mo Prof. Federico Bergenti Candidato: Serri Cristiano Anno Accademico 2012/2013 Ai miei genitori ai miei zii e ai miei colleghi di studio che mi hanno sostenuto e supportato in questi anni Indice 1 Introduzione 2 Introduzione a Jade 2.1 Creazione di Piattaforme e Container . . . 2.2 Creazione di Agenti e Agenti Speciali . . . 2.3 Inizializzazione degli Agenti . . . . . . . . 2.4 Gestione Eccezioni e Terminazione Agente 2.5 I Behaviour . . . . . . . . . . . . . . . . . 2.6 Scambio di Messaggi tra gli Agenti . . . . 2.7 Creare una Ontology e i suoi Componenti 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 4 6 8 10 11 14 17 3 Programmazione su Android 3.1 Ambiente di Sviluppo: ADT . . . . . . . . . 3.2 Creare ed Eseguire un Android Project . . . 3.3 Importare Progetti e Librerie . . . . . . . . 3.4 Activity e Intent . . . . . . . . . . . . . . . 3.5 Manifest della Applicazione . . . . . . . . . 3.6 Costanti di Stringa usate dalla Applicazione 3.7 Descrizione del Menu di una Activity . . . . 3.8 Descrizione della Videata di una Activity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 20 20 22 23 25 27 27 28 4 La Library Dave 4.1 Le Informazioni Geografiche . . . . . . . . . . . 4.1.1 La Posizione: EarthPoint . . . . . . . . 4.1.2 Aggiungiamo la Direzione: EarthVector 4.1.3 Il Punto di Vista: EarthPartialCircle . . 4.2 Classe Helper per Jade . . . . . . . . . . . . . . 4.3 Gli Agenti di Dave . . . . . . . . . . . . . . . . 4.3.1 DaveClientAgent . . . . . . . . . . . . . 4.3.2 FixedAgent . . . . . . . . . . . . . . . . 4.3.3 RandomAgent . . . . . . . . . . . . . . 4.3.4 AndroidRealAgent . . . . . . . . . . . . 4.3.5 DaveServerAgent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 30 30 31 32 33 34 34 37 37 37 38 INDICE iii 4.4 UpdatePlanner . . . . . . . . . . . . . . . . . 4.4.1 NeverUpdatePlanner . . . . . . . . . . 4.5 Riduzione del Carico Computazionale . . . . 4.5.1 LevelAccessRule . . . . . . . . . . . . 4.5.2 AccessRules . . . . . . . . . . . . . . . 4.5.3 QualityOfService . . . . . . . . . . . . 4.5.4 NoService . . . . . . . . . . . . . . . . 4.6 Esempio di Creazione e Configurazione Client 4.7 Struttura Dati del Server . . . . . . . . . . . 4.7.1 ClientMatrix . . . . . . . . . . . . . . 4.7.2 ClientRow . . . . . . . . . . . . . . . . 4.7.3 OtherClient . . . . . . . . . . . . . . . 4.7.4 Levels . . . . . . . . . . . . . . . . . . 4.8 Following . . . . . . . . . . . . . . . . . . . . 4.9 Visualizzazione Agenti sulle GoogleMaps . . . 4.10 DaveReceiver . . . . . . . . . . . . . . . . . . 4.10.1 IntentFilter . . . . . . . . . . . . . . . 4.10.2 Azioni compiute . . . . . . . . . . . . 5 Esempio Applicazione Dave 5.1 Le Fasi del gioco . . . . . . . . . . . . . . 5.1.1 Fase di Posizionamento dei Tesori 5.1.2 Fase di Ricerca dei Tesori . . . . . 5.1.3 Fine del Gioco . . . . . . . . . . . 5.2 Gli Agenti di DaveTreasureHunt . . . . . 5.2.1 TreasureAgent . . . . . . . . . . . 5.2.2 ExplorerAgent . . . . . . . . . . . 5.2.3 PirateAgent . . . . . . . . . . . . . 5.3 GameStorage . . . . . . . . . . . . . . . . 5.3.1 ExplorerMatrix . . . . . . . . . . . 5.3.2 ExplorerRecord . . . . . . . . . . . 5.4 Screenshot del Gioco . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 39 39 41 41 42 42 43 44 44 45 46 46 47 48 51 51 52 . . . . . . . . . . . . 54 54 54 55 56 56 56 57 58 59 60 60 60 6 Conclusioni 64 Bibliografia 66 Capitolo 1 Introduzione Dave (Discover Agents Viewed on Earth) è una libreria scritta in Java che permette ai dispositivi Android che la utilizzano di acquisire la propria posizione geografica (tramite WiFi o GPS) e la direzione in cui sono rivolti (tramite Giroscopio), elaborare tali informazioni (per vedere se ci sono stati cambiamenti significativi) e trasmetterle. Le informazioni geografiche dei vari dispositivi vengono ricevute, memorizzate e rese disponibili da un processo Server che risiede su un pc il cui indirizzo ip è raggiungibile dai vari dispositivi Client. Queste operazioni consentono ad ogni dispositivo Android di individuare (e visualizzare opportunamente su una mappa con cui l’utente del dispositivo può interagire) quali altri dispositivi si trovano nel suo punto di vista. Dave è stata sviluppata utilizzando le funzionalità e i concetti della libreria Jade, sviluppata da Telecom Italia. Jade (Java Agent DEvelopment framework) è un framework che permette di costruire applicazioni Java basate sul concetto di Agente. Gli Agenti si distringuono dai normali oggetti Java (che possiedono solo attributi e metodi) perchè possiedono dei comportamenti (Behaviour) che vengono eseguiti e prendono decisioni in modo autonomo, eseguiti in modo concorrente da uno Scheduler (di tipo Round Robin senza prelazione) interno all’Agente stesso. Gli Agenti che fanno parte di una stessa Piattaforma possono comunicare tramite scambio di messaggi. Questa comunicazione è asincrona poichè i messaggi inviati vengono depositati nelle mailbox dei destinatari, ciascuno dei quali deciderà in modo autonomo se e quando leggerli. Utilizzando Jade in particolare si semplificano le comunicazioni via rete nelle applicazioni multi-utente. In Dave, sia i vari Client (Android) sia il Server (PC) sono implementati come Agenti. Inoltre ci sono alcuni Agenti Client che possono essere creati su PC. Questi Agenti non potranno ovviamente rilevare la propria posizione e direzione, la quale potrà essere definita all’inizio e rimanere costante (op- 2 pure cambiare in modo rotatorio o random). La prima fase del mio lavoro di Tesi consiste nello studio e nell’utilizzo della libreria Jade e nell’allestimento dell’Ambiente di Sviluppo per Android, comprendendo la struttura dei progetti e i principi alla base della programmazione su tale piattaforma. Le nozioni che ho appreso durante questa fase sono trattate (con un approccio di tipo operativo) rispettivamente nei capitoli 2 e 3. La seconda fase consiste nello sviluppo e nel test della libreria Dave (capitolo 4), e di un gioco (DaveTreasureHunt, una caccia al tesoro) che si appoggi su tale libreria (capitolo 5). Capitolo 2 Introduzione a Jade Questo capitolo ha lo scopo di illustrare il funzionamento e i concetti base della libreria Jade (Java Agent DEvelopment framework), evidenziando principalmente le sue parti che sono state utili alla costruzione della libreria Dave. La guida completa, e il download della versione aggiornata della libreria (comprensiva anche di esempi), è possibile scaricarla dal sito ufficiale di Jade, ovvero jade.tilab.com. Come anticipato nell’introduzione, Jade consente di creare applicazioni basate sul concetto di Agente. Verrà descritto come allestire il luogo (la Piattaforma e i vari Container) dove creare gli Agenti, e per ciascuno di questi come definire i vari comportamenti autonomi e come interagire con gli altri Agenti. Nel corso della trattazione di questa sezione riguardante Jade, verrà utilizzato il classico esempio nel quale gli Agenti sono venditori e compratori di Libri (BookTrading): • Ciascun venditore inizia avendo in suo possesso un certo insieme di libri, ciascuno dei quali ha un nome e un prezzo. I venditori vengono contattati dai compratori per sapere se posseggono un determinato libro, i venditori rispondono in modo opportuno, comunicando il prezzo in caso affermativo. Inoltre i venditori vengono contattati dai compratori anche in caso di acquisto, dove forniscono il libro al compratore rimuovendolo dai libri posseduti. • I compratori hanno lo scopo di acquistare un singolo libro, al prezzo minore tra quelli di tutti i venditori che posseggono il libro richiesto, poi terminano. Durante il lavoro di tesi, per integrare lo studio delle funzionalità di Jade con una prova pratica, è stato esteso l’esempio base, ovvero quello presente nella directory examples/bookTrading (scaricando il pacchetto completo di Jade). Le principali modifiche consistono: 2.1 Creazione di Piattaforme e Container 4 • Nell’utilizzo di un automa a stati finiti (FSMBehaviour) come Behaviour principale del compratore. • Nell’aggiunta di una Ontology specifica per i Libri, in modo tale che lo scambio di messaggi non sia più string-based ma costituito da Concetti e Predicati ad hoc. Usufruendo dell’interfaccia grafica per creare e rimuovere compratori o venditori in momenti arbitrari, dello standard output per capire che cosa sta facendo ciascun Agente e come avviene la loro comunicazione, e degli strumenti messi a disposizione da Jade stesso, quali l’Rma e lo Sniffer, questo esempio BookTrading può essere utilizzato come ottimo strumento di studio per i programmatori che si avvicinano a Jade. 2.1 Creazione di Piattaforme e Container Quando avviamo Jade (tramite la procedura descritta in seguito) dobbiamo decidere se creare una nuova Piattaforma con un Container Principale (Main) oppure se connetterci ad una Piattaforma già esistente tramite un Container Periferico. I Container sono il luogo dove gli agenti di Jade sono in esecuzione. Solo gli Agenti che “vivono” nella stessa Piattaforma (all’interno dello stesso container o di container differenti) possono interagire (ovvero comunicare tramite scambi di messaggi). Di seguito vengono descritte le principali caratteristiche delle due tipologie di Container: • Il Main Container deve esistere ed essere unico in ciascuna Piattaforma di Jade. La creazione di un Main Container coincide con la creazione di una Piattaforma. Tale operazione di creazione è possibile effettuarla solo da PC, e non da dispositivo Android. • Non ci sono limiti al numero di Container Periferici che una Piattaforma può avere. Quando si crea un Container Periferico bisogna indicare una Piattaforma già esistente (nella quale in particolare si trova già il Main Contaiber) alla quale tale Container si registrerà. I Container Periferici possono essere creati indifferentemente da Android o da PC. Per poter creare una nuova Piattaforma è necessario che non ci sia alcun servizio attivo sulla porta 1099 (è quella che di default Jade usa, se non specificato diversamente). La porta 1099 può essere occupata ad esempio da vecchie istanze di Jade non terminate correttamente. L’eccezione che tipicamente viene lanciata quando la porta è già in uso è: jade.core.IMTPException: No ICP active at jade.imtp.leap.LEAPIMTPManager.initialize(LEAPIMTPManager.java:138) at jade.core.AgentContainerImpl.init(AgentContainerImpl.java:319) at jade.core.AgentContainerImpl.joinPlatform(AgentContainerImpl.java:489) at jade.core.Runtime.createMainContainer(Runtime.java:166) 2.1 Creazione di Piattaforme e Container 5 Inoltre, per consentire ai Container Periferici di connettersi al Main Container su questa Piattaforma bisogna controllare che la porta non sia bloccata dal Firewall del sistema operativo. Per sbloccarla occorre aggiungere nel Firewall una nuova regola che consenta le connessioni in ingresso su tale porta da parte di qualsiasi applicazione (in Windows è possibile farlo nelle Impostazioni Avanzate del Windows Firewall, accessibile dal Pannello di Controllo nella scheda Sistema e Sicurezza). Queste istruzioni consentono di creare un Main Container su PC: import jade.core.Profile; import jade.core.ProfileImpl; import jade.core.Runtime; ... Profile profile = new ProfileImpl(); AgentContainer container = Runtime.instance().createMainContainer(profile); Supponiamo di avere creato una Piattaforma su un pc con indirizzo Ip 192.168.0.102 nella porta di default (1099), ovvero di avere eseguito su tale pc un programma che contenga le istruzioni soprastanti (il costruttore di default di ProfileImpl imposta come indirizzo ip localhost e come porta la 1099). Se questo punto vogliamo creare da un altro PC un Container Periferico facente parte della stessa piattaforma, utilizziamo il codice seguente: import jade.core.Profile; import jade.core.ProfileImpl; import jade.core.Runtime; ... Profile profile = new ProfileImpl("192.168.0.102", 1099, null, false); AgentContainer container = Runtime.instance().createAgentContainer(profile); Abbiamo usato il costruttore più generico di ProfileImpl. Il terzo parametro è l’etichetta da applicare al container (null significa di usare quella di default), e il quarto parametro impostato a false significa che il container che vogliamo creare non è Main. Creare un Container Periferico da Android è più complesso, poichè bisogna connettere (bindService) il servizio di Jade per Android (MicroRuntimeService) all’Activity (il concetto di Activity, così come quello di Intent, verranno trattati nel capitolo riguardante Android). La ServiceConnection e la CallBack contengono codice che viene eseguito rispettivamente al completamento dell’operazione di bind e di creazione del Container Periferico (tali operazioni non vengono fatte istantaneamente, quindi questo procedimento è necessario). 2.2 Creazione di Agenti e Agenti Speciali 6 Properties properties = new Properties(); properties.setProperty(Profile.MAIN_HOST, "192.168.0.102"); properties.setProperty(Profile.MAIN_PORT, "1099")); properties.setProperty(Profile.MAIN, "false"); properties.setProperty(Profile.JVM, Profile.ANDROID); properties.setProperty(Profile.LOCAL_HOST, AndroidHelper.getLocalIPAddress()); properties.setProperty(Profile.LOCAL_PORT, "1099"); ContainerStartupCallback contCallback = new RuntimeCallback<Void>() { @Override public void onSuccess(Void thisIsNull) { // Container Creato } @Override public void onFailure(Throwable throwable) { // Fallimento nella creazione del Container } }; ServiceConnection ServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { microRuntimeServiceBinder = (MicroRuntimeServiceBinder) service; if (!MicroRuntime.isRunning()) { microRuntimeServiceBinder.startAgentContainer( properties, contCallback ); } }; @Override public void onServiceDisconnected(ComponentName name) { microRuntimeServiceBinder = null; } } //Suppongo che activity sia il puntatore all’Activity che invoca questo codice activity.bindService( new Intent(activity, MicroRuntimeService.class), this.serviceConnection, Context.BIND_AUTO_CREATE ); 2.2 Creazione di Agenti e Agenti Speciali Quando il Container (Main o Periferico) è pronto, possiamo creare gli agenti e collocarli in tale Container. L’istruzione seguente consente (in modo compatto) di creare ed eseguire 1 Agente da PC: container.createNewAgent(nome, classe, args).start(); 2.2 Creazione di Agenti e Agenti Speciali 7 Di seguito descrivo lo scopo di ciascuno dei parametri: • Il nome verrà usato come nome locale del nuovo Agente deve essere univoco all’interno della Piattaforma (quindi non si può scegliere come nome quello di un altro Agente presente nello stesso Container o in un altro) • La classe (comprensiva di package) è una qualsiasi classe che estende jade.core.Agent, usata per creare e personalizzare il nuovo Agente. • Il terzo parametro, args è un array di Object che verrà passato al nuovo Agente una volta creato, che può essere usato per personalizzarlo. Se all’Agente non servono argomenti, è possibile usare null. Prima di vedere come eseguire la stessa operazione su Android, presentiamo un Agente molto comodo, contenuto nella classe jade.tools.rma.rma. Possiamo crearlo con il codice sopradescritto, ovvero: container.createNewAgent("rma","jade.tools.rma.rma", null).start(); L’rma è provvisto di una interfaccia grafica, che offre molte comodità e funzionalità, tra cui: • Possiamo vedere i vari Container presenti nella Piattaforma e per ciascuno di essi gli Agenti contenuti. E’ possibile effettuare le operazioni di creazione o distruzione di utenti e containers. • Nella barra del titolo è presente l’indirizzo Ip e la porta della Piattaforma in uso • Possiamo avviare gli altri Tools di Jade, ad esempio l’utile Sniffer, che consente di catturare e leggere tutti i messaggi inviati tra gli Agenti. 2.3 Inizializzazione degli Agenti 8 Notiamo subito che oltre all’Agente che abbiamo appena creato (l’rma), nel Main Container erano già presenti due Agenti (creati insieme alla Piattaforma stessa): • L’ams registra i nomi di tutti gli Agenti che vengono creati, e assicura che ciascuno abbia un nome univoco. Inoltre rappresenta l’autorità all’interno della piattaforma, ovvero è l’Agente al quale gli altri Agenti devono rivolgersi per compiere determinate operazioni. E’ in grado di fornire l’elenco dei nomi degli Agenti a coloro che lo richiedono, ovvero offre un servizio di “Pagine Bianche”. • Il df fornisce invece un servizio di “Pagine Gialle”, ovvero è in grado di fornire su richiesta la lista degli Agenti che rispecchiano determinate caratteristiche (che offrono determinati servizi). Una differenza importante rispetto all’ams è che gli Agenti presenti nella Piattaforma non sono automaticamente riconosciuti dal df, ma devono registrarsi per poter essere trovati con le ricerche fatte dagli altri Agenti. Per concludere la sezione, il codice seguente consente di fare l’operazione di creazione di un Agente da Android. Anche qui dobbiamo ricorrere ad una Callback: agentStartupCallback = new RuntimeCallback<Void>() { @Override public void onSuccess(Void thisIsNull) { // Agente creato correttamente } @Override public void onFailure(Throwable throwable) { // Creazione fallita } }; microRuntimeServiceBinder.startAgent(nome, classe, args, agentStartupCallback); 2.3 Inizializzazione degli Agenti Una volta che un Agente è stato creato, ha assunto un regolare nome univoco e un indirizzo, e l’ams è venuto a conoscenza della sua presenza, allora il corrispondente Thread può cominciare la sua esecuzione. Il primo metodo dell’Agente che viene invocato è il setup(), nel quale solitamente si effettuano le seguenti operazioni: • si recuperano i parametri passati all’Agente al momento della sua creazione, che serviranno per personalizzarlo. • ci si registra con il DF (fornendo l’elenco dei Servizi offerti) se si desidera essere trovati tramite il servizio di “Pagine Gialle” 2.3 Inizializzazione degli Agenti 9 • si aggiungono tutte le Ontology che l’Agente deve conoscere e gestire (il concetto di Ontology sarà trattato in seguito) • si aggiungono i codec che verranno utilizzati per codificare e decodificare il contenuto degli ACLMessage nella forma appropriata (tra cui Stringhe o sequenze di byte). • si definiscono e si aggiungono all’Agente i suoi comportamenti autonomi (Behaviour) • si creano e inizializzano le strutture dati che servono all’Agente per memorizzare informazioni e prendere così le proprie decisioni. public class BookSellerAgent extends Agent{ // Place where the Books owned by this Seller Agent are stored private Books library; // Codec for the SL Language (ACLMessages) private SLCodec codec; @Override protected void setup(){ codec = new SLCodec(); super.getContentManager().registerLanguage(codec); super.getContentManager().registerOntology(BookOntology.getInstance()); this.library = new Books(); for (Object o: super.getArguments()) if (o instanceof Book){ this.library.add((Book) o); } DFAgentDescription agentDescription = new DFAgentDescription(); agentDescription.setName(super.getAID()); ServiceDescription serviceDescription = new ServiceDescription(); serviceDescription.setName("Book Selling"); serviceDescription.setType("Book Selling"); agentDescription.addServices(serviceDescription); try { DFService.register(this, agentDescription); } catch (FIPAException e) { e.printStackTrace(); super.doDelete(); } // Creation and addition of Behaviours to the scheduler super.addBehaviour(new ReceiveQueriesBehaviour()); super.addBehaviour(new SellBookBehaviour()); } ... } 2.4 Gestione Eccezioni e Terminazione Agente 2.4 10 Gestione Eccezioni e Terminazione Agente Nel metodo setup() del venditore di libri illustrato nella sezione precedente notiamo che se la registrazione con il DF fallisce viene lanciata una Eccezione (di classe FIPAException). Il comportamento che dobbiamo adottare nella gestione delle Eccezioni all’interno di un Agente varia a seconda della categoria: • Per quanto riguarda le Eccezioni che estendono RuntimeException, il linguaggio Java stesso ci permette di non gestirle (ovvero il compilatore non segnala errori se non dichiariamo l’eccezione dell’intestazione del metodo o non allestiamo un blocco try-catch). Esempi di queste Eccezioni sono IllegalArgumentException, NullPointerException, StringIndexOutOfBoundsException ... Le eccezioni non catturate in Jade vengono stampate nella console (con printStackTrace) e innescano la terminazione dell’agente, tramite la chiamata al metodo doDelete() dell’Agente, che inizia la transizione verso lo stato “Dead”. • Le Eccezioni che non estendono RuntimeException (ad esempio FIPAException) devono essere gestite dal programmatore. In particolare dovremo (se non lo facciamo il compilatore segnerà errore) catturarle tramite try-catch, poichè i metodi della classe Agent che abbiamo sovrascritto (tra cui setup) non ci permettono di dichiarare eccezioni tramite la clausola throws. Nel mio caso ho gestito la FIPAException con la stessa strategia di Jade, ovvero stampare l’eccezione e far terminare l’Agente. Durante la transizione di stato verso “Dead” (ovvero poco prima di terminare) viene invocato il metodo speculare di setup(), ovvero takedown(), che possiamo sovrascrivere allo scopo di effettuare tutte le operazioni di cleanup necessarie, tra cui solitamente è presente la deregistrazione al DF se nel setup è stata fatta la registrazione. Ha lo stesso compito del distruttore delle classi del C++. Mentre l’Agente esegue il metodo takeDown è ancora in grado di inviare messaggi agli altri Agenti. @Override public void takeDown(){ // When this Agent terminates, he has to remove himself from the Yellow Page Service (DF) // Otherwise he will still considered a valid result even dead try { DFService.deregister(this); System.out.println(getLocalName()+": Taking down ..."); } catch (FIPAException e) { e.printStackTrace(); } } 2.5 I Behaviour 2.5 11 I Behaviour Le operazioni che l’Agente compie durante la sua vita e le decisioni che vengono compiute in autonomia dall’Agente in risposta a determinati eventi sono contenute all’interno di uno o più Behaviour. Ciascun Agente può avere un numero arbitrario di Behaviour, che può anche cambiare durante l’esecuzione, tramite addBehaviour(Behaviour) e removeBehaviour(Behaviour) dell’Agente. Tuttavia solo un Behaviour per volta è in esecuzione, infatti la classe Agent implementa (in modo automatico e invisibile all’esterno) un meccanismo di Scheduling Round Robin senza Prelazione. Ciò significa che se uno dei Behaviour entra in un ciclo infinito (o svolge operazioni molto pesanti), gli altri non avranno possibilità di essere eseguiti, poichè l’intero Thread dedicato all’Agente in questione sarà bloccato. E’ quindi una buona norma costruire i Behaviour in modo tale da compiere operazioni leggere e brevi, o comunque interrompibili in uno o più punti (e essere in grado di riprendere da dove ci si era interrotti) per lasciare spazio anche agli altri. I metodi che possiamo (o dobbiamo, a seconda dei casi) sovrascrivere quando definiamo un nostro Behaviour sono: • action(), nel quale definiamo le azioni che vengono compiute quando lo Scheduler manda in esecuzione questo Behaviour. Se abbiamo progettato il Behaviour come interrompibile, dobbiamo gestire in modo adeguato i casi in cui il Behaviour ha iniziato il suo lavoro o lo ha ripreso dal punto in cui si era interrotto. Quando il metodo action() termina viene invocato il metodo done(). • done(), nel possiamo distinguere i casi di terminazione di action(). Se action() è terminato perchè il Behaviour ha concluso il suo lavoro allora restituiamo true (in questo caso il Behaviour verrà rimosso dalla coda dello Scheduler). Se invece action() è terminato per interrompersi e riprendere in seguito, allora restituiamo false. Ciò che succede in quest’ultimo caso dipende dal fatto che il programmatore abbia invocato o meno il metodo block() del behaviour. Normalmente (ovvero se block non è stato invocato), il Behaviour torna in fondo alla coda dello Scheduler. • onEnd(), il quale viene invocato quando done() restituisce true, possiamo usarlo per definire un valore di ritorno (intero) per il Behaviour, oltre ad eventuali operazioni di cleanup. • onBegin(), che è speculare rispetto a onEnd(), viene invocato solo una volta, prima di eseguire per la prima volta il metodo action(). I Behaviour possono decidere di portarsi in stato di sleep, qualora stiano aspettando il verificarsi di un determinato evento per proseguire con il loro 2.5 I Behaviour 12 lavoro (ad esempio lo scadere di un certo timeout o la ricezione di un messaggio con determinate caratteristiche). Per spostarsi nello stato di sleep, i Behaviour (dentro il metodo action) devono invocare il metodo block() oppure block(timeout) e fare in modo che done() restituisca false (in caso contrario il Behaviour verrebbe eliminato). I Behaviour che si trovano nello stato di sleep non vengono più considerati dallo Scheduler, il quale farà proseguire altri Behaviour. Un Behaviour esce dallo stato di sleep (e torna quindi nella coda dello Scheduler) quando: • Il timeout associato alla chiamata di sleep è scaduto (ovviamente se si è utilizzato il metodo block con 1 parametro). • Un altro Behaviour (che non si trova a sua volta nello stato di sleep) decide esplicitamente di risvegliarlo con il metodo restart(). • L’Agente ha ricevuto un nuovo messaggio. In questo caso tutti i Behaviour vengono risvegliati. Ciascuno di questi, quando verrà rimandato in esecuzione dallo Scheduler, dovrà verificare che il nuovo messaggio non sia già stato letto da un Behaviour mandato in esecuzione prima di lui, e di essere interessato a quel tipo di messaggio. Nel caso che queste condizioni non siano verificate il Behaviour potrà decidere di tornare nello stato di sleep. Quando definiamo i nostri Behaviour, oltre a poter estendere la classe base Behaviour (in questo modo dobbiamo definire action e done), possiamo anche adottare alcune scorciatoie andando ad estendere alcune sottoclassi (intese nella gerarchia) di Behaviour: • OneShotBehaviour modella un Behaviour che non ha bisogno di tornare nella coda dello Scheduler poichè gli basta una sola esecuzione per compiere il leggero carico di lavoro ad esso assegnato. In particolare il metodo done() restituisce sempre true, quindi non è necessario farne l’override. • CyclicBehaviour modella un Behaviour che deve essere sempre presente durante la vita dell’Agente (in esecuzione, nella coda o in stato di sleep), vengono utilizzati solitamente per gestire i vari tipi di messaggi che l’Agente può ricevere, o per fornire i servizi che ha specificato quando si è registrato al DF. Il metodo done() restituisce sempre false. • TickerBehaviour è un particolare Behaviour che non segue il normale funzionamento della coda. Infatti viene selezionato dello Scheduler e portato in esecuzione solo ed esclusivamente ogni X millisecondi (dove X è stato specificato dal Behaviour stesso), molto utile per effettuare operazioni periodiche (ad esempio la ricerca sul DF di un determinato Agente, effettuata ogni 10 secondi fino a che non viene trovato). Il 2.5 I Behaviour 13 metodo done() restituisce sempre false come per le CyclicBehaviour, quindi per terminare l’esecuzione si deve esplicitamente invocare il metodo removeBehaviour() dell’Agente. • WakerBehaviour è un Behaviour temporizzato come TickerBehaviour. E’ possibile specificare una data superata la quale (o un timeout preciso trascorso il quale) il Behaviour si sveglia e esegue (in una unica soluzione) le istruzioni nel suo metodo onWake(). • FsmBehaviour modella un Behaviour interrompibile che può trovarsi in uno stato (che definisce le azioni da compiere quando lo Scheduler lo rimette in esecuzione) appartenente ad un certo insieme. Ogni volta che il metodo action() termina lo stato del Behaviour può cambiare (o rimanere lo stesso), a seconda delle transizioni che vengono definite. In altre parole il Behaviour è un Automa a Stati Finiti Deterministico (DFA). Il metodo done() restituisce true solo quando il Behaviour è giunto in uno stato finale. • SequentialBehaviour è un DFA semplificato che non necessita di definire le transizioni, perchè per ogni stato è presente solo una transizione, che è verso il successivo, mentre l’ultimo è lo stato finale (quindi conta solo l’ordine nel quale gli stati vengono aggiunti). A titolo di esempio, la seguente immagine rappresenta il Behaviour principale degli Agenti Buyer, si tratta di un Behaviour complesso implementato con un Automa a Stati Finiti Deterministico (ovvero FSMBehaviour). 2.6 Scambio di Messaggi tra gli Agenti 2.6 14 Scambio di Messaggi tra gli Agenti Per identificare univocamente un Agente all’interno della Piattaforma (e quindi poter interagire con esso) si utilizza il suo nome completo (AID Agent IDentifier), che unisce il nome locale dell’agente (univoco, come anticipato nella sezione dedicata alla creazione degli Agenti), la piattaforma di appartenenza e in container in cui trovarlo. Ciascun Agente ottiene il proprio AID appena dopo la sua creazione, che può essere recuperato con il metodo getAID() dell’Agente. Gli Agenti che vivono nella stessa Piattaforma possono comunicare scambiandosi messaggi modellati dalla classe ACLMessage (dove ACL sta per Agent Communication Language). Il messaggio consta di vari campi, accessibili con i metodi getter e setter: • Sender è l’AID dell’Agente che ha creato e che spedirà il messaggio (viene impostato in automatico con il costruttore di ACLMessage). • Receivers è una lista di AID ai quali il messaggio verrà recapitato. Inizialmente la lista di destinatari è vuota, è necessario aggiungerne almeno uno (con il metodo addReceiver) per poter inviare il messaggio. • Performative è l’intento con il quale il messaggio viene inviato, è possibile anche definirlo nel costruttore. La classe ACLMessage prevede alcune costanti che possono essere usate in questo ambito, quali Propose, Refuse, Inform, QueryIf ... • ConversationID (facoltativo) può essere utile per raggruppare logicamente messaggi e risposte facenti parte di una stessa “conversazione”. • Language indica la codifica del contenuto del messaggio (il mittente e i destinatari devono essere in grado di codificare e decodificare il linguaggio scelto), quello di default è SLCodec (che trasferisce stringhe leggibili dall’essere umano), possibili alternative prevedono il trasferimento di sequenze di bytes. • Ontology è un insieme di simboli, concetti, predicati e azioni che hanno senso per il mittente e i destinatari. Di default nessuna Ontology è usata, quindi il contenuto dovrà essere interpretato dai destinatari. • Content è il contenuto del messaggio, ovvero ciò che si vuole comunicare ai destinatari. Deve rispettare il Linguaggio e la Ontology scelte. In particolare per conversazioni poco articolate e monotematiche si può pensare di non utilizzare nessuna Ontology e di lasciare il Language di default, ovvero la conversazione è basata su scambi di Stringhe, in questo caso è possibile specificare il contenuto normalmente con setContent(String). Se invece si utilizzano Language e 2.6 Scambio di Messaggi tra gli Agenti 15 Ontology, quindi la conversazione è più articolata e strutturata, bisogna usare il metodo getContentManager.fillContent(ACLMessage, ContentElement) che in automatico effettua la conversione di ContentElement (che è un Predicato o una Azione che fa parte della Ontology scelta) nel formato specificato dalla Language e lo inserisce nel messaggio come contenuto. Per inviare il messaggio (dopo averlo creato e riempito i campi desiderati) è sufficente invocare il metodo send(ACLMessage) dell’Agente. In questo modo il messaggio sarà recapitato nella mailbox dei destinatari dopo avere individuato il Container nel quale vivono. L’arrivo di un nuovo messaggio nella mailbox di un Agente fà si (come abbiamo visto in precedenza) che tutti i behaviours dell’Agente stesso che avevano invocato il metodo block() vengano risvegliati dallo stato di sleep e posizionati nuovamente nella coda dello Scheduler. Il messaggio verrà gestito da uno solo di questi Behaviour (o di quelli che non erano in sleep), quello che è interessato a quel tipo di messaggio. Ciascun behaviour può definire quali sono i messaggi al quale è interessato mediante la classe MessageTemplate, che definisce dei filtri (combinabili con le operazioni logiche) sui campi dell’ACLMessage. Quando un Behaviour trova un messaggio di suo interesse dentro la mailbox, questo viene rimosso dalla mailbox, e quindi agli eventuali altri Behaviour la mailbox risponderà che non ci sono più nuovi messaggi. Un Agente (o meglio i Behaviour dell’Agente) può accedere alla propria mailbox per prelevare messaggi, con due strategie differenti: • receive, che ritorna null se nessun nuovo messaggio è presente nella mailbox. Se invece ci sono uno o più nuovi messaggi, il metodo restituisce uno di questi (in un oggetto di classe ACLMessage). E’ possibile utilizzare un MessageTemplate per fare sì che il metodo restituisca null anche quando i nuovi messaggi presenti nella mailbox non sono di nostro interesse. • blockingReceive, che funziona esattamente come il precedente, ma quando quest’ultimo restituirebbe null, invece questo manda tutti i behaviour in stato di sleep. 2.6 Scambio di Messaggi tra gli Agenti 16 private class ReceiveQueriesBehaviour extends CyclicBehaviour{ private MessageTemplate t; public ReceiveQueriesBehaviour(){ // Accepting only queries about Books decodable with the SL Codec t = MessageTemplate.and( MessageTemplate.MatchPerformative(ACLMessage.QUERY_IF), MessageTemplate.and( MessageTemplate.MatchOntology(BookOntology.INSTANCE.getName()), MessageTemplate.MatchLanguage(codec.getName()) ) ); } @Override public void action() { try{ ACLMessage message = myAgent.receive(t); if (message == null){ // If no message respecting template arrived super.block(); // The Behaviour goes into the Sleep State return; } // If a message has arrived, I decode the content ContentElement c = getContentManager().extractContent(message); // Creating the reply ACLMessage (Reply receiver = Message sender) ACLMessage reply = message.createReply(); if (c instanceof Own){ // An Agent is asking me if I own a book Own own = (Own) contentElement; // Searching the requested book in the library Book book = library.getBook(own.getBookName()); if (book == null){ // If not found, i reply with a No AbsPredicate not = new AbsPredicate(SLVocabulary.NOT); not.set(SLVocabulary.NOT_WHAT, BookOntology.INSTANCE.fromObject(own)); reply.setPerformative(ACLMessage.FAILURE); myAgent.getContentManager().fillContent(reply, not); } else{ // If found, i reply with the price of the book reply.setPerformative(ACLMessage.PROPOSE); Costs costs = new Costs(book.getBookName(), book.getPrice()); myAgent.getContentManager().fillContent(reply, costs); } } myAgent.send(reply); } catch(Exception e){ e.printStackTrace(); myAgent.doDelete(); } } } 2.7 Creare una Ontology e i suoi Componenti 2.7 17 Creare una Ontology e i suoi Componenti Come anticipato nelle sezioni precedenti, una Ontology è un insieme di Concetti, Predicati (affermazioni sui concetti che possono avere un valore di verità) e Azioni che hanno senso per una certa categoria di Agenti. Prima di creare la Ontology dobbiamo costruire i singoli componenti, implementando rispettivamente le interfacce Concept, Predicate o AgentAction. Non ci sono metodi dei quali dobbiamo fare l’override, tuttavia la classe implementante deve soddisfare i seguenti requisiti: • Deve avere il costruttore di default pubblico e accessibile. In particolare ricordiamo che se non specifichiamo nessun costruttore il compilatore in automatico ci fornisce il costruttore senza parametri che non fa nulla. Nel caso che invece abbiamo dichiarato un costruttore che prevede parametri, il compilatore non ci fornisce nulla, quindi dobbiamo dichiarare a mano anche quello senza parametri. • Per ogni attributo Attr bisogna fornire i metodi getter (per leggere il contenuto della variabile) e setter (per impostare il contenuto della variabile), pubblici e accessibili, nella forma canonica getAttr e setAttr. public class Costs implements Predicate{ private static final long serialVersionUID = -2752651957491763115L; private Price price; private BookName bookName; public Costs(){ } public Costs(BookName bookName, Price price){ this.bookName = bookName; this.price = price; } public Price getPrice() { return price; } public void setPrice(Price price) { this.price = price; } public BookName getBookName() { return bookName; } public void setBookName(BookName bookName) { this.bookName = bookName; } } 2.7 Creare una Ontology e i suoi Componenti 18 Una volta creati tutti i Concept, Predicate e AgentAction implementando l’interfaccia adeguata e soddisfando i requisiti richiesti (non ci sono particolari differenze nel codice che si va a scrivere nei 3 casi), possiamo creare la Ontology. Le Ontology di Jade si distinguono tra di loro per il nome che viene assegnato a ciascuna. In generale non è necessario creare più oggetti di un particolare tipo di Ontology, pertanto si sfrutta il design pattern Singleton (rendendo il costruttore privato e fornendo una sola istanza accessibile staticamente). public class BookOntology extends Ontology{ public static final String ONTOLOGY_NAME = "BookOntology"; // The Ontology should always be a singleton public static final BookOntology INSTANCE = new BookOntology(); public static final BookOntology getInstance(){ return INSTANCE; } ... } Per ciascuna classe che implementa Concept, Predicate o AgentAction che desideriamo inserire nella Ontology e per ciascun attributo di quelle classi definiamo una costante di tipo String. Valorizziamo le costanti associate alle classi con il nome delle classi stesse, e le costanti associati agli attributi con lo stesso nome (case-sensitive) con cui sono chiamati gli stessi attributi dentro le rispettive classi. public class BookOntology extends Ontology{ ... // Class COSTS (Predicate) public static final String COSTS = "Costs"; // Attributes public static final String COSTS_PRICE = "price"; public static final String COSTS_BOOKNAME = "bookName"; ... } Una volta create le costanti di Stringa per ciascuna classe e ciascun attributo andiamo ad implementare il costruttore della BookOntology. Innanzitutto dobbiamo dichiarare qual’è la Ontology che stiamo estendendo. Infatti potremmo decidere di sfruttare l’ereditarietà creando ad esempio una TradingOntology che definisce i concetti alla base del commercio di beni e servizi, in modo tale da riciclare tali concetti nella BookOntology. Se non si desidera sfruttare questa funzionalità, la Ontology che si deve estendere è quella di base. A questo punto dobbiamo aggiungere alla BookOntology tutti i concetti, i predicati e le azioni creati precedentemente, andando a specificare 2.7 Creare una Ontology e i suoi Componenti 19 in quale classe trovarle e qual’è il nome che desideriamo associare al nuovo componente (sfruttando le costanti di stringa definite prima). private BookOntology() { super(ONTOLOGY_NAME, BasicOntology.getInstance()); try { super.add(new super.add(new super.add(new super.add(new super.add(new ... ConceptSchema(PRICE), Price.class); ConceptSchema(BOOK), Book.class); PredicateSchema(COSTS), Costs.class); PredicateSchema(OWN), Own.class); AgentActionSchema(BUYACTION), BuyAction.class); } catch (OntologyException e) { e.printStackTrace(); } } Per concludere il costruttore dobbiamo recuperare uno per uno tutti gli Schemi appena aggiunti alla Ontology, tramite il metodo getSchema(String) e descrivere nel dettaglio ciascuno di essi, aggiungendo tutti gli attributi che li compongono. Quando aggiungiamo un attributo dobbiamo descrivere: • Che nome assume l’attributo all’interno dello Schema (utilizzando la corrispondente costante di Stringa definita prima). • Che tipo di dato è: è uno Schema che è stato precedentemente aggiunto nella BookOntology o è un tipo di dato primitivo? • Questo attributo può anche essere null (ObjectSchema.OPTIONAL) all’interno dello Schema senza invalidarlo, oppure no? • Se l’attributo è una lista di oggetti di un determinato tipo, dobbiamo definire le cardinalità minima e massima. try { ... ConceptSchema c = (ConceptSchema) super.getSchema(BOOK); c.add( BOOK_PRICE, (ConceptSchema) getSchema(PRICE), ObjectSchema.MANDATORY ); c.add( BOOK_NUMBEROFPAGES, (PrimitiveSchema) getSchema(BasicOntology.INTEGER), ObjectSchema.MANDATORY ); ... } catch (OntologyException e) { e.printStackTrace(); } Capitolo 3 Programmazione su Android Questo capitolo descrive la struttura di un Progetto Android e i concetti alla base della programmazione su tale piattaforma. Viene inoltre illustrato come procedere per arrivare ad eseguire la classica applicazione “Hello, world!”. La guida completa (che è stata usata come riferimento nel lavoro di tesi) su tutto ciò che riguarda lo sviluppo e la pubblicazione di applicazioni Android è possibile trovarla su developer.android.com. 3.1 Ambiente di Sviluppo: ADT Per poter sviluppare applicazioni Android dobbiamo procurarci un apposito ambiente di sviluppo, una possibile scelta è ADT (Android Developer Tools). Si tratta dell’usuale ambiente di sviluppo Java, ovvero Eclipse, munito di un plug-in che consente di generare il codice per applicazioni Android (che, come vedremo, saranno costituite da files .java e da files .xml), e di compilarle producendo files .apk che il dispositivo Android è in grado di installare. Inoltre questo plug-in consente di installare ed aggiornare i pacchetti relativi ad Android (window -> android SDK manager) e di simulare una vasta gamma di dispositivi Android stessi. Ogni volta che eseguiamo un Android Project via ADT possiamo infatti scegliere se fare l’upload e l’installazione del file .apk in un dispositivo Android reale connesso via USB, oppure se utilizzare l’emulatore di ADT stesso. Tuttavia l’emulatore non si presta ai nostri scopi, poichè (oltre all’intrinseca lentezza) non presenta sensori di alcun tipo. Possiamo procurarci Android Developer Tools su developer.android.com/sdk/index.html. 3.2 Creare ed Eseguire un Android Project Per creare una nuova applicazione nel nostro ambiente di sviluppo ADT andiamo su File -> New -> Android Application Project. Nella prima finestra che ci viene proposta dobbiamo scegliere il nome dell’applicazione che 3.2 Creare ed Eseguire un Android Project 21 comparirà in Android (quindi è meglio sceglierlo corto in modo tale che non venga tagliato), il nome del progetto (dentro ADT) e il nome del package (composto da almeno due parti, che deve rimanere immutato, quindi è meglio pensarci bene prima di sceglierlo). Nelle finestre successive solitamente non abbiamo la necessità di cambiare nulla. Una volta completata la procedura concentriamo la nostra attenzione sulla struttura di files e directory generate: • La directory src contiene il codice Java da noi scritto per la nostra applicazione. Se non abbiamo cambiato nulla nella procedura di creazione del progetto, notiamo che nel package che abbiamo specificato è già presente una Activity, di nome MainActivity, che attualmente è la prima ad essere eseguita quando la applicazione Android viene lanciata. • La directory gen contiene altro codice Java che fa parte della nostra applicazione. Questo codice è generato e/o modificato automaticamente dal compilatore ogni volta che creiamo o modifichiamo i file .xml dell’applicazione stessa. • Nella directory libs possiamo collocare le librerie (gli archivi .jar) della quale l’applicazione ha bisogno. Attualmente contiene android-supportv4.jar che consiste in un insieme di utility per le operazioni più comuni in Android. La questione di importazione di librerie e/o progetti Java/Android è molto delicata, perchè è facile sbagliarsi e perdere tempo, pertanto sarà trattata più nello specifico. • La directory res (resources) è quella più importante, perchè ci consente di creare e configurare la parte statica della nostra applicazione. In particolare all’interno di res troviamo diverse directory drawable, in tali directory andremo a posizionare tutte le immagini, che potranno ad esempio essere utilizzate come sfondo o come icone nella nostra applicazione. Le varie directory dovranno contenere le stesse immagini (ovvero files con gli stessi nomi) con dimensioni diverse (low, medium, high, extra-high, extra-extra-high), in modo tale che se l’applicazione deve disegnare una immagine (ad esempio ic_launcher.png che dovremmo già avere all’interno di queste directory) può scegliere tra diverse alternative, a seconda dello spazio a disposizione sullo specifico dispositivo Android sulla quale è in esecuzione. E’ stato definito un insieme di immagini standard per le azioni più comuni, che è bene utilizzare per il principio di familiarità nell’ambito dell’usabilità del software. Dentro res inoltre troviamo le directory layout, menu e values, che contengono files .xml riguardanti rispettivamente gli oggetti contenuti nelle videate, le scelte presenti nei menu, e le costanti (di stringa, di colore o di dimensione) utilizzate nell’ambito dell’interfaccia grafica. Ciascuna di queste tre categorie sarà affrontata singolarmente. 3.3 Importare Progetti e Librerie 22 • Nel file AndroidManifest.xml possiamo configurare l’applicazione Android sotto diversi aspetti, che saranno trattati nella sezione apposita. Per lanciare l’applicazione andiamo nel menu run, scegliamo Android Application, e successivamente il device sul quale eseguirla (o l’emulatore, che per questa semplice applicazione può essere usato). Se il dispositivo Android presenta la schermata di blocco (o lo schermo si è spento), passiamo il dito sullo schermo stesso per sbloccarlo. Solitamente è meglio disattivare l’oscuramento dello schermo (e la conseguente schermata di blocco) mentre si sta programmando, andando nel menu Impostazioni del dispositivo Android, nella sezione “Opzioni Sviluppatore”. Se tutto è andato a buon fine, dovremmo vedere una applicazione con il nome che abbiamo scelto (in quella che si chiama Action Bar), e la scritta “hello world!” nella videata. Inoltre ora capiamo a cosa è stata utilizzata la ic_launcher che abbiamo trovato nelle directory Drawable. La applicazione che abbiamo appena lanciato è stata inoltre installata sul dispositivo, quindi potrà essere individuata e lanciata anche dal dispositivo Android stesso. 3.3 Importare Progetti e Librerie Per costruire un Android Project possiamo avere bisogno di importare librerie (ovvero file .jar), altri Android Project, oppure normali progetti Java. Le risorse linkate in modo errato non presentano problemi in fase di stesura del codice (ovvero il compilatore inline di Eclipse non notifica alcun errore), tuttavia poi i problemi si presentano quando la applicazione è in esecuzione su Android, al primo uso di una classe non riconosciuta. Per specificare le risorse che uno specifico progetto deve importare lo selezioniamo e scegliamo “Properties” nel menu “Project”. Le schede che ci interessano sono due: 1. Nella scheda Android possiamo aggiungere (tramite il pulsante Add) altri Android Project che si trovano dentro lo stesso Workspace di questo. Possiamo aggiungere solo ed esclusivamente gli Android Project che hanno spuntato il flag “Is Library”, che vediamo nella scheda Android stessa. Gli Android Project settati con “Is Library” non possono essere installati (e quindi nemmeno eseguiti). I progetti aggiunti in questo modo non devono più essere considerati nel punto successivo. 2. Nella scheda JavaBuild Path possiamo aggiungere i progetti Java (dentro lo stesso workspace) e le librerie (interne o esterne al workspace). E’ importante ricordarsi di spuntare tutti i progetti e le librerie aggiunte in “Order and Export” per fare si che tali risorse finiscano del file .apk. 3.4 Activity e Intent 23 E’ importante fare attenzione a non importare più volte “Android Support v4.jar”, errore che mi è capitato spesso facendo gerarchie complesse di progetti. E’ sconsigliabile creare i .jar dei propri Android Project mentre si sta programmando (per importarli negli altri progetti al posto di importare gli Android Project stessi), perchè si creano altri problemi (tra cui dover fare il refresh di tutti i progetti ogni volta che viene fatta una modifica, e problemi di dipendenze). In particolare se vogliamo creare un progetto Android che si appoggi sulle funzionalità offerte da Dave (descritte nel relativo capitolo) dobbiamo importare gli Android Library Project Dave Android e GooglePlayServicesLib. Quest’ultima libreria consente di utilizzare le Google Maps all’interno della propria applicazione. Possiamo ottenerla istallandola con l’SDK Manager del nostro ambiente di sviluppo, seguendo le semplici istruzioni che troviamo qui. 3.4 Activity e Intent Una applicazione Android è composta da una o più Activity, ciascuna delle quali fornisce una interfaccia grafica (che, nella maggior parte dei casi, copre tutto lo schermo) con la quale l’utente Android può interagire. Una delle Activity deve essere la “launch Activity”, che è quella di partenza della applicazione. Le Activity che fanno parte di una singola applicazione sono collegate tra di loro, ovvero ciascuna Activity ha la possibilità di iniziarne un altra allo scopo di compiere specifiche operazioni. In questo caso la nuova Activity viene messa in foreground, mentre quella precedente viene messa in stato di pausa (vengono compiute le operazioni di scrittura su memoria persistente rimaste in sospeso, interrotte animazioni e rilasciate risorse). Tutte le Activity, ovvero quella in foreground e quelle in background rimangono memorizzate in una struttura dati a pila, la quale permette di riportare velocemente in foreground (tramite l’operazione pop) l’Activity più recente. Le Activity presenti nella pila (tranne quella in foreground) possono essere distrutte per necessità di memoria, in tale caso lo stato della istanza della Activity (che viene salvato ogni volta che la Activity va in stato di pausa) deve essere ripristinato qualora l’utente ritorni sulla Activity (se l’Activity invece è rimasta nella pila l’operazione di ripristino non è necessaria). La Activity ha un ciclo di vita composto da diversi stati (lo stato di pausa ne fa parte), ciascuna transizione verso uno stato corrisponde all’invocazione del corrispondente metodo (ad esempio onPause), che il programmatore può sovrascrivere per effettuare le proprie operazioni (di cleanup, salvataggio, ripristino ...). 3.4 Activity e Intent 24 package it.unipr.informatica.prova; import android.os.Bundle; import android.app.Activity; import android.view.Menu; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } } Quando costruiamo una Activity possiamo sovrascrivere alcuni metodi, per definire i comportamenti che la Activity deve seguire nelle transizioni di stato citate poco fa o al verificarsi di determinati eventi: • onCreate: questo metodo viene invocato quando l’Activity viene creata (savedInstanceState == null) oppure viene ricreata da un istanza precedentemente salvata. Bundle è una struttura dati che associa a chiavi di tipo String valori di tipo arbitrario. Tramite il metodo setContentView(int) facciamo in modo che il contenuto della videata sia quello definito nel file xml oppotuno. • onPause: questo metodo viene invocato quando l’Activity non è più in primo piano (ovvero quando l’utente passa ad un altra applicazione/Activity lasciando aperta questa). Di solito si fa l’override di questo metodo se è possibile rilasciare risorse e/o servizi al fine di ridurre il consumo di batteria. • onResume: speculare di onPause, invocato quando l’Activity torna in primo piano. • onDestroy: speculare di onCreate, invocato quando l’Activity sta per essere distrutta. E’ possibile effettuare operazioni di clean-up aggiuntive (perchè il metodo onPause viene invocato prima di onDestroy). • onSaveInstanceState: metodo invocato prima di onDestroy che consente di creare e riempire un oggetto di tipo Bundle con i dati necessari ad una successiva ricreazione della Activity. 3.5 Manifest della Applicazione 25 • onCreateOptionsMenu: invocato quando il menu sta per essere creato, consentendoci di associare il file .xml che contiene quello desiderato. • onOptionsItemSelected: metodo invocato quando uno degli oggetti del menu viene selezionato. Ci viene fornito l’id dell’oggetto che ha innescato l’evento, in modo tale che con costrutti if o switch possiamo gestirlo in modo appropriato. Un Intent è un messaggio che viene inviato per richiedere una azione da parte di un altro componente della applicazione. L’Intent contiene principalmente: • L’azione che si desidera che il/i riceventi del messaggio compiano. • Le informazioni delle quali necessitano il/i riceventi per compiere l’azione. Si tratta di una mappa <chiave, valore>, chiamata Extra dell’Intent. Gli intent possono essere usati in particolare per iniziare un altra Activity, o per instaurare una dialogo tra diverse Activity. Per definire i tipi di Intent che ciascuna Activity supporta (ovvero le azioni supportate), si costruisce un IntentFilter, del quale discuteremo più avanti. Questa meccanica del filtro è necessaria, poichè tipicamente gli Intent vengono mandati in Broadcast. 3.5 Manifest della Applicazione Nel file Manifest vengono elencate tutte le caratteristiche della applicazione Android, tra cui: • L’icona (nome della immagine dentro la directory Drawable), il nome della applicazione (quello che abbiamo specificato nella procedura di creazione dell’applicazione stessa) e il package di appartenenza. • I dispositivi Android supportati dalla applicazione, stabilendo un range di versioni SDK. Solo i dispositivi che hanno una versione compresa nel range specificato possono eseguire la applicazione. • I permessi che la applicazione deve ottenere per poter funzionare correttamente. I permessi elencati nel manifest sono gli stessi che vengono esposti all’utente al momento del download di una applicazione da Google Play. Possono riguardare l’accesso a Internet, l’uso della Rubrica, la necessità di impedire lo Stand-By ... • I servizi di cui l’applicazione ha bisogno. I servizi sono attivi a tempo continuato e lavorano in background rispetto alle Activity, le quali possono “contattare” (tramite l’operazione di bind) il servizio stesso per poterne usufruire. 3.5 Manifest della Applicazione 26 • L’elenco delle varie Activity che compongono la applicazione, le loro caratteristiche (se è la Launch Activity, se è necessaria una specifica orientazione del telefono ...) e la loro gerarchia. Se ad esempio vogliamo fare sì che la nostra applicazione possa accedere ad Internet per poter scaricare e visualizzare le Google Maps, e ricevere i dati relativi alla posizione tramite GPS e WiFi, dobbiamo aggiungere i seguenti permessi nel file manifest (prima del tag <application>). <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> Tuttavia per poter usufruire delle Google Maps per Android dobbiamo anche richiedere il servizio a Google. Dopo aver fatto il login su Google andiamo su https://code.google.com/apis/console. Dobbiamo creare un nuovo progetto (ci verrà proposto dalla pagina stessa di farlo) con lo stesso nome della applicazione Android che stiamo sviluppando. Il nostro obiettivo è abilitare il servizio Google Maps Android API v2 per il progetto che abbiamo creato, le operazioni da svolgere dipendono dall’interfaccia grafica che vi presenterà Google. Una volta abilitato il servizio, dobbiamo associare il progetto creato in remoto con quello locale in ADT, creando una nuova Android Key. Ci verrà richiesto di inserire l’impronta SHA1 del nostro progetto dentro ADT e il relativo package che avevamo scelto al momento della creazione (ora disponibile nel manifest). Per ottenere l’impronta SHA1 dobbiamo selezionare il progetto nel Package Explorer di ADT, poi andare in Window - > Preferences -> Android -> Build. Una volta inserite queste informazioni (separate da un punto e virgola), Google genererà una Android Key, che dobbiamo aggiungere nel file Manifest (prima del tag <activity>) nel seguente modo: <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="AIzaSyATcQ0JiXaHTOfU-U89F9XNfIOhBcKQ0lQ" /> Se la nostra applicazione necessita di servizi (ad esempio Jade per Android), anche questi vanno specificati in questo punto del file manifest (ovvero prima del tag <activity>): <service android:name="jade.android.MicroRuntimeService" /> 3.6 Costanti di Stringa usate dalla Applicazione 3.6 27 Costanti di Stringa usate dalla Applicazione Nel file strings.xml (che troviamo nella directory values) vengono memorizzate tutte le costanti di Stringa che vengono usate dall’applicazione. In particolare al suo interno troviamo la costante di stringa app_name, che è valorizzata con il nome della Applicazione. Possiamo utilizzare questa (o un altra) costante ovunque, ovvero: • all’interno degli altri file .xml (in particolare “app_name” viene usata nel manifest), nel seguente modo: @string/app_name • nei file .java all’interno della directory src, nel seguente modo: R.string.app_name // la classe R si trova all’interno dei file java generati, ovvero dentro la directory gen Anche dimens e styles seguono lo stesso principio sopradescritto, ma sono di raro utilizzo. 3.7 Descrizione del Menu di una Activity Ad ogni Activity (ad esempio MainActivity) è associato un file xml (ad esempio main.xml) dove viene descritto il suo menu. Il menu è composto da oggetti che possono trovarsi nella Action Bar (ovvero la barra sotto quella delle notifiche, dove è presente anche il nome della Activity), oppure che possono comparire premendo il tasto menu del dispositivo Android. Ogni oggetto del menu possiede i seguenti attributi: • id: utilizzato dall’applicazione per identificare l’oggetto quando deve gestire gli eventi dell’interfaccia e le loro sorgenti. • title: visualizzato su schermo se l’oggetto non si trova nella Action Bar • icon: immagine visualizzata su schermo se l’oggetto si trova nella Action Bar • showAsAction: determina se e quando l’oggetto deve finire nella Action Bar, possibili scelte sono “always”, “never”, o “ifRoom” (se c’è spazio) 3.8 Descrizione della Videata di una Activity 3.8 28 Descrizione della Videata di una Activity Ad ogni Activity (ad esempio MainActivity) è associato un file xml (ad esempio activity_main.xml) dove viene descritto il contenuto della videata. Innanzitutto dobbiamo definire un layout, ovvero come gli oggetti devono essere disposti, i più usati sono: • RelativeLayout, in cui per ogni oggetto si può specificare la posizione rispetto ad un altro oggetto (sotto a, a fianco di ...), cercando di non fare riferimenti circolari. • LinearLayout, che dispone gli oggetti sempre nella stessa direzione (verticale o orizzontale, che possiamo specificare tramite l’attributo android:orientation). Tra gli oggetti che possiamo inserire abbiamo pulsanti (ai quali possiamo rispondere all’evento onClick), campi di testo, checkbox, radio button, pulsanti on/off, menu a tendina, selezionatori (di data e ora ad esempio). Questi oggetti, insieme alle voci del menu trattate nella sezione precedente, costituiscono la principale modalità di interazione dell’utente con la Activity corrente. Capitolo 4 La Library Dave Questo capitolo descrive la libreria Dave (Discover Agents Viewed on Earth) facendo una panoramica sulle principali classi delle quali è composta. Per ciascuna classe verrà spiegato il funzionamento in termini di operazioni effettuate, lo scopo all’interno della libreria, ed eventuali esempi di utilizzo. Verranno utilizzati nella spiegazione alcuni concetti quali Agente, Behaviour, Activity, Intent che sono stati trattati nei capitoli precedenti. Come anticipato nell’introduzione, la libreria Dave consente di creare una architettura Client-Server basata sul concetto di Agente di Jade. Ciascun Agente Client ricava ad intervalli regolari la propria posizione e direzione corrente, creando un oggetto di classe EarthPartialCircle, che viene inviato al Server se il cambiamento rispetto a quello precedente è “significativo” (secondo il giudizio dell’UpdatePlanner). Il Server ad intervalli regolari fornisce ad un certo numero di Client (in accordo alla QualityOfService che tali Client hanno richiesto) gli altri Agenti che tali Client vedono, qualora questi siano cambiati. Le operazioni svolte da Client e Server saranno trattate nello specifico nel capitolo riguardante gli Agenti di Dave. E’ stata anche dedicata una sezione apposita per il Server, per parlare del suo carico computazionale, e delle soluzioni adottate per attenuarlo. La parte finale del capitolo è dedicata alle classi specifiche per Android. Il codice sorgente completo di Dave si articola in tre progetti Java: • Il Progetto Dave Base include la parte di Dave che è necessaria sia su PC che su Android, contenente i concetti e le azioni base. • Il Progetto Dave Pc include le classi specifiche su PC, tra cui gli Agenti dotati di interfaccia grafica Swing. Deve aggiungere come sorgente il Progetto Dave Base. • Il Progetto Dave Android include le classi specifiche per Android, che andranno a gestire gli Intent, i sensori e le GoogleMaps. Deve aggiungere come sorgente il progetto Dave Base. 4.1 Le Informazioni Geografiche 4.1 30 Le Informazioni Geografiche In questa sezione vengono descritte le classi che descrivono le entità geometriche in ambito terrestre, finalizzate alla rappresentazione (e alla trasmissione) della posizione e del punto di vista degli Agenti. Sono contenute nel package dave.onto.concepts.earth 4.1.1 La Posizione: EarthPoint Per identificare univocamente un punto sulla superficie 2D della Terra è sufficiente fornire la corrispondente coppia <latitudine, longitudine>. • La latitudine indica l’angolo formato dalla retta che passa per il centro della terra e il punto rispetto al piano equatoriale. Due punti che hanno la stessa latitudine si trovano sullo stesso parallelo (in particolare l’equatore è il parallelo fondamentale, con latitudine 0). Il polo Nord e il polo Sud formano un angolo retto, ovvero hanno rispettivamente la massima (+90) e la minima latitudine (-90). • La longitudine è la distanza angolare di un punto rispetto al meridiano fondamentale, quello che passa per l’Osservatorio di Greenwich in Inghilterra (che ha quindi longitudine 0). La longitudine varia da +180 a -180. Due punti che hanno la stessa longitudine si trovano sullo stesso meridiano. Il 180-esimo meridiano est (o ovest) è la linea internazionale del cambio di data, situata nell’Oceano Pacifico. La classe EarthPoint, che modella questo concetto, è quindi semplicemente una coppia di reali. Come abbiamo visto sopra, sia la latitudine che la longitudine hanno un range di valori accettati, ma la classe EarthPoint accetta qualsiasi valore per entrambe, effettuando l’operazione di modulo 90 per la latitudine e di modulo 180 per la longitudine. 4.1 Le Informazioni Geografiche 4.1.2 31 Aggiungiamo la Direzione: EarthVector Gli EarthVector sono segmenti orientati (ovvero vettori) sulla superficie 2D della Terra che congiungono due EarthPoint (quello di partenza è il firstPoint, quello di arrivo è il SecondPoint). E’ possibile costruire un EarthVector in due modi: • Specificando il punto di partenza e il punto di arrivo. • Specificando il punto di partenza, la direzione (comprensiva di verso) e il modulo. Per calcolare la direzione dobbiamo prendere come sistema di riferimento quello costituito dai paralleli e dai meridiani (ad esempio scegliamo come asse x l’equatore e come asse y il meridiano di Greenwich). Troviamo l’intersezione della retta su cui giace l’EarthVector con l’asse x e tracciamo un asse y’ parallela a y e passante per quel punto. Abbiamo così individuato due angoli. Per ottenere univocamente la direzione dobbiamo includere anche il verso. Nell’immagine sottostante, la direzione del vettore AB è di 30 gradi se il vettore va da A a B, mentre è di 210 gradi se va da B ad A. Il modulo (o lunghezza) è la distanza in linea d’aria tra i due estremi del vettore. Esistono alcune formule trigonometriche che consentono di calcolare la distanza e la direzione tra due EarthPoint, implementate nella classe EarthGeometry. Queste formule (e le relative nozioni) sono state tratte dai siti sunearthtools.com e movable-type.co.uk, che offrono anche una utile prova interattiva. 4.1 Le Informazioni Geografiche 4.1.3 32 Il Punto di Vista: EarthPartialCircle Un EarthPartialCircle è un cerchio (parziale), ottenuto facendo ruotare sul piano terrestre di un uguale angolo (che non deve superare i 180 gradi) sia in senso antiorario (leftAngle) che in senso orario (rightAngle) un EarthVector attorno al suo firstPoint (che diventa quindi il centro del cerchio parziale). Conoscendo il totalAngle (che è il doppio del rightAngle o del leftAngle, visto che sono uguali) e l’EarthVector è possibile ricavare tutte le altre informazioni, tra cui area e perimetro ad esempio. Questa classe include alcuni metodi di fondamentale importanza per il funzionamento della libreria: • contains(EarthPoint) è in grado di determinare per ogni EarthPoint se cade all’interno della superficie dell’EarthPartialCircle oppure no. Questo metodo ci consente di sapere se un Agente è contenuto nel punto di vista di un altro oppure no. • getRemotenessRatio(EarthPoint) è in grado di determinare per ogni EarthPoint quante volte il raggio dell’EarthPartialCircle è contenuto nella distanza tra il centro e l’EarthPoint stesso, fornendoci una informazione sul suo “grado di lontananza”. • getFrontierPoint(double, double, int) restituisce un determinato punto appartenente alla frontiera dell’EarthPartialCircle (ovvero sulla circonferenza). Questo metodo (chiamato più volte) ci consentirà di disegnare la circonferenza sulla GoogleMap. 4.2 Classe Helper per Jade 33 Ponendo il centro di un EarthPartialCircle nella posizione corrente di un Agente, e la mainDirection (ovvero quella dell’EarthVector) nella direzione in cui il suo disposivo Android è rivolto, siamo in grado di rappresentare il suo punto di vista. Un agente in Dave è quindi descritto dall’EarthPartialCircle corrente, in aggiunta al suo AID, come i normali Agenti di Jade. Le classi che modellano i concetti di descrittore di Agente e di lista di Agenti sono contenute nel package dave.onto.concepts.agent 4.2 Classe Helper per Jade Nel capitolo riguardante Jade abbiamo notato che spesso le istruzioni da utilizzare per le operazioni di creazione di container e agenti sono più di una e cambiano a seconda di trovarci su PC o su Android. Lo scopo è invece avere un unico modo, più compatto ma comunque versatile, per fare queste comuni operazioni. La classe EasyJade definisce le operazioni possibili e la loro struttura, tali operazioni vengono poi implementate in modo diverso dalle classi PcJade e AndroidJade, che estendono EasyJade. Per creare un oggetto PcJade possiamo usare il costruttore con 3 parametri, che sono rispettivamente l’Object che intende creare il PcJade (può anche essere null se non serve tenerlo memorizzato dentro a PcJade), l’host al quale si vuole connettersi (è possibile specificare indifferentemente l’indirizzo Ip oppure il nome), e la porta (è possibile utilizzare la costante EasyJade.DEFAULT_PORT per indicare 1099). Questo costruttore consente di creare un Container Periferico. Notiamo che ci vengono proposti alcuni metodi eventualmente da sovrascrivere: PcJade pcJade = new PcJade(Object caller, String host, int port) { // Invocato quando il servizio di Jade per Android viene disconnesso // Invocato dopo le operazioni di cleanup innescate da doStop() @Override public void onFinish() {} @Override public void onContainerReady() {} // Fallimento nella creazione (o nella connessione) al container. @Override public void onContainerFailure(Throwable throwable) {} // Agente creato con successo! @Override public void onAgentStartUpSuccess() {} // Fallimento nella creazione di un Agente @Override public void onAgentStartUpFailure(Throwable throwable) {} }; 4.3 Gli Agenti di Dave 34 I metodi onContainerFailure() e onAgentStartUpFailure() potranno eventualmente essere utilizzati per stampare l’eccezione nella console e presentare un appropriato messaggio di errore all’utente. Il metodo onAgentStartUpSuccess può essere utilizzato solo a scopo di debug, visto che non è presente il nome dell’Agente creato. Infatti per interagire con il nuovo Agente, si usa invece il Communicator. Il metodo più importante è onContainerReady, invocato (come dice il nome) quando il container (main o periferico) è pronto per creare Agenti. Per creare un Agente (sia usando PcJade che AndroidJade), usiamo uno di questi metodi di EasyJade: createNewAgent(String createNewAgent(String createNewAgent(String non necessita di agentName, String className, Object args); agentName, String className, Object[] args); agentName, String className); // usiamo questo se l’Agente argomenti per il setup Per costruire un AndroidJade, abbiamo lo stesso identico costruttore, e gli stessi metodi eventualmente da sovrascrivere. L’unica differenza è che l’Object caller deve essere non null e instanceOf Activity, perchè necessaria per fare il bind e l’unbind del servizio di Jade per Android. Infine per quanto riguarda PcJade abbiamo ovviamente anche il diritto di creare Main Container locali (“localhost”, sulla porta 1099), usufruendo del costruttore con 1 parametro (anche qui è opzionale, quindi si può usare null): PcJade pcJade = new PcJade(Object caller){ ... } 4.3 4.3.1 Gli Agenti di Dave DaveClientAgent E’ la classe base astratta che consente di definire tutti gli Agenti lato client. E’ astratta perchè i metodi che restituiscono la posizione corrente (getCurrentPosition) e la direzione corrente (getCurrentDirection) sono astratti, in modo tale che le classi che estendono DaveClientAgent siano tenute a restituire ad ogni loro chiamata un valore numerico valido, ottenuto in modo diverso per ciascuna. Questo fa sì che si possano creare Client non dipendenti dai sensori di Android. Un DaveClientAgent dopo essere nato (ovvero quando comincia la sua esecuzione nel metodo setup) si aspetta di trovare un oggetto che sia in- 4.3 Gli Agenti di Dave 35 stanceOf DAVEClientArguments nei suoi argomenti. Un oggetto di classe DAVEClientArguments contiene 4 informazioni: • La distanza massima alla quale l’Agente potrà “vedere”. Sarà utilizzato come raggio degli EarthPartialCircle. • L’ “ampiezza angolare” del suo punto di vista. Sarà utilizzato come totalAngle degli EarthPartialCircle. Questo vincolo e quello soprastante sono necessari, perchè erano gradi di libertà nella costruzione dell’EarthPartialCircle, ovvero non era univocamente determinato solo dalla posizione e direzione corrente. • Un UpdatePlanner, che è un oggetto (che sarà descritto più avanti) che può essere usato per definire quando un cambiamento della direzione o posizione dell’Agente è “significativo”, e che quindi deve essere comunicato al Server. • Una QualityOfService (anch’essa descritta più avanti), che serve per definire la qualità del servizio che il Server ci deve offrire. • Un Communicator (contenuto nel package dave) svolge il ruolo di mediatore tra l’Agente e il suo creatore, consentendo a ciascuno di dialogare con l’altro. In altre parole un Communicator possiede (e può restituire su richiesta) sia il puntatore all’Object che il puntatore all’Agente. Communicator può anche essere null, se questa funzionalità non è necessaria. Quando il metodo setup() termina, il DaveClientAgent si mette a cercare un Agente che offra il servizio di Server, tramite un Behaviour (SearchServerBehaviour) eseguito periodicamente ogni secondo. Quando il Server verrà trovato, tale Behaviour verrà sostituito dai seguenti: • PresentationBehaviour: il Client deve informare il Server della sua presenza. Le informazioni che deve fornire includono la QualityOfService richiesta (che è disponibile, visto che era un argomento), e la sua identità, composta da AID e dall’EarthPartialCircle corrente. Per l’AID non ci sono problemi, visto che il Server può ricavare il mittente del messaggio, mentre l’EarthPartialCircle deve essere costruito con il raggio e il totalAngle (che sono costanti che abbiamo specificato negli argomenti d’ingresso), e con getCurrentPosition() e getCurrentDirection(). Questi ultimi metodi possono anche non essere ancora pronti a fornire tale informazione, magari perchè fanno uso di sensori che sono ancora in fase di inizializzazione. In questo caso la presentazione deve essere rimandata fintanto che tali informazioni non sono disponibili (ovvero anche PresentationBehaviour è eseguita periodicamente fino al suo successo). Se tutte le informazioni 4.3 Gli Agenti di Dave 36 sono pronte, viene mandato un messaggio al Server (il cui contenuto include tali informazioni, è una AgentAction definita nella classe dave.onto.concepts.actions.ClientSubmission), e viene comunicato all’UpdatePlanner che l’EarthPartialCircle iniziale è quello usato nella presentazione. • ServerMessagesHandlerBehaviour: è sempre in coda o in esecuzione (CyclicBehaviour) quando si conosce il Server o ci si è presentati con esso. Il suo compito è ricevere i messaggi provenienti dal Server, e smistarli nei vari handler a seconda della AgentAction presente nel contenuto. Le possibilità includono ServerUpdate, ServerExit, FollowedUpdate e FollowedExit (che si trovano tutte nello stesso package dave.onto.concepts.actions). Il meccanismo di Following verrà descritto più avanti, vediamo le altre due AgentAction. ServerUpdate contiene le identità (AID + EarthPartialCircle) di tutti gli altri Agenti che si trovano dentro il nostro punto di vista (il centro del loro EarthPartialCircle è contenuto nella superficie del nostro), lo riceviamo quando: – La nostra presentazione ha avuto successo, e quindi il Server ci fornisce come risposta la lista iniziale di Agenti che vediamo. – Il Server ci ha mandato una nuova lista poichè è cambiata (in accordo alla QualityOfService che abbiamo richiesto al Server) rispetto a quella precedente. Quando riceviamo la AgentAction ServerExit (invocato dal Server quando desidera terminare la sua esecuzione) il comportamento di default è invocare la doDelete() e quindi terminare anche il Client (si potrebbe invece decidere che, invece di morire, il DaveClientAgent vada in stato di sleep e poi provi a cercare un nuovo Server). Il metodo takedown(), invocato durante il processo innescato da doDelete(), consiste nel mandare una AgentAction (ClientExitRequest) al Server, con lo scopo di far dimenticare il Server della nostra presenza (ciò significa che non siamo più visti, non vediamo più nessuno, e tutti i meccanismi di Following che ci riguardano terminano). L’ultimo Behaviour del quale è provvisto il DaveClientAgent è il MovingBehaviour, che sostituisce il PresentationBehaviour dopo che la presentazione ha avuto successo. Il suo semplice compito è quello di invocare periodicamente getCurrentPosition() e getCurrentDirection() e fornire tali valori all’UpdatePlanner, che decide se è il caso di informare il Server. In caso affermativo viene creato e spedito un messaggio che come contenuto ha la AgentAction ClientUpdate (che contiene la nuova identità dell’Agente). 4.3 Gli Agenti di Dave 4.3.2 37 FixedAgent E’ un DaveClientAgent che fornisce sempre lo stesso EarthPoint ogni volta che viene invocato il metodo getCurrentPosition(). Per quanto riguarda il metodo getCurrentDirection(), possiamo decidere invece: • di fargli restituire sempre la stessa direzione (DO_NOT_ROTATE) • di fargli restituire una direzione che varia ogni volta con passo costante in senso orario o antiorario. Questi parametri aggiuntivi (EarthPoint fisso, direzione iniziale, e rotazione) si devono specificare fornendo come argomento al FixedAgent i FixedAgentArguments, che estendono i DaveClientArguments. E’ disponibile anche la versione FixedAgentWithGUI, che estende FixedAgent e fornisce una interfaccia grafica usufruendo della libreria swing (quindi FixedAgentWithGUI è specifico per PC e pertanto non si può creare su Android). 4.3.3 RandomAgent E’ un DaveClientAgent che restituisce valori random (ma comunque validi) ad ogni chiamata di getCurrentPosition() o getCurrentDirection(). Non ha bisogno di parametri aggiuntivi, quindi lo si può creare usando i DaveClientArguments. Come per il FixedAgent, esiste la versione specifica per pc (RandomAgentWithGUI) con interfaccia grafica swing. 4.3.4 AndroidRealAgent E’ un DaveClientAgent specifico per Android. In questo Agente i metodi getCurrentPosition() e getCurrentDirection() interrogano rispettivamente il provider di localizzazione (GPS o Wi-Fi) e il Giroscopio. Questo Agente si aspetta di trovare come argomento un oggetto di classe RealAgentArguments, la quale estende DaveClientArguments, e impone come vincolo aggiuntivo che il Communicator non sia null e sia instanceOf AACommunicator. L’AACommunicator (Activity-to-Agent Communicator) estende il Communicator e impone che l’oggetto chiamante sia una Activity. Inoltre fornisce ad entrambi un rapido accesso al Giroscopio e al Network Provider (preferibile rispetto al GPS provider che funziona bene solo all’aperto), permettendo di leggere i valori forniti da questi strumenti (a questo sarà interessato l’Agente) o metterli in pausa al fine di risparmiare energia (a questo sarà interessata la Activity). 4.4 UpdatePlanner 4.3.5 38 DaveServerAgent La struttura interna di questo Agente è più semplice rispetto a quella del Client. Infatti non necessita di argomenti in ingresso e ha solo due Behaviour: • ClientMessageHandlerBehaviour è sempre in esecuzione o in coda (CyclicBehaviour) e ha il compito di ricevere i messaggi provenienti dai Client, e smistarli nei vari handler a seconda della AgentAction presente nel contenuto. Le possibilità includono ClientSubmission, ClientUpdate, ClientExitRequest, StartFollow e StopFollow. Il meccanismo di Following sarà trattato più avanti, vediamo ora la gestione degli altri tipi di AgentAction: – ClientSubmission è la AgentAction usata da un nuovo Client quando si presenta al Server, come abbiamo visto nella sottosezione dedicata al DaveClientAgent. Quando viene ricevuta il Server deve aggiungere nella propria struttura dati (che approfondiremo in seguito) le informazioni riguardanti il nuovo Client, e rispondere con un ServerUpdate indicando gli Agent inizialmente visti da quello nuovo (dopo averli trovati). – ClientUpdate viene ricevuto quando uno dei Client ha cambiato in modo significativo (rispetto al proprio UpdatePlanner) il proprio EarthPartialCircle. Il Server deve semplicemente riconoscere il Client (in tempo O(1), in seguito vedremo come) e aggiornare questo dato. – ClientExitRequest viene ricevuto quando uno dei Client ha intenzione di terminare la propria esecuzione. Il Server, dopo averlo riconosciuto, deve rimuovere tale Client dalla propria struttura dati. • UpdateBehaviour è una procedura che il Server esegue ogni secondo. Per ciascuno dei Client, l’obiettivo è aggiornare gli Agenti che questo Client vede, e, se sono cambiati rispetto alla precedente esecuzione di UpdateBehaviour, mandare un messaggio al Client con la lista aggiornata. I cambiamenti sono ovviamente dovuti allo spostamento del Client stesso o allo spostamento di uno o più altri Agenti, che sono entrati (o usciti) dal suo punto di vista. Questa procedura viene fatta in accordo alla QualityOfService che ciascun Client ha richiesto. 4.4 UpdatePlanner L’UpdatePlanner (contenuto nel package dave.update) si occupa di stabilire quando un cambiamento nell’EarthPartialCircle di un Client (in seguito ad una nuova lettura di posizione e direzione) è “significativo” (e in quanto tale, il Server deve venirne a conoscenza). I criteri sono i seguenti: 4.5 Riduzione del Carico Computazionale 39 1. Deve essere passato abbastanza tempo (maggiore del parametro UpdatePeriod, che possiamo specificare nel costruttore) dalla precedente lettura. In caso contrario, la lettura viene considerata “non significativa”. UpdatePeriod, per questo motivo, viene quindi anche utilizzato per temporizzare il MovingBehaviour del Client. 2. La distanza tra i centri dei due EarthPartialCircle (ovvero la differenza di posizione) deve essere superiore di un altro parametro (minimum_delta_position) che possiamo impostare nel costruttore. 3. La differenza di direzione deve essere superiore di un terzo parametro (minimum_delta_direction), anche questo impostabile a nostro piacimento. L’UpdatePlanner non si limita a memorizzare i criteri, è anche in grado di memorizzare le letture e fornire le risposte, infatti: • Il metodo start(EarthPartialCircle) memorizza dentro l’UpdatePlanner l’EarthPartialCircle iniziale e il currentTimeMillis. • Il metodo needToUpdate(EarthPartialCircle) analizza l’EarthPartialCircle passato come parametro e il currentTimeMillis confrontando questi dati con quelli precedentemente memorizzati. Secondo i criteri che abbiamo stabilito, il metodo restituisce una risposta booleana in merito alla significatività del cambiamento. Che il cambiamento sia significativo o meno, i dati precedentemente memorizzati vengono sostituiti da quelli nuovi, in modo tale da essere pronto per la successiva chiamata. 4.4.1 NeverUpdatePlanner Il NeverUpdatePlanner (contenuto nel package dave.update) è un UpdatePlanner che “risponde sempre di no”, ovvero il suo metodo needToUpdate restituisce sempre false. I criteri non vengono quindi utilizzati, e le letture non vengono memorizzate. Può essere utile se usato dai FixedAgent che non ruotano o da Agenti che non vogliono informare il Server dei propri spostamenti, facendogli credere di trovarsi sempre nella posizione iniziale. 4.5 Riduzione del Carico Computazionale Sebbene la struttura interna del DaveServerAgent sia molto semplice, il lavoro che questo Agente deve svolgere, se non si adottano strategie per ridurlo, è molto costoso in termini di operazioni. Infatti ogni volta che UpdateBehaviour è in esecuzione (ogni secondo) dovrebbe controllare per ciascuno degli N Client connessi se vede o meno ciascuno degli N - 1 altri Client, con un 4.5 Riduzione del Carico Computazionale 40 costo quadratico. Le semplificazioni che adottiamo per ridurre questo costo sono le seguenti: • Alcuni Client possono accontentarsi di ricevere aggiornamenti meno frequentemente. Se un Client è interessato agli update ogni 5 secondi, allora UpdateBehaviour (che viene eseguito ogni secondo) dopo averlo servito 1 volta lo ignorerà per le 4 volte successive. • Non è necessario ogni volta ricontrollare tutti gli altri N - 1 Client. Se ad esempio un Agente X è a Milano, e il raggio del suo EarthPartialCircle è di 1 Km, che bisogno c’è di controllare se ora vede l’Agente Y che si trovava a Roma quando è stato fatto l’ultimo aggiornamento? Al contrario un Agente Z che si trovava a 2 Km da X o un Agente W che si trovava a 500m da X ma alle sue spalle avranno bisogno di essere controllati più spesso. Qui entra in gioco il concetto di RemotenessRatio che è stato introdotto nella sezione dedicata agli EarthPartialCircle. L’idea è definire una serie di “Regole” che associano alla RemotenessRatio una frequenza di aggiornamento. Le Regole saranno utilizzate dal Server per creare dei “Livelli” nei quali posizionare gli N - 1 altri Agenti. Gli Agenti facenti parti di un Livello vengono controllati tutti insieme (in rispetto alla Regola associata al Livello) ed eventualmente spostati in altri Livelli a seconda della loro variazione di RemotenessRatio. Per quanto riguarda il ClientMessagesHandlerBehaviour, il suo compito prevalente è riconoscere ciascun Client che ha inviato un messaggio per poter accedere alla struttura dati del Server nel punto opportuno. Se il riconoscimento del Client fosse solo tramite AID, il Server dovrebbe confrontarlo uno per uno con tutti gli N AID dei Client connessi, quindi con un costo di N confronti nel caso pessimo. Per ridurre questo costo viene introdotto un numero intero (Cookie) che identifica univocamente ciascun Client. Ogni volta che un nuovo Client X si connette viene posto in fondo alla lista del Server e gli viene associato il Cookie CX, dove CX è il numero di Client precedentemente presenti nella lista (quindi X escluso). Tale Cookie corrisponde esattamente all’indice della lista in cui si trovano i dati di quel Client. Ogni volta che X manda un messaggio al Server deve presentare il proprio Cookie CX, in modo tale che l’accesso ai suoi dati avvenga in tempo O(1) utilizzando il Cookie come indice nella lista. Che cosa succede quando un Client Y con Cookie CY manda una ClientExitRequest al Server? In questo caso il Client Y viene eliminato dalla lista e tutti i client che avevano Cookies (ovvero posizione all’interno della lista) maggiori di CY vengono shiftati di una posizione verso sinistra, mentre tutti i Client che avevano Cookies minori di CY rimangono nella posizione in cui erano (quindi i loro Cookies rimangono funzionanti). Se un client X è stato 4.5 Riduzione del Carico Computazionale 41 shiftato il suo cookie CX non è più valido, però i suoi dati si trovano nella lista in una posizione sicuramente minore di CX (in particolare in quella precedente se è stato eliminato solo un Client con cookie < CX dall’ultima volta in cui CX ha mandato un messaggio al Server). Tuttavia questo non è un grosso problema, poichè: • Non tutti i Cookies perdono di validità • Le operazioni di eliminazione sono sicuramente meno frequenti rispetto alle operazioni di accesso e modifica ai propri dati • Qualora un Cookie CX diventasse invalido, il riconoscimento del Client X viene fatto tramite il suo AID, andando a ritroso nella lista a partire dalla posizione CX (quindi al più CX confronti vengono fatti). Una volta trovati i dati, il Cookie viene corretto e rimandato al Client, così le successive operazioni torneranno ad essere servite in tempo O(1). 4.5.1 LevelAccessRule La classe LevelAccessRule implementa il concetto di “Regola” che abbiamo introdotto parlando di riduzione del carico di lavoro del Server. E’ ciascun Client che decide quante e quali Regole creare, a seconda delle proprie esigenze (o meglio di quelle dell’applicazione). Per creare una Regola dobbiamo semplicemente specificare una coppia di valori interi: • Limite: solo gli Agenti che hanno RemotenessRatio < Limite vengono considerati da questa regola. E’ possibile fornire anche il valore speciale NO_LIMIT per indicare che la RemotenessRatio può essere qualsiasi. • UpdatePeriod: ogni quanti secondi gli Agenti considerati da questa regola vengono controllati per verificare gli eventuali cambiamenti. 4.5.2 AccessRules Questa classe modella il concetto di insieme di regole. Possiamo aggiungere tutte le LevelAccessRule che vogliamo, in un qualsiasi ordine, e quando siamo soddisfatti, invocare il metodo checkAndNormalize() che ordina le regole e controlla che non ci siano regole discordanti, di troppo o mancanti: • Le Regole non possono essere zero (viene lanciata una EmptyRulesException). • Deve esistere una regola con UpdatePeriod = 1. • Non ci possono essere due regole uguali (viene lanciata una EqualRulesException). Due regole sono uguali quando hanno lo stesso Limite o lo stesso UpdatePeriod. Come conseguenza può esistere al più una sola regola con NO_LIMIT. 4.5 Riduzione del Carico Computazionale 42 • Non ci possono essere due regole discordanti. Se il Limite di R1 è maggiore del Limite di R2 (quindi la regola R1 prende in considerazione Agenti più distanti) allora deve valere che anche l’UpdatePeriod di R1 è maggiore di quello di R2 (ovvero che la regola R1 triggera meno frequentemente della regola R2 ). In caso contrario R1 e R2 sono discordanti (e viene lanciata una DiscordantRulesException). Le regole sono quindi ordinate per UpdatePeriod (o equivalentemente per limite, visti i vincoli soprastanti). In questo modo sappiamo che se i > k allora il Limite e l’UpdatePeriod di Ri sono maggiori rispettivamente del Limite e dell’UpdatePeriod di Rk . Un Client A “risponde” alla regola Rk del Client B (si parla delle regole ordinate come descritto sopra) se e solo se: 1. la RemotenessRatio di A rispetto a B è minore del limite specificato nella regola Rk 2. tra tutte le regole di B che soddisfano il punto precedente, Rk è quella con il Limite minore. Se nelle AccessRules di B è presente una regola con NO_LIMIT, allora tutti i Client A diversi da B “rispondono” a una e una sola delle regole. Nel caso non sia presente, invece, qualora la RemotenessRatio di un client A rispetto a B diventasse maggiore di tutti i Limiti di tutte le Regole, A non sarà mai più considerato da B. 4.5.3 QualityOfService Con questa classe ciascun Client può indicare al Server le AccessRules di cui necessita, e un ulteriore parametro intero, updateFrequency, che indica ogni quanti secondi il Server deve aggiornare gli Agenti che tale Client vede. Se un Client desidera aggiornamenti ogni N secondi, allora il Server manterrà un contatore inizializzato ad N. UpdateBehaviour viene eseguito nel Server ogni secondo, e decrementa in ciascuna esecuzione tutti i contatori dei Client. Se un contatore arriva a zero, allora UpdateBehaviour deve controllare (seguendo ora le AccessRules) gli Agenti visti dal Client associato a quel contatore, e poi ripristinare il contatore al valore iniziale. 4.5.4 NoService Se un Client invia NoService (invece di QualityOfService) al Server, significa che non desidera ricevere aggiornamenti di Agenti visti. In questo caso tale Client riceverà solamente un ServerUpdate, quello di risposta alla ClientSubmission, contenente gli Agenti visti inizialmente. Non ricevendo altri aggiornamenti, il Client non saprà se tali Agenti sono usciti dal suo punto di vista, o se ne sono entrati altri. 4.6 Esempio di Creazione e Configurazione Client 4.6 43 Esempio di Creazione e Configurazione Client new AndroidJade(activity, "192.168.0.2", "1099"){ @Override public void onContainerReady() { AACommunicator communicator = new AACommunicator(activity) { @Override public void onAgentLinked() { doResume(); } }; QualityOfService qualityOfService = new QualityOfService( AccessRules.DEFAULT_ACCESS_RULES, 5 ); AndroidRealAgentArguments arguments = new AndroidRealAgentArguments( 3.0, // Raggio 90.0, // TotalAngle new UpdatePlanner(), qualityOfService, communicator ); createNewAgent( "Luca", "dave.android.agent.AndroidRealAgent", arguments ); } ... } In questo esempio viene creato un Agente di classe AndroidRealAgent, di nome Luca, i cui EarthPartialCircle avranno un raggio di 3 Km e un ampiezza angolare totale di 90 gradi. Luca usa l’UpdatePlanner di Default, cioè quello fornito dal costruttore di default di UpdatePlanner, che in particolare ha: • Mininum_Delta_Direction = 10, ovvero vengono considerate significative solo le rotazioni di almeno 10 gradi. • Mininum_Distance = 0.01, ovvero vengono considerati significativi solo gli spostamenti di almeno 10 metri. • Update_Period = 2000, ovvero devono passare 2 secondi tra una lettura dell’EarthPartialCircle e la successiva. La QualityOfService specificata da Luca impone che Luca deve essere aggiornato (degli Agenti che vede) dall’UpdateBehaviour del Server ogni 5 secondi, con le AccessRules di default, che sono queste: 4.7 Struttura Dati del Server 44 public static final AccessRules DEFAULT_ACCESS_RULES = new AccessRules() .addRule(new LevelAccessRule(1, 1)) .addRule(new LevelAccessRule(20, 2)) .addRule(new LevelAccessRule(40, 4)) .addRule(new LevelAccessRule(80, 8)) .addRule(new LevelAccessRule(100, LevelAccessRule.NO_LIMIT)); Tutto ciò significa: • Ogni 5 secondi (5 * 1) vengono controllati gli Agenti che, al momento dell’ultimo aggiornamento, distavano al più 3 * 1 = 3 Km da Luca. • Ogni 100 secondi (5 * 20) vengono controllati gli Agenti che, al momento dell’ultimo aggiornamento, distavano al più 3 * 2 = 6 Km. • Ogni 200 secondi (5 * 40) vengono controllati gli Agenti che, al momento dell’ultimo aggiornamento, distavano al più 3 * 4 = 12 Km. • Ogni 400 secondi (5 * 80) vengono controllati gli Agenti che, al momento dell’ultimo aggiornamento, distavano al più 3 * 8 = 24 Km. • Gli agenti che, al momento dell’ultimo aggiornamento, distavano più di 24 Km da Luca vengono controllati ogni 5 * 100 = 500 secondi. Se eliminassimo l’ultima regola, otterremmo che se un Agente in un qualsiasi momento viene a distare più di 24 Km da Luca, allora tale Agente non sarà mai più considerato da Luca, nemmeno se la loro distanza dovesse successivamente ridursi a meno di 24 Km. Infine, viene fornito un AACommunicator per far comunicare Luca con la Activity che l’ha creato. In particolare, quando Luca avrà fatto sapere all’AACommunicator che la sua creazione ha avuto successo, verrà invocato il metodo doResume() dell’AACommunicator, che ha l’effetto di iniziare la lettura del giroscopio e del Network Provider. 4.7 4.7.1 Struttura Dati del Server ClientMatrix ClientMatrix è la struttura dati dove il Server contiene tutte le informazioni riguardanti gli Agenti che gli hanno inviato ClientSubmission. E’ semplicemente una lista di oggetti di tipo ClientRow, che gestiscono le informazioni di un singolo Client. Le operazioni che il Server deve svolgere sulla ClientMatrix sono: • Aggiungere una ClientRow ogni volta che riceve una ClientSubmission da parte di un agente B. Per ciascuno dei Client X già presenti nella ClientMatrix deve confrontare l’EarthPartialCircle di X con quello di 4.7 Struttura Dati del Server 45 B, e viceversa, calcolando così le rispettive RemotenessRatio per poter quindi posizionare nel Level corretto B nei confronti dei vari Client X, e i vari Client X nei confronti di B. In particolare così il Server ha ottenuto gli Agenti che B vede, e li può mandare a B come risposta alla ClientSubmission. • Rimuovere una ClientRow ogni volta che riceve una ClientExitRequest da parte di un agente B. Per ciascuno dei Client X rimanenti, deve rimuovere B dal Level di X dove si trovava. Inoltre deve terminare tutte le operazioni di Following da e verso B. • Riconoscere un Client attraverso AID o attraverso AID e Cookie, e verificarne l’esistenza nella ClientMatrix, restituendo la corrispondente ClientRow. Nel caso venga fornito un Cookie non più valido, deve correggerlo e restituirlo corretto al Client. 4.7.2 ClientRow ClientRow contiene tutte le informazioni riguardanti un singolo Client B all’interno della ClientMatrix. Le informazioni che vengono memorizzate sono: • Il descrittore (DaveAgent) di B. L’EarthPartialCircle contenuto nel descrittore viene modificato ogni volta che B manda un ClientUpdate. • I Levels costruiti e governati dalle AccessRules che B ha richiesto. In particolare l’oggetto di classe Levels conterrà tanti oggetti di classe Level pari al numero di regole (size di AccessRules), e assegnerà a ciascun Level una di queste. • Un contatore (Counter) che serve all’UpdateBehaviour per sapere se B deve essere servito (qualora il Counter dopo essere stato decrementato diventa zero) o ignorato (dopo il decremento rimane positivo) nella esecuzione corrente. Il Counter viene resettato al valore iniziale ogni volta che UpdateBehaviour serve B • L’UpdatePeriod, quello della QualityOfService che B ha richiesto, che serve per inizializzare e resettare il Counter. • Gli Agenti che B vede, ricalcolati ogni volta che UpdateBehaviour controlla i Livelli di B. • I Followers di B (descritti più avanti), con la possibilità di aggiungerli, rimuoverli e ottenerne la lista. 4.7 Struttura Dati del Server 4.7.3 46 OtherClient OtherClient è una piccola struttura dati di appoggio riferita ad una coppia di Client <A, B> presenti nella ClientMatrix. Il primo Client (A) assume il ruolo di “straniero”, mentre il secondo Client (B) il ruolo di “locale”. Dentro questa struttura dati viene memorizzato l’ultimo (nel senso l’ultimo che è stato calcolato, visto che può cambiare con il tempo) “rango” assegnato al Client “straniero” da parte del Client “locale”. Se A “risponde” alla regola Rk di B, allora il rango di A è k (che corrisponde all’indice dell’oggetto Level in cui trovare A). Se A “non risponde” a nessuna delle regole di B, allora il rango assume il valore speciale IGNORED. Inizialmente (quando OtherClient viene costruito) il rango memorizzato dentro OtherClient assume il valore speciale “NO_PREVIOUS_RANK”. Gli oggetti di tipo OtherClient vengono creati ogni volta che un nuovo Client C si presenta al Server tramite ClientSubmission, in particolare per ogni Client X già presente viene creato un OtherClient per la coppia <C, X> e un altro per la coppia <X, C>. 4.7.4 Levels I Levels di B sono una lista ordinata di oggetti di tipo Level, ciascuno dei quali è associato ad una delle regole specificate dal Client B con AccessRules (l’ordinamento dei Level corrisponde all’ordinamento dei LevelAccessRule). I vari Level di B contengono tutti gli OtherClient riferiti alle coppie <X, B> tali che: 1. X è un Agente diverso da B 2. Il rango contenuto nell’oggetto OtherClient è diverso da IGNORED Ogni volta che l’Agente X (o meglio l’oggetto OtherClient riferito alla coppia <X, B>) deve essere posizionato in uno dei Livelli di B (questo accade quando X e B si “conoscono” oppure quando UpdateBehaviour agisce sul Livello di B in cui si trova attualmente X), bisogna calcolare il rango di X rispetto a B e confrontarlo con quello precedentemente salvato dentro l’oggetto OtherClient: • Se sono uguali, vuol dire l’Agente X non deve essere spostato dal Level in cui si trova. • Se il rango precedente era NO_PREVIOUS_RANK, allora X deve essere posizionato nel Level di indice corrispondente al nuovo rango. • Se il nuovo rango è IGNORED, allora X deve essere rimosso dal Level in cui si trova (dato dal rango precedente). In questo modo l’oggetto OtherClient riferito alla coppia <X, B> non sarà più puntato da nessuno dei Level, e verrà automaticamente eliminato dalla memoria. X non sarà mai più inserito in uno dei Level di B. 4.8 Following 47 • Altrimenti il rango precedente e quello nuovo sono diversi e non sono i valori speciali sopra elencati. L’oggetto OtherClient dovrà essere eliminato dal Level dato dal rango precedente, e inserito nel Level dato dal nuovo rango. Dopo avere compiuto l’eventuale spostamento, il rango precedente dentro l’oggetto OtherClient viene sostituito con quello nuovo. Ciascun Level ha inoltre un proprio contatore (inizializzato al valore di UpdatePeriod della regola a cui il Livello si riferisce), che viene usato per determinare (con lo stesso funzionamento del Counter di ClientRow) se gli OtherClient presenti nel Livello devono essere analizzati nella corrente esecuzione di UpdateBehaviour oppure no. Ovviamente questo controllo viene fatto per ciascun Livello solo nel caso in cui UpdateBehaviour stia servendo il Client. 4.8 Following E’ un meccanismo che consente ad un Agente A (il “follower”) di ricevere dal Server le notifiche riguardanti gli spostamenti di un Agente B (il “followed”). Non ci sono limiti riguardanti il numero di Agenti che si può seguire o il numero di Agenti dal quale si può essere seguiti. Questo meccanismo è del tutto slegato dall’aggiornamento degli Agenti che A vede, infatti anche se A sta seguendo B, continuerà a ricevere gli usuali messaggi contenenti la AgentAction ServerUpdate, nei quali potrà essere presente anche B. 1. L’Agente A decide di voler seguire l’Agente B, quello che deve fare è mandare un messaggio al Server contenente la AgentAction StartFollow, dove può specificare l’AID di B. 2. Il Server ricava A come mittente del messaggio, e B all’interno della StartFollow contenuta nel messaggio, e fa alcuni controlli (di conoscere sia A che B, che A sia diverso da B, che A non stia già seguendo B). Se tutti i controlli vengono superati, l’Agente A viene aggiunto alla lista dei Followers dentro la ClientRow di B. Infine il Server manda un messaggio di risposta ad A comunicandogli l’EarthPartialCircle iniziale di B (e il suo AID, visto che A potrebbe stare seguendo più Agenti contemporaneamente) tramite una AgentAction FollowedUpdate. 3. Ogni volta che B manda un ClientUpdate al Server, oltre ad aggiornare l’EarthPartialCircle contenuto nella ClientRow di B, il Server deve anche mandare un messaggio (contenente la AgentAction FollowedUpdate) a tutti gli Agenti che fanno parte della lista dei Followers di B (A è uno di questi). 4. Se B decide di mandare una ClientExitRequest al Server, quest’ultimo deve mandare un messaggio a tutti i Followers, contenente la Agen- 4.9 Visualizzazione Agenti sulle GoogleMaps 48 tAction FollowedExit. Dalla ricezione di questo messaggio, A non riceverà più FollowedUpdate riguardanti B (in particolare neanche le ServerUpdate conterranno B, visto che B non c’è più). 5. Anche l’Agente A può far terminare l’operazione di Following, qualora decidesse di inviare a sua volta una ClientExitRequest, oppure la AgentAction StopFollow, che ha l’effetto di terminare solamente l’invio delle FollowedUpdate riguardanti B senza causare la terminazione di A. 4.9 Visualizzazione Agenti sulle GoogleMaps Le mappe di Google sono uno strumento molto comodo e versatile, poichè: • Possiamo visualizzare ciascun luogo con tre modalità diverse: stradale, terreno, oppure ibrida. • Possiamo visualizzare ciascun luogo a diversi livelli di zoom (fornendo un parametro tra 1 e 20) • E’ possibile spostare la telecamera in una qualsiasi posizione e con una qualsiasi angolazione (e possiamo animare o meno gli spostamenti) • E’ possibile posizionare marcatori nei punti di interesse, e associare a ciascuno un nome e un colore • E’ possibile gestire tramite handler personalizzati gli eventi touch o drag-and-drop sui marcatori DaveMap è una sovrastruttura della GoogleMap che le consente di interagire con gli Agenti di Dave. Funziona in modo simile a quanto previsto dal Design Pattern “Adapter”, ovvero i metodi di DaveMap internamente si appoggiano ai metodi della GoogleMap. I Marcatori (Marker) che la GoogleMap è in grado di posizionare (in un punto qualsiasi) e visualizzare su se stessa verranno utilizzati per rappresentare gli Agenti. Anche i Marcatori dovranno avere una opportuna sovrastruttura (DaveMarker), che consentirà loro di contenere più informazioni. In particolare, per ciascun DaveMarker abbiamo bisogno di memorizzare: • L’AID dell’Agente a cui il marcatore si riferisce. In questo modo si stabilisce una corrispondenza biunivoca tra l’Agente e il suo DaveMarker. • Vogliamo sapere se l’Agente a cui questo marcatore si riferisce è quello locale (quello che “vive” nel dispositivo Android che sta visualizzando la mappa), oppure se è visto e/o seguito da quello locale. Questa informazione sarà utilizzata per scegliere il colore con cui disegnare il marcatore (se non specificato esplicitamente dal programmatore nel 4.9 Visualizzazione Agenti sulle GoogleMaps 49 costruttore), e se disegnare o meno l’EarthPartialCircle associato all’Agente. Un EarthPartialCircle può essere disegnato (con una certa precisione che influenzerà le prestazioni) con una poligonale (data una lista di N+1 punti, la poligonale consiste negli N segmenti ciascuno dei quali ha come estremi due punti consecutivi nella lista). Di seguito un esempio di codice che consente di creare una DaveMap a partire da una GoogleMap: MapFragment m = (MapFragment) getFragmentManager().findFragmentById(R.id.map); GoogleMap g = m.getMap(); g.setMapType(GoogleMap.MAP_TYPE_HYBRID); DaveMap daveMap = new DaveMap(g) { @Override public void onAgentSelected(DaveMarker daveMarker) { onAgentPicked(daveMarker.getDaveAgent()); } @Override public boolean acceptToViewAgent(DaveAgent daveAgent) { return true; } @Override public Float onAgentColorRequested(DaveAgent daveAgent) { Agent a = androidJade.getCommunicator().getAgent(); AndroidPirateAgent p = (AndroidPirateAgent) a; TreasureList treasures = p.getGameStorage().getTreasureList(); if (treasures.contains(daveAgent.getAid())){ return DaveMarker.getHue(Color.YELLOW); } return null; } }; Il codice soprastante fa parte della PirateActivity del gioco DaveTreasureHunt, trattato nel capitolo 5. Le prime due istruzioni consentono di recuperare (tramite identificatore) dal file xml contenente la descrizione della videata della PirateActivity (ovvero pirate.xml) il Fragment contentente la GoogleMap, e estrarre quest’ultima. Nel file pirate.xml troviamo infatti: <fragment android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent" class="com.google.android.gms.maps.MapFragment" /> Il FragmentManager è così in grado di riconoscere la GoogleMap corretta e restituirla. Dopo aver impostato il tipo di mappa nella linea successiva, notiamo che quando creiamo la DaveMap ci viene chiesto di implementare tre metodi: 4.9 Visualizzazione Agenti sulle GoogleMaps 50 • onAgentSelected viene invocato quando l’Utente tocca un Marcatore sulla GoogleMap. L’argomento del metodo è il DaveMarker che l’utente ha toccato. In questo modo possiamo estrarre l’Agente a cui il marcatore si riferisce e compiere le azioni appropriate a seconda della sua identità. • acceptToViewAgent viene invocato quando l’Agente che troviamo come argomento è “visto” dall’Agente locale. Dobbiamo restituire true se desideriamo che tale Agente venga visualizzato sulla mappa, o false se vogliamo “nasconderlo”. Anche qui possiamo fare azioni diverse a seconda dell’identità dell’Agente stesso. Da notare che gli Agenti che “nascondiamo” rimangono comunque nella memoria della DaveMap, quindi è possibile decidere di disegnarli in un secondo momento. Gli Agent che possiamo nascondere sono solo quelli “visti”, infatti questo metodo non viene invocato per l’Agenti “locale” e quelli “seguiti”. • onAgentColorRequested viene invocato quando l’Agente che troviamo come argomento ha superato il test dato da acceptToViewAgent e sta per essere disegnato. Dobbiamo scegliere il colore da associare a tale Agente. Nell’esempio esposto sopra viene controllato che l’Agente sia un Tesoro, in tale caso viene colorato in giallo, altrimenti vengono usati i colori di default (blu per quello locale, verde per quelli seguiti, rosso per quelli visti). Le aggiunte apportate alla GoogleMap grazie alla DaveMap riguardano principalmente un maggior supporto ai Marcatori: • I DaveMarker rimangono memorizzati in due liste (una per quelli visualizzati sulla mappa, una per quelli nascosti) all’interno della DaveMap stessa. • In un qualsiasi momento è possibile usare il metodo refresh() che riunisce le due liste in una sola, e innesca nuovamente acceptToViewAgent e onAgentColorRequested su ciascuno dei DaveMarker, per dividerli nuovamente nelle due liste (eventualmente in modo diverso da prima). • Ogni volta che un nuovo DaveMarker viene aggiunto alla mappa, prima ancora di invocare i metodi acceptToViewAgent e onAgentColorRequested, viene verificato che non esista già in una delle due liste un altro DaveMarker associato allo stesso Agente. In caso esista viene deciso quale dei due DaveMarker cancellare, così da mantenere la corrispondenza biunivoca tra Agente e DaveMarker. • E’ possibile ottenere uno specifico DaveMarker all’interno di una delle due liste con il metodo getMarker(AID). E’ possibile ottenere anche quello “locale”, con getMyMarker(). 4.10 DaveReceiver 51 • E’ possibile cancellare i DaveMarker, in modo selettivo (sempre per AID), tutti quelli presenti (con il metodo clear), o solo quelli “visti”. 4.10 DaveReceiver Il compito di DaveReceiver è ricevere gli Intent che vengono inviati dagli Agenti di classe AndroidRealAgent (o sottoclassi). In particolare ogni volta che riceve un Intent deve ricavarne l’azione (tra tutte quelle che il DaveReceiver può gestire), estrarne gli eventuali Extra, per poter così compiere l’azione appropriata sulla DaveMap e/o sulla Activity (tipicamente visualizzare delle notifiche). 4.10.1 IntentFilter Per definire le azioni che il DaveReceiver è in grado di gestire è stato costruito un IntentFilter (memorizzato all’interno del DaveReceiver stesso): IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(DISPLAY_MY_POSITION); intentFilter.addAction(DISPLAY_NEAR_AGENTS); intentFilter.addAction(GO_TO_MY_POSITION); intentFilter.addAction(AGENT_DEAD); intentFilter.addAction(FOLLOWED_ADD); intentFilter.addAction(FOLLOWED_CHANGED); intentFilter.addAction(FOLLOWED_REMOVED); intentFilter.addAction(PRESENTATED); intentFilter.addAction(SERVER_EXIT); Questo intentFilter deve essere utilizzato per registrare il DaveReceiver alla Activity (una Activity può avere uno o più BroadcastReceiver che catturano gli Intent diretti a quest’ultima). Pertanto, dopo aver creato un oggetto (this.receiver) di classe DaveReceiver nel metodo onStart della Activity, dovremo anche effettuare l’operazione di registrazione del corrispondente IntentFilter in questo modo: @Override protected void onResume() { super.onResume(); ... super.registerReceiver(this.receiver, this.receiver.getIntentFilter()); }; La speculare operazione di de-registrazione serve per consumare meno batteria quando non ci si aspetta di ricevere Intent su quella Activity: @Override public void onPause(){ super.onPause(); ... super.unregisterReceiver(this.receiver); } 4.10 DaveReceiver 4.10.2 52 Azioni compiute Visto che abbiamo utilizzato l’IntentFilter sappiamo che tutti gli Intent catturati dal DaveReceiver (viene invocato il metodo onReceive quando ne arriva uno nuovo) hanno come action una di quelle sopra elencate. Gli Extra sono invece un insieme di coppie <chiave, valore> che l’Intent può portare con sè. A seconda della action dell’Intent sappiamo se aspettarci degli Extra oppure no. Di seguito elenco le operazioni compiute a seconda della action dell’Intent. Tali operazioni sono contenute in metodi handler (ad esempio followedAdd). Al termine di ciascuno di questi metodi (ad esempio followedAdd), viene invocato un ulteriore metodo astratto (ad esempio onFollowedAdd), del quale possiamo fornire una implementazione per compiere azioni aggiuntive. • DISPLAY_MY_POSITION: L’AndroidRealAgent avvisa la Activity che ha calcolato un (nuovo) EarthPartialCircle riferito a se stesso. Il DaveReceiver accede all’Agente (tramite il suo puntatore), recupera il nuovo valore dell’EarthPartialCircle, e fà si che la DaveMap aggiorni il DaveMarker “locale”. • DISPLAY_NEAR_AGENTS: L’AndroidRealAgent avvisa la Activity della ricezione di un ServerUpdate. Come nel caso precedente, il DaveReceiver accede all’Agente per recuperare le informazioni sugli Agenti visti. Dopodichè tutti i DaveMarker che si riferivano ad Agenti “visti” vengono cancellati dalla DaveMap e sostituiti con quelli nuovi. • GO_TO_MY_POSITION: L’AndroidRealAgent chiede che la DaveMap della Activity venga centrata nella propria posizione. Per fare questo il DaveReceiver accede all’Agente per ricavare l’EarthVector dal suo EarthPartialCircle corrente. L’EarthVector viene utilizzato per calcolare lo zoom (lunghezza dell’EarthVector), il bearing (direzione dell’EarthVector) e la posizione (firstPoint dell’EarthVector) della telecamera della GoogleMap. • FOLLOWED_ADD: L’AndroidRealAgent avvisa la Activity del fatto che ha richiesto al Server di seguire un Agente. Non vengono compiute operazioni, visto che non abbiamo ancora la posizione dell’Agente seguito. Tuttavia viene ricavato dagli Extra il nome dell’Agente seguito, in modo tale che sovrascrivendo onFollowedAdd potremmo eventualmente visualizzare una notifica a schermo. • FOLLOWED_CHANGED: L’AndroidRealAgent ci avvisa di aver ricevuto una FollowedUpdate dal Server. Dopo aver ricavato (dagli Extra) il nome dell’Agente a cui si riferisce l’aggiornamento, il DaveReceiver deve sostituire nella DaveMap il DaveMarker relativo a tale Agente. 4.10 DaveReceiver 53 • FOLLOWED_REMOVED: L’AndroidRealAgent ci avvisa di aver ricevuto una FollowedExit dal Server. Dopo aver ricavato (dagli Extra) il nome dell’Agente del quale la FollowedExit si riferisce, il DaveReceiver deve rimuoverlo dalla DaveMap. Dopo averlo rimosso controlla se l’Agente (non più seguito) è però visto da quello locale, in tal caso deve ri-aggiungerlo (verrà colorato diversamente). • I metodi che gestiscono tutte le altre action definite nell’intentFilter invocano semplicemente il metodo astratto corrispondente. Con AGENT_DEAD l’AndroidRealAgent comunica all’Activity che sta per terminare la sua esecuzione (ad esempio in seguito ad una eccezione), quindi potremmo decidere di far terminare anche la Activity. Con PRESENTATED l’AndroidRealAgent comunica alla Activity che ha mandato il messaggio di presentazione al Server. Infine con SERVER_EXIT l’AndroidRealAgent comunica alla Activity di avere ricevuto l’ononimo messaggio dal Server. Capitolo 5 Esempio Applicazione Dave Questo capitolo descrive il primo gioco tratto dalla libreria Dave, ovvero DaveTreasureHunt. Si tratta di una caccia al tesoro, nella quale gli Esploratori devono trovare tutti gli oggetti nascosti dal Pirata in una certa area di gioco, prima dello scadere del tempo fissato dal Pirata stesso. Il gioco può essere fruito in diverse modalità a seconda del numero dei giocatori: • cooperativo: una squadra di persone collabora per trovare i tesori e sconfiggere così il Pirata (uno solo di questi è l’Esploratore, e ha il gioco attivo sul telefono, gli altri lo aiutano) • competitivo: gli Esploratori sono persone che cercano individualmente i tesori, cercando di trovarli prima degli altri (e prima dello scadere del tempo) • competitivo a squadre: come competitivo, ma ciascun Esploratore ha i propri collaboratori. Verranno in seguito descritte le regole e lo svolgimento del gioco, e come il gioco stesso è stato costruito. 5.1 5.1.1 Le Fasi del gioco Fase di Posizionamento dei Tesori Il giocatore che assumerà il ruolo di Pirata avvierà l’Applicazione “Dave Treasure Hunt Pirate” sul proprio dispositivo Android, immettendo il proprio nome e l’indirizzo Ip (pubblico) del PC dove il Main Container di Jade è in esecuzione. A questo punto, mantenendo la connessione 3G attiva, si muoverà nella zona prestabilita per il gioco (una città, un bosco ...). Ogni volta che giunge in una posizione nella quale desidera nascondere un tesoro, deve: 5.1 Le Fasi del gioco 55 1. Scrivere su un foglio di carta o un cartoncino un codice alfanumerico da associare all’oggetto che si vuole nascondere, e incollarlo all’oggetto stesso. 2. Collocare l’oggetto nella posizione nascosta scelta. 3. Posizionandosi il più vicino possibile all’oggetto appena nascosto, toccare l’icona del GamePad nella ActionBar dell’applicazione, e scegliere l’opzione “Place Treasure”, fornendo i dati richiesti, ovvero: nome del tesoro (univoco all’interno del Container Jade), codice di sblocco (il codice alfanumerico che abbiamo associato all’oggetto), e indizio per trovarlo. Lo scopo dell’indizio è quello di far giungere gli Esploratori in prossimità di questo tesoro (e non nel punto esatto nel quale è nascosto), e può essere (a discrezione del Pirata, per rendere più o meno difficile il gioco) scritto in forma enigmatica, può essere un indovinello, può essere correlato al ritrovamento di tesori precedenti ... 4. Una volta inserite queste 3 informazioni, il Pirata può allontanarsi dal tesoro, e verificare che nella mappa è comparso un marcatore di colore giallo nella posizione in cui l’ha collocato. Il Pirata procede così fino a mettere un numero adeguato di Tesori sulla mappa. Quando è soddisfatto, torna alla “Base”, cioè dove si trovano gli altri giocatori, e li avvisa verbalmente che il gioco sta per iniziare. A questo punto il Pirata deve scegliere l’opzione “Start Game”, e impostare la data e l’ora di fine del gioco (se nessuno trova tutti i tesori entro quel limite, il Pirata vince). 5.1.2 Fase di Ricerca dei Tesori Gli Esploratori che concorrono (o i capitani delle loro squadra/e) avviano l’applicazione “Dave Treasure Hunt Explorer” sul proprio dispositivo Android, e immettono il loro nome (o quello della squadra) e lo stesso indirizzo Ip e Porta immessi precedentemente dal Pirata. A questo punto possono toccare l’icona del GamePad, che ha l’effetto di ricercare il Pirata. Se il Pirata non ha ancora fatto “Start Game”, questa operazione fallirà, e verranno fatti nuovi tentativi periodicamente. Non appena il Pirata farà “Start Game”, l’operazione avrà successo, e l’Esploratore entrerà nel gioco. Gli Esploratori possono entrare nel gioco in momenti arbitrari, purchè nessuno degli altri Esploratori abbia già vinto e il tempo limite non sia già stato superato (ovviamente arrivando in ritardo si è comunque svantaggiati). Quando un Esploratore è in partita ha a disposizione la lista dei Tesori nascosti dal pirata (la trova nel menu accessibile toccando l’icona del GamePad), e per ciascuno può accedere all’indizio che gli consente di avvicinarsi ad esso. Una volta giunto in prossimità del tesoro (ovvero nel luogo in cui l’indizio porta, se interpretato correttamente), l’Esploratore dovrebbe vedere il marcatore 5.2 Gli Agenti di DaveTreasureHunt 56 giallo del tesoro sulla mappa della propria applicazione. Una volta giunto sul posto indicato dal marcatore, l’Esploratore cerca il tesoro guardando nei possibili nascondigli locali. Quando l’avrà trovato, l’Esploratore dovrà leggere il codice di sblocco sul tesoro stesso, e selezionare l’opzione “UNBLOCK” dal menu contestuale del tesoro. Se il codice inserito è quello corretto, arriverà una notifica di successo, e il tesoro stesso scomparirà dalla mappa. Nel caso il gioco sia cooperativo, il tesoro trovato può essere raccolto e portato con sè nelle successive ricerche, in caso invece che ci siano altri Esploratori (o altre Squadre), il tesoro va rimesso esattamente dove lo si ha trovato, nascosto nello stesso modo (è il Pirata che alla fine del gioco raccoglie tutti i tesori e controlla che nessuno abbia barato). 5.1.3 Fine del Gioco Il gioco può terminare per uno dei seguenti motivi: • Il Pirata annulla il gioco dalla propria applicazione. Gli esploratori possono attendere la creazione di una nuova partita • Il tempo limite fissato all’inizio del gioco viene superato, in questo caso il Pirata vince. Gli esploratori possono comunque continuare le proprie ricerche fino a quando il Pirata non annulla il gioco. • Uno degli Esploratori ha trovato tutti i tesori nascosti dal Pirata. In questo caso il Pirata e tutti gli altri Esploratori perdono. Gli Esploratori che hanno perso possono comunque continuare le proprie ricerche, fino a quando il Pirata non annulla il gioco. Il fatto che un Esploratore, o tutti quelli presenti, decidano di abbandonare il gioco, non comporta il termine dello stesso. Infatti finchè il tempo limite non viene superato, altri Esploratori possono entrare in partita. 5.2 5.2.1 Gli Agenti di DaveTreasureHunt TreasureAgent Questo Agente rappresenta 1 singolo Tesoro di quelli nascosti dal Pirata, ed è un FixedAgent, visto che il Tesoro rimane fermo nella posizione in cui era il Pirata quando l’ha creato, per tutta la durata del gioco. Si aspetta di ricevere come arguments[0] un oggetto che sia instanceOf TreasureAgentArguments, nel quale vengono specificati, tra le altre cose, l’indizio e l’unblock code associati al Tesoro, e l’AID del Pirata. La sua struttura interna è molto semplice, visto che consiste in un unico Behaviour che ha il compito di ricevere e gestire messaggi contenenti una di queste AgentAction: 5.2 Gli Agenti di DaveTreasureHunt 57 • HintRequest: Uno degli Esploratori ha richiesto l’indizio associato al Tesoro. La risposta viene fornita al mittente tramite la AgentAction HintResponse. • Unblock: Uno degli Esploratori ha inviato un codice di sblocco. Tale codice deve essere confrontato con quello esatto. Il TreasureAgent deve mandare una AgentAction UnblockDone al Pirata in caso di successo, oppure una UnblockFailed all’Esploratore in caso di fallimento. • GameCanceled: il Pirata ha deciso di annullare il gioco (questo può succedere sia prima dell’inizio, sia durante il gioco, sia dopo la fine dello stesso), comportando il doDelete() di tutti i Tesori. 5.2.2 ExplorerAgent Questo Agente ha una struttura più complessa rispetto al TreasureAgent, poichè deve interagire sia con il Pirata, sia con i Tesori, sia con la Activity (e quindi con l’Utente Android Stesso). Non ha bisogno di argomenti in ingresso aggiuntivi, vengono utilizzati quelli della superclasse (estende AndroidRealAgent). Di seguito entro nel merito di ciascuna di queste interazioni: • L’ExplorerAgent possiede alcuni metodi pubblici (startGame, unBlock, askHint e askRemainingTime) che la Activity può invocare e rendere disponibili all’Utente Android attraverso la propria interfaccia grafica (in particolare sono tutte le azioni che si possono compiere dal menu identificato dall’icona GamePad e dai suoi sottomenu). Per quanto riguarda la comunicazione nel senso opposto, l’Agente può visualizzare o cambiare informazioni sull’interfaccia grafica mandando all’Activity degli Intent con un determinato tipo e i corrispondenti Extra. • L’ExplorerAgent conosce l’AID di tutti i tesori che deve cercare (la lista dei tesori è fornita dal Pirata). Ogni volta che la Activity invoca askHint(AID), questo Agente manda un messaggio al tesoro corrispondente, contenente la AgentAction HintRequest. I messaggi in arrivo (dai Tesori o dal Pirata) vengono catturati e gestiti, come per gli altri Agenti finora visti, tramite un apposito Behaviour (quindi in particolare viene catturato il messaggio contenente la HintResponse). L’altro tipo di interazione con i Tesori è tentare di sbloccarli mandando una Unblock, in questo caso l’ExplorerAgent non si aspetta una risposta da parte del Tesoro (a meno che lo sbloccaggio fallisca), ma dal Pirata. • Quando la Activity invoca il metodo startGame, l’ExplorerAgent aggiunge a se stesso un Behaviour (PirateFinderBehaviour), che ha lo scopo di effettuare una ricerca periodica di un Agente che fornisca il 5.2 Gli Agenti di DaveTreasureHunt 58 “servizio di Pirata” e che abbia già avviato la partita (tutti i tesori siano già stati collocati, e il tempo limite sia già stato impostato). Il Behaviour terminerà la propria esecuzione quando la ricerca avrà successo, dopo aver mandato una JoinGameRequest al Pirata, così da entrare nella partita e ricevere dal Pirata la lista dei tesori da cercare (JoinGameResponse). L’ExplorerAgent può interagire direttamente con il Pirata chiedendogli il tempo mancante alla fine del gioco (TimeRequest), che viene mantenuto e restituito (TimeResponse) dal Pirata, per evitare eventuali differenze tra gli orologi dei vari ExplorerAgent. Gli ExplorerAgent possono ricevere dal Pirata messaggi relativi ai loro successi nello sbloccaggio dei Tesori, e un ulteriore messaggio alla fine della partita, il cui contenuto dipende dall’esito della stessa (GameWin, GameLost, GameCanceled) 5.2.3 PirateAgent Questo Agente svolge sia il ruolo di Pirata (nasconde i tesori), sia il ruolo di Server (accoglie e mantiene informazioni dei vari ExplorerAgent durante la partita). Il Servizio che questo Agente offre, “PirateService”, viene registrato al DF per permettere ai vari ExplorerAgent di trovarlo. Di questo Agente è stata fatta sia una versione per Android, sia per PC. Siccome Java non permette l’ereditarietà multipla (AndroidPirateAgent estende già AndroidRealAgent, quindi non posso costruire un altra classe base con le operazioni comuni), il codice di questi due Agenti (Pirata per PC e Pirata per Android) si riduce ad un “copia-incolla”. Per ridurre il problema della doppia manutenzione, alcune parti imporanti, tra cui GameBehaviour (il Behaviour principale di AndroidPirateAgent e PcPirateAgent) sono stati isolati in classi a sè stanti. GameBehaviour riceve i messaggi da parte degli Explorer e dei Treasure e li gestisce in modo appropriato: • JoinGameRequest: è inviato da un Explorer, quando vuole unirsi alla partita. Viene controllato che realmente sia un Explorer (e non un Treasure), che non stia già partecipando alla partita, e che quest’ultima sia già iniziata ma non ancora finita. Se uno di questi controlli fallisce, viene mandata una JoinGameRefuse al mittente del messaggio. Altrimenti la struttura dati del Pirata (GameStorage) viene aggiornata con i dati del nuovo Explorer, e viene risposto all’Explorer con una JoinGameResponse, nella quale è presente la lista dei tesori (che il Pirata conserva all’interno del GameStorage). • UnblockDone: viene inviato da un Treasure quando un Explorer è riuscito a sbloccarlo. L’AID del Tesoro è il mittente del messaggio, mentre l’AID dell’Explorer è contenuto all’interno dell’UnblockDone. Anche in questo caso vengono fatti alcuni controlli di validità, che se non superati fanno sì che il messaggio venga ignorato. Il GameStorage viene 5.3 GameStorage 59 aggiornato (in seguito vedremo come), e l’Explorer viene avvisato con un UnblockConfirm del successo dell’operazione. Inoltre se quel tesoro era l’ultimo che mancava a tale Explorer (ovvero con quello li ha sbloccati tutti), allora è il vincitore. In questo caso il Pirata manda un GameWin a costui, e un GameLost a tutti gli altri Explorer. Anche il Pirata stesso ha perso in questo caso, quindi deve mandare un Intent appropriato alla propria Activity. • TimeRequest: uno degli Explorer ha richiesto il tempo rimanente alla fine del gioco. Viene calcolata la differenza tra il tempo limite del gioco e il currentTimeMillis, e inclusa in un messaggio TimeResponse, spedito come risposta al mittente. • ExplorerExit: uno degli Explorer vuole abbandonare il gioco. Tale Explorer e tutti i dati riferiti a quest’ultimo vengono rimossi dal GameStorage. L’altro Behaviour di cui dispone il Pirata è il TimerBehaviour, che viene innescato solo una volta, al raggiungimento del tempo limite fissato all’inizio del gioco. Il suo compito è sancire la vittoria del Pirata, ovvero mandare un GameLost a tutti gli Explorer, e un Intent alla activity del Pirata così da visualizzare la notifica della propria vittoria. 5.3 GameStorage Questa struttura dati è mantenuta dal Pirata, e contiene tutte le informazioni relative: • allo stato del gioco, ovvero se è iniziato e/o è finito (informazioni booleane), qual’è il tempo limite (in millisecondi), e se è finito chi è l’eventuale vincitore (l’Explorer che ha trovato tutti i tesori). Come informazione aggiuntiva abbiamo il tempo mancante alla fine della partita, ricavabile dal tempo limite e da quello corrente. Quando GameStorage viene creato il gioco non è iniziato e neanche finito. Per iniziare il gioco si deve invocare il metodo startGame(long endTime), che imposta gameStarted a true dopo aver controllato che non lo era già, e che l’endTime fornito sia nel futuro. Per terminare il gioco si deve invocare stopGame(AID winnerAID), dove si può fornire l’AID dell’Explorer che ha trovato tutti i tesori, oppure null per indicare che è il Pirata ad aver vinto. Lo stesso oggetto GameStorage si può riutilizzare per un altra partita, usufruendo del metodo reset() che ripristina l’oggetto così com’era dopo essere stato costruito. • ai tesori, ovvero viene mantenuta la lista degli AID dei TreasureAgent che il Pirata ha creato. Con questa lista possiamo: 5.4 Screenshot del Gioco 60 – aggiungere nuovi tesori (nella prima fase del gioco) – copiarla dentro un ACLMessage e spedirla agli Explorer (nella seconda fase del gioco) – spedire GameCanceled ai TreasureAgent – stabilire se un determinato AID corrisponde ad uno dei Tesori oppure no, con il metodo isTreasure(AID) – usare il puntatore alla lista all’interno di ExplorerMatrix, per mantenere i punteggi. • agli esploratori, all’interno di una ulteriore struttura dati (ExplorerMatrix) contenuta dentro GameStorage stesso. GameStorage può interrogare ExplorerMatrix per sapere se un determinato AID corrisponde ad uno degli Explorer che stanno attualmente partecipando alla partita, con il metodo isPlayer(AID). 5.3.1 ExplorerMatrix L’ExplorerMatrix ha una struttura molto simile alla ClientMatrix, ovvero possiede un oggetto di classe ExplorerRecord per ciascuno dei partecipanti alla partita. Questi ExplorerRecord possono essere aggiunti, rimossi o selezionati dalla lista (con i relativi metodi di ExplorerMatrix), rispettivamente quando un Explorer entra nel gioco, esce dal gioco o trova un Tesoro. 5.3.2 ExplorerRecord In questa classe vengono mantenute tutte le informazioni riguardanti un singolo Explorer B che sta partecipando al gioco, ovvero il suo AID, e i punteggi (incapsulati in un oggetto di classe Scores) ottenuti durante la partita stessa. Dentro Scores viene memorizzata una informazione booleana “trovato” per ciascuno dei Tesori nascosti dal Pirata, in questo modo è possibile: • verificare se B ha trovato tutti i tesori (ovvero se tutti i flag sono true). • verificare, dato un tesoro T, se B ha già trovato T oppure no. • gestire l’operazione “B ora ha trovato T”, impostando a true l’opportuno flag trovato. 5.4 Screenshot del Gioco Nelle pagine successive verranno esposte alcune delle schemate facenti parte delle applicazioni Pirate ed Explorer che costituiscono il gioco. 5.4 Screenshot del Gioco 61 (a) Dati per Creazione Agente. (b) Schermata di Attesa Iniziale. (c) Azioni Pirata. (d) Inserimento Nome del Nuovo Tesoro . Figura 5.1: Alcuni Screenshot della Applicazione Pirate 5.4 Screenshot del Gioco 62 (a) Indizio per trovare il Tesoro. (b) Tesoro Posizionato Correttamente. (c) Inserimento Data Fine Gioco. (d) Inserimento Ora Fine Gioco . Figura 5.2: Alcuni Screenshot della Applicazione Pirate 5.4 Screenshot del Gioco 63 (a) Messaggio di Inizio Ricerca Pirata. (b) Messaggio di Benvenuto in Partita. (c) Azioni Esploratore. (d) Menu Tesori. Figura 5.3: Alcuni Screenshot della Applicazione Explorer Capitolo 6 Conclusioni Questo lavoro di tesi ha portato alla realizzazione di una libreria (capitolo 3) che permette ai dispositivi che la utilizzano di rilevare (e interagire con) altri dispositivi che si trovano geograficamente nel loro punto di vista. Questa libreria è stata chiamata Dave (Discover Agent Viewed on Earth), nel tentativo di inglobare le sue funzionalità e il modo in cui è stata costruita. Dave al momento è compatibile con i dispositivi Android (che è diventato in breve tempo dal suo debutto il sistema operativo più diffuso per i telefoni cellulari, superando gli antagonisti Windows Phone e iPhone) e con PC. Sviluppare una libreria è molto più difficile che sviluppare una applicazione, poichè mentre si programma si deve pensare a tutti i possibili usi “corretti” e “scorretti” che gli utilizzatori potrebbero pensare di fare, quindi bisogna cercare di scrivere codice rigido ma versatile. La compatibilità con Android risulta semplice, infatti le applicazioni per tale piattaforma sono scritte in un dialetto di Java, ovvero ad un insieme di classi Java (linguaggio con cui è scritto anche Jade) e di file xml (capitolo 3). Tuttavia la programmazione su Android non è certo da sottovalutare, ha costituito uno degli ostacoli più grandi, infatti nell’approccio iniziale è difficile capire quale concetto iniziare ad approfondire, visto che sono tutti legati e mescolati fra di loro. Risulta più difficile per chi non aveva neanche mai utilizzato un dispositivo Android, che quindi ha solo una vaga idea della struttura delle applicazioni. Un altro dettaglio molto delicato è quello legato alla importazione di progetti e librerie, che conduce facilmente ad errori. Dave può essere usata come base per sviluppare applicazioni di svariato tipo. Ad esempio si può pensare di sviluppare una applicazione di ricerca ristoranti che si trovino nella direzione in cui stiamo camminando, ad una certa distanza massima. La stessa applicazione può permettere di leggere il menu dei vari ristoranti, di riservare i tavoli e di ordinare (quest’ultima funzionalità può essere permessa se l’utente che usa l’applicazione possiede 65 feedback positivi). A questo proposito, sempre come lavoro di tesi, è stata sviluppata una caccia al tesoro (capitolo 5), nella quale i giocatori seguono degli indizi per trovare sulla mappa (sfruttando la libreria) i marcatori dei Tesori precedentamente nascosti dal Pirata. Per lo sviluppo di questa libreria ci si è appoggiati alla libreria Jade (capitolo 2), che facilita la costruzione di applicazioni multi-utente basate sulla rete. Jade è molto versatile e dotata di un ampia gamma di funzionalità, tra cui poter implementare applicazioni basate sullo scambio di messaggi asincroni tra componenti autonome. E’ il programmatore che decide quali funzionalità approfondire, a seconda delle esigenze che ha bisogno, non è necessario comprenderle tutte per iniziare a lavorare con Jade. Lo studio e l’applicazione per esercizio delle funzionalità di Jade ha portato all’ampliamento dell’esempio BookTrading, che potrà essere utilizzato a scopo didattico. La libreria o il gioco sviluppati in questa tesi possono essere oggetto di future estensioni, in particolare: • La rilevazione della direzione del dispositivo in seguito alla lettura del giroscopio non è ancora perfetta, in quanto risulta precisa solo mantenendo il telefono in una posizione obliqua rispetto al terreno, con un angolo di 45 gradi circa. Negli altri casi la direzione rilevata è affetta da errore che varia a seconda dell’inclinazione del telefono. • Recentemente Telecom Italia ha sviluppato una libreria che facilita la creazione di giochi, standardizzando operazioni come la ricerca dei giocatori, la creazione delle partite (scegliendo uno o più avversari o creando una “stanza di gioco” dove gli avversari possono entrare) e il calcolo dei punteggi. Tale libreria si chiama Amuse (Agent-based Multi-User Social Environment) ed è una diretta evoluzione di Jade, infatti i giocatori saranno implementati come Agenti. Si possono pensare a due diverse integrazioni con Amuse: la prima consiste di implementare una ricerca degli avversari che si trovino nel punto di vista dei giocatori, la seconda consiste di modificare la caccia al tesoro in modo tale da adeguarsi agli standard di Amuse. Bibliografia [1] F. Bellifemine, G. Caire, A. Poggi, G. Rimassa Jade programmer’s guide http://jade.tilab.com/doc/programmersguide.pdf [2] G. Caire Jade programming for beginners http://jade.tilab.com/doc/tutorials/JADEProgramming-Tutorialforbeginners.pdf [3] G. Caire, D. Cabanillas Application-defined content languages and http://jade.tilab.com/doc/tutorials/CLOntoSupport.pdf ontologies [4] Getting Started on Android http://developer.android.com/training/index.html [5] Location and Sensors API http://developer.android.com/guide/topics/sensors/index.html [6] Map Object https://developers.google.com/maps/documentation/android/map [7] User Interface http://developer.android.com/guide/topics/ui/index.html [8] Android Developers Italia http://www.anddev.it/ [9] Calcolo della distanza e della direzione tra due punti del Pianeta http://www.sunearthtools.com/it/tools/distance.php [10] Calculate distance and bearing between Latitude/Longitude points http://www.movable-type.co.uk/scripts/latlong.html [11] F. Bergenti, G. Caire, D. Gotta An Overview of the AMUSE Social Gaming Platform http://ceur-ws.org/Vol-1099/paper9.pdf
© Copyright 2024 Paperzz