Progettazione e realizzazione di una library per agenti context

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