Proiectarea Unei Aplicatii Software pe Platforma Android

GHID TURISTIC AL MUNICIPIULUI CLUJ-NAPOCA, CU FACILITĂȚI DE OPTIMIZARE A RUTELOR

Fig. 3.1. Stiva software a sistemului de operare Android

Fig. 3.2. Design Pattern-ul MVC

Fig. 3.3. Ciclul de viață al unei aplicații Android

Fig. 3.4. Stiva de activități într-o aplicație Android

Fig. 3.5. Ciclul de viață al activităților în Android

Fig. 3.6. Ierarhia obiectelor de tip View și ViewGroup

Fig. 3.7. Ierarhia clasei AdapterView

Fig. 4.1. Modelul arhitectural general utilizat

Fig. 4.2. Diagrama arhitecturală generală a aplicației

Fig. 4.3. Ecranul de pornire

Fig. 4.4. Meniul principal

Fig. 4.5. Submeniul care permite alegerea categoriei de POI

Fig. 4.6. Vizualizarea, pe hartă, a poziției curente (culoarea turcoaz) și a punctelor de interes

(culoarea roșie)

Fig. 4.7. Vizualizarea POI sub formă de listă

Fig. 4.8. Parametri locației curente

Fig. 4.9. Ecranul cu detalii pentru POI selectat

Fig. 4.10. Ecranul cu indicații pentru atingerea destinaței dorite

Fig. 4.11. Adăugarea unei note pentru POI dorit

Fig. 4.12. Adăugarea unui task pentru POI dori

Fig. 4.13. Vizualizarea tuturor notelor

Fig. 4.14. Vizualizarea tuturor task-urilor

Fig. 4.15. Ecranul cu informații despre starea vremii

Fig. 4.16. Diagrama UML pentru clasa MyMapActivity

Fig. 4.17. Diagrama UML pentru clasa DirectionsActivity

Fig. 4.18. Diagrama UML pentru clasa NotePad

Fig. 4.19. Diagrama UML pentru clasa ItineraryDirectionsActivity

Fig. 4.20. Diagrama UML pentru clasa ItineraryListActivity

Fig. 4.21. Diagrama UML pentru clasa NoteEdit

Fig. 4.22. Diagrama UML pentru clasa NoteView

Fig. 4.23. Diagrama UML pentru clasa TaskManagerActivity

Fig. 4.24. Diagrama UML pentru clasa TaskManagerListActivity

Fig. 4.25. Diagrama UML pentru clasa POIDetailActivity

Fig. 5.1. Ecranul de pornire, caseta de dialog pentru activarea GPS-ului și harta cu poziția curentă a utilizatorului

Fig. 5.2. Meniul principal,submeniul cu categoriile de POI și lista de obiective localizate.pag.

Fig. 5.3. Obiectivul localizat, lista de detalii pentru POI și operațiile ce se pot efectua asupra acestuia

Fig. 5.4. Ecranul pe care sunt afișate indicațiile de ghidare

Fig. 5.5. Adăugarea unei note pentru POI și vizualizarea tuturor notelor

Fig. 5.6. Ecranul de adăugare a unui task și ecranul în care s-a generat notificarea

I. INTRODUCERE

1.1. PLANIFICAREA ACTIVITĂȚII

1.2. DOMENIUL APLICAȚIILOR PENTRU TERMINALE MOBILE

1.3. MOTIVATIA SI OBIECTIVELE PROIECTULUI

1.4. STRUCTURA LUCRĂRII

II. STADIUL ACTUAL

III. FUNDAMENTARE TEORETICĂ: TEHNOLOGII ȘI INSTRUMENTE FOLOSITE

3.1. PLATFORMA GOOGLE ANDROID

3.1.1. Prezentare generală

3.1.2. Mașina virtuală Dalvik

3.1.3. Stiva software a sistemului de operare Android

3.1.4. Instrumentele disponibile pentru dezvoltarea aplicațiilor

3.2. ARHITECTURA MODEL-VIEW-CONTROLLER (MVC)

3.3. APLICAȚII PE PLATFORMA ANDROID

3.3.1. Componentele de bază ale unei aplicații în Android

3.3.2. Tipuri de aplicații în Android

3.3.3. Fișierul Android Manifest

3.3.4. Ciclul de viață al aplicațiilor Android

3.3.5. Ciclul de viață al activităților în Android

3.3.6. Ciclul de viață al serviciilor în Android

3.3.7. Furnizorii de conținut

3.3.8. Intent-uri, intent-uri în așteptare, filtre pentru intent-uri și Broadcast Receivere

3.3.8.1. Intent-uri

3.3.8.2. Intent-uri în așteptare

3.3.8.3. Filtre pentru intent-uri

3.3.8.4. Broadcast Receivere

3.3.9. Fire de execuție

3.3.9.1. Crearea unui fir de execuție prin extinderea clasei Thread

3.3.9.2. Crearea unui fir de execuție folosind interfața Runnable

3.3.9.3. Controlul unui fir de execuție

3.3.9.4. Prioritatea firelor de execuție

3.4. INTERFAȚA CU UTILIZATORUL

3.4.1. Privire generală

3.4.2. Layout-uri

3.4.3. Widget-uri

3.4.4. Meniuri și submeniuri

3.4.5. Casete de dialog și notificări

3.4.6. Adaptoare

3.5. MODUL DE ORGANIZARE AL RESURSELOR ÎN ANDROID

3.6. LOCALIZARE ȘI HĂRȚI

3.6.1. Clasele pentru lucrul cu Google Maps

3.6.2. Procesul de geocodare

3.7. STOCAREA PERSISTENTĂ A DATELOR

IV. IMPLEMENTAREA SOLUȚIEI ADOPTATE

4.1. ANALIZA PROIECTULUI

4.1.1. Descrierea aplicației

4.1.2. Contextul software

4.1.3. Scenarii de utilizare

4.1.4. Profil utilizator vizat

4.1.5. Considerente speciale de utilizare

4.2. PROIECTAREA APLICAȚIEI SOFTWARE

4.2.1. Design arhitectural

4.2.1.1. Modelul arhitectural general

4.2.1.2. Diagrama arhitecturală generală a aplicației

4.2.2. Descrierea componentelor

4.2.3. Descrierea interfeței cu utilizatorul

4.2.4. Diagrame UML pentru clase

4.2.5. Implementarea în cod

4.2.5.1. Implementarea modulului Manager general

4.2.5.2. Implementarea modulului referitor la hărți și geolocalizare

4.2.5.3. Implementarea modulului care gestionează partea de Notepad

4.2.5.4. Implementarea funcțiior de sinteză de voce (text-to-speech) și vibrație

4.2.5.5. Implementarea părții de stocare persistentă a datelor

4.2.5.5.1. Adaptorii – modul de acces la bazele de date

4.2.5.5.2. Structura bazelor de date locale

4.2.5.6. Implementarea modulului pentru prognoza METEO

4.2.5.7. Implementarea modulului care gestionează partea de Task Management

4.2.5.8. Impelmentarea modulului pentru indicații privind ghidarea și itinerar

4.2.5.9. Implementarea activității care se referă la afișarea de informații detaliate

V. REZULTATE EXPERIMENTALE

5.1. TESTAREA FUNCȚIEI "DIRECTIONS"

5.2. TESTAREA FUNCȚIEI "ADD NOTE"

5.3. TESTAREA FUNCȚIEI "TASK MANAGER

5.4. TESTAREA FUNCȚIEI "ITINERARY

VI. CONCLUZII

VII. BIBLIOGRAFIE

ANEXE

INTRODUCERE

1.1. Planificarea activității

Tabelul 1. Planificarea activității

1.2. Domeniul aplicațiilor pentru terminale mobile

Dezvoltarea aplicațiilor pentru terminale mobile este procesul prin care aplicațiile software sunt gândite pentru dispozitive cu putere de calcul și consum de energie reduse, cum ar fi telefoanele mobile. Piața terminalelor mobile și, implicit, cea a software-ului pentru terminale mobile a cunoscut o creștere exponențială în ultimul deceniu datorata, în primul rând, unei competiții continue între marii jucători. Astfel, aplicații care deservesc utilități precum jocuri, mesagerie, acces la rețelele sociale sau e-business au fost și sunt dezvoltate și îmbunătățite în permanență cu scopul de a ne simplifica traiul.

1.3. Motivația și obiectivele proiectului

Ideea de baza a întregii aplicații este facilitarea deplasării spațiale în municipiul Cluj-Napoca care a devenit din ce în ce mai dificilă din cauza dezvoltării accentuate a întregului spațiului urban, precum și a perspectivelor privind creșterea ulterioară a acestuia.

Un posibil scenariu ar putea fi descris în felul următor : presupunem ca ajungeți, ca turist, în "Orașul de sub Feleac" despre care ați auzit multe cuvinte frumoase și ați dori să îi cunoasteți cât mai multe din obiectivele care i-au adus faima, dar prima problemă de care vă loviți este faptul că aveți puțin timp la dispoziție. Desigur, o soluție ar fi să abordați metoda clasică și anume, să căutați o hartă turistică pe care să vă marcați toate punctele de interes la care ați dori să ajungeți și după aceea să va consumați o grămadă din timpul prețios socotind în ce ordine ar trebui să le vizitați și, mai ales, cum să ajungeți acolo. Nu ar fi frumos ca toate aceste calcule consumatoare de timp să fie gata făcute pentru dumneavoastră ? Singura cerință în materie de hardware ar fi posesia unui terminal mobil cu sistem de operare Android și conexiune permanentă de date.

Astfel, aceste cerințe minime fiind îndeplinite, aplicația poate fi instalată și rulată. Determinarea poziției în spațiu a terminalului mobil se face fie cu ajutorul modulului GPS integrat, fie pe baza situării acestuia într-o anumită celulă a operatorului de telefonie sau în raza de acțiune a unui anumit Wi-fi hot-spot. De asemenea, după cum a fost specificat anterior, este imperioasă existența unei conexiuni de date pentru ca aplicația să fie capabilă să foloseasca Google Maps, să poată folosi serviciile web pentru preluarea informațiilor de pe starea vremii și pentru descărcarea anumitor elemente grafice care nu au fost stocate local din motive care țin de ocuparea memoriei.

Alegerea sistemului de operare Android nu a fost întâmplătoare. S-a ales această platformă deoarece este utilizată tot mai mult de marii producători de terminale mobile (HTC, Samsung, Sony Ericsson, Motorola, LG, Acer, DELL, ZTE, Huawei), este foarte flexibilă și bine documentată atât oficial, cât și neoficial (blog-uri, formuri, comunități on-line).

1.4. Structura lucrării

În acest capitol au fost prezentate elementele de bază care stau la temelia dezvoltării acestui proiect, precum și scopul pentru care a fost aleasă tema curentă.

În capitolul II, intitulat sugestiv Stadiul actual, vor fi enumerate o serie de aplicații asemanătoare celei prezentate în lucrarea de față, iar dintre acestea va fi descrisă mai pe larg TripJournal, dezvoltată de firma Iqest din Cluj-Napoca.

Capitolul III se va focaliza exclusiv pe prezentarea aspectelor teoretice ale conceptelor utilizate în dezvoltarea proiectului. Vor fi descrise în detaliu elementele și tehnologiile folosite, iar acolo unde acest lucru nu este posibil prin prisma complexității prea mari se va face trimitere către referința bibliografică corespunzătoare.

Cel mai voluminos capitol al proiectului, capitolul IV, este dedicat prezentării detaliate a modului în care au fost dezvoltate și integrate părțile componente care prin relațiile ce se stabilesc între ele, fac aplicația să funcționeze.

Capitolul V conține o descriere a câtorva scenarii de test astfel încât modul de funcționare și facilitățile oferite de aplicație să fie înțelese cât mai bine.

În fine, în capitolul VI sunt trase câteva concluzii și este descris modul în care această aplicație

poate fi îmbunătățită prin șlefuirea componentelor curente sau integrarea altora noi.

II. STADIUL ACTUAL

În acest capitol vor fi enumerate și, unde este cazul, descrise aplicațiile asemenătoare celei care

constituie tema acestui proiect și care sunt disponibile pe piață la ora actuală.

Aplicații gen ghid turistic care folosesc facilitățile de geolocalizare oferite de platforma Android au fost dezvoltate de o serie de firme de profil, dar niciuna dintre acestea nu se concentrează asupra municipiului Cluj-Napoca. Demne de menționat ar fi: Aloqua, Where, Zgat To Go 10 și, cea mai importantă, Trip Journal dezvoltată de firma Iquest din Cluj-Napoca și asupra căreia se va focaliza acest capitol.

[19] Trip Journal este o aplicație dezvoltată pentru a rula pe cele mai importante platformele mobile: iOS, Android, Symbian și Bada. Dezvoltarea aplicației pentru aceste platforme a prezentat o mare provocare pentru echipele de design, dezvoltare și testare. Fiecare platformă are propriile caracteristici, interfață pentru utilizatori și practici. S-a reușit oferirea unei soluții care ia în considerare toate aceste lucruri dar în același timp, păstrează similară experiența folosirii Trip Journal pe toate aceste platforme. Echipa de dezvoltare a fost responsabilă pentru interfața de utilizare, funcționalitatea GPS de urmărire a rutei, Gmap și OpenMap, pentru reverse- geotagging (extragerea locației exacte din coordonatele geografice) și afișarea rutei, a pozelor, a conținutului video, a punctelor de referință, pentru email și integrarea cu rețelele de socializare.

Funcționalitățile Trip Journal [19]:

 Urmărirea rutei GPS – una dintre cele mai precise soluții pentru urmărirea rutei;

 Înregistrarea și administrarea de puncte de referință pentru destinații vizitate (poziționare manuală și automată);

 Locația este înregistrată automat pentru toate punctele de referință, dacă este disponibilă

(țară, oraș, adresă);

 Geotagging pentru poze și conținut video;

 Pozele făcute cu Trip Journal sunt salvate automat în galeria foto a smartphone-ului, la

rezoluția ma-Napoca care a devenit din ce în ce mai dificilă din cauza dezvoltării accentuate a întregului spațiului urban, precum și a perspectivelor privind creșterea ulterioară a acestuia.

Un posibil scenariu ar putea fi descris în felul următor : presupunem ca ajungeți, ca turist, în "Orașul de sub Feleac" despre care ați auzit multe cuvinte frumoase și ați dori să îi cunoasteți cât mai multe din obiectivele care i-au adus faima, dar prima problemă de care vă loviți este faptul că aveți puțin timp la dispoziție. Desigur, o soluție ar fi să abordați metoda clasică și anume, să căutați o hartă turistică pe care să vă marcați toate punctele de interes la care ați dori să ajungeți și după aceea să va consumați o grămadă din timpul prețios socotind în ce ordine ar trebui să le vizitați și, mai ales, cum să ajungeți acolo. Nu ar fi frumos ca toate aceste calcule consumatoare de timp să fie gata făcute pentru dumneavoastră ? Singura cerință în materie de hardware ar fi posesia unui terminal mobil cu sistem de operare Android și conexiune permanentă de date.

Astfel, aceste cerințe minime fiind îndeplinite, aplicația poate fi instalată și rulată. Determinarea poziției în spațiu a terminalului mobil se face fie cu ajutorul modulului GPS integrat, fie pe baza situării acestuia într-o anumită celulă a operatorului de telefonie sau în raza de acțiune a unui anumit Wi-fi hot-spot. De asemenea, după cum a fost specificat anterior, este imperioasă existența unei conexiuni de date pentru ca aplicația să fie capabilă să foloseasca Google Maps, să poată folosi serviciile web pentru preluarea informațiilor de pe starea vremii și pentru descărcarea anumitor elemente grafice care nu au fost stocate local din motive care țin de ocuparea memoriei.

Alegerea sistemului de operare Android nu a fost întâmplătoare. S-a ales această platformă deoarece este utilizată tot mai mult de marii producători de terminale mobile (HTC, Samsung, Sony Ericsson, Motorola, LG, Acer, DELL, ZTE, Huawei), este foarte flexibilă și bine documentată atât oficial, cât și neoficial (blog-uri, formuri, comunități on-line).

1.4. Structura lucrării

În acest capitol au fost prezentate elementele de bază care stau la temelia dezvoltării acestui proiect, precum și scopul pentru care a fost aleasă tema curentă.

În capitolul II, intitulat sugestiv Stadiul actual, vor fi enumerate o serie de aplicații asemanătoare celei prezentate în lucrarea de față, iar dintre acestea va fi descrisă mai pe larg TripJournal, dezvoltată de firma Iqest din Cluj-Napoca.

Capitolul III se va focaliza exclusiv pe prezentarea aspectelor teoretice ale conceptelor utilizate în dezvoltarea proiectului. Vor fi descrise în detaliu elementele și tehnologiile folosite, iar acolo unde acest lucru nu este posibil prin prisma complexității prea mari se va face trimitere către referința bibliografică corespunzătoare.

Cel mai voluminos capitol al proiectului, capitolul IV, este dedicat prezentării detaliate a modului în care au fost dezvoltate și integrate părțile componente care prin relațiile ce se stabilesc între ele, fac aplicația să funcționeze.

Capitolul V conține o descriere a câtorva scenarii de test astfel încât modul de funcționare și facilitățile oferite de aplicație să fie înțelese cât mai bine.

În fine, în capitolul VI sunt trase câteva concluzii și este descris modul în care această aplicație

poate fi îmbunătățită prin șlefuirea componentelor curente sau integrarea altora noi.

II. STADIUL ACTUAL

În acest capitol vor fi enumerate și, unde este cazul, descrise aplicațiile asemenătoare celei care

constituie tema acestui proiect și care sunt disponibile pe piață la ora actuală.

Aplicații gen ghid turistic care folosesc facilitățile de geolocalizare oferite de platforma Android au fost dezvoltate de o serie de firme de profil, dar niciuna dintre acestea nu se concentrează asupra municipiului Cluj-Napoca. Demne de menționat ar fi: Aloqua, Where, Zgat To Go 10 și, cea mai importantă, Trip Journal dezvoltată de firma Iquest din Cluj-Napoca și asupra căreia se va focaliza acest capitol.

[19] Trip Journal este o aplicație dezvoltată pentru a rula pe cele mai importante platformele mobile: iOS, Android, Symbian și Bada. Dezvoltarea aplicației pentru aceste platforme a prezentat o mare provocare pentru echipele de design, dezvoltare și testare. Fiecare platformă are propriile caracteristici, interfață pentru utilizatori și practici. S-a reușit oferirea unei soluții care ia în considerare toate aceste lucruri dar în același timp, păstrează similară experiența folosirii Trip Journal pe toate aceste platforme. Echipa de dezvoltare a fost responsabilă pentru interfața de utilizare, funcționalitatea GPS de urmărire a rutei, Gmap și OpenMap, pentru reverse- geotagging (extragerea locației exacte din coordonatele geografice) și afișarea rutei, a pozelor, a conținutului video, a punctelor de referință, pentru email și integrarea cu rețelele de socializare.

Funcționalitățile Trip Journal [19]:

 Urmărirea rutei GPS – una dintre cele mai precise soluții pentru urmărirea rutei;

 Înregistrarea și administrarea de puncte de referință pentru destinații vizitate (poziționare manuală și automată);

 Locația este înregistrată automat pentru toate punctele de referință, dacă este disponibilă

(țară, oraș, adresă);

 Geotagging pentru poze și conținut video;

 Pozele făcute cu Trip Journal sunt salvate automat în galeria foto a smartphone-ului, la

rezoluția maximă a camerei;

 Opțiuni de calitate pentru conținutul video înregistrat;

 Adăugarea de poze din galerie și conținut video la punctele de referință create anterior;

 Comentarii la poze și conținutul video;

 Note de călătorie pentru a descrie experiențele din vacanțe;

 Managementul călătoriilor anterioare, managementul mai multor călătorii în același timp;

 Conectare automată la conturile de Facebook ale utilizatorilor și postarea călătoriilor inlcusiv hărți, ruta parcursă, fotografii, video si comentarii prin Trip Journal Facebook

Application;

 Inserarea călătoriei pe blog-ul/site-ul personal prin opțiunea ”Embed trip” accesibiă din

Trip Journal Facebook Application;

Trip Journal este o aplicație mobilă de succes cu peste 220.000 de instalări în primul an după lansare. Aplicația a primit mai multe premii în cadrul unor concursuri nationale si internaționale din domeniu:

 Premiul pentru cea mai bună aplicație de călătorie pentru Android, Concursul Google

pentru dezvoltatorii de aplicații pentru Android 2, Decembrie 2009;

 Locul doi pentru aplicații utilizate în aer liber, Premiile Best App Ever, San Francisco,

Februarie 2010;

 Finalist pentru cea mai bună aplicație pentru Android, Premiile Gettie, San Francisco,

Iulie 2010;

 Excelență în design și Cea mai bună aplicație plătită, Conferința mondială Mobile App,

Londra, Octombrie 2010;

 Cea mai bună aplicație mobilă, Concursul Orange România pentru aplicații, București, Octombrie 2010;

 Cea mai bună aplicație creată, Conferința 360|iDev, Austin-Texas, Noiembrie 2010;

III. FUNDAMENTARE TEORETICĂ: TEHNOLOGII ȘI

INSTRUMENTE FOLOSITE

3.1. Platforma Google Android

3.1.1. Prezentare generală

În această secțiune a lucrării vor fi prezentate succint elementele sistemului de operare Android și ale SDK (Standard Developer Kit) acestuia, va fi realizată o trecere în revistă a principalelor pachete și vor fi enumerate avantajele programării folosind platforma Google Android.

Android face parte dintr-un nou val de sisteme de operare special gândite pentru terminale mobile. Windows Mobile, Apple iOS, BlackBerry și Palm Pre oferă un mediu de dezvoltare simplificat pentru aplicațiile mobile. Totuși, comparativ cu Android, acestea sunt bazate pe un sistem de operare proprietar care în unele cazuri favorizează aplicațiile native în fața celor dezvoltate de terțe surse, restricționează accesul aplicațiilor create de alți vendori la resursele telefonului și limitează distribuirea software-ului provenit de la terțe surse.

Conform [1], sistemul de operare Android înlătură aceste neajunsuri printr-un mediu de dezvoltare bazat pe o variantă open-source a kernel-ului de Linux. Accesul direct la resursele terminalului mobil este posibil pentru orice aplicație printr-o serie de librării și, interacțiunea între aplicații este de asemenea permisă, dar foarte strict controlată.

Tot conform sursei citate [1], toate aplicațiile sunt tratate în mod egal, în sensul că nu se face discriminare între cele native Google sau cele dezvoltate de terțe surse. Astfel, utilizatorii vor avea libertatea să aleagă dacă doresc utilizarea software-ului nativ Google sau dezvoltat de un alt vendor.

Platforma Android poate fi caracterizată drept “cuprinzătoare”, fiind bazată pe sistemul de operare Linux și folosind o stivă pentru gestionarea memoriei și a proceselor. De asemenea, librăriile disponibile acoperă o gamă largă de aspecte, cum ar fi partea de telefonie, video, grafică sau interfața cu utilizatorul [2].

Andy Rubin, vice-președinte al Google și persoană cheie care a stat la baza sistemului de operare Android, descrie această platformă ca: “prima platformă cu adevărat cuprinzătoare pentru terminalele mobile, toate aplicațiile putând rula pe un terminal mobil fără obstacolele impuse de sistemele de operare proprietare care au împiedicat inovațiile în acest domeniu [1].

Pe scurt, sistemul de operare Android este o combinație de trei elemente [1]:

 Un sistem de operare gratuit, open-source, pentru terminale mobile

 O platformă open-source pentru dezvoltarea de aplicații mobile

 Dispozitive, în special telefoane mobile, care rulează sistemul de operare Android

Mai detaliat, platforma Android este construită dintr-o serie de elemente precum următoarele [1]:

 O serie de elemente de referință privind cerințele hardware astfel încât terminalele mobile să poată rula sistemul de operare.

 Un nucleu bazat pe cel al sistemului de operare Linux care oferă o interfațare de nivel scăzut cu hardware-ul, memoria și controlul proceselor.

 Librării open-source pentru dezvoltarea de aplicații mobile, incluzând SQLite, WebKit

sau OpenGL

 O arhitectură gandită pentru expunerea serviciilor de sistem stratului aplicație, precum managerul de locație, furnizorii de conținut, partea de telefonie și senzori.

 O interfață cu utilizatorul

 Aplicații preinstalate ca parte a stivei

 Un mediu de dezvoltare folosit pentru crearea aplicațiilor care include plug-in-uri și documentație

3.1.2. Mașina virtuală Dalvik

În procesul de dezvoltare al sistemului de operare Android, Google s-a concentrat pe optimizarea acestuia pentru terminalele mobile care au memorie și putere de calcul limitate și se axează pe consumul redus de energie.

Aceste considerente au dus la necesitatea regândirii modului de implementare al mașinii virtuale

Java (JVM) în mai multe aspecte [2].

În primul rând, mașina virtuală Dalvik preia fișierele .class și le combină într-unul sau mai multe executabile cu extensia .dex. Ideea de bază este de a refolosi elementele duplicate din fiecare clasă, reducând astfel spațiul de memorie utilizat.

În al doilea rând, Google a îmbunătățit mecanismul de garbage collection și a folosit compilatorul “just-in-time”. O particularitate a acestui compilator este aceea că realizează compilarea codului după rularea aplicației, având astfel acces la elemente care se execută la run- time.

Nu în ultimul rând, mașina virtuală Dalvik folosește o metodă alternativă de generare a fișierelor ce conțin codul în limbaj de asamblare prin folosirea regiștrilor pentru stocarea datelor în dterimentul stivei. În acest fel se reușește executarea cu până la 30% mai puține instrucțiuni.

Mașina virtuală Dalvik gestionează resurse precum partea de securitate, multi-threading,

managementul pri’oceselor și al memoriei [2].

3.1.3. Stiva software a sistemului de operare Android

La baza platformei Android stă nucleul de Linux versiunea 2.6.29 care este responsabil cu partea de drivere, accesul la resurse, management-ul energiei și alte elemente ale sistemului de operare [2]. Deși nucleul platformei Android este bazat pe Linux, toate aplicațiile sunt dezvoltate în mediul Java și sunt rulate folosind mașina virtuală Dalvik.

La următorul nivel, imediat deasupra nucleului, se situează o serie de librării native C/C++ [1],

[2]:

 Media – librării bazate pe OpenCORE-ul PacketVideo responsabile pentru redarea

fișierelor audio și video.

 SQLite – librării care oferă support pentru lucrul cu baze de date relaționale.

 Graphics – librărie pentru suportul părții grafice 2D și 3D.

 WebKit și Secure Sockets Layer (SSL) – pentru partea de internet security și navigare

pe internet

 FreeType – oferă support pentru font-uri.

 Surface Manager – controlează accesul la display.

Pe același nivel se situează mașina virtuală Dalvik care permite rularea simultană a mai multor instanțe și librăriile sistemului de operare care includ majoritatea funcționalităților din mediul Java [1].

Un nivel mai sus se gasește secțiunea Application Framework [2] care conține clasele folosite pentru dezvoltarea de aplicații.

În fine, pe nivelul cel mai de sus regasim stratul aplicație care conține atât aplicațiile native Google, cât și cele dezvoltate de terțe persoane. Acest strat folosește clasele și serviciile oferite de nivelul imediat inferior și anume Application Framework.

Figura 3.1. Stiva software a sistemului de operare Android – conform [2]

3.1.4. Instrumentele disponibile pentru dezvoltarea aplicațiilor

SDK-ul de Android include o serie de instrumente care facilitează dezvoltarea, testarea și depanarea aplicațiilor construite pe aceasta platformă. Conform [1] acestea sunt:

 SDK-ul Android și managerul pentru dispozitive virtuale: folosite pentru crearea și gestionarea dispozitivelor virtuale (Android Virtual Devices – AVD) și a pachetelor din SDK. AVD-ul include și un emulator pe care se poate rula orice versiune existentă a sistemului de operare.

 Emulatorul din Android: o implementare a mașinii virtuale folosită pentru testarea li depanarea aplicațiilor.

 Dalvik Debug Monitoring Service (DDMS): perspectivă folosită pentru monitorizarea

și controlul instanțelor mașinii virtuale Dalvik.

 Android Asset Packaging Tool (AAPT): generează fișierul .apk din codul sursă.

 Android Debug Bridge (ADB): aplicație client-server cu ajutorul căreia se pot copia și instala fișiere .apk gata compilate pe un emulator activ.

Pe lângă aceste cinci elemente de bază, platforma Android mai oferă următoarele tool-uri [1]:

 SQLite3: instrument care permite gestionarea bazelor de date SQLite.

 Traceview: instrument de analiză pentru vizualizarea log-urilor din aplicație.

 MkSDCard: crează o imagine pe hard-disk care va fi folosită de emulator pentru a

simula un card de memorie extern.

 Dx: convertește fișierele bytecode .class specifice mediului Java în fișiere bytecode .dex

specifice mediului Android.

 activityCreator: script pentru construirea fișierelor Ant care pot fi folosite pentru compilarea aplicațiilor Android fără plug-in-ul ADT.

 layoutOpt: instrument care analizează layout-urile și sugerează imbunătățiri. Toate elementele prezentate în această secțiune pot fi consultate mai în profunzime în [1].

3.2. Arhitectura Model-View-Controller (MVC)

Modelele, numite în literatura de specialitate design patterns, oferă un real ajutor pe partea de proiectare a aplicațiilor. Pe de o parte ne scutesc de un efort de proiectare, iar pe de altă parte ușurează foarte mult evoluția ulterioară a aplicațiilor. În acest subcapitol va fi prezentat modelul Model-View-Controller (MVC) utilizat în cadrul acestui proiect.

Model-View-Controller este un model (pattern) care a fost folosit pentru prima dată în limbajul Smalltalk devenind apoi unul dintre cele mai utilizate design pattern-uri. Ideea de bază este ca într-o aplicație să se separe clar următoarele părți [3]:

 Model: structurile de date folosite de aplicație și partea de calcule.

 View: interfața cu utilizatorul.

 Controller: gestionar de evenimente.

Acest principiu este descris în figura 3.2. Acest pattern se potrivește la majoritatea aplicațiilor. Este ușor să descoperim că cele trei părți M (Model), V (View), C (Controller) există la orice aplicație care intracționează direct cu utilizatorul. Dacă separăm aceste trei părți, le oferim posibilitatea de a evolua separat.

Figura 3.2. Design pattern-ul MVC [4]

Un alt avantaj al acestui model este că ne ajută să construim mai ușor arhitectura aplicației

deoarece am identificat deja 3 macro-blocuri separate, ori, tocmai acesta este scopul în

proiectarea arhitecturii: să distingem blocuri care pot fi construite și testate într-un mod cât mai independent unele de altele.

În figura 3.2 linia continuă cu săgeată indică un mechanism direct de acțiune în sensul săgeții – un apel de metodă, de exemplu, o modificare la nivel View se repercutează asupra părții Model. Pentru aceasta, View posedă o referință spre partea model și poate apela metode din interfața Model-ului.

Partea Model la rândul său trebuie concepută astfel încât să nu depindă de modul în care este realizată partea View, aspect figurat prin linia punctată. Liniile punctate reprezintă mecanisme pe care le numim “indirecte” cum ar fi, de exemplu, utilizarea de evenimente. Aceste evenimente anunță partea View despre eventualele modificări apărute la nivelul părții Model. Pentru aceasta partea View se va înregistra ca listener (ascultător) de evenimente produse de model.

Partea View conține elemente care produc evenimente, de exemplu un eveniment când este apăsat de către utilizator. Partea Controller se înregistrează ca listener pentru astfel de evenimente. View posedă o referință către Model și poate deci apela metode care modifică structura acestuia ca răspuns la evenimentele ce apar. În practică, din motive de simplificare a codului, putem alege să nu separăm părțile View de Controller, este însă foarte important să separăm Modelul față de celălalte două părți.

Pentru aplicațiile mobile sau nomade, unde apare în plus conceptual de context dynamic (tip terminal, rețea, localizare), a fost propus în [3] un model MVC extins care conține mai multe părți de tip View precum și extinderea funcțiilor părții Controller pentru a gestiona evenimente legate de context (de exemplu schimbarea poziției utilizatorului).

3.3. Aplicații pe platforma Android

Înainte de a începe dezvoltarea efectivă a aplicațiilor pe platforma Android este necesară cunoașterea modului în care sunt construite și a ciclului de viață al unei activități și al unui serviciu. În acest subcapitol vor fi prezentate subcomponentele care alcătuiesc o aplicație în Android și modul în care sunt conectate între ele prin intermediul fișierului Android Manifest.

3.3.1. Componentele de bază ale unei aplicații în Android

Următoarele șase componente descriu în termeni generali structura unei aplicații pe platforma

Android [1]:

 Activitățile: fiecare ecran din aplicație este o extindere a clasei Activity. Activitățile

folosesc View-uri pentru construirea interfeței cu utilizatorul.

 Serviciile: partea invizibilă a aplicației. Serviciile rulează în background, actualizând datele și activitățile și declanșând notificările. Ele sunt folosite pentru a efectua operațiuni care trebuie să ruleze chiar dacă o anumită activitate este sau nu activă.

 Providerii de conținut: acești provideri sunt folosiți pentru a gestiona și transmite datele în cadrul aplicației. Sunt, de asemenea, modalitatea recomandată pentru a expune datele în afara aplicației astfel încât acestea să poată fi folosite de alte aplicații.

 Intent-urile: modalitatea de a pasa date și mesaje între activități și servicii.

 Broadcast Receiverele: sunt folosite pentru a “prinde” intent-urile care au fost înregistrate pentru un anumit receiver, pe baza unor filter.

 Widget-urile: componente vizuale care pot fi adăugate pe ecranul de început.

 Notificările: modul de alertare al utilizatorului privind evenimentele care se petrec în aplicație, fără a întrerupe activitatea curentă. În funcție de notificare se poate permite sau nu interacțiunea cu utilizatorul.

3.3.2. Tipuri de aplicații în Android

Majoritatea aplicațiilor dezvoltate pe platforma Android se vor încadra într-una din următoarele

categorii [1]:

 Foreground: aplicații care este utilă doar când rulează în prim-plan, de exemplu un joc.

 Background: aplicații în cazul cărora interacțiunea cu utilizatorul este limitată și, în afara momentelor în care este configurată, rulează în mod ascuns.

 Intermittent: aceste aplicații sunt asemănătoare celor din a doua categorie, dar necesită o mai mare responsivitate din partea utilizatorului. De cele mai multe ori rularea se face în background, iar utilizatorul va fi alertat doar atunci când este nevoie de acțiunile sale.

 Widget: aplicații care pot fi atașate ecranului de start.

Aplicațiile mai complexe sunt dificil de încadrat într-o singura categorie și, de cele mai multe ori, vor împrumuta caracteristici din fiecare categorie. Mai multe detalii despre tipurile de aplicații din Android se pot consulta în [1].

3.3.3. Fișierul Android Manifest

Fiecare proiect dezvoltat pe platforma Android conține un fișier care poartă numele de Manifest și se găsește la rădăcina proiectului. În acest fișier se specifică structura aplicației, componentele sale și cerințele. Include, de asemenea, noduri pentru fiecare componentă (activități, servicii, provideri de conținut și broadcast receivere) și, prin folosirea filtrelor și a permisiunilor, se specifică felul în care acestea vor intracționa între ele și cu alte aplicații.

Principalul scop al fișierului Manifest este acela de a realize o conexiune între componentele

aplicației și, mai departe, cu sistemul de operare.

În mod tipic în acest fișier avem un nod rădăcină cu eticheta <manifest> și un atribut care se referă la calea canonică al pachetului rădăcină din proiect.

Atributul versionCode se referă la versiunea curentă și este utlilă doar în cadrul aplicației pentru a compara diversele versiuni. Atributul versionName reprezintă versiunea comercială a aplicației și va fi afișată utilizatorului. Nodul de bază <manifest> are următoarea structură [1]:

<manifest xmlns:android=http://schemas.android.com/apk/res/android package="com.my_domain.my_app"

android:versionCode="1" android:versionName="0.9 Beta"> [ … manifest nodes … ]

</manifest>

Câmpul “[ … manifest nodes … ]” poate conține următoarele atribute [1]:

 Uses-sdk: acest atribut permite definirea versiunii minime, maxime și recomadată a SDK-ului care trebuie să existe pe terminalul mobil pentru ca aplicația să funcționeze corect. Folosind o combinație ale atributelor minSDKVersion, maxSDKVersion și targetSDKVersion se poate restricționa numărul de terminale pe care se instalează

aplicația. Totuși, folosirea maxSDKVersion nu este recomandata (deprecated) deoarece

se dorește asigurarea compatibilității cu versiunile mai noi ale sistemului de operare.

 Uses-configuration: acest atribut specifică ce tipuri de dispozitive de intrare sunt suportate de aplicație:

o reqFiveWayNav: aplicația necesită un dispozitiv de intrare precum un

trackball sau D-pad.

o reqHardKeyBoard: este necesară prezența unei tastaturi hard

o reqKeyboardType: specifică tipul de tastatură care va fi utilizat: querty,

nokeys, twelvekey sau nedefinit.

o reqNavigation: specifică dacă este necesară prezența unui dispozitiv precum

un trackball.

o reqTouchScreen: specifică dacă este necesară existența unui terminal mobil cu

ecran tactil.

 Uses-feature: unul din avantajele platformei Android este gama largă de terminale mobile pe care poate rula. De aceea în acest nod se specifică anumite cerințe hardware speciale pe care aplicația le poate necesita, cum ar fi de exemplu prezența unei camere foto sau a unei camere foto cu autofocalizare.

 Supports-screens: de la terminalele mobile care pot rula sistemul de operare Android și aveau ecrane de rezoluție HVGA, s-a ajuns în anul 2009 la unele care suportă rezoluții superioare precum WVGA și QVGA. Astfel, pentru ca aplicația să funcționeze corect pe un anumit terminal, în acest nod se pot specifica ce dimensiuni de ecrane sunt sau nu suportate de către aplicație:

o SmallScreens: ecrane de rezoluție QVGA.

o NormalScreens: ecrane de rezoluție HVGA, WVGA și WQVGA.

o LargeScreens: ecrane de rezoluție mult mai mare decât HVGA.

o AnyDensity: aplicația se poate scala la orice rezoluție.

 Application: orice aplicație pe platforma Android poate conține cel mult un astfel de nod

care joacă rolul de container pentru celălalte etichete:

o Activity: o etichetă <activity> este necesară pentru fiecare activitate folosită în aplicație. Atributul android:name specifică numele clasei din care face parte activitatea. În fiecare etichetă <activity> este perimisă existența unor subetichete <intent-filter> care impune o restricție asupra intent-urilor care pot rula activitatea respectivă.

o Service: asemănătoare cu eticheta <activity> doar că specifică crearea unui

serviciu.

o Provider: în aceste etichete se specifică fiecare din providerii de conținut existenți în aplicație.

o Receiver: prin adăugarea acestei etichete, se pot înregistra broadcast receivere

fără a fi necesară rularea aplicației.

 Uses-permision: este unul din nivelurile de securitate din aplicațiile Android. Prin această etichetă se specifică funcțiile pe care aplicația are permisiunea să le acceseze (efectuarea de apeluri telefonice, interceptarea SMS-urilor, folosirea GPS-ului, etc.). Aceste permisiuni vor fi afișate utilizatorului la instalarea aplicației.

 Permission: nod prin ale cărui attribute se definesc anumite restricții privind folosirea

propriilor resurse de către alte aplicații.

3.3.4. Ciclul de viață al aplicațiilor Android

Aplicațiile dezvoltate pe platforma Android au control limitat asupra propriului ciclu de viață. Componentele aplicațiilor trebuie să monitorizeze în permanență schimbările privind starea acestora și să reacționeze corespunzător.

În mod implicit, fiecare aplicație rulează într-un process propriu care la rândul său rulează într-o

instanță separată a mașinii virtuale Dalvik [1].

Platforma Android își gestionează resursele astfel încât terminalul mobil să rămână responsiv chiar și în momentele în care este supra-solicitat. Astfel, unele aplicații pot fi închise forțat pentru a elibera resurse necesare altor procese de prioritate mai mare, in general cele care țin de aplicații cu care utilizatorul interacționează în acest moment.

Pe scurt, prioritatea unei anumite aplicații sau process se poate explica după cum urmează: prioritatea unei aplicații se definește ca fiind egală cu valoarea priorității celei mai prioritare componente ale sale. Dacă la un moment dat două aplicații au aceeași prioritate, procesul care a avut prioritatea mai scăzută mai multă vreme va fi terminat primul. În figura 3.3 este prezentat în mod sintetic modul în care sunt clasificate procesele în funcție de prioritate.

Figura 3.3. Ciclul de viață al unei aplicații (conform [1])

Avem următoarele elemente [1]:

 Procesele active: de obicei procesele care țin de elementele cu care utilizatorul interacționează în mod direct.

 Procesele vizibile: procese inactive, de exemplu o activitate vizibilă, dar care nu se află efectiv în prim-plan și nu interacționează cu utilizatorul.

 Procesele serviciilor active: sunt procesele care țin de serviciile care au fost pornite și efectuază operații, dar fără a fi vizibile utilizatorului.

 Procesele din background: procesele ce țin de activități care nu sunt vizibile și nu au servicii active.

 Procese goale: procese ce țin de anumite elemente ale activităților care și-au încheiat ciclul de viață pentru a îmbunătăți timpul de execuție la rulările ulterioare.

3.3.5. Ciclul de viață al activităților în Android

O bună înțelegere al ciclului de viață al activităților este vital pentru a dezvolta o aplicație eficientă din punctul de vedere al resurselor. În acest paragraf al lucrării vor fi prezentate câteva elemente care țin de această temă.

Stiva de activități

Starea curentă a fiecărei activități este determinată de poziția în cadrul stivei de activități. În momentul în care o nouă activitate este lansată, ea este poziționată în vârful stivei.

Figura 3.4. Stiva de activități într-o

aplicație Android [1]

Conform referinței bibliografice [1], stările unei activități sunt următoarele:

 Activă: activitatea este în vârful stivei, este vizibilă și interacționează cu utilizatorul.

 Pauză: activitatea poate fi vizibilă total sau parțial, dar nu interacționează cu utilizatorul.

 Oprită: activitatea nu este vizibilă și se află pe lista activităților care vor fi terminate pentru a elibera resurse.

 Inactivă: activitatea a fost terminată

și nu se mai regăsește în stivă.

Revenind la ciclul de viață al unei activități, acesta se poate regăsi în figura 3.5. Cele 7 stări prin care trece o activitate pe parcursul execuției sunt următoarele [1], [5], [16]:

 onCreate (): metodă care se execută la rularea inițială a activității.

 onRestart(): metodă apelată daca activitatea a fost oprită și se dorește repornirea ei.

 onStart(): metodă care se apelează chiar înainte ca activitatea să devină vizibilă.

 onResume(): metodă care se apelează înainte ca activitatea să interacționeze cu

utilizatorul.

 onPause(): metodă care se apelează atunci când o altă activitate se poziționează în vârful stivei de activități.

 onStop(): această metodă se apelează atunci când activitatea nu mai este vizibilă.

 onDestroy(): metodă care se apelează înainte ca activitatea să fie terminată.

Figura 3.5. Ciclul de viață activităților în Android (conform [5], [16]).

SDK-ul Android conține o serie de subclase care se referă la activități și care conțin widget-uri

pentru interfața cu utilizatorul. Cele mai importante sunt [1]:

 MapActivity: encapsulează resursele necesare pentru gestionarea widget-ului MapView

în cadrul unei activități.

 ListActivity: clasă wrapper pentru activități care sunt utilizate împreună cu un ListView.

 ExpandableListView: similar cu clasa ListView, dar suportă liste expandabile.

 TabView: permite integrarea mai multor activități sau view-uri într-un singur ecran, comutarea între ele facându-se folosind un tab widget.

3.3.6. Ciclul de viață al serviciilor în Android

Ciclul de viață al serviciilor este asemănător cu cel al activităților, dar diferă în privința câtorva

aspecte [5]:

 Diferențe în cazul metodelor onCreate() și onStart(): serviciile sunt pornite atunci când se apelează metoda Context.startService(Intent). Dacă serviciul este pornit pentru prima data, atunci se apelează metodele onCreate() și onStart() în această ordine. Dacă serviciul a fost pornit anterior, atunci se apelează doar metoda onStart() pentru noul intent.

 Metodele onResume(), onPause() și onStop() nu sunt necesare deoarece serviciile nu

interacționează cu utilizatorul și rulează mereu în background.

 Metoda onBind(): dacă un client are nevoie de o conexiune persistentă la un serviciu, apelează metoda Context.bindService. Aceasta crează serviciul respective și apelează metoda onCreate(). Metoda onBind() primește ca parametru intentul clientului și returnează un obiect IBind care poate fi folosit pentru a face alte cereri pentru serviciul respectiv.

 Metoda onDestroy(): se apelează atunci când se dorește terminarea serviciului, iar acesta nu mai are clienți care îl folosesc.

3.3.7. Furnizorii de conținut

Furnizorii de conținut preiau și stochează datele pentru a fi accesate din cadrul altor aplicații. Sunt singura cale de a transmite date între diferite aplicații deoarece nu există o locație comună a sistemului de operare pe care să o poată accesa toate pachetele din Android.

Modul în care un furnizor de conșinut stochează datele depinde de modul în care este proiectat [16]. Un provider de conținut este o interfață pe care clienții o pot accesa direct printr-un obiect de tip ContentResolver. Cu ajutorul metodelor din ContentResolver se pot accesa elementele oricărui furnizor de conținut [2].

Când o cerere către un furnizor de conținut este inițiată, sistemul de operarea Android identifică providerul respectiv și se asigură ca acesta este activ. În mod tipic există o singură instanță pentru fiecare furnizor de conținut, dar acesta poate comunica cu mai multe obiecte de tipul ContentResolver [16].

Providerii de conținut rețin înregistrările într-un tabel pe al cărui coloane se află tipurile de date, iar pe linii se regasesc înregistrările pentru fiecare dată în parte. De asemenea, fiecare înregistrare este referită printr-o valoare numerică unică numita id [16].

Fiecare furnizor de conținut expune un URI în mod public care identifică în mod unic setul de date conținut în providerul respectiv. Un furnizor care controlează mai multe set-uri de date, de exemplu tabele multiple, va expune câte un URI pentru fiecare set în parte [16].

Operațiunile care se pot efectua asupra unui furnizor de conținut sunt asemănătoare cu cele pentru baze de date [1]: inserare, ștergere și actualizare înregistrări. Inserarea unei înregistrări se realizează prin două metode: insert și bulkInsert. Diferența dintre cele două constă în faptul că

prima metodă acceptă un singur obiect de tip ContentValue, pe când a doua acceptă o mulțime de obiecte. Funcția de ștergere a înregistrărilor permite ștergerea unei singure înregistrări sau a unor înregistrări multiple. La fel ca în cazul celorlalte două operațiuni, actualizarea datelor se poate face pentru caz singular sau pentru înregistrări multiple.

Crearea unui furnizor de conținut presupune trei pași simpli [1]:

 Setarea unui spațiu pentru stocarea persistentă a datelor

 Crearea unui clase proprii prin extinderea clasei ContentProvider

 Declararea providerului de conținut în fișierul Android Manifest

3.3.8. Intent-uri, intent-uri în așteptare, filtre pentru intent-uri și Broadcast Receivere

În acest paragraf vor fi introduse câteva concepte care nu ar fi exagerat să fie considerate cele mai importante elemente în dezvoltarea de aplicații pe platforma Android. Va fi prezentat modul în care intent-urile sunt folosite pentru a pasa date și mesaje între componentele aplicațiilor și pentru a porni noi activități și servicii, atât imediat cât și după o anumită perioadă bine determinată.

3.3.8.1. Intent-uri

Intenturile sunt folosite ca un mecanism de transmitere a mesajelor și datelor, atât între componentele aceleiași aplicații, cât și între aplicații. În general, un intent are următoarele utilități [1]:

 Pornirea unei noi activități sau a unui nou serviciu

 Transmiterea de mesaje către o serie de componente ale aplicație pentru a le înștiința că

un anumit eveniment a avut loc

Intent-urile pot fi, de asemenea, folosite pentru a facilita interacțiunea între componentele aplicației.

Cea mai comună utilizare a intent-urilor este aceea de a porni noi activități, fie explicit prin specificarea numelui clasei, fie implicit prin specificarea unei acțiuni care trebuie să se execute asupra unui anumit segment din cod [1].

Intent-urile au, de asemenea, ca utilitate difuzarea de mesaje către componentele aplicației. Orice aplicație iși poate înregistra un Broadcast Receiver pentru a monitoriza și a reacționa la primirea unui intent. Propagarea de mesaje și date folosind intent-uri este unul din principiile fundamentale care se referă la proiectarea unei aplicații, permițând modularizarea acesteia [1].

Un intent poate fi privit ca un pachet care conține diverse informații de maxim interes, atât pentru componenta care recepționează intent-ul respectiv, cât și pentru sistemul de operare. In linii mari, un intent poate conține următoarele [16]:

 Numele componentei care va gestiona intent-ul, specificat prin calea canonică. Acest parametru este opțional, iar dacă lipsește, sistemul de operare va folosi alte elemente pentru a determina destinația intent-ului cum ar fi alegerea componentei celei mai potrivite pentru a se ocupa de intent.

 Acțiunea care va fi executată sau, în cazul intenturile difuzate, acțiunea care s-a petrect și despre care vor fi înștiințate componentele sistemului.

 URI-ul setului de date asupra căruia se va efectua acțiunea.

 Un șir de caractere cu referire la categoria componentei care va gestiona intent-ul.

 Date adiționale, în formatul pereche cheie/valoare [2], care vor fi pasate către

componenta care va gestiona intent-ul.

 Alte indicații privind la modul în care va fi rulată activitatea și cum va fi tratată după ce a fost lansată.

3.3.8.2. Intent-uri în așteptare

Foarte pe scurt, un intent în așteptare (pending intent), disponibil prin crearea unei instanțe a clasei PendingIntent este un mecanism prin care se poate seta ca un anumit intent să fie executat la un moment ulterior. Lansarea poate fi declanșată de un eveniment care are loc în aplicație cum ar fi apăsarea unui buton [1].

Când sunt folosite, intent-urile în așteptare execută intent-ul la care se referă cu aceleași permisiuni ca și cum ar fi fost executat direct de utilizator [1].

3.3.8.3. Filtre pentru intent-uri

În cazul în care un intent este instruit să efectueze anumite acțiuni pe un set de date, sistemul de operare trebuie să cunoască ce componentă va fi folosită pentru realizarea acțiunii. În acest sens se folosesc filtrele pentru intent-uri.

Filtrele pentru intent-uri sunt folosite pentru a înregistra Activități, Servicii și Broadcast Receivere ca fiind capabile să realizeze o acțiune pe un anumit set de date. În cazul Broadcast Receiverelor, filtrele sunt utile pentru a permite recepționarea doar a unor anumite intent-uri [1].

Pentru a specifica filtre pentru o anumită componentă, se adaugă o etichetă <intent-filter> în nodul componentei respective din fișierul Android Manifest, iar în cadrul etichetei se pot specifica următorii parametri [1], [16]:

 Action: folosește atributul android:name pentru a specifica numele acțiunii. Fiecare filtru trebuie să aibă una și numai una astfel de etichetă.

 Category: folosește atributul android:name pentru a indica condițiile în care acțiunea va fi executată.

 Data: această etichetă permite restricționarea tipurilor de date asupra cărora intent-ul

respectiv va putea efectua acțiuni

3.3.8.4. Broadcast Receivere

O utilizarea importantă a intent-urilor în cadrul unei aplicații o constituie posibilitatea de a difuza mesaje către componentele aplicației. Pentru a recepționa aceste mesaje este necesară crearea și înregistrarea unui Broadcast Receiver care va asculta, răspunde și reacționa la intent-urile pe care le primește [1].

Difuzarea de mesaje prin această metodă este simplă. In cadrul aplicației se constuiește intent-ul care se dorește a fi difuzat și se folosește metoda sendBroadcast pentru a-l trimite. Toate considerentele prezentate în secțiunea 3.3.8.1 se aplică și în cazul intentului de difuzat [1].

Pentru a asculta și reacționa la intent-urile difuzate sunt folosite Broadcast Receiverele. Pentru

ca un astfel de receiver să fie activ și să recepționeze mesaje, el trebuie înregistrat, fie prin cod,

fie prin fișierul Android Manifest. Când un receiver este înregistrat este necesară folosirea unor filtre să restricționeze intent-urile care vor fi recepționate [1].

Pentru crearea unui Broadcast Receiver se obține o instanță a clasei BroadcastReceiver și se suprascrie metoda onReceive care va fi executată atunci când un intent este recepționat de către receiver.

Dacă un receiver este înregistrat în fișierul Android Manifest el va fi intotdeauna activ și va cotinua să monitorizeze mesaje și intent-uri chiar daca aplicația nu mai este activă.

În cazul înregistrării prin cod, acesta va fi activ doar atunci când componenta aplicației de unde a fost înregistrat este activă.

În mod tipic, Broadcast Receiverele sunt folosite pentru actualizarea conținutului, pentru lansarea serviciilor, pentru a actualiza interfața cu utlizatorul sau pentru a genera notificări cu privire la evenimentele care se petrec în aplicație [1].

3.3.9. Fire de execuție

Programarea cu fire de execuție (multithread) este un aspect important al mediului Android. În

literatura de specialitate, sinonimele pentru multithreading sunt paralelism și concurență [6].

Multithreading înseamnă capacitatea unui program de a executa mai multe secvențe de cod în același timp. O astfel de secvență de cod se numește fir de execuție sau thread. Datorită posibilității creării mai multor thread-uri, un program în Android poate să execute mai multe sarcini simultan.

Limbajul Android suportă multithreading prin clase disponibile în pachetul java.lang. În pachetul java.lang există două clase și o interfață cu care se pot dezvolta programe multithread: clasele Thread și ThreadGroup și interfața Runnable. Clasa Thread și interfața Runnable oferă suport pentru lucrul cu fire de execuție ca entități separate, iar clasa ThreadGroup pentru crearea unor grupuri de thread-uri în vederea tratării acestora într-un mod unitar [6].

Clasa Thread implementează interfața Runnable, iar obiectele de tip ThreadGroup conțin mai

multe obiecte de tip Thread. Un ThreadGroup poate conține mai multe ThreadGroup-uri.

Conform [6], există două metode pentru crearea unui thread:

 Creăm o clasă derivată din clasa Thread.

 Creăm o clasă care implementează interfața Runnable.

3.3.9.1. Crearea unui fir de execuție prin extinderea clasei Thread

Pentru ca firul de execuție să ruleze propria metodă run(), trebuie să extindem clasa Thread și să

suprascriem metoda run(). Există următoarele etape [6]:

 Crearea unei clase derivate din clasa Thread.

 Suprascrierea metodei run() moștenită din clasa Thread și care este apelată când se

execută un Thread.

 Instanțierea unui obiect Thread folosind new.

 Pornirea thread-ului instanțiat

3.3.9.2. Crearea unui fir de execuție folosind interfața Runnable

Această metodă este de real folos deoarece, astfel, clasa de tip Thread pe care o implementăm poate moșteni capabilități de la altă clasă având în vedere că în Android nu este permisă moștenirea multiplă. Pentru a crea un fir de execuție prin această metodă, se parcurg următoarele etape [6]:

 Crearea unei clase care implementează interfața Runnable.

 Implementarea metodei run() din interfața Runnable.

 Instanțierea unui obiect al clasei utilizând operatorul new.

 Crearea unui obiect din clasa Thread folosind un constructor care are ca parametru un obiect de tip Runnable.

3.3.9.3. Controlul unui fir de execuție

Un fir de execuție se poate afla la un moment dat în următoarele stări [6]:

 New: firul de execuție a fost creat, dar nu a fost pornit.

 Runnable: starea de execuție sau de așteptare a disponibilității unei resurse a sistemului.

 Blocked: stare de blocare în care se așteaptă accesul la o resursă partajată.

 Waiting: starea de așteptare la care s-a ajuns datorită unei metode wait() sau join() fără a se specifica o limită de timp.

 Time waiting: starea de așteptare a trecerii unei anumite limite de timp.

 Terminated: firul de execuție și-a terminat execuția.

Un fir de execuție terminat nu mai poate fi repornit. Această încercare este privită ca o eroare de execuție. Metoda wait() trece un fir de execuție din starea runnble în starea waiting, iar metodele notify() și notifyAll() scot firele de execuție din starea waiting.

3.3.9.4. Prioritatea firelor de execuție

Fiecare fir de execuție are o prioritate de execuție. In general, firul cu prioritatea cea mai mare este cel care va accesa resursele sistemului. Clasa Thread oferă trei constante de tip int pentru priorități [6]:

 MAX_PRIORITY: prioritate maximă pentru un fir de execuție.

 MIN_PRIORITY: prioritatea cea mai mică pentru un fir de execuție.

 NORM_PRIORITY: prioritate normală, care este și cea implicită.

Mai avem două funcții importante pentru obținerea, respectiv stabilirea priorității: getPriority()

și setPriority().

3.4. Interfața cu utilizatorul

Proiectarea unei interfețe cu utilizatorul cât mai intuitivă și stilată este vitală pentru succesul aplicației. Din acest motiv, cele două considerente ar trebui să fie în topul listei de priorități. În acest capitol vor fi prezentate elementele referitoare la design-ul interfeței cu utilizatorul precum view-urile, layout-urile și widget-urile.

3.4.1. Privire generală

Într-o aplicație Android, la baza interfeței cu utilizatorul stau două clase: android.view.View și android.view.ViewGroup. După cum sugerează și numele, clasa View reprezintă o serie de obiecte de tip View de uz general. La fel se întâmplă și în cazul clasei ViewGroup, doar ca într- un obiect de acest timp sunt conțunute mai multe View-uri. Platforma Android folosește concept- ul de layout pentru a determina modul cum sunt aranjate obiectele în cadrul interfeței [2].

Există două posibilități de a construi interfețe grafice în Android: fie prin cod XML, fie prin hard

code, fie printr-o combinație a celor două [2].

În literatura de specialitate, deseori se fac referiri la trei elemente de bază care stau la temelia

interfeței cu utilizatorul [1], [16]:

 View-urile: sunt elementele de bază ale tuturor părților componente din UI. Casetele de

dialog, layout-urile, butoanele, toate sunt derivate din clasa View.

 View Group-urile: sunt extensii ale clasei View care pot conține mai multe View-uri.

 Activitățile: descrise în secțiunea 3.3, reprezintă partea aplicației vizibilă utilizatorului și cu care acesta poate să interacționeze direct.

Pe platofrma Android, UI-ul unei activități este definit pe baza unei ierarhii în ale cărei noduri se

gasesc obiecte de tipul View și ViewGroup după cum se vede în figura 3.6.

Figura 3.6. Ierarhia obiectelor de tip

View și ViewGroup (conform [n])

Pentru a atașa un nou view la ierarhie cu scopul de a fi afișat, în cadrul activității trebuie apelată

metoda setContentView().

3.4.2. Layout-uri

Layout-urile reprezintă extensii ale clasei ViewGroup care pot fi intercalate permițând crearea unor interfețe mai complexe [1].

Layout-urile pot fi construite folosind metoda bazată pe scriere de cod Java, însă o metodă mai des utilizată este prin scrierea de cod în format XML. Fiecare fișier XML conține un arbore prin care se specifică ierarhia de obiect View și ViewGroup. Elementele unui fișier XML sunt proprietățile care definesc felul în care trebuie să arate sau să se comporte un widget. Avantajul folosirii de cod XML în detrimentul codului Java pentru construirea layout-urilor este faptul că în acest format lucrează editoarele GUI cum ar fi, de exemplu, DroidDraw. Motivul pentru care aceste editoare folosesc cod XML în loc de cod Java rezidă în considerentul că dacă se dorește editarea ulterioară a fișierului, interpretarea codului XML este mult mai facilă [8].

Platforma Android oferă o serie de layout-uri predefinite pentru construirea interfeței grafice [7]:

 LinearLayout: toate elementele sunt aranjate în ordine descrescătoare începând de la stânga la dreapta sau de sus în jos. Fiecare element poate avea specificată o anumită valoare pentru gravitație și greutate prin care se indică felul în care iși pot modifica dimensiunile pentru a ocupa spațiul.

 RelativeLayout: fiecare element este poziționat în raport cu celălalte elemente din layout. Relațiile privind poziționare pot fi stabilite astfel încât un element poate începe acolo unde se termină alt element. Fiecare element se poate raporta doar la elementul premergător. Este considerat cel mai flexibil layout nativ din Android.

 TableLayout: este un layout care permite definirea unui tabel. Sistemul de operare va încerca aranjarea fiecărui element pe rândul și coloana corespunzătoare. Coloanele pot fi setate astfel încât să aibă dimensiuni variabile.

 FrameLayout: poate conține mai multe obiecte de tip View, dar la un moment dat doar un singur obiect va fi vizibil.

Fiecare layout are o serie de parametri și atribute dintre care unele sunt specifice pentru o anumită categorie de layout-uri, iar alte sunt comune tuturor layout-urilor. Dintre acestea cele mai importante ar fi: android:id, android:layout_height, android:layout_width, android:text, android:margin, android:padding, android:gravity, android:weight, android:orientation, etc. [16]. Când codul este executat, fiecare fișier XML este compilat într-o resursă de tip View. În mod tipic fișierele XML sunt încărcate în aplicație prin apelarea metodei setContentView() în metoda onCreate().

3.4.3. Widget-uri

Un widget este un obiect de tip View care îndeplinește rolul de interfață pentru interacțiunea dintre utilizator și aplicație. Platforma Android oferă câteva astfel de widgeturi native care ușurează munca creării de interfețe grafice. Cele mai importante widget-uri native sunt, conform [1], [2], [7], [8], sunt:

 TextView: widget pentru afișarea de text. Suportă funcții precum afișare pe mai multe linii, formatare sau împachetarea automată a textului.

 EditText: widget care permite afișarea și editarea textului. Suportă funcții precum afișare

pe mai multe linii, formatare sau împachetarea automată a textului.

 ListView: grup de view-uri care crează și gestionază o listă de elemente pe care le afișează ca și coloane.

 Button: buton standard.

 CheckBox: buton care poate fi in stare activă sau inactivă,lucru marcat prin prin faptul că

este sau nu bifat.

 TimePicker: widget care permite utilizatorului setarea orei fie în formatul cu 24h sau

AM/PM.

 DatePicker: widget care permite setarea datei.

 ImageView: widget care permite afișarea unei imagini.

 ImageSwitcherView: widget care afișează o listă derulantă de imagini sub forma orizontală și, de asemenea, afișează în format mare imaginea selectată în mod curent.

3.4.4. Meniuri și submeniuri

Meniurile sunt o parte importantă a interfeței cu utilizatorul deoarece oferă posibilitatea de a expune funcțiile aplicației fără a ocupa spațiu din ecran. Platforma Android oferă în mod nativ trei tipuri de meniuri care permit efectuarea de diverse acțiuni: meniul de opțiuni, meniul context și sub-meniul [16].

Meniul de opțiuni este o colecție de obiecte de tip meniu care se afișează la apăsarea tastei Menu. Poate afișa o pictogramă insoțită de text pentru fiecare element pentru un număr de până la șase elemente. Dacă acest număr este depășit, există posibilitatea vizualizării unei liste expandate prin apăsarea butonului More.

Meniul context constituie o listă de obiecte de tip meniu care este afișată în momentul în care utilizatorul apasă îndelung un obiect de tip view înregistrat să afișeze un astfel de meniu.

Submeniul poate fi considerat o categorie secundară a celorlalte două tipuri de meniuri care se afișează atunci când este selectat un element dintr-un meniu de opțiuni sau context.

3.4.5. Casete de dialog și notificări

Pe parcursul rulării aplicației pot apărea uneori situații care necesită aleratarea utilizatorului sau, de cele mai multe ori, necesită acțiunea lui directă pentru a specifica modul în care ar trebui tratat un anumit eveniment. În acest sens, mediul Android oferă o serie de modalități privind notificările [16]:

 Notificări cu ajutorul casetelor de dialog

 Notificări în bara de stare

 Notificări de tip toast

În această secțiune vor fi tratate punctual fiecare dintre aceste notificări și va include referințe către surse unde pot fi consultate informații mai în profunzime.

O casetă de dialog este o fereastră de dimensiuni mici care apare peste activitatea curentă. Activitatea trece în plan secund, iar utilizatorul va putea interacționa cu ferastra nou apărută. Casetele de dialog au legătură cu notificările care privesc execuția activității curente și care necesită acțiune din partea utilizatorului

Notificările în bara de stare, după cum le spune și numele, alertează utilizatorul asupra unui anumit eveniment prin afișarea în bara de stare a unei pictorgrame. Când este selectat mesajul din alertă, aplicația va lansa un intent definit de programator. Acest tip de notificare este ideal a fi folosit atunci când se primesc alerte de la un serviciu care lucrează în background. Notificările pot fi configurate astfel încât să alerteze user-ul sonor, vizual sau prin vibrații.

Notificările de tip toast sunt mesaje care sunt afișate pe ecran peste activitatea curentă, dar fără a o trimite în plan secund astfel încât interacțiunea cu utilizatorul să fie posibilă. Aceste notificări ocupă spațiu doar cât este necesar pentru afișarea textului și nu oferă posibilitatea intercțiunii cu utilizatorul.

3.4.6. Adaptoare

Adaptoarele sunt clase care permit realizarea de legături între date și view-uri (de exemplu un ListView). În cele mai multe cazuri nu este necesară crearea unui nou adaptor de la zero deoarece platforma Android oferă o serie de astfel de adaptoare care permit afișarea datelor în interfața cu utilizatorul [1]. Clasele care extind clasa AdapterView includ ListView, GridView, Spinner și Gallery (figura 3.7). Însăși clasa AdapterView extinde clasa ViewGroup ceea ce înseamnă că clasele ListView, GridView, etc. sunt controlere de containere.

Figura 3.7. Ierarhia clasei AdapterView (conform [2])

Lista următoare scoate în evidență trăsăturile importante ale câtorva dintre cele mai versatile și

utile adaptoare native din Android [1]:

 ArrayAdapter: acest adaptor este folosit pentru a realiza legătura dintre un AdapterView și o mulțime de obiecte de un anumit tip. În mod implicit, ArrayAdapter-ul folosește valoarea toString pentru fiecare obect din mulțime pentru a creea și a popula un TextView. Adaptorul nativ se poate modifica prin extinderea clasei astfel încât să poată fi folosit cu layout-uri mai complexe.

 SimpleCursorAdapter: adaptor care permite legarea unui obiect de tip cursor la un ListView, folosind un layout preferențial construit pentru a defini modul de organizare pe linii și coloane.

3.5. Modul de organizare al resurselor în Android

Resursele sunt o parte esențială din arhitectura sistemului de operare Android [2]. Este o idee bună păstrarea resurselor, precum imagini sau string-uri, separate de cod deoarece în acest fel este mai facilă gestionarea și modificarea lor și, de asemenea, se pot crea noi resurse pentru o gamă largă de configurații hardware [1].

Conform [n], avem două tipuri de resurse:

 Resurse implicite: pot fi folosite pentru dezvoltarea de aplicații dedicate oricărui terminal mobil, indiferent de configurația hard a acestuia.

 Resurse alternative: gândite pentru a fi folosite doar pentru anumite configurații

hardware.

Resursele sunt organizate în directorul /res din ierarhia proiectului, fiecare în propriul dosar.

Astfel, avem următoarele categorii [1], [2]:

 Șirurile de caractere (strings): se definesc în fișiere XML care rezidă în subdirectorul

/res/values. Externalizarea șirurilor de caractere ajută la menținerea consistenței aplicației. Id-urile stringurilor sunt expuse în codul Java prin sintaxa R.string.*.

 Culorile: reprezintă identificatori care pointează către codul în hexa al culorilor. Aceste

resurse pot fi accesate din cod prin sintaxa R.color.*.

 Imaginile: reprezintă resurse grafice în format .jpg, .png, .gif care pot fi accesate prin sintaxa R.drawable.*. Organizarea lor în directoare se face în funcție de rezoluție, astfel încat avem folderele drawable ldpi, mdpi și hdpi.

 Stiluri și teme.

 Layout-uri.

 Meniuri.

3.6. Localizare și hărți

Unul dintre cele mai populare servicii oferite de Google este fără dubiu Google Maps [8]. Într-o eră a terminalelor mobile capabile să iși determine propria localizare prin diverse metode, dezvoltarea de aplicații bazate pe geolocație este tot mai populară [5].

Servicii bazate pe geolocație (Location Based Services) este un termen folosit pentru tehnologiile folosite de un terminal mobil pentru a-și determina propria poziție în spațiu [1] și la care se va face referire în continuare prin acronimul LBS.

Cele două elemente ale LBS sunt [1]:

 Managerul de locație: oferă legături către serviciile bazate pe geolocație.

 Furnizorii de locație: reprezintă diversele tehnologii pe care un terminal mobil le folosește pentru a-și determina poziția curentă.

Folosind managerul de locație se pot efectua următoarele [1]:

 Determinarea coordonatelor spațiale care descriu poziția curentă.

 Monitorizarea parametrilor deplasării

 Setarea de alerte bazate de geolocație

 Determinarea furnizorilor de locație disponibili

Un terminal mobil își poate determina locația prin una din următoarele metode [5]:

 Cu ajutorul identificatorului celulei sau al ariei de localizare în care se găsește

 Folosind triangularea

 Folosind sistemul de poziționare prin sateliți (GPS), cel mai utilizat, dar are ca dezavantaj costul ridicat al integrării modulului în terminalul mobil, scăderea duratei de viață a bateriei și nedisponibilitatea în spații închise.

3.6.1. Clasele pentru lucrul cu Google Maps

Una dintre aplicațiile care vine în mod implicit cu sistemul de operare Android este Google

Maps. Pentru a suporta această facilitate, platforma Android conține o serie de clase native [1]:

 MapView: folosită pentru controlul hărții

 MapActivity: clasa de bază care poate fi extinsă pentru a crea o activitate conținând un

obiect de tip MapView.

 Overlay: clasă folosită pentru anotații pe hartă. Cu ajutorul acestei clase și un obiect de tip Canvas se pot agăuga oricâte layere peste hartă.

 MapController: folosită pentru controlul hărții, cum ar fi operațiile de facalizare sau

zoom.

 MyLocationOverlay: clasă specială folostă pentru a desena pe hartă poziția curentă a

utilizatorului.

 ItemizedOverlay și Overlay Item: sunt folosite împreună pentru a poziționa pe hartă o

serie de markeri.

3.6.2. Procesul de geocodare

Procesul de geocodare permite conversia dintr-o pereche de coordonate de forma latitudine/longitudine în adresă și vice-versa. Aceste conversii sunt realizate pe serverul Google, așadar pentru implementarea funcției este necesară conexiunea la internet a aplicației [1].

Procesul de geocodare se împarte în două categorii [16]:

 Geocodare directă: detemină perechea de puncte latitudine/longitudine dintr-o adresă.

 Geocodare inversă: determină adresa corespunzătoare unei perechi

latitudine/longitudine.

Geocodarea directă, referită de cele mai multe ori în literatura de specialitate ca geocodare [1], reprezintă procesul de aflare a latitudinii și longitudinii corespunzătoare unei adrese. Lista de adrese returnate poate să includă mai multe înregistrări, dar fiecare adresă va include latitudinea și longitudinea prin care poate fi referită, precum și alte informații.

Geocodarea inversă returnează adresa străzii specificată printr-o pereche latitudine/longitudine. Acuratețea acestui proces este în întregime dependentă de corectitudinea datelor de pe serverul unde se realizează, așadar calitatea rezultatelor poate varia de la o regiune la alta.

3.7. Stocarea persistentă a datelor

În acest capitol va fi prezentată una din metodele folosite de mediul Android pentru stocarea datelor și anume, stocarea în baze de date SQLite. Pe lângă această metodă, pe platforma Android datele mai pot fi stocate în: shared preferences, sandbox-ul aplicației, memoria internă, memoria externă și pe un server [16].

SQLite oferă o serie de librării foarte puternice și flexibile pentru lucrul cu baze de date asupra cărora programatorul are control deplin. În mod implicit, accesul la bazele de date este restricționat de către aplicația care le-a creat [1].

Folosind SQLite, se pot creea baze de date relaționale și independente în cadrul aplicației pentru stocarea si gestionarea unor structuri complexe de date. Sistemul SQLite este foarte apreciat din acest punct de vedere deoarece este [1]:

 Open-source

 Corespunzător standardelor

 Consumator redus de resurse

În Android, sistemul SQLite a fost implementat ca o librarie C ce face parte din stiva software a sistemului de operare (vezi capitolul 3.1). Faptul că este implementat ca o librărie și nu ca un proces separat, fiecare bază de date SQLite este o parte integrată a aplicației care a creat-o. Acest lucru reduce dependențele de alți factori externi și minimizează timpul de acces. Din această

cauză sistemul SQLite și-a câștigat reputația de sistem deosebit de flexibil și precis fiind preferat de marii producători de terminale mobile [1].

IV. IMPLEMENTAREA SOLUȚIEI ADOPTATE

Acest capitol se va concentra pe descrierea în detaliu a modului în care s-a construit aplicația, arhitectura generală a aplicației, precum și modul în care au fost integrate conceptele și tehnologiile prezentate în capitolul anterior.

Aplicația a fost dezvoltată folosind mediul EclipseTM 3.6.2 Helios și scrisă în limbajul de programare Java (Android). Motivul pentru care am ales mediul Eclipse rezidă în faptul că este singura platformă pentru care există suport oficial din partea Google pentru dezvoltarea de aplicații.

4.1. Analiza proiectului

În acest paragraf al capitolului va fi realizată o prezentare ale celor mai importante elemente care prin relaționare constituie aplicația în sine.

4.1.1. Descrierea aplicației

După cum sugerează și numele acestui proiect, aplicația este un ghid turistic al municipiului reședință de județ Cluj-Napoca. Ideea a pornit de la faptul că momentan Google nu oferă suport pentru ghidarea în timp real folosind Google Maps în România, iar prin faptul ca s-a reușit implementarea acestei funcții în aplicația de față a fost creat un mediu propice și un punct de start excelent pentru extinderea în continuare a proiectului.

Aplicația în sine folosește elementele native oferite de platforma Android pentru construirea interfeței cu utilizatorul care s-a incercat a fi păstrată cât mai simplă și mai intuitivă pentru a nu pune probleme utilizatorilor fără experință în folosirea terminalelor mobile. Așadar, la pornirea aplicației, deoarece modulul GPS este esențial în funcționarea acesteia, se verifică daca este activită funcția de localizare a terminalului mobil în spațiu. Daca GPS-ul este dezactivat, cu ajutorul unui alert modal, utilizatorul va fi întrebat dacă doresște sau nu activarea lui. În cat de răspuns afirmativ, user-ul va fi condus la ecranul de unde poate seta preferințele privind localizarea. Revenind la aplicație, utilizatorul are posibilitatea să aleagă, prin intermediul unui meniu de aplicație următoarele opțiuni: localizare puncte de interes, parcurgere itinerar, vizualizarea tuturor notelor, vizualizarea tuturor taskurilor, vizualizarea informațiilor despre starea vremii și vizualizarea informațiilor privind versiunea și copyright-ul aplicației. Totodată, prin intermediul unor tab-uri, se va putea face comutarea între modul de vizualizare tip hartă, modul de vizualizare tip listă și un ecran care oferă detalii despre poziția curentă.

La selectarea opțiunii localizare puncte de interes, va fi afișat un submeniu din care se poate alege categoria de puncte de interes (POI) dorită: accommodation, cafe/bars, parking, petrol stations, art/culture, etc. În urma selectării categoriei dorite, obiectivele vor fi afișate pe hartă sub forma unor buline de culoare roșie, distinctiv față de culoarea turcoaz a bulinei care marchează poziția curentă a utilizatorului. Prin „apăsarea” oricărei buline care marchează un anumit POI va fi afișat un balon în care se specifică numele și adresa obiectivului. Facând click și pe balon, aplicația va afișa un ecran cu informații detaliate pentru POI-ul respectiv care conține trei fotografii sub formă de galerie, numele, adresa și câteva facilități. Prin intermediul

unui meniul de opțiuni, tot din acest ecran, se pot efectua anumite operațiuni asupra obiectivului

localizat:

 Indicații pentru ghidarea în spațiu de la poziția curentă, atât pentru deplasarea cu un autovehicul (driving), cât și pietonal (walking).

 Adăugare ca punct de interes într-un itinerar.

 Adăugare a unor scurte note.

 Adăugare a unor task-uri.

 Apelare telefonică direct din aplicație.

 Vizitarea paginii web direct din aplicație.

Secțiunea Itinerary este o parte a aplicației care permite utilizatorului să iși constuiască un traseu pe lungimea căruia se află obiective adăugate prin modalitatea descrisă în cadrul aliniatului anterior. Din acest modul al aplicației se vor putea vizualiza și edita punctele care constituie itinerarul și se va putea realiza parcurgerea efectivă a acestuia. O particularitate constă în ideea că ordinea de parcurgere a punctelor de interes este astfel încât lungimea întregului itinerar să fie minimă. La fel ca în cazul indicațiilor de ghidare pentru un obiectiv singular și aici se poate alege modul de parcurgere cu autovehicul sau pedestru.

Tot din meniul principal al aplicației se ajunge la ecranul unde pot fi vizualizate sub formă de listă notele salvate în mod curent pentru POI. În această listă sunt vizibile titlul notei, obiectivul pentru care a fost salvata și data și ora salvării notei. Prin selectarea unei înregistrări din listă se poate consulta efectiv nota respectivă sau, cu ajutorul unui meniu de opțiuni, se poate edita sau șterge. Totodată este posibilă ștergerea simultană a tuturor înregistrărilo din listă.

Categoria în care se pot vizualiza toate task-urile este destinată consultării și editării taskurilor care au fost salvate pentru punctele de interes. La fel ca în cazul notelor, acestea sunt afșate sub formă de listă în care pentru fiecare element sunt vizibile titlul, ora și data la care a fost salvat și la care expiră.

Pentru consultarea informațiilor despre starea vremii este disponibilă opțiunea Weather Forecast. Această preia datele de pe serverul Google și îi afișează utilizatorului condițiile pentru momentul curent, precum și prognoza pentru trei zile într-o interfață simplă și intuitivă.

În fine, butonul About, după cum îi spune și numele, afișează un ecran care conține câteva informații comerciale despre aplicație.

Ce ar mai fi demn de menționat este faptul că a fost folosit suportul pentru sinteza de voce pe care îl oferă platforma Android, așadar au fost implementate o serie de elemente care facilitează interacțiunea cu utilizatorul. În întreaga aplicație acesta se realizează pe trei căi: vizual, auditiv (text-to-speech) și tactil (vibrații).

4.1.2. Contextul software

Platforma Google Android nu a fost aleasă întamplător. În ultimii ani, telefoanele pe care rulează acest sistem de operare au câștigat tot mai mult din cota de piață, iar tendința marilor producători de terminale mobile (HTC, Samsung, Sony Ericsson, Motorola, etc.) este de a folosi Android-ul pe scară tot mai largă pentru segmentul smartphone. Totodată, suportul teoretic este foarte bine pus la punct, atât prin documentația nativă Google, cât și prin prisma comunităților on-line existente. Nu în ultimul rând, deoarece nucleul aplicației se bazează pe lucrul cu harți și geolocalizare, faptul că sistemul de operare Android are integrate o serie de facilități care

simplifică mult lucrul cu aceste elemente, au facut din acesta candidatul ideal ca suport de implementare.

Mai multe informații despre mediul Android pot fi consultate în capitolul III, paragraful 3.1 al lucării de față, precum și în referințele bibliografice.

4.1.3. Scenarii de utilizare

Un scenariu posibil privind utilizarea aplicației ar putea fi următorul: prespunem că un turist ajunge în municipiul Cluj-Napoca despre care a auzit multe lucruri frumoase, dar, din păcate, îl cunoaște prea puțin din punctul de vedere al organizării spațiale. Mai mult, din cauza timpului limitat pe care îl are la dispoziție, nu își permite să piardă vreme abordând metoda clasică de a consulta o hartă turistică și de a încerca să își faca o idee cât mai precisă asupra modului în care ar trebui să își organizeze întreaga activitate. Nu ar fi frumos ca altcineva să facă pentru el toată această muncă consumatoare de timp?

Mai concret, să facem un exercițiu și să ne imaginăm că turistul nostru tocmai a ajuns cu trenul în stație după o călătorie obositoare și nu își dorește altceva decât să se instaleze cât mai repede la hotelul la care are rezervare. Totuși, înainte de asta constată că a rămas fără bani cash. Cum ar putea să ajungă la cel mai apropiat bancomat din rețeaua sa bancară? Simplu, cu ajutorul aplicației de față. După instalarea la hotel pe care, desigur, l-a găsit fără prea multe bătăi de cap datorită ghidului turistic, cu foarte mare ușurință și într-un timp foarte scurt reușește să își construiască o listă cu obiectivele culturale pe care dorește să le viziteze. Pentru că distacția este un ingredient important pentru reușita oricărui concediu, punctele de interes care definesc tot ceea ce înseamnă viață de noapte nu au fost lăsate pe dinafară. Și, pentru ca nu putem exclude nevoile de bază, ghidul include și o listă cu cele mai populare restaurante și fast-food-uri.

4.1.4. Profil utilizator vizat

Aplicația care constituie tema proiectului de față este destinată unei categorii largi a publicului, singura condiție fiind posesia unui terminal mobil cu sistem de operare Android.

4.1.5. Considerente speciale de utilizare

Deoarece aplicația este una complexă sunt câteva elemente esențiale de care trebuie ținut cont atunci când se decide utilizarea ei. În primul rând, fiind dezvoltată pe platforma Android este imperioasă prezența unui telefon cu acest sistem de operare, minim versiunea 2.2. Limitarea din punctul de vedere al versiunii vine din faptul că au fost folosite tehnologii mai noi ale sistemului de operare care nu au fost implementate au versiunile anterioare ale SDK-ului.

O altă cerință este existența unei conexiuni permanente de date Wi-fi sau mobilă. Pentru conexiunea la internet mobilă se recomandă viteză 3G datorită traficului de date destul de intens din cauza căruia o conexiune mai lentă ar înrăutăți experința de utilizare. De asemenea, tot pentru conexiunea mobilă este recomandat un plan pentru traficul de date din partea furnizorului de telefonie și date pentru a evita costul ridicat al facturii.

4.2. Proiectarea aplicației software

4.2.1. Design arhitectural

4.2.1.1. Modelul arhitectural general

Figura 4.1. Modelul arhitectural general utilizat

Figura 4.1 prezintă modelul arhitectural general după care a fost gândită aplicația. Acesta are

următoarele elemente constituente:

 MicroDataBase: reprezentat în cadrul aplicației curente de bazele de date SQLite.

 Persistent Data Access: reprezentat de adaptorii care facilitează accesul la bazele de

date locale.

 Network Data Access: reprezintă nivelul prin care se realizează accesul la datele

preluate de pe internet prin intermediul serviciilor web.

 Business Logic: reprezintă partea de algoritmi, calcule și procesări de date a aplicației.

 Managers, Coordinators: pentru aplicația curentă, reprezintă o serie de clase care sunt

folosite pentru a prelua, stoca și pasa date între componentele aplicației.

 User Interface: nivelul prin intermediul căruia se realizează interacțiunea dintre aplicație și utilizator. Cu alte cuvinte reprezintă ceea ce vede utilizatorul pe ecranul terminalului mobil.

 Data Structures: reprezintă modalitatea de a stoca și structura datele, în cadrul aplicației, pentru a eficientiza utilizarea lor.

4.2.1.2. Diagrama arhitecturală generală a aplicației

Figura 4.2. Diagrama arhitecturală generală a aplicației

În figura 4.2 este prezentată diagrama arhitecturală generală a aplicației. Așadar, pe scurt, aplicația are următoarele elemente constituente:

 Managerul general

 Interfața cu utilizatorul care comunică cu manager-ul prin intermediul unui controller

 Google Maps și Overlay-urile folosite pentru anotații sau amplasarea diferitelor obiecte pe hartă

 Modulul care furnizează pe baza serviciilor web indicații pentru ghidarea în spațiu

 NotePad-ul

 Modulul pentru sinteza de voce

 TaskManager-ul

 Adaptoarele pentru bazele de date locale prin intermediul cărora se facilitează operațiunile de citire/scriere.

 Modulul pentru prognoza METEO

 Bazele de date locale folosite pentru stocarea persistentă a datelor

 Serviciile web prin intermediul cărora se preiau de pe serverul Google informații despre starea vremii, indicațiile pentru ghidare și, de asemenea, pozele pentru punctele de interes.

4.2.2. Descrierea componentelor

După cum rezultă din paragraful anterior, aplicația este constituită din mai multe module care,

prin relațiile dintre ele, constituie un angrenaj cu ajutorul căruia programul este funcțional.

Managerul general: este un grup de clase proiectate pe baza patter-ului Singleton care sunt folosite pentru preluarea, stocarea și transmiterea datelor între componentele aplicației cu ajutorul unor metode getter și setter. Există o clasă manager pentru fiecare componentă a aplicației, astfel:

 DirectionsManager: folosit pentru preluarea și transmiterea modului de ghidare (driving

sau walking).

 ItineraryManager: folosit pentru preluarea și pasarea listei cu punctele de interes care alcătuiesc itinerarul, a modului în care se dorește parcurgerea acestuia (driving sau walking).

 ServicesListManager: folosit pentru a prelua și a transmite în aplicație listele care conțin informații referitoare la punctele de interes: lista de nume, lista de adrese, lista cu numerele de telefon, etc.

 TextToSpeechManager: folosit pentru preluarea și pasarea instanței TextToSpeech acolo unde este folosită aceasă facilitate.

 WeatherCurrentConditionManager: folosit pentru preluarea și transmiterea parametrilor referitori la condițiile curente.

 WeatherForecastConditionManager: folosit pentru preluarea și transmiterea parametrilor referitori la prognoza pe următoarele trei zile.

Interfața cu utilizatorul: reprezintă ceea ce îi este afișat utilizatorului pe ecranul terminalului mobil pentru a putea interacționa cu aplicația. Pentru constuirea interfeței cu utilizatorul a fost aleasă metoda folosirii codului XML (vezi capitolul 3.4, Interfața cu utilizatorul) care este parsat în cadrul aplicației prin intermediul metodelor corespunzătoare care constituie controller-ul pentru interfața cu utilizatorul.

Google Maps și MapOverlays: se referă la partea aplicației care se ocupă de tot ceea ce

înseamnă lucrul cu hărți, inclusiv geolocalizare cu ajutorul modululi GPS.

Modulul pentru informații privind ghidarea în spațiu: este o parte importantă a aplicației, deoarece cu ajutorul acestui modul, prin intermediul serviciilor web, se realizează parsare de cod KML de pe serverul Google pentru a desena ruta și a oferi informații adiționale referitoare la deplasarea către o destinație.

NotePad-ul: se referă la acea parte a aplicației care se ocupă de adăugarea de note pentru

punctele de interes.

TaskManager-ul: idem cu modulul anterior, doar ca se ocupă cu gestionarea task-urilor.

Adaptoarele pentru bazele de date locale: reprezintă o serie de clase special constuite pentru a facilita foarte mult lucrul cu bazele de date locale. În acest sens au fost definite metode pentru crearea, deschiderea, închiderea, citirea, scrierea, modificarea și ștergerea înregistrărilor din bazele de date locale.

Bazele de date locale: sunt metoda preferată în această aplicație pentru stocarea persistentă a

datelor. Există două astfel de baze de date cu o multitudine de tabele, astfel: prima bază de date

poate fi accesată doar în modul citire și conține tabele cu informații din diverse categorii de obiective. A doua bază de date poate fi și scrisă și citită și se ocupă cu stocare datelor referitoare la note, itinerar și task-uri.

Serviciile web: sunt modalitatea prin care se preiau date de pe serverele Google referitoare la starea vremii (condițiile curente, cât și prognoza pe trei zile), referitoare la indicațiile privind ghidarea în spațiu și, de pe un server propriu, pozele care sunt parte integrantă a ecranului cu informații detaliate pentru punctele de interes.

4.2.3. Descrierea interfeței cu utilizatorul

Succesul oricărei aplicații este indubitabil determinat într-o foarte mare măsură de modul în care este construită interfața cu utilizatorul. Aceasta trebuie să fie cât mai simplă și intuitivă, dar atragătoare în același timp.

Pentru aplicația ce constituie tema acestui proiect s-a incercat pe cât posibil respectarea acestor principii după cum se vede în capturile de ecran de mai jos. Interfața cu utilizatorul a fost construită folosind ambele metode prezentate în cadrul capitolului dedicat fundamentării teoretice: cod XML și cod Java. Partea de cod java a fost detaliată în descrierea claselor din cadrul modulului pentru prognoza METEO și în descrierea clasei POIDetailActivity. Modul de construire al interfețelor prin intermediul codului XML a fost detaliată în cadrul teoretic și nu se va mai reveni asupra sa aici.

Figura 4.3. Ecranul de pornire Figura 4.4. Meniul principal

În figura 4.3 este prezentat ecranul afișat utilizatorului la pornirea aplicației. După cum sugerează și îndemnul din cadrul său, prin atingerea ecranului va fi rulată activitatea care este afișată în figura 4.4. Interfața cu utilizatorul din acest ecran este constituită de trei tab-uri în care sunt conținute, pe rând, harta, lista de obiective și lista parametrilor locației curente. Aici este vizibilă doar harta, celălalte două perspective fiind prezentate în cele ce urmează. În ecran mai este prezentat, de asemenea, meniul principal al aplicației care este activat prin apăsarea tastei Menu.

Figura 4.5. Submeniul care permite alegerea categoriei de POI

Figura 4.6. Vizualizarea, pe

hartă, a poziției curente (culoarea turcoaz) și a punctelor de interes (culoarea roșie).

În figura 4.5. este prezentat submeniul care este afișat în urma alegerii opțiunii Find Places din meniul principal al aplicației. Acesta conține diverse categorii de puncte de interes care pot fi afișate pe hartă sau sub formă de listă. Afișarea punctelor de interes pe hartă alături de poziția curentă a utilizatorului este arătată în figura 4.6. Astfel, bulina de culoare turcoaz indică unde se situează terminalul mobil în spațiu, în timp ce bulinele roșii indică punctele de interes din categoriile selectate. La atingerea uneia din bulinele roșii, după cum se poate observa și în captura de ecran, este afișat un balon care conține drept informații numele punctului de interes, precum și adresa acestuia. În colțul din dreapta jos al ecranului se poate observa butonul de activare/dezactivare a sunetelor.

Figura 4.7. Vizualizarea punctelor

de interes sub formă de listă

Figura 4.8. Parametri locației curente

Celălalte două perspective conținute de tab-uri sunt prezentate în figurile 4.7, respectiv 4.8. Astfel, in tab-ul List View sunt afișate obiectivele localizate sub formă de listă. Prin selectarea unei înregistrări din această listă, tab-ul va fi comutat la cel care conține harta, iar aceasta din urmă va fi centrată pe punctul de interes.

În tab-ul Where Am I? sunt afișați sub formă de listă parametri locației curente. Astfel, aici putem vizualiza latitudinea, longitudinea, adresa obținută prin procesul de geocodare inversă din perechea latitudine/longitudine, altitudinea și viteza de deplasare.

Figura 4.9. Ecranul cu detalii pentru punctul de interes selectat

Figura 4.10. Ecranul cu indicatii privind atingerea destinației dorite

La atingerea balonului unei punct de interes este afișat un ecran care conține o serie de informații detaliate. Astfel, în figura 4.9 se poate observa în partea superioară o galerie de imagini și, imediat sub aceasta, imaginea selectată în mod curent din galerie. De asemenea, sub imagine, este afișat numele, adresa și o listă de facilități pentru obiectiv. Aceste informații nu sunt vizibile în imagine datorită afișării meniului de opțiuni pentru obiectivul curent.

În urma selectării din acest meniu a opțiunii Directions, se deschide un submeniu din care se poate alege modul de deplasare, cu autovehicul sau pedestru, iar prin alegerea unui mod este rulată activitatea prezentată în figura 4.10. În această activitate este trasată în permanență ruta către destinație, este marcată poziția terminalului mobil, iar în partea superioară se oferă informații asupra adresei curente, a adresei de destinație, a timpului și distanței care a mai rămas de parcurs, precum și asupra vitezei de deplasare.

Figura 4.11. Adăugarea unei note

pentru punctul de interes dorit

Figura 4.12. Adăugarea unui task

pentru punctul de interes dorit

Pentru un anumit punct de interes se pot adăuga note și task-uri lucru prezentat în figurile 4.11 și

4.12. Astfel, pentru partea de note, se poate salva un titlu și o descriere, iar pentru partea de task- uri, pe lângă acestea, se poate seta data și ora la care expiră task-ul, precum și dacă se dorește alertarea utilizatorului chiar dacă task-ul a expirat.

Figura 4.13. Vizualizarea tuturor notelor

Figura 4.14. Vizualizarea tuturor

task-urilor

Din meniul principal al aplicației pot fi alese spre a fi vizualizate toate notele sau toate task-urile adăugate până în acel moment (figura 4.13 și 4.14). În figura 4.13 este prezentată lista tuturor notelor. În listă fiecare notă este identificată prin titlul ei, punctul de interes pentru care a fost adăugată și data și ora la care s-a facut salvarea. Același principiu stă și la baza listei de task-uri, doar că în acest caz este afișată data și ora la care expiră task-ul.

Figura 4.15. Ecranul cu informații

despre starea vremii

4.2.4. Diagrame UML pentru clase

O altă opțiune care se poate alege din cadrul meniului principal este aceea de a afișa informții despre starea vremii. Ecranul din figura 4.15 prezintă acest lucru. Astfel, se poate observa că pentru condițiile curente este afișată o pictogramă, temperatura curentă în grade celsius, descrierea condițiiloe curente, viteza vântului în km/h și umiditatea.

Pentru prognoza pe următoarele trei zile este afișată o pictogramă sugestivă pentru condițiile estimate, ziua pentru care este valabilă prognoza, temperatura minimă, respectiv maximă estimată și o descriere a condițiilor estimate.

În acest paragraf va fi menționat felul în care arată diagramele UML pentru clasele aplicației. Datorită numărului mare de clase, diagramele au fost construite doar pentru clasele care conțin activități deoarece în cadrul lor sunt apelate metode și din celălalte clase. Relațiile reprezentate pe diagramele UML sunt de asociere, moștenire și dependență.

MyMapActivity.java

Figura 4.16. Diagrama UML pentru clasa MyMapActivity

DirectionsActivity.java

Figura 4.17. Diagrama UML pentru clasa DirectionsActivity

NotePad.java

Figura 4.18. Diagrama UML pentru clasa NotePad

ItineraryDirections.java

Figura 4.19. Diagrama UML pentru clasa ItineraryDirectionsActivity

ItineraryList.java

Figura 4.20. Diagrama UML pentru clasa ItineraryListActivity

NoteEdit.java

Figura 4.21. Diagrama UML pentru clasa NoteEdit

NoteView.java

Figura 4.22. Diagrama UML pentru clasa NoteView

TaskManagerActivity.java

Figura 4.23. Diagrama UML pentru clasa TaskManagerActivity

TaskManagerListActivity.java

Figura 4.24. Diagrama UML pentru clasa TaskManagerListActivity

POIDetailActivity.java

Figura 4.25. Diagrama UML pentru clasa POIDetailActivity

4.2.5. Implementarea în cod

În acest subcapitol accentul va fi pus pe modul în care au fost implementate și integrate componentele în aplicație. Codul sursă, clasele, metodele și comenzile vor fi explicate în detaliu. De notat că în cazul în care se fac referiri la funcții, ele vor fi ilustrate doar prin prototip, forma de implementare a întregii metode (prototip + corp) putând fi consultată în cadrul anexelor.

4.2.5.1. Implementarea modulului Manager general

Conform diagramei arhitecturale generale prezentată în paragraful 4.3.1.2, partea centrală a aplicației este constituită de managerul general care are rolul de a prelua, stoca și transmite date de la și către componentele aplicației. Managerul este constituit de o serie de clase bazate pe patter-ul Singleton care conțin o suită de metode getter și setter.

DrivingManager.java

Implementarea acestei clase este destul de simplă. Este constituită dintr-un constructor privat

fără parametri – DrivingManager() – și, de asemenea, metoda statică getInstance() folosită

DrivingManager care ia valoarea new DrivingManager().

Constructorul clasei DrivingManager:

private DrivingManager() {

}

Metoda getInstance():

public static DrivingManager getInstance() {

if (instance == null) {

instance = new DrivingManager();

}

return instance;

}

Această clasă are menirea de a prelua și pasa modul în care se dorește a fi primite indicațiile pentru deplasarea în spațiu. În acest scop ea mai conține două metode getter și setter care îndeplinesc această funcție. Metoda setMode() este de tipul void și primește ca parametru modul de deplasare care este o variabilă de tip String. În consecință, metoda getMode() are tipul returnat String și returnează modul de deplasare.

ItineraryManager.java

Această clasă care face parte din pachetul Manager se ocupă cu transmiterea datelor necesare funcționării modulului itinerar. La fel ca toate clasele din acest pachet este bazată pe pattern-ul singleton având un constructor privat fără parametri ItineraryManager() și o metodă statică getInstance() având rolul de a creea o instanță a clasei. În acest sens, variabila statică instance ia, asemănător cazului anterior, valoarea new ItineraryManager().

Clasa descrisă are rolul de a facilita funcționarea modulului care se ocupă de parcurgerea Itinerariului. În acest sens, ea deține o serie de metode getter și setter. Astfel, avem metoda setItineraryList() care primește ca paramteru o listă de obiecte de tipul Location care conțin coordonatele punctelor de interes ce descriu itinerariul.Mai departe, metoda setMode() primește ca parametru o variabilă de tip String și este folosită pentru a seta modul în care se dorește parcurgerea itinerariului. Nu în ultimul rând avem metoda setItineraryNameList() al cărei parametru constituit de o listă de obiecte String se referă la numele punctelor de interes.

Pentru preluarea în aplicație a elementelor setate prin metodetele setter, în această clasă mai avem o serie de metode getter după cum urmează: getItineraryList(), getMode() și getItineraryNameList() al căror tip returnat sunt o listă de obiecte de tip Location, o variabilă de tip String și o lista de obiecte de tip String.

ServicesListManager.java

Din cadrul pachetului manager această clasă este de departea cea mai stufoasă. Ea are rolul de a prelua o serie de liste care conțin informații referitoare la punctele de interes, de a le stoca temporar și de a le pasa în aplicație în funcție de nevoi. Și clasa curentă merge pe aceeași direcție: un constructor privat ServicesListManager(), o metodă statică getInstance() care returnează variabila statica de tip ServicesListManager ce ia valoarea new ServicesListManager() și metodele getter și setter.

Referitor la metodele setter se poate spune că există 12 astfel de metode. Ele au rolul de a permite setarea listelor cu numele punctelor de interes dintr-o anumită categorie, setarea numerelor de telefon, a paginii web, a facilităților, precum și a trei URL-uri care sunt folosite pentru preluarea unor poze. Tot prin metode de tip setter se setează lista de indecși a obiectivelor dintr-o anumită categorie, precum și index-ul obiectivului care a fost ales.

TextToSpeechManager.java

Aceasta este o clasă care merge tot pe pattern-ul Singleton cu un constructor privat TextToSpeechManager() și o metodă statică getInstance() care retunează o variabilă statică de tipul TextToSpeechManager ce are valoarea new TextToSpeechManager().

Scopul clasei, după cum îi spune și numele este de a prelua și pasa instanța pentru partea de sinteză de voce acolo unde s-a dorit implementarea acestei faciliăți. În mod natural, pentru atingerea acestui țel clasa are o metodă setter – setTTS() – pentru setarea instanței TTS printr- un paramteru de tip TextToSpeech transmis funcției și o metodă getter – getTTS() – pentru preluarea instanței TTS acolo unde este nevoie. Metoda getTTS() returnează un obiect de tip TextToSpeech.

WeatherCurrentConditionManager.java

Spre deosebire de celălalte clase din pachetul Manager, aceasta nu se bazează pe pattern-ul Singelton. Ea are un constructor public fără parametri folost pentru instanțierea clasei și metodele tipice getter și setter foloste pentru a seta și prelua parametri care descriu condițiile curente.

WeatherForecastConditionManager.java

Clasa la care se face referire funcționează pe același principiu ca cea dinainte, doar că getionează

parametri care descriu prognoza pe trei zile.

4.2.5.2. Implementarea modulului referitor la hărți și geolocalizare

Prin natura ei, aplicația folosește la maxim potențialul SDK-ului de Android de a oferi suport pentru lucrul cu hărți (Google Maps) și pentru toată gama de operațiuni care se referă la geolocalizare. Acest paragraf se va focaliza asupra modului în care a fost implementată această parte, precum și asupra modului în care conlucrează părțile componente ale modulului în funcționarea aplicației.

Modulul referitor la hărți și geolocalizare este implementat în pachetul dat prin calea canonică com.google.tourguide.gmaps (cu clasele componente MyMapActivity.java, MyMapView.java și GPSStatus.java) și subpachetul acestuia com.google.tourguide.gmaps.overlay (cu clasele componente CurrentLocationOverlay.java,MapItemizedOverlay.java,BaloonItemizedOverlay.java BaloonOverlayView.java și RouteOverlay.java). În cele ce urmează va fi facută o descriere al modului de implementare pentru fiecare clasă cu mențiunea că clasele BaloonItemizedOverlay.java, BaloonOverlayView.java au fost integrate din referința bibliografică [18].

MyMapActivity.java

Această clasă poate fi privită ca fiind clasa centrală a modulului deoarece în cadrul ei se efectuează majoritatea operațiilor ce au loc pe hartă și nu numai. Pentru construirea clasei a fost derivată clasa de bază MapActivity (vezi capitolul 3.6) și implementate interfețele OnTabChangeListener, OnClickListener și OnInitListener.

Deoarece extinde o clasă care se referă la activități, clasa MyMapActivity este necesar să implementeze și să suprascrie metoda onCreate() (vezi subcapitolul 3.3.5) în care se execută diverse operațiuni la prima pornire a activității.

@Override

public void onCreate(Bundle savedInstanceState) {

…………………………………..

//se verifica daca GPS-ul telefonului este activat

if (!gpsStatus.isGPSenabled()) {

//daca nu este activat, se genereaza un alert modal gpsStatus.buildAlertMessageNoGps();

}

//initializare tab-uri

initTabHost();

//initializare harta initMapView();

//initializare lista initListView();

//initializare ecran cu parametri pozitiei curente

initWhereAmIView();

//initializarea locatiei curente

initCurrentLocationOverlay();

//initializare TTS si vibrator

initAlerts();

//adaugare view-uri in taburile corespunzatoare addViews();

}

Așadar, la pornirea activității se setează layout-ul care va fi folosit pentru interfața grafică în activitatea curentă. Mai multe referiri la acesta vor fi facute în paragraful care tratează modul de implementarea al interfeței grafice. Tot la prima rulare a activității se aplează funcția isGPSenabled() din clasa GPSStatus.java (vezi paragraful curent) pentru a verifica dacă modulul GPS al telefonului este activat. Daca modulul este dezactivat, prin apelarea metodei buildAlertMessageNoGps(), se generază un alert modal care permite interacțiunea cu utilizatorul și din care acesta poate specifica dacă dorește sau nu activarea GPS-ului la momentul respectiv. Mai departe, se apelează funcțiile de inițializare a suportului care va gazdui tab-urile, de inițializare a view-urilor hărții, listei și ecranului pe care vor fi afișați parametri referitori lapoziția curentă și, de asemenea, se inițializează instanța pentru funcția de sinteză de voce și vibratorul. În final, se adaugă view-urile inițializate anterior în tab-urile corespunzătoare.

Tot din ciclul de viață al activității face parte și metoda onResume() care se apelează atunci când activitatea revine în prim-plan (vezi subcapitolul 3.3.5). În această metodă operațiunile care se execută sunt de a reactiva primirea actualizărilor de la GPS-ul telefonului, precum și de la senzorul busolei. Aceste actualizări sunt necesare în cadrul procesului de determinare și afișare a poziției curente.

@Override

public void onResume() {

super.onResume();

//activarea updateurilor pentru determinarea pozitiei curente currentLocationOverlay.enableMyLocation();

//activarea actualizarilor de la senzorul busolei

currentLocationOverlay.enableCompass();

}

În metoda onPause() se definesc operațiuni care se execută atunci când activitatea intră în pauză (vezi ciclul de viață al activității în capitolul 3.3.5). În cazul de față, în această metodă sunt dezactivate actualizările de la GPS și senzorul busolei din cauză că acestea nu sunt necesare atunci când activitatea nu se află în prim-plan și ar consuma resursele telefonului nejustificat. Dezactivările se execută prin apelarea metodelor disableMyLocation() și disableCompass() din clasa nativă MyLocationOverlay.

Pe lângă metodele care se referă la ciclul de viață al activității, în această clasă există și funcții în care se inițializeatză diverse elemente care țin de hartă, listă, tab-uri, etc.

Pentru integrarea hărții în cadrul aplicației se apelează metoda initMapView() la pornirea

activității.

private void initMapView() {

//incarcarea hartii din directorul de resurse mapView = (MapView) findViewById(R.id.mapview);

//activarea butoanelor care controleaza operatiunea de zoom mapView.setBuiltInZoomControls(true);

//setarea modului implicit de vizualizare la "StreetView" mapView.setSatellite(false);

//crearea obiectului pentru controlul hartii (focalizare, zoom)

mapController = mapView.getController();

//setarea valorii implicite a zoom-ului la 15

mapController.setZoom(15);

}

În metoda initCurrentLocationOverlay() se execută liniile de cod cu ajutorul cărora aplicația poate realiza operațiuni asupra determinării poziționării spațiale.

private void initCurrentLocationOverlay() {

//obtinerea unei instante a clasei CurrentLocationOverlay currentLocationOverlay = new CurrentLocationOverlay(this, mapView,

whereAmIView);

//activarea actualizarilor de la modulul GPS

currentLocationOverlay.enableMyLocation();

//activarea actualizarilor de la senzorul busolei currentLocationOverlay.enableCompass();

//marcarea pozitiei curente pe harta

mapView.getOverlays().add(currentLocationOverlay);

//planificarea unui fir de executie pentru centrarea

hartii la pozitia curenta currentLocationOverlay.runOnFirstFix(new Runnable() {

public void run() {

mapController.animateTo(currentLocationOverlay.getMyLocation());

}

});

}

Așadar, prima dată se obține o instanța a clasei CurrentLocationOverlay în care sunt implementate o serie de metode pentru geolocalizare. După cum se poate observa, parametri constructorulului sunt contextul curent, mapView care se referă la harta încărcată în aplicație din cod XML și whereAmIView. Mai multe detalii despre parametri pe care îi primește acest constructor, în paragrafele urmatoare. După obținerea instanței, aceasta este folosită pentru activarea actualizărilor de la GPS și senzorul busolei pe baza cărora se desenează poziția curentă a utilizatorului prin obținerea listei de overlay-uri și apelarea metodei add(). Animarea la poziția curentă se face de fiecare dată când se obține o nouă actualizare de la GPS prin apelarea metodei runOnFirstFix() care pornește un fir de execuție implementat prin interfața Runnable și în care se folosește funcția animateTo().

După desenarea pe hartă a poziției curente este necesara amplasarea markerilor care arata poziția punctelor de interes din categoria selectata. Acest scop este atins prin executarea funcției initMapItemizedOverlay().

private void initMapItemizedOverlay() {

//obtine lista de overlay-uri mapOverlays = mapView.getOverlays();

//creaza markerul care va marca fiecare punct pe harta

marker = this.getResources().getDrawable(R.drawable.marker);

marker.setBounds(0, 0, marker.getIntrinsicWidth(), marker.getIntrinsicHeight());

//creaza un nou obiect de tipul MapItemizedOverlay itemizedOverlay = new MapItemizedOverlay(marker, mapView);

//pentru fiecare punct din lista creaza un obiect de tip

OverlayItem

for (int i = 0; i < indexList.size(); i++) {

lat = latList.get(i); lon = lonList.get(i); name = nameList.get(i);

address = addressList.get(i);

POIs = new GeoPoint((int) (lat * 1E6), (int) (lon * 1E6));

overlayItem = new OverlayItem(POIs, name, address);

itemizedOverlay.addOverlay(overlayItem);

}

//arata toate markerele pe harta mapOverlays.add(itemizedOverlay);

}

Prima dată se obține lista de overlay-uri pentru harta curentă și se incarcă din directorul de resurse imaginea care va fi utilizată pentru a marca pe hartă poziția corespunzătoare punctelor de interes. Mai departe, se obține o instanță a clasei MapItemizedOverlay și se transmit ca parametri variabilele care se referă la marker și la harta curentă. În ciclul for se realizează operația propriu-zisă de creare a punctelor. Astfel, pentru fiecare punct de interes, se extrag din lista de latitudini, longitudini, nume și adrese elementele corespunzătoare, se crează un obiect de tipul GeoPoint pe baza perechii latitudine/longitudine și cu ajutorul acestuia, a numelui și adresei se obține un obiect de tipul OverlayItem. Fiecare obiect de acest tip este apoi adăugat într-o listă care este alipită la lista de overlayuri a hărții

Listele care conțin elementele la care s-a făcut referire anterior sunt obținute prin apelarea metodei getServicesList(). În această metodă, prin intermediul clasei corespunzătoare din manager, sunt încărcate în clasa curentă listele construite în clasa ServicesListBuilder la care se va face referire ulterior în această lucrare.

Deoarece obiectivele pot fi vizualizate și sub formă de listă este necesară includerea acesteia în același ecran cu harta. Acest lucru se realizează prin includerea fiecărui element într-o structură de tab-uri, comutarea între ele făcându-se prin alegerea tabului corespunzător. Astfel, în clasa curentă există și metoda initTabHost() prin a cărei apelare se încarcă structura de tab-uri din fișierul XML corespunzător și este setată pentru a găzdui view-urile și a reacționa la atingeri prin apelarea metodelor setup() și setOnTabChangedListener().

private void initTabHost() {

// incaraca tab-urile din fisierul de resurse

tabHost = (TabHost) findViewById(android.R.id.tabhost);

// pregateste tab-urile pentru adaugarea view-urilor

tabHost.setup();

// seteaza tab-urile pentru a raspunde la atingeri tabHost.setOnTabChangedListener(this);

}

Metoda addViews() este aplelată pentru adăugarea efectivă a view-urilor în tab-uri. În cadrul acestei metode este apelată funcția addView() pentru fiecare element care trebuie adăugat, argumentele acestei metode fiind denumirea care va aparea în cadrul tab-ului, imaginea pe care tab-ul o va conține, precum și view-ul de gazduit. Metoda setCurrentTab() specifică ce tab va fi activ în mod implicit, în acest caz parametrul cu valoarea 0 trasmis metodei arătând faptul ca harta va fi afișată la pornirea aplicației.

Metoda onTabChanged() ce primește ca argument numele tab-ului și este implementată din interfața OnTabChangeListener are rolul de a efectua diverse operații atunci când se detectează comutarea între tab-uri. În metoda curentă se comunică utiizatorului numele tabului în care s-a realizat comutarea și se ascund view-urile gazduite de celalalte tab-uri prin apelarea metodei setVisibility().

@Override

public void onTabChanged(String tabName) {

// verifica tabul in care s-a realizat comutarea

if (tabName.equals(MAP_TAB_TAG)) {

//verifica daca instanta TTS este valida

if (tts != null) {

// comunica numele tab-ului in care s-a comutat tts.speak(MAP_TAB_TAG, TextToSpeech.QUEUE_ADD, null);

}

// vibreaza pentru 250 ms vibrator.vibrate(250);

// ascunde view-urile necorespunzatoare tabului curent

mapView.setVisibility(View.VISIBLE); listView.setVisibility(View.INVISIBLE); whereAmIView.setVisibility(View.INVISIBLE); soundSwitchButton.setVisibility(View.VISIBLE);

}

……………………………………………………………..

}

Un al mod de vizualizare al obiectivelor localizate dintr-o anumită categorie este sub formă de listă. În acest sens este necesară încărcarea suportului pentru listă din directorul de resurse prin apelarea funcției initListView() și popularea acestuia prin apelul metodei populateListView(). În metoda amintită cel mai din urmă, legătura dintre partea de date și partea de view este făcută cu ajutorul unui adaptor prin apelarea metodei setAdapter(). În această funcție se obține o instanță a clasei ArrayAdapter argumentele constructorului acesteia fiind contextul curent, layout-ul listei și lista de nume din care se vor extrage datele afișate. De asemenea, lista este înregistrată pentru a fi detectate click-urile asupra elementelor din cadrul ei.

Astfel, atunci când se face click pe o înregistrare, se realizează comutarea la vizualizarea în modul hartă și aceasta este animată la obiectivul selectat. De asemenea, valoarea zoom-ului este setată la valoarea maximă.

private void populateListView() {

// introducerea datelor in lista cu ajutorul unui adaptor listView.setAdapter(new ArrayAdapter<String>(this,

android.R.layout.simple_list_item_1, nameList));

// inregistrarea listei pentru a raspunde la click-uri listView.setOnItemClickListener(new OnItemClickListener() {

@Override

public void onItemClick(AdapterView<?> a, View v, int

position, long id) {

// comunica numele punctului de interes selectat din

// lista tts.speak(nameList.get((int) id), TextToSpeech.QUEUE_ADD, null);

// comuta in modul de vizualizare harta tabHost.setCurrentTab(0);

// construieste obiectul de tip GeoPoint care descrie

// pozitia obiectivului selectat POILat = latList.get((int) id); POILon = lonList.get((int) id);

POI = new GeoPoint((int) (POILat * 1E6), (int) (POILon

* 1E6));

// seteaza valoarea zoom-ului hartii la valoarea

// maxima

mapController.setZoom(mapView.getMaxZoomLevel());

// animeaza harta la punctul de interes mapController.animateTo(POI);

}

});

}

MyMapView.java

Clasa nativă MapView este clasa din SDK-ul Android responsabilă, după cum îi spune și numele, cu elementele grafice ale hărții, precum și cu partea de control a acesteia. În proiectul de față acestă clasă a fost derivată obținându-se clasa MyMapView cu scopul de a adăuga unele funcționalități hărții, recte posibilitatea de a comuta între modul de vizualiare stradal și modul de vizualizare satelit prin executarea unei atingeri duble a ecranului telefonului. Partea esențială în implementarea acestei facilități este suprascrierea metodei onDoubleTap().

@Override

public boolean onDoubleTap(MotionEvent event) {

//obtinerea instantei pentru sinteza de voce

tts = TextToSpeechManager.getInstance().getTTS();

//verifica daca modul curent este satelit

if (isSatellite()) {

//daca modul curent este satelit, seteaza modul stradal setSatellite(false);

//vibreaza pentru o durata de 250 ms vibrator.vibrate(250);

//verifica daca instanta TTS este nula

if (tts != null) {

//daca nu este nula, comunica comutarea in modul stradal

tts.speak("Map view has been set to street view", TextToSpeech.QUEUE_ADD, null);

}

} else {

//daca modul curent este stradal, seteaza modul satelit setSatellite(true);

//vibreaza pentru o durata de 250 ms vibrator.vibrate(250);

//verifica daca instanta TTS este nula

if (tts != null) {

//daca nu este nula, comunica comutarea in modul satelit

tts.speak("Map view has been set to satellite view", TextToSpeech.QUEUE_ADD, null);

}

}

return true;

}

Metoda execută următoarele: la detectarea unei duble atingeri a ecranului telefonului, se obține prin intermediul clasei corespunzătoare din manager instanța pentru siteza de voce. După aceea, se verifică daca modul curent de vizualizare al hărții este satelit. Dacă modul este satelit, atunci se comută în modul stradal, telefonul va vibra pentru o durată 250 ms și va comunica faptul că s- a realizat schimbarea modului de vizaulizare în cel stradal. Altfel, dacă harta este deja în modul stradal, la dubla atingere a ecranului telefonului se va realiza comutarea în modul satelit și, la fel ca în cazul anterior, utilizatorul va fi înștiințat prin feedback auditiv și tactil. De menționat că pentru a putea suprascrie metoda onDoubleTapEvent() este necesară implementarea interfețelor OnGestureListener și OnDoubleTapListener.

GPSStatus.java

Clasa GPSStatus implementează o serie de metode care au rolul de a verifica dacă modulul GPS este activ și de a alerta utilizatorul în caz contrar. Acest lucru este necesar deoarece aplicația nu poate funcționa fără a primi actualizări de la GPS. Astfel, clasa conține trei metode: isEnabled() în care se verifică dacă GPS-ul este activ sau nu, buildAlertMessageNoGps() care construiește un alert modal pentru a-i permite utilizatorului să aleagă dacă dorește activarea sau nu a GPS-ului și launchGPSOptions() prin apelarea căreia se realizează comutarea la ecranul care permite setarea preferințelor pentru localizare.

Pe scurt, după cum am facut referire și la descrierea clasei MyMapActivity.java, la pornirea aplicației, în metoda onCreate() a clasei amintite, se verifică dacă GPS-ul este activ. Dacă nu, se generează un alert modal cu care utilizatorul poate interacționa și poate alege sau nu activarea GPS-ului. În caz de răspuns alternativ, GPS-ul se activează manual, după care se poate reveni la aplicație. Codul sursă pentru această clasă va fi prezentat în anexă pentru a evita ocuparea inutilă a spațiului curent.

Clasele la care am facut referire până în momentul de față în ceea ce privește lucrul cu hărțile fac parte din pachetul definit prin calea canonică com.google.tourguide.gmaps. Acest pachet conține la rândul să un subpachet în care sunt incluse o serie de clase care implementează funcții pentru lucrul cu overlay-uri. În paragrafele ce urmează se va face o trecere în revistă a claselor din pachetul com.google.tourguide.gmaps.overlay.

BalloonItemizedOverlay.java

Clasa BalloonItemizedOverlay a fost integrată în cadrul proiectului din referința bibliografică [18]. Ea este o extensie abstractă a clasei native ItemizedOverlay pentru a permite afișarea unui balon atunci când se detectează o atingere pe un marker care reprezintă un punct de interes.

Constructorul clasei primește ca parametri merkerul al cărui balon va fi afișat, precum și variabila care se referă la hartă, mapView. În paragraful de față vor fi descrise punctual metodele esențiale implementate în această clasă, codul sursă putând fiind consultat în anexe.

Pentru afișarea corectă a balonului deasupra marker-ului, clasa conține metodele setBalloonBottomOffset() și getBalloonBottomOffset() care permite setarea distanței ca număr de pixeli între marker și balon.

Metoda onTap() este apelată atunci când se detectează o atingere pe un marker. În această metodă se verifică dacă balonul este desenat în mod curent pentru marker. Dacă nu, se apelează metoda createBalloonOverlayView() prin intermediul căreia se crează suportul pentru afișarea balonului, partea grafică a acestuia fiind încărcată din directorul de resurse. Daca balonul este deja desenat, metodele de mai sus nu se mai execută. În continuare se aplează funcția hideOtherBalloons() pentru a ascunde celălalte baloane existente pe hartă și se setează un listener pentru a detecta atingerile ce se realizează asupra balonului cu metoda setBalloonTouchListener(). Funcțiile hideBalloon() și hideOtherBalloons() au rolul de a ascunde un singur balon sau toate baloanele simultan, dacă este cazul.

BalloonOverlayView.java

Această clasă este folosită pentru a creea balonul propriu-zis prin derivarea clasei native FrameLayout. Codul sursă pentru clasa curentă poate fi consultat în anexe. Important de menționat ar fi faptul că resursele pentru constuirea balonului sunt parsate din directorul de resurse din fișierele XML. De asemenea, tot din directorul de resurse se încarcă imaginile necesare pentru butonul de inchidere, cât și pentru background-ul balonului.

CurrentLocationOverlay.java

Clasa CurrentLocationOverlay este o derivare a clasei native MyLocationOverlay care este folosită pentru operațiuni privind geolocalizarea. Ea moștenește o serie de metode din clasa de bază și, pe lângă acestea, implementează metode proprii care vor fi descris în cele ce urmează.

Consturctorul clasei este unul cu parametri, aceștia fiind: contextul utilizat, harta utilizată referită prin variabila mapView și parametrul referitor la view-ul în care se va face afișarea informațiilor pentru poziția curentă.

Metodele moștenite din clasa de bază sunt: onLocationChanged(), onProviderEnabled() și

onProviderDisabled().

Metoda onLocationChanged(), după cum îi sugerează și numele, preia de la modulul GPS, pe

baza parametrului location, datele care descriu poziția curentă și modul de deplasare.

@Override

public void onLocationChanged(Location location) {

super.onLocationChanged(location);

//transmiterea locatiei, contextului si handlerului catre metoda care se ocupa de geocodare

getAddressFromLocation(location, context, handler);

//determinarea latitudinii

latitude = location.getLatitude();

//determinarea longitudinii

longitude = location.getLongitude();

//determinarea altitudinii

altitude = location.getAltitude();

//determinarea vitezei

speed = (float) (location.getSpeed() * 3.6);

}

Așadar, la fiecare actualizare de la modul GPS, metoda va apela funcția getAddressFromLocation() care realizează geocodarea inversă (obținerea adresei din perechea latitudine/longitudine – vezi capitolul 3.6). Mai departe, deoarece clasa MyLocationOverlay implementează interfața LocationListener, cu ajutorul metodelor implementate din aceasta se determină latitudinea, longitudinea, altitudinea și viteza curentă a terminalului mobil. Acest proces se execută, de asemenea, la fiecare actualizare primită de la GPS.

Metodele moștenite onProviderEnabled() și onProviderDisabled() permit executarea de operațiuni atunci când este detectată activarea, respectiv dezactivarea GPS-ului. În acest sens, în corpul acestor metode a fost folosită facilitatea de siteză de voce pentru a înștiința utilizatorul când s-a schimbat starea modulului GPS.

@Override

public void onProviderEnabled(String provider) {

//obtinerea instantei TextToSpeech

tts = TextToSpeechManager.getInstance().getTTS();

//verifica daca instanta obtinuta este nula

if (tts != null) {

//faca nu este nula, instiinteaza utilizatorul asupra activarii GPS-ului

tts.speak("GPS has been enabled", TextToSpeech.QUEUE_ADD,

null);

}

}

@Override

public void onProviderDisabled(String provider) {

//obtinerea instantei TextToSpeech

tts = TextToSpeechManager.getInstance().getTTS();

//verifica daca instanta obtinuta este nula

if (tts != null) {

//faca nu este nula, instiinteaza utilizatorul asupra dezactivarii GPS-ului

tts.speak("GPS has been disabled", TextToSpeech.QUEUE_ADD,

null);

}

}

Așa cum a fost menționat și în cadrul descrierii clasei MyMapActivity.java, una din facilitățile aplicației este de a afișa într-unul din tab-uri parametri referitori la poziția curentă. La acest subiect se va face referire mai târziu, în cadrul paragrafului care tratează implementarea interfeței cu utilizatorul. Totuși, deoarece subcapitolul curent se referă inclusiv la geocodare, este necesar să fie detaliat modul în care sunt obținuți acești parametri. Aspecte toeretice referitoare la geocodare pot fi consultate în capitolul 3.6. Conceptele prezentate acolo au fost implementate în cod în cadrul funcției getAddressFromLocation(). Deoarece este o operație care necesită resurse multe, aceasta se execută în thread separat deoarece în caz contrar va fi blocată interfața cu utilizatorul.

Pașii sunt următorii: se obține o instanță a clasei Geocoder și, prin apelarea funcției getFromLocation(), pe baza parametrului location care i se transmite funcției la fiecare actualizare de la GPS (vezi metoda onLocationChanged()), se determină o listă de adrese.

Variabila location, pe lângă alți parametri, este descrisă de perechea latitudine/longitudine. Pe baza acestei perechi se realizează conversia din latitudine/longitudine în adresa locației curente pe serverul Google. Deoarece serverul poate returna mai multe înregistrări care să corespundă unei variabile location, adresele sunt stocate într-o listă, prima înregistrare fiind cea mai relevantă și ea este cea care va fi reținută într-o variabilă de tip Address. Rezultatul va fi, prin apelarea metodelor corespunzătoare, un string care va conține acolo unde sunt disponibile aceste date adresa străzii, numele cartierului, numele localității, numele județului și numele țării. Folosind o variabilă de tip Bundle rezultatul este pasat către un handler care se ocupă de afișarea lui. Afișarea se realizează sub formă de listă, iar legătura dintre date și partea de view se face printr-un adaptor (vezi subcapitolul 3.4.6).

Metoda getAddressFromLocation()

private void getAddressFromLocation(final Location location,

final Context context, final Handler handler) {

//obtinerea unei instante anonime a clasei Thread

Thread thread = new Thread() {

@Override

public void run() {

//obtinerea instantei clasei Geocoder Geocoder geocoder = new Geocoder(context, Locale.ENGLISH);

String result = null;

try {

//obtinerea listei de adrese

List<Address> list = geocoder.getFromLocation(

location.getLatitude(),

location.getLongitude(), 1);

//verifica daca lista contine elemente

if (list != null && list.size() > 0) {

//daca are cel putin un element, retine prima inregistrare din lista

Address address = list.get(0);

//obtinerea adresei sub forma de string

result = address.getAddressLine(0) + ", "

+ address.getSubLocality() +

", "

+ address.getLocality() + ", "

+ address.getSubAdminArea() +

", "

+ address.getCountryName();

}

} catch (IOException e) {

} finally {

//crearea unei variabile de tip Message

Message msg = Message.obtain();

//setarea handlerului ca destinatar al mesajului

msg.setTarget(handler);

if (result != null) {

msg.what = 1;

//obtinerea unei instante a clasei Bundle

Bundle bundle = new Bundle();

//alipirea adresei la instanta obtinuta bundle.putString("address", result); msg.setData(bundle);

} else

msg.what = 0;

//transmiterea mesajului catre handler msg.sendToTarget();

}

}

};

thread.start();

}

Handler-ul care se o cup ă de afi șare

//obtinerea unei instante anonime din clasa Handler

private Handler handler = new Handler() {

@Override

public void handleMessage(Message message) {

String result;

//receptionarea mesajului trimis de functia de geocodare

switch (message.what) {

case 1:

Bundle bundle = message.getData(); result = bundle.getString("address"); break;

default:

result = null;

}

String newLine = System.getProperty("line.separator");

//adaugarea parametrilor referitori la locatia curenta in lista

whereAmIList.clear();

whereAmIList.add("Latitude: " + newLine + latitude); whereAmIList.add("Longitude: " + newLine + longitude); whereAmIList.add("Address: " + newLine + result); whereAmIList.add("Altitude: " + newLine + altitude + " m"); whereAmIList.add("Speed: " + newLine + speed + " km/h");

//legarea listei la view-ul corespunzator printr-un adaptor whereAmIView.setAdapter(new ArrayAdapter<String>(context,

android.R.layout.simple_list_item_1, whereAmIList));

}

};

MapItemizedOverlay.java

Clasa MapItemizedOverlay este o extensie a clasei BalloonItemizedOverlay cu ajutorul căreia sunt afișate markerele pe hartă împreună cu funcționalitatea de a arăta un balon pentru fiecare marker. Implementarea clasei constă dintr-un constructor care are ca parametri markerul care va fi afișat și harta corespunzătoare. Metoda addOverlay() facilitează adăugarea unui nou obiect de timp Overlay în lista de overlay-uri care va fi afișată pe hartă. De asmenea, există funcția removeAllItems() care permite ștergerea listei de overlay-uri și funcția size() care returnează numărul elementelor din listă. O metodă moștenită din clasa BalloonItemizedOverlay este onBalloonTap() în care se definește ceea ce trebuie să se execute atunci când este detectată o atingere pe suprafața unui balon. În cazul de față se pornește o activitate în care sunt afișate detalii despre obiectivul pe al cărui balon s-a detectat atingerea. De asemenea, se pasează către manager index-ul balonului pe baza căruia se va realiza extragerea elementelor corespunzătoare din liste pentru obiectivul curent. Codul pentru clasa MapItemizedOverlay poate fi consultat în anexe.

RouteOverlay.java

RouteOverlay este o clasă care extinde clasa de bază Overlay și, după cum ii sugerează și numele, este responsabilă pentru desenarea rutei de la poziția curentă la punctul de destinație. În

acest scop a fost suprascrisă metoda draw() din clasa de bază și a fost definită metoda

drawRoute().

4.2.5.3. Implementarea modulului care gestionează partea de Notepad

O facilitate a aplicației este aceea de a oferi posibilitatea adăugării de scurte note pentru fiecare punct de interes. Funcționalitatea aceasta este indeplinită prin clasele din pachetul com.google.tourguide.notes. Acestea iși impart sarcinile de la a afișa lista de note disponibilă într-un anumit moment – clasa NotePad – până la a afișa și a edita notele propriu- zise în mod singular (clasele NoteView și NoteEdit). Stocarea persistentă a notelor se face într- un tabel din baza de date locală ce funcționează în mod citiere-scriere. Totuși, referirile la modul de stocare nu vor fi făcute în acest subpunct, ci în paragraful dedicat stocării presistente. Ar mai trebui menționat faptul că implementarea acestui modul se bazează pe notepad tutorialul de la Google, care însă a suferit unele modificări [16].

NotePad.java

Clasa NotePad poate fi privită ca fiind clasa centrală a modulului care se ocupă de partea de note a aplicației. Ea este derivată din clasa ListActivity, deci va avea obligatoriu metoda onCreate(). În această metodă, la prima rulare a activității, se setează layout-ul pentru interfața cu utilizatorul (încărcat prin parsare de cod XML din directorul de resurse), se obține o instanță a adaptorului pentru accesul la baza de date locală în modul citire-scriere și se deschide conexiunea la baza de date. De asemenea, se apelează metoda fillData() care are rolul de a popula lista cu notele extrase din baza de date.

@Override

public void onCreate(Bundle savedInstanceState) {

…………………………………..

//obtinerea instantei adaptorului pentru accesul la baza de date notesDbHelper = new NotesDbAdapter(this);

//desciderea conexiunii la baza de date notesDbHelper.open();

//popularea listei cu titlurile notelor extrase din baza de date fillData();

}

Metoda fillData() este cea responsabilă de popularea listei cu titlurile notelor existente în baza de date. În această metodă, folosind instanța obținută în metoda onCreate(), se apelează funcția fetchAllNotes() din clasa NotesDbAdapter. Această funcție are rolul de a extrage toate notele din baza de date și de a le reține într-o variabilă de tip Cursor. Legătura dintre datele extrase din baza de date și reținute în cursor și listă se face cu ajutorul unui SimpleCursorAdapter (vezi subcapitolul 3.4.6). Câmpul Title și obiectul de tip TextView care definește un rând din listă sunt legate prin definirea a doi vectori: unul care conține coloanele bazei de date din care se extrag datele – referite prin constantele KEY_TITLE, KEY_TIMEDATE și KEY_POI – și altul care conține referințe către view-urile în care vor fi afișate datele extrase din fiecare cloană. Metoda startManagingCursor() are rolul de a gestiona ciclul de viață al obiectului de tip Cursor. Urmează apoi instanțierea clasei native SimpleCursorAdapter căreia îi pasăm contextul curent, view-ul în care va fi afișat fiecare înregistrare, precum și cei doi vectori despre care s-a facut referire anterior. Și în această clasă s-au utilizat funcționalitățile oferite platforma Android privind sinteza de voce și asupra cărora se va reveni în subcapitolul următor.

private void fillData() {

………………………………………………..

//crearea obiectului de tip cursor in care sunt retinute inregistrarile bazei de date

Cursor notesCursor = notesDbHelper.fetchAllNotes();

//apelarea functiei care permite aplicatiei sa gestioneze cursorul

startManagingCursor(notesCursor);

………………………………………………..

//crearea vectorului care specifica coloanele bazei de date din care vor fi extrase datele

String[] from = new String[] { NotesDbAdapter.KEY_TITLE,

NotesDbAdapter.KEY_TIMEDATE, NotesDbAdapter.KEY_POI };

//crearea vectorului care specifica view-urile in care vor fi

afisate datele extrase anterior

int[] to = new int[] { R.id.notes_row_title, R.id.notes_row_name,

R.id.notes_row_date };

//obtinerea unei instante a clasei SimpleCursorAdapter

SimpleCursorAdapter notes = new SimpleCursorAdapter(this,

R.layout.notes_row, notesCursor, from, to);

//setarea adaptorului setListAdapter(notes);

………………………………………………..

}

Astfel, în listă vor fi afișat, pe coloane, titlul fiecărei note împreună cu numele punctului de interes și data și ora la care a fost adăugată.

Prin intermediul unui meniu de opțiuni este disponibilă operațiunea de ștergere a tuturor notelor din listă. Aceasta se realizează prin apelarea metodei deleteAllNotes(). De asemenea, atingerea unui rând din listă este gestionată de metoda onListItemClick() prin executarea unui intent care pornește activitatea ce permite vizualizarea notei corespunzătoare. Pentru a știi ce înregistrări să se extragă din baza de date, acestui intent îi este atașată o instanță a clasei Bundle care conține indexul rândului selectat.

NoteView.java

Clasa NoteView permite vizualizarea conținutului fiecărei note atunci când acesta este selectată din listă în cadrul activității din clasa NotePad. Deoarece și clasa NoteView este la rândul ei derivată din clasa nativă Activity, conține metoda onCreate(). Astfel, la prima rulare a activității, în această funcție se obține instanța adaptorului la baza de date și se deschide conexiunea la baza de date. De asemenea, pentru a determina care notă trebuie extrasă din baza de date, se preaia printr-un bundle index-ul rândului din listă care a fost selectat. După toate aceste operațiuni se execută metoda populateFields() care are rolul de a afișa în câmpurile corespunzătoare din interfața cu utilizatorul textul referitor la titlul, respectiv conținutul notei.

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

//obtinerea instantei adaptorului pentru baza de date notesDbHelper = new NotesDbAdapter(this);

//deschiderea conexiunii la baza de date notesDbHelper.open();

//setarea layoutului pentru activitatea curenta setContentView(R.layout.note_view); setTitle(R.string.view_note);

//incarcarea textview-ului pentru afisarea titlului notesTitleText = (TextView) findViewById(R.id.note_view_title);

//incarcarea textview-ului pentru afisarea continutului notesBodyText = (TextView) findViewById(R.id.note_view_body);

//preluarea indexului randului selectat din lista notesRowId = (savedInstanceState == null) ? null

: (Long) savedInstanceState

.getSerializable(NotesDbAdapter.KEY_ROWID);

if (notesRowId == null) {

Bundle extras = getIntent().getExtras(); notesRowId = extras != null ? extras.getLong(NotesDbAdapter.KEY_ROWID): null;

}

//popularea textview-urilor cu textul corespunzator

populateFields();

}

Metoda populateFields() este o metodă privată în care, cu ajutorul instanței obținută în metoda onCreate() se construiește un obiect de tip Cursor prin apelarea funcției fetchNote() din clasa NotesDbAdapter. În acest obiect vor fi reținute titlul și conținutul notei extrasă din baza de date pe baza indexului rândului descris anterior. Funcția startManagingCursor() permite sistemului de operare să gestioneze ciclul de viață al cursorului, aceasta nefiind responsabilitatea programatorului. Se setează apoi în cele două textview-uri textul corespunzător titlului și conținutului notei. De remarcat faptul că pentru a extrage din cursor titlul și descrierea notei se folosește funcția getColumnIndexOrThrow() care primește ca parametru constantele KEY_TITLE și KEY_BODY din clasa NotesDbAdapter.

private void populateFields() {

//verifica daca index-ul randului este valid

if (notesRowId != null) {

//stocheaza in variabila de tip Cursor continutul coloanelor corespunzatoare din baza de date

Cursor note = notesDbHelper.fetchNote(notesRowId);

//permite sistemului de operare sa gestioneze curosrul startManagingCursor(note);

//setarea in textview a titlului notei notesTitleText.setText(note.getString(note

.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE)));

//setarea in textview a continutului notei notesBodyText.setText(note.getString(note

.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY)));

}

}

Pentru acest ecran a fost construit un meniu de opțiuni din care utilizatorului îi este permis să șteargă nota pe care o vizualizează în mod curent sau să lanseze activitatea în care poate realiza editarea ei. Astfel, dacă se apasă butonul EditNote, cu ajutorul unui intent va fi rulată activitatea din clasa NoteEdit.java, utilizatorul va fi înștiințat asupra acestui fapt prin folsirea funcției de sinteză de voce, după care activitatea va fi încheiată prin apelarea metodei finish(). Inainte de acesta, intentul va primi un parametru extra și anume, index-ul rândului pentru care se dorește editarea notei. Dacă se dorește ștergerea notei, va fi apelată metoda deleteNote() din clasa NotesDbAdapter care primește ca parametru index-ul rândului al cărei notei se dorește ștergerea. Și aici utilizatorul va fi înștiințat prin funcția text-to-speech, după care activitatea va fi terminată. Ar mai trebui menționat că, după cum sugerează și numele, această clasă permite doar vizualizarea notelor, nu și editarea lor.

NoteEdit.java

Clasa NoteEdit este, la fel ca celălalte două clase din acest pachet, derivată din clasa de bază Activity și are drept scop editarea notelor de către utilizator. În metoda onCreate() mai întâi se obține o instanță a clasei adaptor NotesDbAdapter, după care această instanță este folosită pentru apelarea metodei open() pentru deschiderea conexiunii la baza de date. De asemenea, se setază layout-ul care vi fi utilizat în activitatea curentă și se definesc, prin parsare de cod XML, cele două edit text-uri în care vor fi afișate titlul, respectiv conținutul notei de editat. De asemenea, la fel ca în cazul clasei NoteView, se primește index-ul rândului din listă al cărei note dorim să o edităm. În final, se apelează metoda populateFields() pentru afișarea efectivă a textului care va fi modificat și se înregistrează butonul de salvare a modificărilor pentru a răspunde la atingeri.

Metoda populateFields() nu va mai fi detaliată deoarece este identică cu cea din clasa NoteView, singura modificare apărând în faptul că cele două text view-uri devind edit text-uri, pentru a permite editarea textului, nu doar vizualizarea lui.

Metoda saveState() este cea cu ajutorul căreia se realizează scrierea sau modificarea unei înregistrări din baza de date. Deoarece o notă salvată este descrisă prin titlu, numele punctului de interes pentru care a fost salvată și data și ora la care a fost salvată, este necesară preluarea din manager a listei de nume pentru obiective, precum și a index-ului corespunzător punctului de interes pentru a cunoaște ce nume trebuie extras din listă. De asemenea, se obține o instanță a clasei Calendar pentru a determina ora la care a fost salvată nota pe baza orei sistemului de operare. Dacă nota nu există în baza de date, lucru sugerat prin valoarea nulă a variabilei care se referă la rândul din lista de note, ea va fi creată prin paelarea funcției createNote() din clasa NotesDbAdapter care primește ca paramteri titlul și conținutul notei, numele punctului de interes și data și ora salvării, toate ca șiruri de caractere. De aceea pentru unele dintre acestea a fost necesară apelarea metodei toString(). Dacă nota există deja, se apelează metoda updateNote() prin care se realizează modificările de rigoare asupra înregistrării din baza de date.

private void saveState() {

// obtinerea unei instante a clasei SimpleDateFormat pentru formatarea datei si orei

SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy

kk:mm:ss");

// preluarea din manager a listei de nume

nameList = ServicesListManager.getInstance().getNameList();

// preluarea din manager a indexului obiectivului index = ServicesListManager.getInstance().getIndex();

// conversia in siruri de caractere

String title = notesTitleText.getText().toString(); String body = notesBodyText.getText().toString(); String POI_name = nameList.get(index).toString();

String time = sdf.format(Calendar.getInstance().getTime());

//se verifica daca indexul randului este valid

if (notesRowId == null) {

//daca este null, inseamna ca nota nu exista si va fi creata long id = notesDbHelper.createNote(title, body, POI_name, time);

if (id > 0) {

notesRowId = id;

}

} else {

//daca nota exista, ea va fi modificata

notesDbHelper.updateNote(notesRowId, title, body, POI_name, time);

}

}

4.2.5.4. Implementarea funcțiior de sinteză de voce (text-to-speech) și vibrație

Începând de la versiunea 1.6 a sistemului de operare, platforma Android oferă librării pentru a include capabilități de sinteză de voce în cadrul aplicației. În linii foarte mari este necesare doar obținerea unei instanțe a clasei TextToSpeech și apelarea funcției speak() pentru a obține vorbire din text. În cele ce urmează va fi detaliat exact modul în care a fost inclusă în aplicație această funcție și ce pregătiri preliminare sunt necesare pentru a o putea folosi efectiv.

În primul rând trebuie menționat că aceasta este singura capabilitate din aplicație pentru care nu a fost implementată clasă separată. Motorul de sinteză de voce este inițializat în clasa MyMapActivity deoarece aceasta este prima activitate care se execută la pornirea aplicației. Deși majoritatea terminalelor mobile care rulează sistemul de opearare Android vin cu suport pentru sinteza de voce, la unele această capabilitate ar putea să lipsească în mod implicit datorită capacității de stocare limitată. De aceea la pornirea aplicației se verifică dacă sistemul de operare include librariile pentru text-to-speech și în caz de răspuns negativ se poate iniția descărcarea și instalarea lor printr-o conexiune de date. Pentru realizarea acestei verificări, în cadrul funcției initAlerts() se include următorul intent:

private void initAlerts() {

//obtinerea unei instante a clasei Intent

Intent checkIntent = new Intent();

//definirea operatiei pe care trebuie sa o execute intent-ul, in acest caz verificare existentei librariilor TTS checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);

//lansarea in executie a intentului startActivityForResult(checkIntent, MY_DATA_CHECK_CODE);

………………………………………………………..

}

Dacă parametrul ce reprezintă resultCode din metoda onActivityResult() are valoarea constantei CHECK_VOICE_DATA_PASS, se poate creea instanța pentru sinteza de voce deoarece librăriile sunt instalate. Dacă nu, acestea trebuiesc descărcate de pe internet lucru îndeplinit prin lansarea intent-ului ACTION_INSTALL_DATA_TTS. Instalarea se face automat deîndată ce fișierele au fost descărcate de către utilizator de pe Google Market.

@Override

protected void onActivityResult(int requestCode, int resultCode, Intent

data) {

//verifica valoarea pe care o primeste requestCode-ul

if (requestCode == MY_DATA_CHECK_CODE) {

//verifica daca librariile TTS sunt instalate

if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS)

{

//respuns afirmativ -> crearea instantei TTS

tts = new TextToSpeech(this, this);

//pasarea instantei TTS catre manager

TextToSpeechManager.getInstance().setTTS(tts);

} else {

// in cazul in care librariile lipsesc, executa

intentul pentru descarcarea si instalarea lor Intent installIntent = new Intent(); installIntent.setAction(TextToSpeech.Engine.ACTION_INS TALL_TTS_DATA);

startActivity(installIntent);

}

}

În metoda onInit() utilizatorul va fi înștiințat că motorul text-to-speech a fost inițializat cu succes. O particulariate a aplicației este aceea că sunetele pot fi activate, respectiv dezactivate. Acest lucru se realizează prin intermediul unui buton care este înregistrat pentru a răspunde la atingeri în metoda initAlerts(). Tot în această funcție se obține, cu ajutorul clasei Vibrator, accesul aplicatiei la funcția de vibrații a telefonului. Activarea și dezactivarea sunetelor este posibilă datorită metodei onClick() implementată din interfața OnClickListener. Astfel, atunci când este detectat un click asupra butonului, se verifică prima data dacă instanța TTS este validă ceea ce înseamnă că sunetele sunt activate. În acest caz, instanța TTS va lua valoarea null și va fi pasată către manager. În cazul în care instanța TTS are deja valoarea null, înseamnă că sunetele sunt dezactivate și va fi creată o nouă instanță validă a clasei TextToSpeech. De asemenea, în ambele cazuri se face schimbarea corespunzătoare a imaginii afișată pe buton și înștiințarea utilizatorului despre faptul că sunetele au fost activate sau dezactivate.

@Override

public synchronized void onClick(View v) {

//vibreaza pentru 250 ms la apasarea butonului vibrator.vibrate(250);

//verifica daca este apasat butonul pentru activarea si dezactivarea sunetelor

if (v.getId() == R.id.enable_disable_sounds) {

//verifica daca instanta TTS este valida

if (tts != null) {

//daca este valida, schimba fundalul butonului cu imaginea care arata ca sunetele sunt oprite

((Button)v).setBackgroundResource(R.drawable.sound_off);

//comunica utilizatorului ca suntele au fost

dezactivate

tts.speak("Sounds have been disabled", TextToSpeech.QUEUE_ADD, null);

//afiseaza un mesaj de tip toast pentru a instiinta asupra dezactivarii sunetelor

Toast.makeText(this, "Sounds have been disabled",

Toast.LENGTH_LONG).show();

//asociaza instantei TTS valoarea null tts = null;

//paseaza instanta TTS catre manager TextToSpeechManager.getInstance().setTTS(tts); return;

} else {

//daca instanta are deja valoarea null, schimba fundalul butonului cu imaginea care arata ca sunetele sunt pornite

((Button)v).setBackgroundResource(R.drawable.sound_on);

//obtine o instanta valida a clasei TTS

tts = new TextToSpeech(this, this);

//paseaza instanta catre manager

TextToSpeechManager.getInstance().setTTS(tts);

//afiseaza un mesaj de tip toast pentru a instiinta asupra activarii sunetelor

Toast.makeText(this, "Sounds have been enabled",

Toast.LENGTH_LONG).show();

return;

}

}

}

În cele ce urmează vor fi prezentate câteva secvențe de cod care exemplifică modul de utilizare a instanțelor din clasele TextToSpeech și Vibrator pentru alertarea utilizatorului.

Pentru a transforma textul în vorbire, modalitatea este foarte simplă: se apelează metoda speak() din clasa TextToSpeech care primește ca parametru esențial șirul de caractere ce constituie suportul vorbirii. De asemenea, pentru funcția de vibrație se folosește metoda vibrate() din clasa Vibrator al cărei parametru este durata pentru care telefonul trebuie să vibreze.

De exemplu, in clasa POIDetailScreen avem posibilitatea de a adăuga la itinerar printr-un meniu de opțiuni punctul de interes pentru care se face vizualizarea în mod curent. Acest lucru este comunicat și utilizatorului prin folosirea funcției de sinteză de voce:

//verifica daca instanta TTS este valida

if (tts != null) {

//daca este valida, comunica faptul ca punctul de interes a fost adaugat la itinerar

tts.speak(nameList.get(index) + "has been added to itinerary", TextToSpeech.QUEUE_ADD, null);

}

În secvența de cod de mai sus, se observă faptul că prima data se verifică daca instanta clasei TextToSpeech este validă. Acest lucru este necesar deoarece în cazul în care are valoarea null (situația în care sunetele sunt dezactivate), s-ar obține NullPointerException. Daca este validă, se comunică faptul că punctul de interes a fost adăugat la itinerar, obiectivul fiind referit prin numele extras din lista de nume. Dacă sunetele sunt dezactivate (instanța TextToSpeech este nulă, nu se comunică nimic deoarece nu este îndeplinită condiția pentru executarea blocului instrucțiunii if.

4.2.5.5. Implementarea părții de stocare persistentă a datelor

Stocarea datelor este la rândul ei o parte esențială a aplicației deoarece fără existența unui suport pe care să fie păstrate informațiile despre POI, notele, task-urile și punctele de interes preferate, majoritatea modulelor ar fi nefuncționale. Pentru stocarea datelor s-a ales metoda utilizării bazelor de date locale SQLite datorită suportului foarte bun pe care platforma Android îl oferă pentru lucrul cu aceste baze de date a flexibilității și resurselor consumate reduse.

Pentru stocarea datelor există două baze de date locale: una care poate fi doar citită și care conține o serie de tabele, câte unul pentru fiecare categorie de puncte de interes și în fiecare tabel avem coloanele care se referă la caracteristicile obiectivelor – nume, adresă, număr de telefon, pagină web, facilități și trei link-uri care conțin calea către serverul de unde vor fi descărcate imaginile ce sunt afișate în galerie. De menționat că acestă bază de date nu este creată la runtime, ci se gasește în sandbox-ul aplicației fiind construită manual cu programul SQLiteDatabaseBrowser. A doua bază de date este în modul citire/scriere și este creată de aplicație în momentul în care se introduce prima înregistrare într-unul din tabele. Acestă bază de date conține un număr de trei tabele (notes, tasks, itinerary), fiecare având coloanele corespunzătoare tipurilor de date pe care le conțin.

4.2.5.5.1. Adaptorii – modul de acces la bazele de date

Pentru a facilita lucrul cu bazele de date locale, aplicația are o serie de clase adaptor. Aceste clase implementează metode care permit crearea, deschiderea, citirea, scrierea și ștergerea bazelor de date și a înregistrărilor din tabelele acestora. Există doi adaptori pentru bazele de date, unul pentru accesul la baza de date care conține informații despre POI și altul pentru accesul la baza de date care conține înregistrările referitoare la note, intinerar și task-uri. De asemenea, tot în aceste rânduri va fi descrisă clasa în care sunt construite listele ce conțin datele extrase din baza de date referitoare la obiective.

ServicesDbAdapter.java

Adaptorul care este folosit pentru lucrul cu baza de date în modul citire (baza de date care conține datele pentru POI) este o clasă derivată din clasa nativă SQLiteOpenHelper. În pricipiu acest adaptor crează o bază de date goală în directorul corespunzător din sistemul de operare (/data/data/com.google.tourguide/databases/) după care copiază baza de date populată manual din sandbox-ul aplicației. Ca structură clasa are un constructor ServicesDbAdapter care primește parametrul reprezentând contextul aplicației în care este utilizat. Parametrii constructorului clasei de bază sunt contextul utilizat, numele bazei de date specificat prin cosntanta DB_NAME și versiunea bazei de date specificată prin constanta DB_VERSION. Contextul aplicației este necesar pentru a putea accesa resursele.

//constructorul clasei adaptor

public ServicesDbAdapter(Context context) {

//apelarea constructorului clasei de baza

super(context, DB_NAME, null, DB_VERSION);

//asignarea contextului aplicatiei la contexul clasei curente

this.context = context;

}

Metoda createDataBase() este folosită pentru a crea în directorul de sistem / data/data/com.google.tourguide/databases/ o bază de date fără nicio înregistrare prin apelarea metodei getReadableDatabase() din clasa de bază. După crearea acestei baze de date, aplicația va copia baza de date construită de utilizator din directorul assets din ierarhia proiectului apelând funcția copyDataBase(). Pentru a evita procesul de creare și copiere al bazei de date de fiecare dată când aplicația este rulată, metoda verifica mai întâi dacă baza de date există deja prin apelarea metodei checkDataBase(). Un dezavantaj al acestui procedeu este că în cazul în care se face o modificare a bazei de date într-o versiune ulterioară a aplicației, noua variantă nu va fi copiată doar prin actualizarea aplicației. Acest lucru se rezolvă, însă, printr-o dezinstalare completă și reinstalare.

//metoda pentru crearea unei baze de date vida care poate fi suprascrisa

//cu propria baza de date

public void createDataBase() throws IOException {

//verifica daca baza de date exista deja pentru a nu o copia de

//fiecare data la rularea aplicatiei

boolean dbExist = checkDataBase();

if (dbExist) {

//daca exista, nu crea baza de date

} else {

//daca nu exista, creaza o baza de date goala in directorul

//corespunzator din sistem

this.getReadableDatabase();

try {

//copiaza baza de date creata manual din directorul de

//assets in directorul de sistem

copyDataBase();

} catch (IOException e) {

throw new Error("Error copying database");

}

}

}

Mai departe, metoda getDatabase() deschide baza de date din directorul assets pentru citire. Acest lucru se realizează prin apelarea metodei openDatabase() și specificarea căii către baza de date și a modului în care va fi deschisă, în acest caz doar pentru citire. Tipul returnat de metodă este un obiect SQLiteDatabase.

public SQLiteDatabase getDatabase() {

//calea catre baza de date specificata prin cosntantele DB_PATH si

//DB_NAME

String path = DB_PATH + DB_NAME;

//deschide baza de date specificata prin calea definita in modul

//citire

return SQLiteDatabase.openDatabase(path, null,

SQLiteDatabase.OPEN_READONLY);

}

Metoda checkDataBase(), după cum îi sugerează și numele, verifică dacă baza de date există în mod curent în directorul sistemului de operare pentru a evita crearea și copierea acesteia de fiecare dată când este rulată aplicația. În acest sens, se folosește același principiu ca la metoda getDataBase() și anume, se încearcă deschiderea acesteia în modul citire. Dacă operația este un succes, atunci se consideră că baza de date există deja și nu va mai fi creată și copiată. În final, conexiunea la baza de date este închisă. Metoda returnează o valoarea booleană diferită de null: true dacă baza de date există și false dacă baza de date trebuie creată.

private boolean checkDataBase() {

//verifica faptul ca baza de date exista prin incercarea de a

//deschide o conexiune SQLiteDatabase checkDB = null; try {

//se incearca conectarea la baza de date

String myPath = DB_PATH + DB_NAME;

checkDB = SQLiteDatabase.openDatabase(myPath, null,

SQLiteDatabase.OPEN_READONLY);

} catch (SQLiteException e) {

//in cazul in care conexiunea esueaza, baza de date nu

//exista

}

//verifica daca s-a reusit conectarea la baza de date

if (checkDB != null) {

//daca conexiunea este un succes, inchide conexiunea la baza

//de date checkDB.close();

}

//returneaza true daca baza de date exista sau fals daca nu exista

return checkDB != null ? true : false;

}

Copierea efectivă a bazei de date, după ce a fost creată baza de date goală care are rolul de suport, se realizează cu ajutorul funcției copyDataBase(), așa cum a fost menționat mai sus. Copierea din directorul assets din ierarhia aplicației în directorul de sistem de unde va putea fi

gestionată se face folosind fluxuri de biți.

private void copyDataBase() throws IOException {

// deschide baza de data de copiat ca flux de intrare

InputStream inputStream = context.getAssets().open(DB_NAME);

// calea catre baza de date goala din directorul de sistem

String outFileName = DB_PATH + DB_NAME;

// deschide baza de date din directrul de sistem ca flux de iesire

OutputStream outputStream = new FileOutputStream(outFileName);

// transfera datele din baza de date din directorul assets in baza

// de date goala din directorul de sistem sub forma de blocuri de

// dimensiunea 1024 biti

byte[] buffer = new byte[1024];

int length;

while ((length = inputStream.read(buffer)) > 0) {

outputStream.write(buffer, 0, length);

}

// Inchide fluxurile de intrare si de iesire

outputStream.flush(); outputStream.close(); inputStream.close();

}

Baza de date de copiat este deschisă ca flux de intrare, iar baza de date în care se copiază – cea din directorul de sistem – ca flux de ieșire. Baza de date de unde se face copierea este specificată prin numele ei dat de constanta DB_NAME, iar baza de date în care se face copierea este specificată prin constantele DB_PATH + DB_NAME. Copierea datelor se realizează în blocuri de câte 1024 biți, atâta timp cât există date în baza de date din directorul assets. Când toate datele au fost copiate, fluxurile de intrare și de ieșire sunt închise.

Deschiderea și închiderea bazei de date se face în metoda openDataBase() și metoda suprascrisă close().

Metoda pentru deschierea conexiunii cu baza de date:

public void openDataBase() throws SQLException {

//deschide baza de date specifiata prin calea path in modul citire

String path = DB_PATH + DB_NAME;

myDataBase = SQLiteDatabase.openDatabase(path, null,

SQLiteDatabase.OPEN_READONLY);

}

Metoda pentru inchiderea conexiunii cu baza de date:

@Override

public synchronized void close() {

//inchide conexiunea cu baza de date daca aceasta este deschisa

if (myDataBase != null)

myDataBase.close();

super.close();

}

ReadWriteDbAdapter.java

Celălalt adaptor pentru lucrul cu bazele de date locale este puțin diferit față de cel prezentat anterior deoarece baza de date pe care o gestionează este creată la runtime și, în plus, poate fi atât citită, cât și scrisă. Clasa ReadVriteDbAdapter are o structură care conține o clasă interioară

privată care este o extensie a clasei de bază SQLiteOpenHelper, un constructor care primește ca parametru contextul curent și o serie de metode care permit gestionarea datelor din tabele.

Clasa interioară, DatabaseHelper, este o clasă privată și statică ce are un constructor care primește ca parametru contextul aplicației și apelează constructorul clasei de bază transmițându-i acestuia contextul, numele bazei de date specificat prin constanta DATABASE_NAME și versiunea bazei de date specificată prin constanta DATABASE_VERSION. De asemenea, clasa moștenește din clasa de bază metodele onCreate() și onUpgrade() care sunt apelate atunci când baza de date este creată, respectiv actualizată. În metoda onCreate() se execută scriptul de actualizare a bazei de date specificat prin constanta DATABASE_CREATE și care arată în felul următor:

private static final String DATABASE_CREATE = "create table notes (_id integer primary key autoincrement, "

+ "title text not null, body text not null, UTC text not null, POI_name

not null);"

+ "create table itinerary (_id integer primary key autoincrement, "

+ "latitude text not null, longitude text not null, poi_name text not

null);" + "create table tasks (_id integer primary key autoincrement, "

+ "title text not null, description text not null, POI_name text not

null, expiration text not null);";

În script, după cum se vede, se specifică crearea unei baze de date care să conțină următoarele:

 Tabelul notes cu înregistrările: pe prima coloană index-ul fiecărei linii care se autoincrementează de fiecare dată când este adăugată o nouă inregistrare, titlul notei, continutul notei, ora la care a fost salvata nota, pecum și numele punctului de interes pentru care a fost salvata.

 Tabelul itinerary cu inregistrările: indexul fiecărei linii cu autoincrementare, latitudinea punctului de interes, longitutinea punctului de interes, numele punctului de interes.

 Tabelul tasks care conține indexul fiecărei linii cu autoincrementare, titlul taskului, descrierea taskului, numele punctului de interes pentru care a fost adăugat și durata de timp după care expiră taskul, in milisecunde.

Părăsind clasa interioară și revenind la clasa ReadWriteDbAdapter, aceasta conține și un constructor care primește ca parametru contextul aplicației și este folosit pentru a creea și deschide baza de date.

//constructorul clasei ReadWriteDbAdapter

public ReadWriteDbAdapter(Context context) {

//asignarea contextului aplicatiei la contextul clasei

this.context = context;

}

În continuare vor fi descris metodele care sunt folosite pentru deschiderea și inchiderea bazei de date, precum și cele care sunt folosite pentru a adăuga, șterge sau actualiza înregistrările din tabele.

Pentru deschiderea bazei de date se folosește metoda open(). În cadrul acestei metode se obține o instanță a clasei interioare DatabaseHelper care va fi folostă și în restul clasei și, de asemenea, se obține o conexiune la baza de date prin care se permite atât scrierea, cât și citirea. Tipul returnat de metoda este ReadWriteDbAdapter și metoda va returna obiectul curent.

public ReadWriteDbAdapter open() throws SQLException {

//obtine instanta clasei DatabaseHelper databaseHelper = new DatabaseHelper(context);

//deschide conexiunea la baza de date

mDb = databaseHelper.getWritableDatabase();

//returneaza obiectul curent

return this;

}

Închiderea bazei de date se realizează prin apelarea metodei close() din clasa nativă

SQLiteOpenHelper.

Metodele pentru int roduc erea d e noi î nre gist ră ri

Pentru introducerea unor noi înregistrări în baza de date sunt folosite metodele createNote(), createTask() și createItineraryPoint().

Metoda createNote() primește ca parametri titlul și conținutul notei sub formă de șiruri de caractere, ora la care a fost salvată și obiectivul pentru care a fost salvată nota, tot sub forma de string-uri. Se obține apoi o instanță a clasei ContentValues în care sunt adăugate valorile primite ca parametri după care este apelată metoda insert() pentru a adăuga înregistrările specificate prin instanța ContentValues în tabelul primit ca parametru. Metoda va returna index-ul rândului dacă înregistrarea a fost inserată cu succes sau valoarea -1 dacă aceasta a eșuat.

//creaza o noua nota pe baza valoarilor primite ca parametri

public long createNote(String title, String body, String UTC,

String POI_name) {

//obtine o instanta a clasei ContentValues

ContentValues initialValues = new ContentValues();

//adauga valorile primite ca parametri la instanta obtinuta initialValues.put(KEY_TITLE, title); initialValues.put(KEY_BODY, body); initialValues.put(KEY_TIMEDATE, UTC); initialValues.put(KEY_POI, POI_name);

//insereaza nota in baza de date

return mDb.insert(DATABASE_TABLE, null, initialValues);

}

Metoda createTask() este construită pe același principiu ca metoda prezentată anterior, ceea ce diferă fiind lista de parametri. Astfel, funcția primește titlul și corpul task-ului, numele punctului de interes pentru care a fost adăugat și durata după care task-ul expiră. Acești parametri sunt adăugați la instanța clasei ContentValues și inserați în baza de date prin procedeul explicat anterior.

Și în cazul metodei createItineraryPoint() se primește o listă de parametri (numele punctului de interes pentru care se crează taskul și latitudinea și longitudinea care descriu punctul de interes) care sunt adăugați la instanța clasei ContentValues și inserați în baza de date folosind metoda insert().

Metode pentru șter gere a înregist r ărilor

Metodele pentru ștergerea înregistrărilor din baza de date se împart în două categorii: cele care permit ștergerea unei singure înregistrări pe baza indexului rândului notei care a fost aleasă din listă (vezi clasa NotePad.java) și cele care permit ștergerea simultană a tuturor înregistrărilor din baza de date.

Metodele pentru ștergerea unei singure înregistrări din baza de date sunt: deleteNote(), deleteTask() și deleteItineraryPoint(). Implementarea lor este destul de simplă:

metodele primesc ca parametru indexul rândului notei selectată din listă care corespunde indexului înregistrării din baza de date și apelează metoda delete() din clasa nativă SQLiteOpenHelper. Această metodă primește ca parameteri numele tabelului din care se dorește ștergera înregistrării și indexul rândului de pe care se dorește ștergerea înregistrării.

Metodele pentru ștergerea tuturor înregistrăriloe sunt: deleteAllNotes(), deleteAllTasks()

și deleteAllItineraryPoints(). Ele sunt perfect identice cu metodele destinate ștergerii unei singure înregistrări doar că nu se mai specifică indexul rândului, în acest caz sistemul de operare luând în considerare toți indecșii.

Metode pentru a ctualiz ar ea înre gist ră rilor ex ist ente

Uneori sunt necesare unele modificări asupra înregistrărilor existente în baza de date. Pentru aceasta avem nevoie de o serie de metode care să permită schimbarea anumitor date fără a modifica întreaga înregistrare. În acest sens, adaptorul include o gamă de metode cu ajutorul cărora pot fi realizate update-uri asupra câmpurilor unei anumite înregistrări: updateNote() și updateTask().

Metoda updateNote() primește o listă de argumente care se referă la indexul înregistrării care se dorește a fi modificată, noul titlu și noul corp al notei, precum și ora la care a fost modificată și numele punctului de interes. Toți acești parametri sunt adăugați la instanța clasei ContentValues care va fi folosită alături de numele tabelului și indexul înregistrării ca argument al metodei update() din clasa nativă SQLiteOpenHelper. Tipul returnat de metodă este boolean, true în caz de succes și false dacă inserarea actualizărilor a eșuat.

public boolean updateNote(long rowId, String title, String body, String UTC, String POI_name) {

//obtine o instanta a clasei ContentValues

ContentValues args = new ContentValues();

//adauga parametri transmisi metodei la instanta obtinuta args.put(KEY_TITLE, title);

args.put(KEY_BODY, body); args.put(KEY_TIMEDATE, UTC); args.put(KEY_POI, POI_name);

//apeleaza metoda update pentru inserarea modificarilor in baza de

//date

return mDb.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId,

null) > 0;

}

Metoda updateTask() este identică cu metoda prezentată mai sus, mai puțin în privința listei de argumente. Astfel, metoda updateTask() primește ca parametri indexul rândului al cărui înregistrare este modificată, noul titlu și noul corp al task-ului, numele obiectivului și timpul după care task-ul expiră.

Metode pentru ex tra ge re a înre gist rărilor ex ist ente

La fel ca în cazul metodelor pentru ștergerea înregistrărilor și aici avem două tipuri de funcții: cele pentru extragerea unei singure înregistrări pe baza indexului rândului înregistrării selectate din listă și care corespunde cu indexul înregistrării din baza de date și cele pentru extragerea simultană a tuturor înregistrărilor

Metodele pentru extragerea unei sigure înregistrări sunt: fetchNote(), fetchTask() și fetchItineraryPoint(), iar cele pentru extragerea tuturor înregistrărilor fetchAllNotes(), fetchAllTasks() și fetchAllItineraryPoints().

Tipul metodei fetchNote() este Cursor, deoarce într-o variabilă de acest tip vor fi reținute elementele extrase din baza de date, referirea la fiecare făcându-se după aceea pe baza unor constante (vezi paragraful care se referă la implementarea NotePad-ului). Valoarea variabilei de tip Cursor este obținută pe baza apelării metodei query() din clasa nativă SQLiteOpenHelper care primește ca argumente, printre altele, numele tabelului care va fi interogat și indexul înregistrării care trebuie extrasă din tabel. De asemenea, pentru cursorul obținut se apelează metoda moveToFirst() care îi indică acestuia să pointeze către prima înregistrare din listă.

public Cursor fetchNote(long rowId) throws SQLException {

// interogarea tabelul exprimat prin constanta DATABASE_TABLE si

// retinerea datelor intr-un cursor

Cursor cursor = mDb.query(true, DATABASE_TABLE, null, KEY_ROWID +

"=" + rowId, null, null, null, null, null);

// daca cursorul are inregistrari, pointeaza catre prima

//inregistrare din lista

if (cursor != null) {

cursor.moveToFirst();

}

// returneaza cursorul obtinut

return cursor;

}

Metodele fetchTask() și fetchItineraryPoint() sunt identice și nu vor mai fi tratate.

Pentru extragerea simultană a tuturor înregistrărilor dintr-un tabel, au fost definite metodele fetchAllNotes(), fetchAllTasks() și fetchAllitineraryPoints(). Acestea returnează o variabilă de tip cursor care conține toate înregistrările extrase și nu mai folosesc index-ul rândului al cărei înregistrare a fost selectată deoarece nu mai este necesar.

ServicesListBuilder.java

Clasa de față este folosită pentru contruirea listelor ce definesc caracteristicile punctelor de interes. Structura clasei constă dintr-un constructor care primește ca parametru activitatea în care este apelată instanța clasei, o metodă în care este inițializată baza de date care conține informații despre POI, o metodă care constuiește listele și altă metodă care pasează listele către manager.

Metodele implementate de această clasă sunt apelate din clasa MyMapActivity în momentul în care din meniul principal al aplicației se alege localizarea unor puncte de interes dintr-o anumită categorie. Făcând din nou referire la metodele din clasa menționată anterior, la apăsarea butonului FindPlaces din meniul de opțiuni se deschide un submeniu care permite alegerea categoriei de obiective care se dorește a fi afișată. În amblele cazuri utilizatorul va fi înștiințat prin funcția de sinteză de voce asupra numelui etichetei butonului apasat și va primi feedback tactil prin funcția de vibrație a telefonului.

La alegerea unei categorii de obiective, dacă este cazul, mai întâi sunt șterse de pe hartă punctele și baloanele care marchează obiectivele din altă categorie prin apelarea metodelor removeAllItems() și removeBalloon() din clasa MapItemizedOverlay. După aceea se apelează funcțiile din clasa care este tratată în acest paragraf și anume se inițializează baza de date cu ajutorul metodei initDB(), se interoghează baza de date apelând funcția queryDB() al cărei argument este numele tabelului din care se dorește extragerea datelor, se construiesc listele în care sunt reținute caracteristicile punctelor de interes, se apelează funcția passServicesLists() cu ajutorul căreia listele sunt pasate și stocate în manager și se închide conexiune la baza de date apelând metoda close(). Restul metodelor care se apelează la

apăsarea unui buton din meniul de aplicație sunt tratate în paragraful corespunzător clasei

MyMapActivity.

Metoda initDB() are rolul realiza crearea și copierea bazei de date dacă aceasta nu există (vezi clasa ServicesDbAdapter) și deschiderea conexiunii la baza de date. Prima dată este necesar să se obțină o instanță a clasei adaptor ServicesDbHelper căreia îi este pasat contextul activității în care va fi utilizată (MyMapActivity) prin parametrul constructorului. Mai departe se apelează metoda createDataBase() (vezi clasa ServicesDbAdapter) și se deschide conexiunea la baza de date prin apelarea funcției openDataBase().

public void initDB() {

//obtinerea instantei clasei adaptor si pasarea contextului

//corspunzator

servicesDbHelper = new ServicesDbAdapter(activity);

//se incearca crearea si copierea bazei de date

try {

servicesDbHelper.createDataBase();

} catch (IOException ioe) {

throw new Error("Unable to create database");

}

//se deschide conexiunea la baza de date

try {

servicesDbHelper.openDataBase();

} catch (SQLException sqle) {

throw sqle;

}

}

În metoda queryDB() care primește ca parametru numele tabelului ce se dorește a fi interogat, se obține o variabilă de tip Cursor în care vor fi reținute înregistrările. Așadar tipul returnat de metodă este Cursor. Se obține baza de date în modul citire prin apelul metodei getReadableDatabase() și se interoghează baza de date prin apelul metodei query() din clasa nativa SQLiteOpenHelper ai cărei parametri sunt numele tabelului de interogat și faptul că se dorește obținerea rezultatelor în ordine alfabetică. În final se apelează metoda startManagingCursor() pentru a permite sistemului de operare să gestioneze ciclul de viață al cursorului.

public Cursor queryDB(String category) {

// obtinerea instantei clasei adaptor servicesDbHelper = new ServicesDbAdapter(activity);

// obtinerea bazei de date in modul citire

SQLiteDatabase db = servicesDbHelper.getReadableDatabase();

// interogarea tabelului primit ca parametru si retinerea

//rezultatelor

// in ordine alfabetica intr-un curosr

cursor = db.query(category, null, null, null, null, null, "name");

// gestionarea cursorului revine sistemului de operare activity.startManagingCursor(cursor);

// parametrul returnat de metoda

return cursor;

}

Metoda buildLists() este folosită pentru a consturui listele ce caracterizează punctele de interes. Există câte o listă pentru fiecare caracteristică: listă de nume, adrese, numere de telefon, etc. Înainte de a fi populate cu noi date, listele sunt golite pentru cazul în care au fost folosite anterior. Înregistrările introduse în liste sunt extrase din varabila cursor obținută anterior prin apelarea asupra acestuia a metodei corespunzptoare get() al cărei argument este indexul

coloanei ale cărei înregistrări vor fi citite. Înainte este necesară apelarea metodei moveToFirst() asupra cursorului pentru ca cursorul să pointeze asupra primei înregistări din listă (la început el este poziționat deasupra primei înregistrări). De asemenea, după ce toate înregistrările au fost extrase din cursor, pentru a elibera resursele, acesta este închis prin apelarea metodei close().

public void buildLists() {

// sterge continutul listelor daca acestea sunt ocupate indexList.clear();

nameList.clear();

………………………………………………….

// pozitionarea cursorului la prima inregistrare din lista

if (cursor.moveToFirst()) {

// extrage inregistrarile din cursor pe baza indexului // //

// fiecarei coloane si populeaza listele

do {

index = cursor.getInt(cursor.getColumnIndex("_id"));

indexList.add(index);

………………………………………………….

} while (cursor.moveToNext());

}

//inchide cursorul dupa ce a fost terminata extragerea datelor

if (cursor != null && !cursor.isClosed()) {

cursor.close();

}

}

După construirea listelor acestea sunt pasate către clasa ServicesListManager din pachetul care conține managerii aplicației prin apelarea metodei passServicesLists() în cadrul căreia sunt apelate funcțiile setter corespunzătoare.

public void passServicesLists() { servicesListManagerInstance.setIndexList(indexList); servicesListManagerInstance.setNameList(nameList);

………………………………………………….

}

4.2.5.5.2. Structura bazelor de date locale

În acest paragraf va fi descrisă, pe scurt, structura bazelor de date locale SQLite. După cum a fost menționat anterior, aplicația folosește două baze de date pentru stocarea persistentă:

 Una creată manual și care conține tabelele pentru diversele categorii de obiective.

 Cealaltă creată la runtime de aplicație și care conține

Baza de date în care sunt stocate caracteristicile punctelor de interes a fost creată folosind utilitarul SQLiteDatabaseBrowser. Înainte de a începe introducerea înregistrărilor în tabelele corespunzătoare categoriei de obiective din care fac fiecare parte a fost necesară crearea tabelului android_metadata care conține o singură înregistrare, en_US, necesară sistemului de operare Android pentru a știi care este modul de formatare al textului. Mai departe, a fost creat câte un tabel pentru fiecare categorie de puncte de interes: accommodation, café/bars, parking, restaurants, etc., în fiecare tabel existând următoarele câmpuri: nume, adresă, număr de telefon,

pagină web, facilități, latitudinea care descrie poziționarea pe hartă, longitudinea, link pentru imaginea 1,link pentru imaginea 2, link pentru imaginea 3. Foarte important de menționat este faptul că, la fel ca în toate bazele de date folosite în Android, prima coloană trebuie să fie numită

_id, iar tipul de date al acestei coloane să fie întreg cu autoincrementare. Coloana în cauză este folosită de aplicație pentru a știi care element trebuie extras din tabel. O altă particularitate a acestei baze de date este faptul că ea nu poate fi scrisă, ci doar citită.

Cealaltă bază de date poate fi accesată atât pentru scriere, cât și pentru citire. Ea conține trei tabele pentru următoarele module ale aplicației: modulul de note, modulul care ține de itinerar și modulul dedicat taskmanager-ului. Tabelul pentru stocarea notelor conține, pe prima coloană, indexul cu autoincrementare despre care s-a pomenit mai sus. De asemenea, conține o coloană pentru titlul notei, alta pentru corpul notei, o coloană pentru numele obiectivului pentru care a fost salvată nota și o altă coloană pentru ora la care s-a produs inserarea notei. Tabelul care stochează punctele de interes ce descriu itinerariul are și el pe prima coloană indexul cu autoincrementare, pe a doua coloană numele obiectivului, pe a treia latitudinea sa, iar pe a patra longitudinea. In fine, ultimul tabel este cel in care sunt stocate task-urile. Pe prima coloană se regasește, ca în celălalte cazuri, indexul cu autoincrementare, pe a doua titlul task-ului, pe a treia descrierea sa, pe a patra numele punctului de interes pentru care a fost salvat, iar pe ultima coloană timpul în care task-ul expiră exprimat în milisecunde.

Mai multe detalii despre structura bazelor de date se pot consulta în secțiunile corespunzătoare fiecărui modul al cărui date le stochează.

4.2.5.6. Implementarea modulului pentru prognoza METEO

Aplicația care constituie tema acestei lucrări include și facilitatea de a prelua de pe serverele Google a informațiilor privind starea vremii și de a le afișa utilizatorului. Acest lucru este îndeplinit de clasele din pachetul com.google.tourguide.weatherforecast, cu subpachetele corespunzătoare. Pentru implementarea acestui modul a fost folosit API-ul Google Weather care deși nu este suportat oficial, oferă o bună flexibilitate și precizie. Aplicația este așadar capabilă să afișeze condițiile curente (temperatură, umiditate, viteza vântului, etc.), precum și temperaturile minime/maxime privitoare la următoarele trei zile. Trebuie menționat faptul că o parte din acest pachet a fost preluat din referința bibliografică [18] și a fost modificat pentru a corespunde cerințelor aplicației actuale. În continuare va fi detaliat modul în care sunt constuite doar clasele proprii și se va face referință la maetodele folosite din clasele integrate dacă este cazul.

SingleCurrentWeatherInfoView.java

Clasa de față definește un view care este capabil să afișeze o pictogramă și, la stânga ei, parametri pentru condițiile curente: temperatura, umiditate, viteza vântului, etc. Ca structură, clasa este o derivare a clasei native LinearLayout și conține doi constructori expliciți. Primul primește ca parametru contextul curent al aplicației, iar al doilea contextul aplicației și o listă de parametri.

Elementele care constituie acest layout sunt inițializate în constructor. Astfel, se construiește un obiect a clasei native ImageView căruia îi este asociată, din directorul de resurse, imaginea care indică faptul că nu s-a putut face preluarea datelor de pe server. Mai departe, sunt obținute patru instanțe ale clasei native TextView care vor fi folosite pentru afișarea textului ce descriu parametri conditiilor curente. Textul este formatat Tahoma de mărime 16. Obiectele de tip TextView sunt incluse într-un layout liniar, lucru realizat prin construirea unui obiect de acest tip

și apelarea funcției addView() ai cărei parametri sunt view-ul care se dorește a fi adăugat și modul în care acesta va fi afișat în layout.

public SingleCurrentWeatherInfoView(Context context, AttributeSet attrs)

{

super(context, attrs);

// seteaza view-ul in care va fi afisata pictograma this.myWeatherImageView = new ImageView(context); this.myWeatherImageView.setImageDrawable(getResources()

.getDrawable(R.drawable.dunno));

// seteaza view-ul in care va fi afisata temperatura this.myTempTextView = new TextView(context); this.myTempTextView.setText("?"); this.myTempTextView.setTextSize(16); this.myTempTextView.setTypeface(Typeface

.create("Tahoma", Typeface.BOLD));

// seteaza view-ul in care vor fi afisate conditiile curente

……………………………………………………

// seteaza view-ul in care va fi afisata viteza vantului

……………………………………………………

// seteaza view-ul in care va fi afisata umiditatea

……………………………………………………

// adauga textview-urile la un layout liniar weatherParameters = new LinearLayout(context); this.setOrientation(HORIZONTAL); weatherParameters.setOrientation(VERTICAL); weatherParameters.setPadding(5, 0, 0, 0); weatherParameters.addView(this.myTempTextView,

new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,

LayoutParams.WRAP_CONTENT));

………………………………………………..

// adauga pictograma si layout-ul care contine textview-urile la

// layoutul de baza this.addView(this.myWeatherImageView, new LinearLayout.LayoutParams(

LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

this.addView(weatherParameters, new LinearLayout.LayoutParams(

LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

}

Metoda reset() are rolul de a reseta view-ul la valorile implicite. Este cazul în care, din diverse motive, datele nu au putut fi citite de pe server.

public void reset() {

// seteaza pictograma implicita din directorul de resurse

this.myWeatherImageView.setImageDrawable(getResources()

.getDrawable(R.drawable.dunno));

// seteaza textul implicit pentru conditiile curente this.myTempTextView.setText("?"); this.conditionTextView.setText("");

}

Metoda setRemoteImage() este folosită pentru a prelua de pe server pictograma referitoare la condițiile curente. Ea primește ca parametru URL-ul de unde se face descărcarea și, cu ajutorul acesteia se obține un obiect de tipul URLConnection care la rândul său este folosit pentru conectarea efectivă la server prin apelarea metodei connect() și deschiderea unui stream de intrare prin apelarea funcției getInputStream(). Cu ajutorul unei instanțe a clasei BitmapFactory stream-ul este decodat și se obține imaginea in format bitmap care mai apoi este setată în view-ul corespunzător. Dacă decodare imaginii din fluxul de intrare nu se poate realiza, în view va fi setată imaginea implicită.

public void setRemoteImage(URL aURL) {

try {

// obtine o instanta a clasei URLConnection

URLConnection conn = aURL.openConnection();

// realizeaza conexiunea la server conn.connect();

// creaza un obiect de tip inputstream pentru preluarea

// datelor

InputStream is = conn.getInputStream();

// creaza un obiect de tip buffered input stream

BufferedInputStream bis = new BufferedInputStream(is);

// creaza o instanta a clasei BitmapFactory pentru decodarea

// fluxului de intrare

Bitmap bm = BitmapFactory.decodeStream(bis);

// inchide fluxurile de intrare bis.close();

is.close();

// seteaza imaginea obtinuta in view-ul corespunzator this.myWeatherImageView.setImageBitmap(bm); this.myWeatherImageView.setMinimumHeight(90); this.myWeatherImageView.setMinimumWidth(90);

} catch (IOException e) {

// seteaza imaginea implicita daca nu se poate realiza

// decodarea

this.myWeatherImageView.setImageDrawable(getResources()

.getDrawable(R.drawable.dunno));

}

}

Pe lângă metodele prezentate mai sus, în clasă mai există o serie de metode setter pentru setarea efectivă a elementelor în view-urile create anterior. Aceste metode primesc ca parametru valorile datelor preluate de la server și sunt apelate în clasa GoogleWeatherHandler. O astfel de metodă arată la fel ca cea prezentată în secvența de cod de mai jos.

public void setWindCondition(String windCondition) {

this.windConditionTextView.setText(windCondition);

}

SingleForecastWeatherInfoView.java

Clasa SingleForecastWeatherInfoView este construită pe același principiu ca și clasa anteriorară și definește un obiect de tip View pentru afișarea prognozei METEO pe următoarele zile. La fel ca în cazul clasei SingleCurrentWeatherInfoView, clasa curentă este o extindere a clasei de bază LinearLayout și are doi constructori: unul explicit care primește ca parametru contextul curent și altul care primește ca parametri contextul curent și o listă de atribute. Modul de inițializare al elementelor care alcătuiesc view-ul, precum și funcțiile prezente în clasă nu vor mai fi detaliate deoarece sunt identice cu cele din clasa anterioară. Totuși, pentru o bună înțelegere, codul clasei SingleForecastWeatherInfoView poate fi consultat în cadrul anexei.

TransparentLinearLayout.java

Clasa descrisă în acest paragraf este o extindere a clasei native de bază LinearLayout cu scopul de a crea un view în care să fie incluse elementele care vor fi afișate utilizatorului și care au fost descrise în ultimele două secțiuni.

Deoarece este o derivare a clasei LinearLayout, ea conține doi constructori expliciți, unul care primește parametru ce se referă la contextul curent al aplicației și altul care primește ca parametru atât contextul, cât și o listă de atribute. Tot în constructori este apelată și metoda init() care are rolul de a defini fundalul layout-ului.

public TransparentLinearLayout(Context context, AttributeSet attrs) {

// apeleaza constructorul clasei de baza

super(context, attrs);

// apeleaza metoda init init();

}

public TransparentLinearLayout(Context context) {

// apeleaza constructorul clasei de baza

super(context);

// apeleaza metoda init init();

}

În metoda init() este creat un obiect de tipul Paint și se apelează funcția setARGB() din clasa Paint care primește ca parametri gradul de transparență al culorii, precum și proporția culorilor primare R (roșu), G (verde), B (albastru) care formează culoarea fundalului. De asemenea, mai este folosită și metoda setAntiAlias() pentru a îmbunătăți modul de afișare.

private void init() {

//creaza un obiect de tip Paint innerPaint = new Paint();

//defineste parametri culorii innerPaint.setARGB(200, 75, 75, 75);

//seteaza folosirea anti-alias innerPaint.setAntiAlias(true);

}

Clasa moștenește, de asemenea, metoda dispatchDraw() din clasa de bază pentru desenarea care primește ca parametru un obiect de tip Canvas. Această metodă este folosită pentru desenarea efectivă a view-ului. În acest sens se definește un obiect de tipul RectF, se setează dimensiunile acestuia și se aplează metoda metoda drawRoundRect() din clasa LinearLayout care primește ca argumente obiectul RectF, două valori întregi care se referă la gradul de rotunjire al colțurilor și obiectul de tipul Paint descris la punctul anterior.

@Override

protected void dispatchDraw(Canvas canvas) {

//crearea unui obiect de tipul RectF RectF drawRect = new RectF();

//setarea dimensiunilor obiectului creat

drawRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());

//desenarea efectiva a layout-ului

canvas.drawRoundRect(drawRect, 15, 15, innerPaint);

//apelarea metodei din clasa de baza

super.dispatchDraw(canvas);

}

WeatherSet.java

Clasa WeatherSet combină view-urile create anterior pentru a fi afișate în layout-ul custom descris în paragraful TransparentLinearLayout. Implementarea clasei este simplă: patru metode getter și setter pentru setarea și preluarea condițiilor curente și a celor pentru zilele următoare.

We atherFore cast.j ava și GoogleWeatherHandler.java

Clasele din titlul acestui paragraf nu au fost construite de autor, ci au fost preluate din referința bibliografică (m). Ele sunt folosite pentru a realiza parsarea datelor de pe serverul Google în cod XML și citirea codului XML. Parsarea datelor se face cu ajutorul unei cereri HTTP. Deoarece au fost preluate din referințele bibliografice, modul de implementare al acestor clase nu va fi descris în cadrul lucrării, dar va putea fi consultat în cadrul anexelor.

Clasa WeatherForecast este clasa centrală a modulului pentru informațiile privind starea vremii, activitatea din cadrul ei executându-se atunci când se alege din meniul principal al aplicației opținea WeatherForecast.

4.2.5.7. Implementarea modulului care gestionează partea de Task Management

Deoarece platforma Android oferă suport foarte bun în ceea ce privește acest gen de facilități, aplicația de față include și posibilitatea de a salva task-uri pentru punctele de interes. TaskManager-ul nu este unul tipic punctul său forte fiind că folosește din plin partea de alerte bazate pe geolocație. Astfel, utilizatorul își poate defini un task pentru un anumit obiectiv, iar aplicația îl va alerta în momentul în care se află la o anumită distanță de acesta asupra faptului că și planificat ceva anume. Bineînțeles, asta dacă task-ul nu și depășit durata de valabilitate care poate fi setată de utilizator. Totuși, utilizatorul are posibilitatea specificării faptului ca taskul să nu expire niciodată.

Pachetul în care se găsesc clasele ce se ocupă de taskmanager este cel referit prin calea canonică com.google.tourguide.taskmanager. Acesta găzuiește trei clase pentru lucrul cu taskuri: una care se ocupă cu salvarea taskului în baza de date și transmiterea informațiilor corespunzătoare către Broadcast Receiver, alta care gestionează lista din care taskurile pot fi vizualizate și ultima clasă este Broadcast Receiverul care „prinde” intent-urile referitoare la task-uri.

TaskManagerActivity.java

Deoarece este lansată în momentul în care utilizatorul alege adăgarea unui task din meniul de opțiuni al ecranului specific detaliilor unui anumit obiectiv, această clasă este necesar să conțină o activitate. În acest sens, ea a fost derivată din clasa nativă Activity. În consecință, conține metoda onCreate() specifică ciclului de viață al activității în care sunt efectuate o serie de operațiuni la prima rulare. Aici sunt definite, prin încărcarea din directorul de resurse, toate elementele care vor fi afișate utilizatorului și, de asemenea, tot din cod XML este încărcat layout-ul care specifică cum vor fi aranjate view-urile pe ecran. Mai departe, se obține o instanță a clasei adaptor care gestionează accesul la tabelul din baza de date în care sunt stocate task-urile și se deschide conexiunea la aceasta. Totodată, se primește, dacă este cazul, indexul rândului al cărui task a fost selectat din listă (în cazul în care se dorește vizualizarea și modificarea task- ului) și se înregistrează butonul prin apăsarea căruia taskul va fi salva pentru a răspunde la clickuri. În fine, tot în metoda onCreate() se preiau, prin apelarea funcției getServicesLists(), listele cu caracteristicile punctelor de interes pentru a extrage din cadrul

lor informațiile necesare construirii taskului și se apelează metoda în care se inițializează receiverul ce va „prinde” intent-urile.

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

// seteaza layout-ul folosit in cadrul ativitatii setContentView(R.layout.task_manager_create);

// creaza un obiect de tipul location manager

locationManager = (LocationManager)

getSystemService(Context.LOCATION_SERVICE);

// defineste elementele care vor fi continute in layout editTitle = (EditText) findViewById(R.id.task_edit_title);

………………………………………………….

// obtine a instanta a clasei adaptor mDbHelper = new TaskManagerDbAdapter(this);

// deschide conexiune la baza de date mDbHelper.open();

// preia indexul randului al carui task se doreste a fi

// vizualizat/modificat

mRowId = (savedInstanceState == null) ? null

: (Long) savedInstanceState

.getSerializable(TaskManagerDbAdapter.KEY_ROWID);

if (mRowId == null) {

Bundle extras = getIntent().getExtras();

mRowId = extras != null ? extras

.getLong(TaskManagerDbAdapter.KEY_ROWID) : null;

}

// preia listele cu informatii despre punctele de interes

getServicesLists();

// inregistreaza receiverul care va fi folosit pentru prinderea

// intenturilor initializeReceiver();

// inregisteaza butonul prin apasarea caruia sunt salvate

//taskurile pentru a raspunde la clickuri saveTask.setOnClickListener(new View.OnClickListener() {

public void onClick(View view) {

setResult(RESULT_OK);

}

});

}

Deoarece un task este definit de numele punctului de interes pentru care este adăugat, de latitudinea și longitudinea care definesc poziția acestuia pe hartă, este necesară preluarea tuturor elementelor care se referă la aceste caracteristici. În acest sens clasa implementează metoda getServicesLists() prin apelarea cărei se preia lista de nume, lista de latitudini, lista de longitudini și indexul obiectivului care corespunde indexului balonului pe care s-a facut click pentru a cunoaște ce înregistrare trebuie extrasă din listă. Trebuie menționat faptul că pentru preluarea listelor se folosește clasa ServicesListManager din pachetul de manageri (vezi paragraful care se referă la implementarea managerilor).

private void getServicesLists() {

// preia indexul obiectivului

index = servicesListManagerInstance.getIndex();

// preia lista de nume

nameList = servicesListManagerInstance.getNameList();

// preia lista de latitudini

latList = servicesListManagerInstance.getLatList();

// preia lista de longitudini

lonList = servicesListManagerInstance.getLonList();

}

Pentru adăugarea efectivă a task-ului în baza de date, se apelează metoda addProximityAlert(). Un task este definit prin trei caracteristici: latitudinea și longitudinea obiectivului pentru care este adăugat și durata după care expiră.

Durata pentru care un task este valabil poate fi setată de utilizator prin intermediul unor widgeturi care permit definirea orei și datei (vezi capitolul referitor la interfața cu utilizatorul). Datele introduse sunt preluate cu ajutorul metodelor getYear(), getMonth() și getDayOfTheMonth() pentru dată și getCurrentHour() și getCurrentMinute() pentru oră. Ele sunt reținute într-un obiect de tip Calendar. Mai departe, apelând metoda toMillis() asupra obiectului nou construit se face conversia timpului în milisecunde, iar acesta reprezintă exact timpul după care taskul nu mai este valabil. De asemenea, se preiau din edit box-uri conținutul titlului și corpului taskului, iar din lista de nume denumirea obiectivului pentru care se salveaza task-ul.

Pentru ca broadcast receiverul să poată „prinde” mai multe intent-uri, se definește un obiect de tipul Bundle care va conține ca înregistrări numele punctului de interes și un request code care se incrementează la fiecare nouă adăugare de task-uri. Acest lucru este necesar deoarece altfel receiverul nu va putea deosebi intenturile cu care este înregistrat. Pentru ca întregul angrenaj să funcționeze, se construiește un intent care va fi transmis către receiver și care va conține obiectul de tip Bundle descris anterior. Acest intent este folosit la rândul său pentru construirea unui pendingIntent (vezi capitolul 3.3 pentru detalii).

Utilizatorul are posibilitatea să aleagă, prin intermediul unui check box, dacă dorește sau nu ca task-ul să expire. Astfel, prin apelarea funcției addProximityAlert() din clasa LocationManager se setează un nou task, iar prin apelarea funcției createTask() din clasa adaptor, conținutul acestuia va fi salvat în baza de date. Parametri metodei addProximityAlert() sunt latitudinea și longitudinea care descriu poziția obiectivului, raza cercului în interiorul căruia trebuie să se afle utilizatorul pentru a fi alertat, durata după care taskul va expira și pending intentul folosit pentru lansarea alertei.

De asemenea este înregistrat broadcast receiverul care va prinde intenturile referitoare la task manager. Astfel, este creat un obiect de tipul IntentFilter și este apelată metoda registerReceiver() care primește ca parametri instanța clasei ProximityIntentReceiver și filtrul creat anterior. Crearea unui filtru este necesară pentru ca receiverul să cunoască faptul că trebuie să recepționeze doar intenturi referitoare la taskuri.

private void addProximityAlert() {

// preia valoarea indexului request code

int requestCode = ServicesListManager.getInstance().requestCode;

// retine intr-un obiect de tipul Calendar valorile care definesc

//timpul expirarii

calendar.set(datePicker.getYear(), datePicker.getMonth(),

datePicker.getDayOfMonth(),timePicker.getCurrentHour(),

timePicker.getCurrentMinute());

// converteste timpul in milisecunde

expiration = calendar.getTimeInMillis();

// preia din campul corespunzator titlul taskului

title = editTitle.getText().toString();

// preia din campul corespunzator continutul taskului description = editDescription.getText().toString();

// preia din lista de nume denumirea obiectivului pentru care se

// salveaza taskul

POI_name = nameList.get(index);

// creaza un nou obiect de tipul Bundel

Bundle extras = new Bundle();

// adauga obiectului creat numele obiectivului si request codeul

// pentru a fi identificat de reciever extras.putString("name", POI_name); extras.putInt("id", requestCode);

// creaza un nou intent

Intent intent = new Intent(PROX_ALERT_INTENT);

// adauga intentului obiectul de tip bundle intent.putExtra(PROX_ALERT_INTENT, extras);

// creaza un pending intent pentru lansarea alertei

PendingIntent proximityIntent = PendingIntent.getBroadcast(this,0,

intent, PendingIntent.FLAG_CANCEL_CURRENT);

// verifica daca check box-ul este bifat

if (checkBox.isChecked()) {

// creaza un nou task, salveaza continutul in baza de date

// si seteaza validitate infinita locationManager.addProximityAlert(latList.get(index),

lonList.get(index), POINT_RADIUS, -1, proximityIntent);

mDbHelper.createTask(title, description, POI_name, "Never");

} else {

// defineste un obiect de tipul SimpleDateFormat SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy kk:mm:ss");

// adauga un nou task, salveaza continutul in baza de dte si

// seteaza timpul de expirare la valoarea definita de user

// convertita in milisecunde

locationManager.addProximityAlert(latList.get(index), lonList.get(index), POINT_RADIUS, expiration, proximityIntent);

mDbHelper.createTask(title, description, POI_name, sdf.format(calendar.getTime()));

}

// creaza un filtru pentru intent-uri

IntentFilter filter = new IntentFilter(PROX_ALERT_INTENT);

// inregistreaza receiverul care va prinde intent-urile registerReceiver(new ProximityIntentReceiver(), filter);

// incrementeaza indexul request code requestCode++;

ServicesListManager.getInstance().requestCode = requestCode;

}

Atunci când este selectat un task din listă în cadrul activității TaskManagerListActivity, acesta este afișat utilizatorului, iar conținutul poate fi vizualizat și modificat. Pentru ca aplicația să cunoască ce înregistrări trebuiesc preluate din baza de date pentru afișare, clasa implementează metoda populateFields(). Ea este construită pe același principiu ca cele din clasele NoteEdit și NoteView. Dacă indexul rândului preluat în metoda onCreate() este valid, se crează un obiect de tip Cursor în care prin apelarea metodei fetchTask() se reține, prin extragere din baza de date, a conținutului taskului corespunzător indexului care este transmis ca argument metodei. Mai departe, prin apelarea metodelor setText() se afișează în câmpurile corespunzătoare titlul și corpul taskului, acestea fiind extrase din obiectul cursor cu ajutorul metodei getColumnIndexOrThrow() care primește ca argument indexul coloanei reprezentat de constantele KEY_TITLE și KEY_DESCRIPTION din clasa adaptor.

private void populateFields() {

//verfica daca indexul randului este valid

if (mRowId != null) {

//creaza un obiect de tip Cursor in care retine continutul

//taskului corespunzator

Cursor task = mDbHelper.fetchTask(mRowId);

//permite aplicatiei sa gestioneze cursorul startManagingCursor(task);

//seteaza titlul in campul corespunzator editTitle.setText(task.getString(task

.getColumnIndexOrThrow(TaskManagerDbAdapter.KEY_TITLE)));

//seteaza descrierea in campul corespunzator

editDescription.setText(task.getString(task

.getColumnIndexOrThrow(TaskManagerDbAdapter.KEY_DESCRIPTION)

));

}

}

TaskManagerListActivity.java

Clasa TaskManagerListActivity este o derivare a clasei de bază native ListActivity și, după cum este sugerat prin denumire, ea conține activitatea ce este lansată în momentul apăsării butonului ViewAllTasks din meniul principal al aplicației și afișează sub formă de listă taskurile prezente în mod curent în baza de date. Deoarece este o extindere a unei subclase a clasei Activity, ea conține metoda onCreate(). În această metodă care se execută la pornirea activității, se setează layoutul folosit pentru afișarea task-urilor, se obține o instanță a clasei adaptor folosită pentru gestionarea accesului la baza de date și se deschide conexiunea la baza de date prin apelarea metodei open() din clasa adaptor. De asemenea, se execută funcția fillData() în cadrul căreia se extrag din baza de date și se afișează sub formă de listă înregistrările.

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

//seteaza layout-ul corespunzator activitatii curente setContentView(R.layout.task_manager_list);

//obtine o instanta a clasei adaptor mDbHelper = new ReadWriteDbAdapter(this);

//deschide conexiunea la baza de date mDbHelper.open();

//populeaza lista cu inregistrarile extrase din baza de date fillData();

}

Metoda fillData() este cea responsabilă de popularea listei cu titlurile taskurilor existente în baza de date. În această metodă, folosind instanța obținută în metoda onCreate(), se apelează funcția fetchAllTasks() din clasa ReadWriteDbAdapter. Această funcție are rolul de a extrage toate taskurile din baza de date și de a le reține într-o variabilă de tip Cursor. Legătura dintre datele extrase din baza de date și reținute în cursor și listă se face cu ajutorul unui SimpleCursorAdapter (vezi subcapitolul 3.4.6). Câmpul Title și obiectul de tip TextView care definește un rând din listă sunt legate prin definirea a doi vectori: unul care conține coloanele bazei de date din care se extrag datele – referite prin constantele KEY_TITLE, KEY_EXPIRATION și KEY_POI – și altul care conține referințe către view-urile în care vor fi afișate datele extrase din fiecare cloană. Metoda startManagingCursor() are rolul de a gestiona ciclul de viață al obiectului de tip Cursor. Urmează apoi instanțierea clasei native SimpleCursorAdapter căreia îi pasăm contextul curent, view-ul în care va fi afișat fiecare înregistrare, precum și cei doi vectori

despre care s-a facut referire anterior.

private void fillData() {

// crearea obiectului de tip cursor in care sunt retinute

// inregistrarile bazei de date

Cursor tasksCursor = mDbHelper.fetchAllTasks();

// apelarea functiei care permite aplicatiei sa gestioneze

// cursorul

startManagingCursor(tasksCursor);

// crearea vectorului care specifica coloanele bazei de date din

// care vor fi extrase datele

String[] from = new String[] { TaskManagerDbAdapter.KEY_TITLE,

TaskManagerDbAdapter.KEY_EXPIRATION, NotesDbAdapter.KEY_POI };

// crearea vectorului care specifica view-urile in care vor fi

// afisate datele extrase anterior

int[] to = new int[] { R.id.task_row_title,

R.id.task_row_expiration, R.id.task_row_name };

// obtinerea unei instante a clasei SimpleCursorAdapter

SimpleCursorAdapter tasks = new SimpleCursorAdapter(this,

R.layout.task_manager_row, tasksCursor, from, to);

// setarea adaptorului setListAdapter(tasks);

}

Astfel, în listă vor fi afișate, pe coloane, titlul fiecărui task împreună cu numele punctului de

interes și data și ora la care expiră.

ProximityIntentReceiver.java

Clasa ProximityIntentReceiver este o extensie a clasei native BroadcastReceiver destinată interceptării intenturilor referitoare la alertarea utilizatorului când se află în apropierea unui punct de interes pentru care și-a definit o alertă. Clasa moștenește metoda onReceive() care primește ca parametri contextul curent și intent-ul cu care a fost înregistrat receiverul. La executarea funcției, mai întâi se obține din intent o cheie pe baza căreia se determină dacă terminalul mobil intră în aria definită prin valoarea razei cercului (vezi adăugarea unui task) sau iese din această arie.

Dacă se detectează intrarea terminalului mobil în regiunea de interes, se obține o instanță a clasei NotificationManager care va fi ulterior folosită pentru crearea unei notificări în bara de stare (vezi capitolul 3.4.5). Deoarece se dorește ca atunci când se face click pe această notificare să fie lansat ecranul în care poate fi vizualizat task-ul, se definește, de asemenea, un intent care va lansa activitatea TaskManagerActivity. Mai departe, notificarea propriu-zisă este obținută prin crearea unui obiect de tipul Notification care este inițializat cu valoarea determinată de apelarea metodei createNotification(). Pentru notificarea nou creată se apalează metoda setLatestEventInfo() care determină textul ce va fi afișat în cadrul notificării și, de asemenea, metodei îi este transmis ca parametru pending intentul folosit pentru lansarea intentului care rulează la rândul să activitatea de vizualizare a task-ului. De asemnea, se primește parametrul extra inclus în intentul trimis către receiver și care se referă la numele punctului de interes.

Cu ajutorul instanței clasei NotificationManager se apelează funcția notify() care afișează notificarea în bara de stare. Pentru a fi posibil acest lucru, la fel ca în cazul descris mai sus, se preia parametru extra din intentul lansat către receiver și care se referă la indexul request code.

@Override

public void onReceive(Context context, Intent intent) {

// creaza si extrage valoarea cheii din intentul lansat pentru a

// determina intrarea/iesirea din aria de interes String key = LocationManager.KEY_PROXIMITY_ENTERING; Boolean entering = intent.getBooleanExtra(key, false);

// pe baza valorii cheii se detecteaza sau nu intrarea in aria de

// interes

if (entering) {

// obtine o instanta a clasei NotificationManager NotificationManager notificationManager = (NotificationManager) context

.getSystemService(Context.NOTIFICATION_SERVICE);

// creaza intentul care va fi lansat din notificare Intent viewTaskIntent = new Intent(context, TaskManagerActivity.class);

// creaza pending intentul care gestioneaza lansarea

// intentului

PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, viewTaskIntent, 0);

// creaza o noua notificare

Notification notification = createNotification();

// seteaza continutul notificarii

notification.setLatestEventInfo(context, "Proximity Alert!", "You are approaching: " + intent.getBundleExtra( "com.google.tourguide.taskmanager.TaskManagerActivity.")

.get("name"), pendingIntent);

// arata notificarea corespunzatoare in bara de stare

notificationManager.notify(intent.getBundleExtra( "com.google.tourguide.taskmanager.TaskManagerActivity.")

.getInt("id"), notification);

}

}

Pentru crearea unei notificări, în cadrul clasei curente a fost definită metoda createNotification() al căruit tip returnat este un obiect Notification. În cadrul metodei se obține o instanță a clasei Notification și, cu ajutorul ei, se setează pictograma care va fi afișată în bara de stare pentru indicarea primirii unei noi notificări, precum și comportamentul terminalului mobil din punctul de vedere al alertării luminoase și tactile.

private Notification createNotification() {

// creaza un nou obiect de tip notification

Notification notification = new Notification();

// specifica pictograma prin care sa fie marcata notificarea notification.icon = R.drawable.ic_menu_notifications; notification.when = System.currentTimeMillis();

// seteaza modul in care se realizeaza alertarea vizuala si

// auditiva

notification.flags |= Notification.FLAG_AUTO_CANCEL;

notification.flags |= Notification.FLAG_SHOW_LIGHTS;

notification.defaults |= Notification.DEFAULT_VIBRATE; notification.defaults |= Notification.DEFAULT_LIGHTS; notification.ledARGB = Color.WHITE; notification.ledOnMS = 1500;

notification.ledOffMS = 1500;

// returneaza notificarea

return notification;

}

4.2.5.8. Impelmentarea modulului pentru indicații privind ghidarea și itinerar

Alături de hărți, modulul aplicației care gestionează partea ce se ocupă de preluarea și afișarea indicațiilor cu privire la ghidarea în oraș este una părțile critice ale aplicației. Clasele care implementează aceste funcții sunt distribuite în mai multe pachete depinzând de menierea pe care o indeplinește fiecare. Astfel, în pachetul com.google.tourguide.directions se regăsesc clasele care se ocupă de parsarea codului KML, acesta fiind formatul în care serverul Google returnează datele. Mai departe, aceste clase interpretează codul și pe baza informațiilor extrase este desenată ruta de la poziția curentă la punctul de interes cu ajutorul metodelor implementate de clasa RouteOverlay din pachetul com.google.tourguide.gmaps.overlays și care a fost descrisă la punctul 4.3.4.2. Trebuie menționat faptul că partea de cod cu ajutorul căreia se realizează parsarea codului KML a fost preluată din referința bibliografică [17]. Așadar în lucrarea curentă nu va fi descris modul de implementare pentru pasajul de cod respectiv, dar acesta va putea fi consultat în cadrul anexei.

Tot în pachetul com.google.tourguide.directions se găsește clasa ce implementează activitatea care va fi lansată în momentul în care se dorește atingerea unei anumite destinații reprezentată de un obiectiv. De asemenea, în subpachetul com.google.tourguide.directions.itinerary au fost implementate două clase care permit construirea unui itinerar reprezentat de mulțime de puncte de interes.

DirectionsActivity.java

Clasa DirectionsActivity este o extindere a clasei de bază native MapActivity și conține activitatea care este lansată atunci când din meniul de opțiuni al unui obiectiv este apăsat butonul Directions. Așadar, după cum este natural, activitatea curentă furnizează indicații cu privire la ruta ce trebuie parcursă pentru a ajunge la punctul de interes respectiv.

Deoarece este o derivare a unei subclase a clasei Activity, această clasă moștenește metoda onCreate() la care s-a făcut referire în cadrul părții teoretice în paragraful care tratează ciclul de viață al unei activități. La pornirea activității, atunci când se execută metoda onCreate(), prin prisma operațiilor definite pentru această funcție se setează în primul rând layout-ul folosit în această activitate. Se obține apoi harta ce va fi afișată, precum și obiectul MapController ce este utilizat pentru controlul hărții. De asemenea, pe baza obiectului din clasa LocationManager se setează parametri pentru obținerea actualizărilor de la modului GPS necesare ghidării în spațiu. Totodată este apelată metoda getServicesLists() prin a cărei executare se preiau din managerul de liste o serie de elemente necesare definirii caracteristicilor punctului de interes destinatie din această activitate. În fine, pentru ca activitatea să reacționeze atunci când utilizatorul a atins destinația dorită a fost necesară implementarea metodei setProximityAlertForStoppingDrivingDirections().

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

// starea layoutului activitatii setContentView(R.layout.route);

// obtinerea hartii din directorul de resurse mapView = (MapView) findViewById(R.id.mapview);

// activarea butoanelor pentru control zoom mapView.setBuiltInZoomControls(true);

// obtinerea controlerului pentru gestionarea hartii mapController = mapView.getController();

// asignarea variabilei de tipul LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

// setarea obtinerii actualizarilor de la modulul GPS

locationManager.requestLocationUpdates(LocationManager

.GPS_PROVIDER, 0, 0, new GeoUpdateHandler());

// preluarea de la manager a listelor cu caracteristicile //

// punctului destinatie getServicesLists();

// setarea detectiei atingerii punctului de interes setProximityAlertForStoppingDrivingDirections();

}

Deoarece actualizările de la modulul GPS trebuiesc dezactivate atunci când activitatea nu se află în prim plan și să fie reactivate atunci când această revine în vârful stivei de activități, a fost necesară suprascrierea metodelor onPause() și onResume(). Astfel, în metoda onPause(), prin apelarea funcției removeUpdates(), modulul GPS nu va mai furniza actualizări privitoare la locație. Invers, prin apelul funcției requestLocationUpdates() în metoda onResume(), actualizările vor fi reactivate.

@Override

public void onResume() {

// apeleaza metoda din clasa de baza

super.onResume();

// activeaza primirea actualizarilor de la modulul GPS

locationManager.requestLocationUpdates(LocationManager

.GPS_PROVIDER, 0, 0, geoUpdateHandler);

}

@Override

public void onPause() {

//apeleaza metoda din clasa de baza

super.onPause();

// dezactiveaza primirea actualizarilor de la modulul GPS

locationManager.removeUpdates(geoUpdateHandler);

}

Metoda startDrivingDirections() este cea care se execută în cadrul funcției onLocationChanged() din clasa interioară GeoUpdateHandler, de fiecare dată când este primită o actualizare de la modulul GPS. Funcția conține o instanță anonimă a clasei Thread care crează un nou fir de execuție pentru efectuarea operațiunilor. În acest thread prima dată este preluat de la manager modul în care se dorește parcurgerea rutei. Modul a fost pasat către manager în momentul în care, din cadrul submeniului deschis de apăsarea butonului Directions din meniul de opțiuni, a fost selectată modalitatea Driving sau Walking. Mai departe, se apelează metoda getUrl() din clasa RoadProvider ai cărei parametri sunt latitudinea și longitudinea poziției curente și a punctului de destinație, precum și modul preluat de la manager. Funcția returnează un obiect de tip String cu ajutorul căreia este deschis un flux de intrare pe care se preiau datele de la server. Fluxul de intrare este transmis ca parametru metodei getRoute() din clasa RoadProvider care va returna un obiect de tipul Road. Acest obiect este pasat unui handler care se ocupă, printre altele, de desenarea rutei.

private void startDrivingDirections() {

//creaza un nou fir de executie

new Thread() {

// suprascrie metoda run

@Override

public void run() {

// preia de la manager modul in care se doreste

// parcurgerea itinerariului

mode = DrivingManager.getInstance().getDrivingMode();

// constuieste URL-ul pe baza caruia se realizeaza

// conexiunea la server

String url = RoadProvider.getUrl(fromLat, fromLon, toLat,toLon, mode);

// preia datele de la server intr-un flux de intrare

InputStream is = getConnection(url);

// creaza un obiect de tipul Road

mRoad = RoadProvider.getRoute(is);

// paseaza obiectul creat catre handler

mHandler.sendEmptyMessage(0);

}

// porneste firul de executie

}.start();

}

Detectarea faptului că terminalul mobil a atins punctul destinație se face în metoda setProximityAlertForStoppingDrivingDirections(). Acesta folosește facilitățile oferite de paltforma Android și anume posibilitatea de a detecta intrarea într-o arie de interes a unui terminal mobil. Astfel, în această metodă este construit un pending intent pe baza unui intent, iar acesta se va ocupa de transmiterea intentului pe care il conține către un broadcast receiver. Prin apelarea metodei setProximityAlert() din clasa LocationManager se setează o arie circulară cu raza de 20 de metri al cărei centru este definit de perechea latitudine/longitudine a punctului destinație. Pentru trimiterea intentului către receiver este nevoie de constuirea unui filtru, după care se realizează înregistrarea broadcast receiverului pentru a primi intenturi.

private void setProximityAlertForStoppingDrivingDirections() {

// construieste intentul care va fi lansat catre receiver

Intent returnIntent = new Intent(

"android.intent.action.PROXIMITY_ALERT");

// defineste pending intentul care se ocupa de gestionarea

// intentului

PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0,

returnIntent, 0);

// adauga o alerta pentru detectarea situarii langa POI locationManager.addProximityAlert(toLat, toLon, 20f, -1, pendingIntent);

// seteaza filtrul pentru inregistrarea receiverului

IntentFilter intentFilter = new IntentFilter(

"android.intent.action.PROXIMITY_ALERT");

// inregistreaza broadcast receiverul

registerReceiver(broadcastReceiver, intentFilter);

}

Pentru implementarea receiverului care să prindă intenturile, a fost creată o instanță anonimă din clasa BroadcastReceiver. În cadrul acestei metode a fost suprascrisă metoda onReceive() care primește ca parametri contexul curent al aplicației, precum și intentul care a fost lansat către receiver. Se extrage, din intentul recepționat, cheia a cărei valoare indică dacă se intră sau se iese din cadrul arie de interes. Dacă cheia are valoarea care indică intrarea în aria de interes, activitatea curentă va fi terminată deoarece s-a ajuns în punctul destinație.

private void setProximityAlertForStoppingDrivingDirections() {

// construieste intentul care va fi lansat catre receiver Intent returnIntent = new Intent( "android.intent.action.PROXIMITY_ALERT");

// defineste pending intentul care se ocupa de gestionarea

// intentului

PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0,

returnIntent, 0);

// adauga o alerta pentru detectarea situarii langa POI locationManager.addProximityAlert(toLat, toLon, 20f, -1, pendingIntent);

// seteaza filtrul pentru inregistrarea receiverului

IntentFilter intentFilter = new IntentFilter(

"android.intent.action.PROXIMITY_ALERT");

// inregistreaza broadcast receiverul

registerReceiver(broadcastReceiver, intentFilter);

}

După cum a fost menționat mai sus, obiectul de tip Road obținut în metoda startDrivingDirections() îi este pasat unui handler care, pe baza lui, desenează ruta de la punctul sursă la punctul destinație. Pe lângă aceasta, handlerul mai este folosit și pentru afișarea într-un textview în partea superioară a ecranului a adresei stăzii unde se află utilizatorul în mod curent, a adresei străzii unde se află punctul destinație, a distanței care a mai rămas de parcurs, precum și a vitezei curente de deplasare exprimată în km/h. Tot în handler poziția curentă și punctul de interes care reprezintă destinația finală sunt marcate prin plasarea overlay-urilor corespunzătoare. Pentru desenarea overlay-ului poziției curente, se folosește clasa internă CurrentPositionOverlay, iar pentru marcarea punctului destinație se folosesc metodele din clasa MapItemizedOverlay. Handlerul realizează la fiecare actualizare primită de la GPS ștergerea tuturor overlay-urilor de pe hartă, prin apelarea metodei invalidate(), și redesenarea lor corespunzător noilor date. Astfel, ruta va fi în permanență desenată pornind de la poziția curentă a utilizatorului.

Handler mHandler = new Handler() {

public void handleMessage(Message msg) {

// creaza textview-ul in care vor fi afisati parametri

// deplasarii

TextView textView = (TextView)

findViewById(R.id.description);

String newLine = System.getProperty("line.separator");

// seteaza in textview afisarea adreselor sursa si

// destinatie, distantei care a ramas de parcurs si a

// vitezei in km/h

textView.setText(mRoad.mName + newLine + mRoad.mDescription

+ newLine + "Speed: " + speed + "km/h");

// crearea unei instante a clasei CurrentPositionOverlay

// pentru marcarea pozitiei curente currentPositionOverlay = new CurrentPositionOverlay();

// crearea unei instante a clasei RouteOverlay pentru

// desenarea rutei

routeOverlay = new RouteOverlay(mRoad, mapView);

// specificarea punctului unde se doreste marcarea

// destinatiei

currentPositionOverlay.setPointToDraw(currentPosition);

// obtinerea listei de overlay-uri

List<Overlay> listOfOverlays = mapView.getOverlays();

// incarcarea din directorul de resurse a imaginii care va

// marca destinatia

marker = mapView.getContext().getResources()

.getDrawable(R.drawable.marker);

marker.setBounds(0, 0, marker.getIntrinsicWidth(), marker.getIntrinsicHeight());

// obtinerea unei instante a clasei MapItemizedOverlay mapItemizedOverlay = new MapItemizedOverlay(marker,

mapView);

// obtinerea unei instante a clasei OverlyItem

overlayItem = new OverlayItem(POI, name, address);

// adaugarea la overlayuri a unui obiect de tipul

// OverlayItem

mapItemizedOverlay.addOverlay(overlayItem);

// sterge lista de overlay-uri

listOfOverlays.clear();

// adauga overlay-ul pozitiei curente pe harta

listOfOverlays.add(currentPositionOverlay);

// adauga overlay-ul punctului destinatie pe harta listOfOverlays.add(mapItemizedOverlay);

// adauga overlay-ul rutei pe harta listOfOverlays.add(routeOverlay);

// sterge toate overlay-urile de pe harta

mapView.invalidate();

};

};

Metoda getConnection() a fost creată pentru a facilita descărcarea datelor de pe server pe baza URL-ului pe care îl primește ca și parametru. Prima dată este creat un obiect de tip InputStream căruia îi este asignată valoarea determinată de apelarea metodei getInputStream() din clasa URLConnection. Înainte de acesta, însă, este deschisă conexiunea prin apelarea metodei openConnection(). Metoda returnează un obiect de tipul InputStream.

private InputStream getConnection(String url) {

// creaza un obiect de tipul InputStream

InputStream is = null;

try {

// deschide conexiunea cu serverul

URLConnection conn = new URL(url).openConnection();

is = conn.getInputStream();

} catch (MalformedURLException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

// obiectul returnat de metoda

return is;

}

Așa cum a fost menționat la începutul descrierii clasei curente, pentru definirea punctului destinație este necesară preluarea de la manager a listelor care se referă la anumite caracteristici pentru definirea obiectivului destinație. Preluarea acestor liste se face la pornirea activității prin apelarea metodei getServicesLists() în metoda onCreate(). Tot în această metodă sunt extrase din liste numele, adresa, latitudinea și longitudinea punctului de interes pe baza indexului acestuia și folosind perechea latitudine/longitudine este creat un obiect de tipul GeoPoint care va fi folosit pentru poziționarea overlay-ului punctului destinație.

private void getServicesLists() {

// preia indexul punctului de interes

index = servicesListManagerInstance.getIndex();

// preia lista de nume

nameList = servicesListManagerInstance.getNameList();

// preia lista de adrese

addressList = servicesListManagerInstance.getAddressList();

// preia lista de latitudini

latList = servicesListManagerInstance.getLatList();

// preia lista de longitudini

lonList = servicesListManagerInstance.getLonList();

// extrage din lista de nume denumirea punctului de interes

name = nameList.get(index);

// extrage din lista de adrese adresa punctului de interes address = addressList.get(index);

// extrage din lista de latitudini latitudinea punctului de

// interes

toLat = latList.get(index);

// extrage din lista de longitudini longitudinea punctului de

// interes

toLon = lonList.get(index);

// defineste un obiect de tip GeoPoint pe baza latitudinii si

// longitudinii

POI = new GeoPoint((int) (toLat * 1E6), (int) (toLon * 1E6));

}

Pentru a obține actualizări de la modulul GPS este necesară obținerea instanței unei clase care să implementeze interfața LocationListener. În acest sens a fost construită clasa interioară GeoUpdateHandler care conține o serie de metode pentru lucrul cu modulul GPS. Astfel, ea implementează din cadrul interfeței amintite metoda onLocationChanged() unde, pe baza parametrului location se obțin latitudinea și longitudinea definitorii pentru poziția curentă pe baza cărora este construit un obiect de tipul GeoPoint folosit pentru a marca actuala locație a utilizatorului. Tot în această metodă este apelată de fiecare dată funcția animateTo() ce primește ca parametru geopoint-ul definitor pentru poziția curentă cu scopul de a centra harta la fiecare actualizare asupra utilizatorului. De asemenea, după cum a fost menționat mai sus, aici se apelează funcția startDrivingDirections().

@Override

public void onLocationChanged(Location location) {

// obtine latitudinea curenta fromLat = location.getLatitude();

// obtine longitudinea curenta fromLon = location.getLongitude();

// obtine viteza curenta

speed = (float) (location.getSpeed() * 3.6);

// construieste GeoPoint-ul pentru pozitia curenta currentPosition = new GeoPoint((int) (fromLat * 1E6),

(int) (fromLon * 1E6));

// animeaza si centreaza harta la pozitia curenta mapController.animateTo(currentPosition);

// seteaza valoara zoom-ului la maxim

mapController.setZoom(mapView.getMaxZoomLevel());

startDrivingDirections();

}

Alte metode prezente în această clasă sunt onProviderEnabled() și onProviderDisabled() și în ele se execută operații atunci când modulul GPS este activat, repsectiv dezactivat. În acest caz, așa cum a fost descris și în situația altor clase care implementează interfața LocationListener, se folosește facilitatea de sinteză de voce cu ajutorul căreia i se comunică utilizatorului asupra pornirii sau opririi recepționării actualizărilor de la modulul GPS.

Clasa interioară CurrentPositionOverlay este o derivare a clasei Overlay, iar scopul său este acela de a desena, pe hartă, poziția curentă a utilizatorului. Structura clasei este destul de simplă: moștenște și suprascrie metoda draw() din clasa de bază și implementează o metodă setter pentru setarea punctului care se dorește a fi desenat.

private class CurrentPositionOverlay extends Overlay {

private GeoPoint pointToDraw;

// seteaza punctul unde va fi desenat overlay-ul

public void setPointToDraw(GeoPoint point) {

pointToDraw = point;

}

@Override

public boolean draw(Canvas canvas, MapView mapView, boolean

shadow, long when) {

// apeleaza metoda din clasa de baza

super.draw(canvas, mapView, shadow);

// creaza un obiect de tip Point

Point screenPts = new Point();

// specifica desenarea obiectului creat mapView.getProjection().toPixels(pointToDraw, screenPts);

// preia din directorul de resurse imaginea care va marca

// pozitia curenta

Bitmap bmp = BitmapFactory.decodeResource(getResources(),

R.drawable.current_position);

// deseneaza punctul pe harta

canvas.drawBitmap(bmp, screenPts.x, screenPts.y – 30, null);

return true;

}

}

ItineraryListActivity.java

Clasa ItineraryListActivity conține activitatea ce este lansată la apăsara butonului Itinerary din meniul principal al aplicației. Ea este o derivare a clasei native de baza Activity, așadar moștenește din aceasta metoda onCreate() în cadrul căreia se aplează metoda buildItineraryAndNameList() care are drept scop construirea listei pentru a fi afișata utilizatorului. Pentru afișare este necesară încărcarea din directorul de resurse a layout-ului activității curente. De asemenea, tot în metoda onCreate(), se apelează funcția populateListView() în care se realizează afișarea efectivă a numelor punctelor de interes care alcătuiesc itinerariul și se înregistrează meniul de context pentru listă.

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

// incarca layout-ul folosit in activitate setContentView(R.layout.itinerary_list);

// construieste lista de nume care va fi afisata buildItineraryAndNameList();

// populeaza listview-ul cu inregistrarile din lista de nume populateListView();

// inregistreaza meniul de context pentru lista

registerForContextMenu(listView);

}

În metoda buildItineraryAndNameList() sunt reținute într-o serie de array list-uri înregistrările din baza de date care vor fi afișate pe ecran. Crearea array list-urilor este necesară pentru folosirea adaptorului care face legătura între partea de date și partea de view. Prima dată este obținută o instanță a clasei adaptor pentru accesul la baza de date și cu ajutorul acesteia este deschisă conexiunea prin apelarea metodei open(). Se apelează apoi funcția fetchAllItineraryPoints() și toate înregistrările din tabelul bazei de date vor fi reținute într- o variabilă de tip Cursor. De asemenea, se folosește metoda startManagingCursor() pentru a permite sistemului de operare gestionarea curosrului. Se poziționează cursorul pe prima linie și fiecare înregistrare este introdusă în lista corespunzătoare pe baza indexului coloanei, iar după ce toate înregistrările au fost citite din cursor acesta se închide prin apelarea metodei close(). În final se pasează, către manager, lista care conține obiectele de tip Location ce descriu coordonatele punctelor ce alcătuiesc itinerariul și lista de nume a acelorași obiective. Ele sunt

necesare în activitatea ItineraryDirectionsActivity.

private void buildItineraryAndNameList() {

// obtine instanta clasei adaptor mDbHelper = new ItineraryDbAdapter(this);

// deschide conexiunea la baza de date mDbHelper.open();

// retine toate inregistrarile din baza de date in variabila

// cursor

cursor = mDbHelper.fetchAllItineraryPoints();

// permite sistemului de operare sa gestioneze cursorul

startManagingCursor(cursor);

// pozitioneaza cursorul pe prima linie

if (cursor.moveToFirst()) {

// extrage inregistrarile in listele corespunzatoare

do {

……………………………………………. index = cursor.getInt(cursor.getColumnIndex("_id"));

…………………………………………….

indexList.add(index);

} while (cursor.moveToNext());

}

// inchide cursorul

if (cursor != null && !cursor.isClosed()) {

cursor.close();

}

// paseaza listele catre manager

itineraryManagerInstance.setItineraryList(itineraryList);

itineraryManagerInstance.setItineraryNameList(itineraryNameList);

}

Popularea listei cu înregistrările din lista de nume construită anterior se face în metoda populateListView(). In această metodă este încărcată mai întâi lista din directorul de resurse. Apoi se apelează metoda setAdapter() ce crează o instanță a clasei ArrayAdapter care primește ca parametri contextul curent, layout-ul listei și array list-ul din care conține datele de afișat. De asemenea, tot în metoda curentă se folosesc facilitățile de sinteză de voce pentru a comunica utulizatorului numărul de elemente conținute în listă.

private void populateListView() {

// incarca din directorul de resurese lista

listView = (ListView) findViewById(R.id.itinerary_list);

// seteaza adaptorul care face legatura intre parte de view si

// partea de date

listView.setAdapter(new ArrayAdapter<String>(this,

android.R.layout.simple_list_item_1, itineraryNameList));

// obtine instanta TTS

tts = TextToSpeechManager.getInstance().getTTS();

// comunica numarul inregistrarilor din lista

if (itineraryNameList.isEmpty()) {

if (tts != null) {

…………………………………….

}

…………………………………….

} else {

if (tts != null) {

…………………………………….

}

}

}

Pentru parcurgerea efectivă a itinerariului există butonul StartItinerary din cadrul meniului de opțiuni al clasei. La fel ca în cazul indicațiilor de ghidare către o destinație singulară și aici există posibilitatea de a alege modul de deplasare cu un autovehicul sau modul de deplasare pedestru.

Punctele itinerariului pot fi șterse prin intermediul unui meniu de context. Astfel, atunci când se detectează o atingere prelungită a unei înregistrări din listă, se va deschide un meniu care are ca singură opțiune DeletePoint. În cazul în care se apasă acest buton, se șterge înregistrarea corespunzătoare din baza de date, se reconstruiesc array list-urile prin apelarea metodei buildItineraryAndNameList() și se repopulează lista prin apelarea funcției populateListView().

ItineraryDirectionsActivity.java

Clasa ItineraryDirectionsActivity îndeplinește aproximativ aceleași funcții ca și clasa descrisă anterior. Scopul ei este acela de a crea o activitate în cadrul căreia este parcurs itinerariul construit de către utilizator prin selectarea unor puncte de interes. Clasa este o derivare a clasei de bază native MapActivity și, în consecință, conține metoda onCreate(). In plus față de aceeași metodă a clasei anterioare, aici se obține lista punctelor de interes care constituie itinerariul date sub formă de obiecte de tipul Location și lista numelor acelorași obiective.

Deoarece unul din scopurile aplicației este partea de optimizare a rutei, adică parcurgerea itinerariului astfel încât lungimea să fie minimă, clasa curentă implementează o metodă care sortează lista de puncte de interes pentru atingerea acesui țel. Așadar, metoda sortItineraryList() constuiește prima data un obiect de tip Location care se referă la poziția curentă a utilizatorului. Obiectul menționat este definit pe baza latitudinii și longitudinii curente obținute ca în cazul clasei DirectionsActivity. Se specifică apoi o distanță de referință care este dinstanța de la poziția curentă până la poziția primului punct din listă și, pentru fiecare punct de interes, se calculează distanța de la poziția actuală. Dacă distanța până la un anumit obiectiv este mai mică decât distanța de referință, se vor realiza permutările corespunzătoare în listă și noua distanță cea mai scurtă va devein distanța de referință. Algoritmul este foarte simplu, dar eficient.

private List<Location> sortItineraryList() {

// construieste un nou obiect de tip Location pentru pozitia

// actuala

startLocation = new Location("CurrentPosition");

startLocation.setLatitude(fromLat);

startLocation.setLongitude(fromLon);

// parcurge lista de puncte care alcatuiesc itinerariul

for (int i = 0; i < itineraryList.size(); i++) {

// calculeaza distanta care va fi de referinta float ref_distance = startLocation.distanceTo(itineraryList.get(0));

// calculeaza distanta pana la punctul de interes de pe

// pozitia i float distance = startLocation.distanceTo(itineraryList.get(i));

// verifica daca distanta calculata este mai mica decat cea

// de referinta

if (distance < ref_distance) {

// daca este mai mica, realizeaza permutarile necesare

// in lista de POI

temp = itineraryList.get(i);

itineraryList.set(i, itineraryList.get(0));

itineraryList.set(0, temp);

// realizeaza permutarile necesare in lista

// denumirilor POI

name = itineraryNameList.get(i);

itineraryNameList.set(i, itineraryNameList.get(0));

itineraryNameList.set(0, name);

}

}

// returneaza lista de puncte de interes

return itineraryList;

}

Modificări apar și în cazul implementării Broadcast Receiver-ului deoarece trebuiesc deosebite situațiile în care s-a detectat intrarea terminalului mobil într-o arie construită în jurul unei POI intermediar și intrarea acestuia într-o arie construită în jurul POI-ului final al itinerariului. Pentru extragerea latitudinii și longitudinii corespunzătoare obiectivului la care trebuie ajuns în mod curent, se folosește un index j care este inițializat cu valoarea 0 corespunzătoare indexului primului punct din listă. Când terminalul mobil intră în aria cu raza de 20 de metri din jurul acestui punct, se comunică numele obiectivului care a fost atins, se incrementează valoarea lui j și se execută metoda startDrivingDirections() pentru noile valori ale latitudinii și longitudinii extrase din listă pe baza indexului. Dacă s-a detectat intrarea în aria din jurul punctului destinație, condiție care se stabilește prin compararea valorii lui j cu mărimea listei de puncte de interes minus unu, se consideră că itinerariul a fost parcurs și activitatea curentă va fi terminată prin apelarea metodei finish(), dar nu înainte de a i se comunica utilizatorului că a ajuns la destinația finală.

BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

// extrage din intentul primit valoarea cheii care indica

// intrarea/iesirea din aria de interes

String key = LocationManager.KEY_PROXIMITY_ENTERING; Boolean entering = intent.getBooleanExtra(key, false);

// verifica daca punctul la care s-a ajuns este ultimul din

// lista

if (j == itineraryList.size() – 1) {

…………………………………………….

// termina activitatea curenta finish();

}

// nu s-a ajuns la ultimul punct din itinerar

} else {

// verifica daca conditia intrarii in aria de interes

// este indeplinita

if (entering) {

…………………………………………….

// incrementeaza valoarea index-ului j++;

}

}

}

};

Parcurgerea itinerariului se poate stopa înainte de a ajunge în punctul destinație prin apăsarea butonului Stop, care execută metoda finish(), din meniul de opțiuni disponibil pentru această activitate. De asemenea, trebuie menționat că metodele startDrivingDirections() și sortItineraryList() se execută în metoda onLocationChanged().

4.2.5.9. Implementarea activității care se referă la afișarea de informații detaliate

POIDetailActivity.java

Pentru ca unul din obiectivele aplicației este acela de a permite vizualizarea infirmațiilor detaliate pentru un anumit punct de interes, a fost creată o clasă care conține o activitate în acest sens. Așadar, clasa în cauză este o extindere a clasei de bază native Activity și moștenește din cadrul acesteia metoda onCreate() în care se încarcă din directorul de resurse layout-ul care va fi folosit în cadrul activității. De asemenea, se obține o instanță a clasei adaptor pentru accesul la tabelul din baza de date în care sunt salvate punctele de interes care alcătuiesc itinerariul și se deschide conexiunea către baza de date respectivă. Mai departe este apelată metoda gerServicesList() pentru a prelua din manager listele care descriu caracteristicile punctului de interes al cărui informații detaliate sunt afișate. Tot aici se apelează funcția care realizează descărcarea de pe internet a imaginilor pentru punctul de interes curent. În final, toată obiectele sunt afișate prin folosirea metodelor makeView() și setData().

În metoda getServicesLists() sunt preluate din manager listele care conțin informațiile referitoare la punctele de interes și, de asemenea, indexul punctului de interes pentru care se vizualizează informațiile în mod curent pentru a știi care înregistrări trebuiesc extrase din listă.

private void getServicesLists() {

// preia indexul obiectivului curent

index = servicesListsManagerInstance.getIndex();

// preia lista de nume

nameList = servicesListsManagerInstance.getNameList();

………………………………………………

}

Imaginile afișate în cadrul acestei activități nu sunt stocate local pentru a nu ocupa inutil spațiu în sandbox-ul aplicației. În schimb ele sunt descărcate de pe internet, iar pentru aceasta a fost creată metoda loadImage() care este apelată in mod multiplu în metoda buildImageLists(). Metoda loadImage() primește ca parametru URL-ul către serverul care stochează poza respectivă și, după decodarea acestuia, returnează un obiect de tip Drawable care este imaginea în sine. Obiectul respectiv este apoi reținut în lista de imagini. Pentru a cunoaște ce URL trebuie folosit, aplicația se folosește de indexul obiectivului.

Metoda loadImage()

private Drawable loadImage(String url) {

try {

// creaza un stream de intrare pe care vor fi citite datele

// de la server

InputStream is = (InputStream) new URL(url).getContent();

// creaza un obiect Drawable pe baza datelor preluate din

// stream

Drawable d = Drawable.createFromStream(is, "src");

// returneaza obiectul creat

return d;

} catch (Exception e) {

return null;

}

}

Metoda buildImageList()

private void buildImageList(int i) {

// sterge lista de imagini imageList.clear();

// decodeaza imaginea nr. 1 pentru punctul de interes curent image_1 = loadImage(imageLinkList_1.get(i));

// adauga imaginea nr. 1 in lista de imagini imageList.add(image_1);

……………………………………………………

}

Pentru setarea datelor în view-urile corespunzătoare a fost creată funcția setData() care primește ca parameteru indexul obiectivului curent. În această metodă este necesar prima dată să se obțină o instanță a clasei LayoutInflater necesară pentru încarcarea și crearea legăturilor între conținutul din directorul de resurse care definește view-ul curent. Este, mai apoi, creat un obiect de tipul RelativeLayout care reprezintă layout-ul de bază și pe seama căruia este creat view-ul în care vor fi conținute restul obiectelor. Se construiește apoi un obiect ImageSwitcher care constituie suportul pentru a afișa la dimensiune mai mare poza selectată în mod curent din lista galeriei. După cum am anticipat, mai departe este creat un obiect de tipul Gallery care este reprezentat sub forma unei liste orizontale ce se poate derula și care conține pozele propriu-zise. Pentru a detecta atingerile ce se realizează asupra unui element, galeria este înregistrată pentru a răspunde la clickuri. Trebuie menționat că pentru a putea creea galeria de imagini și obiectul de tip ImageSwitcher a fost necesară implementarea interfeței ViewFactory. În ecranul curent mai este, de asemenea, inclus un text view pentru a afișa datele precum numele obiectivului, facilitățile pe care acesta le oferă, etc.

Operațiunile ce se pot efectua asupra punctului de interes sunt disponibile prin intermediul unui meniu de opțiuni ce este afișat la apăsarea tastei Menu. Astfel, pentru a obține indicații asupra modului în care se poate ajunge la destinația reprezentată de punctul de interes curent, se apasă butonul Directions care va lansa activitatea DrivingDirections. Înainte de aceasta, însă, utilizatorul este interogat asupra modului de deplasare care este pasat, prin manager, către activitatea corespunzătoare. Obiectivul se poate adăuga, de asemenea, la itinerar. Acest lucru este îndepinit prin alegerea opțiunii AddToItinerary care apelează metoda addToItinerary() în cadrul căreia se obțin din liste numele, latitudinea și longitudinea obiectivului și se apelează metoda createItineraryPoint() ce primește ca argumente parametri extrași anterior. Pentru a apela numărul de telefon corespunzător punctului de interes este lansată, cu ajutorul unui intent, activitatea nativă care efectuează apeluri telefonice. În același mod se procedează și pentru vizitarea paginii web. În final, pentru obiective se pot adăuga note sau task-uri tot din meniul de opțiuni al activității curente prin lansarea activităților EditNote și TaskManagerActivit

V. REZULTATE EXPERIMENTALE

Capitolul V al lucrării de față are drept scop prezentarea unui scenariu de test pentru o mai bună înțelegere a modului în care funcționează aplicația. În cadrul acestui scenariu au fost selectate pentru a fi testate toate funcțiile aplicației: ghidarea către un obiectiv singular, adăugarea unei note pentru un obiectiv, adăugarea unui task pentru un obiectiv, apelarea și vizitarea paginii web a obiectivului dorit direct din aplicație, parcurgerea itinerariului și vizualizarea informațiilor despre starea vremii.

Figura 5.1. Ecranul de pornire, caseta de dialog pentru activarea GPS-ului și harta cu poziția curentă a utilizatorului

La pornirea aplicației este afișat ecranul de Bun Venit! prin a cărui atingere este rulată activitatea principală. Deoarece GPS-ul telefonului a fost dezactivat, aplicația întreabă dacă se dorește activarea lui. Opțiunea selectată a fost Yes și, în consecință s-a realizat trecerea la ecranul din care se pot seta preferințele pentru localizare (nu este prezentat în figura 5.1). Din ecranul menționat a fost activat modulul GPS și s-a revenit în aplicație unde s-a realizat, după o așteptare de câteva minute, localizarea poziției curente a utilizatorului.

5.1. Testarea funcției "Directions"

Pentru testarea funcției Directions, cea care oferă indicații de deplasare pentru atingerea unei destinații singulare, deplasarea s-a făcut de pe strada Ceahlău de lângă decanatul Facultății de Arhitectură până pe strada Târnavelor la obiectivul denumit ABI Hotel. Modul de deplasare ales a fost cel pedestru.

Figura 5.2. Meniul principal, submeniul categoriilor POI și lista de obiective localizate

Punctul de interes ABI Hotel a fost localizat pe hartă prin parcurgerea pașilor prezentați în figura

5.2. Astfel, din meniul principal afișat prin apăsarea tastei Menu a fost aleasă opțiunea Find Places care a dus la deschiderea submeniului ce conține categoriile de puncte de interes și s-a ales opțiunea Accommodation. S-a facut apoi comutarea în tab-ul ListView pentru o vizualizare și alegere mai facilă a punctului de interes dorit. Astfel, fiind selectată înregistrarea ABI Hotel s-a făcut comutarea înapoi la hartă, iar aceasta a fost centrată asupra obiectivului localizat și, de asemenea, valoarea zoom-ului a fost setată la maxim (vezi figura 5.3).

Figura 5.3. Obiectivul localizat, lista de detalii pentru punctul de interes și operațiunile ce

se pot efectua asupra acestuia

La detectarea unei atingeri asupra marker-ului ce marchează punctul de interes localizat, a apărut un balon care conține numele obiectivului, precum și adresa acestuia. La atingerea acestui balon a fost deschis ecranul care permite vizualizarea detaliilor pentru obiectiv. De asemenea, prin apăsarea tastei Menu a fost afișat un meniu de opțiuni din care se pot efectua diverse operațiuni. De interes este cea care facilitează deplasarea prin furnizarea de indicații asupra direcțiilor care

trebuiesc urmate pentru atingerea destinației. Astfel, din meniul de opțiuni se apasă butonul Directions care va duce la deschiderea submeniului din care se poate alege modul de parcurgere al rutei. În cazul acestui test a fost ales modul pedestru.

Figura 5.4. Ecranul pe care sunt

afișate indicațiile de ghidare

În figura 5.4. este prezentat modul în care s-a făcut ghidarea către punctul de interes destinație. Distanța parcursă a fost de aproximativ 4.8 km, iar estimarea timpului pe care a facut-o aplicația a fost bună deoarece același rezultat a fost obținut pe baza unei cronometrări alternative. De asemenea, viteza a fost și ea comparată cu una de referință și s-a constatat că ce furnizată în cadrul aplicației este foarte precisă. În momentul în care a fost atins punctul terminus, funcția de sinteză de voce a funcționat corespunzător comunicând acest fapt. De asemenea, atunci când s-a ajuns la destinație activitatea curentă a fost terminată automat exact așa cum s-a dorit în cadrul proiectării și dezvoltării aplicaței.

5.2. Testarea funcției "Add Note"

Opțiunea de a adăuga o notă pentru un anumit obiectiv a fost și ea testată în cadrul acestui scenariu. În acest sens a fost necesară navigarea la ecranul care afișează detaliile pentru punctul de interes (vezi subpunctul anterior) și alegerea din meniul de aplicație a opțiunii Add Note. Au fost introduse, în scop demonstrativ, un titlu și o descriere pentru notă și prin apăsarea butonului Save Note aceasta a fost reținută în baza de date corespunzătoare.

Figura 5.5. Adăugarea unei note pentru POI și vizualizarea tuturor notelor

Întregul proces este ilustrat în figura 5.5. De asemenea, în aceeași imagine se poate observa lista

tuturor notelor adăugate până în acel moment pentru diversele obiective. Când a fost aleasă o înregistrare din listă s-a putut vizualiza conținutul notei salvate corespunzătoare și, de asemenea, prin apăsarea butonului Delete all notes disponibil prin intermediul meniului de aplicație ce poate fi accesat prin apăsarea tastei Menu și vizibil în aceeași figură, au fost șterse toate notele simultan.

5.3. Testarea funcției "Task Manager"

Testarea funcției Task Manager a fost o procedură destul de complexă deoarece a necesitat deplasarea în teritoriu până lângă punctul de interes pentru care a fost adăugat task-ul. Pentru a fi alertat atunci când se află în apropierea unui anumit obiectiv a fost necesar adăugarea înainte a unui task. Acest lucru s-a făcut tot din meniul de opțiuni disponibil pentru punctul de interes despre care s-a vorbit anterior, prin alegerea opțiunii Add Task. Task-ul a fost programat să expire în data de 19.06.2011 la ora 21:00, suficient timp pentru a ajunge la punctul de interes pentru care a fost adăugat (Golden Tulip Ana Dome). Procesul este ilustrat în figura 5.6. Trebuie menționat faptul că au fost tratate toate trei cazurile posibile: atingerea obiectivului înainte de expirarea task-ului, atingerea obiectivului după expirarea task-ului și atingerea obiectivului după expirarea task-ului atunci când acesta a fost setată opțiunea ca utilizatorul să fie alertat chiar dacă task-ul a expirat.

În primul caz, aplicația s-a comportat corespunzător și anume, telefonul a generat o notificare la apropierea de punctul de interes, iar prin selectarea notificării aceasta a fost afișată spre a fi vizualizată. Și în celalalte două cazuri reacția a fost cea dorită, alertarea nu a făcută dacă task-ul a expirat, dar a fost generată dacă s-a selectat opțiunea de alertare chiar dacă obiectivul a fost atins după expirarea task-ului.

Figura 5.6. Ecranul de adăugarea a unui task și ecranul în care s-a generat notificarea

5.4. Testarea funcției "Itinerary"

Testarea acestei funcții nu va fi detaliată deoarece este aproape identică cu cea descrisă în secțiunea Directions. Este oarecum o generalizare a facilității prezentată în subcapitolul menționat în sensul că dacă nu s-a ajuns la punctul final din itinerar, activitatea nu va fi terminată, ci va continua oferind indicații pentru ajungerea la următorul punct din listă. Trebuie menționat faptul că așa cum a fost planificat, punctele ce alcătuiesc itinerariul au fost sortate în ordinea distanței de la poziția curentă, astfel încât întreaga distanță parcursă să fie minimă.

VI. Concluzii

Ultimul capitol al lucării este destinat concluzionării modului de implementare și de integrare al tehnologiilor și conceptelor care constituie elementele acestei aplicații.

S-a reușit dezvoltarea unei aplicații complexe ce integrează o serie de idei care sunt prezente în unele aspecte și în alte aplicații existente pe piață la ora actuală, dar niciuna dintre acestea nu a reușit concentrarea lor într-un pachet singular. De asemenea, se poate spune că proiectul curent este o fundație solidă pentru dezvoltarea, pe viitor, a unui soft care are toate șansele de a fi un concurent puternic pe o piață a aplicațiilor mobile care în ultimul deceniu a cunoscut o creștere exponențială, iar tendința pe viitor este de continuă ascendență.

Prin faptul că a fost îndeplinit scopul principal al proiectului și anume că s-a reușit integrarea unui algoritm care folosește datele preluate de pe serverul Google pentru a sugera ruta optimă pentru atingerea unei anumite destinații, au fost create premisele pentru dezvoltarea unui veritabil sistem de navigație. Așadar, în perspectivă, pincipala îmbunătățire a aplicației se cade a fi implementarea unor facilități precum ghidarea folosind funcția de sinteză de voce și crearea unui algoritm care să integreze conceptul de turn-by-turn navigation. Această sintagmă se referă la posibilitatea de a oferi utilizatorului indicații despre schimbarea direcției în fiecare interescție, despre distanța pe care o mai are de parcurs până la schimbarea direcției, precum și numele străzii pe care urmează să se situeze. Pe lângă asistența vocală oferită și despre care a fost menționat mai sus, o altă idee demnă de implementat este ghidarea acestuia cu ajutorul unor elemente grafice, cum ar fi săgeți plasate pe hartă sau pe ecran, sugerând noua direcție de deplasare.

Bazat pe partea de ghidare către o destinație singulară este și modulul care permite construirea unui itinerar și prin a cărui implementare s-a reușit indeplinirea țelului de a eficientiza deplasarea între destinații multiple. Eficiența în această aplicație se referă la ordonarea punctelor de interes în cadrul itinerariului astfel încât prin parcurgerea lor distanța totală să fie minimă. Scopul principal al minimizării distanței a fost acela de a pemite utilizarea mai eficientă a timpului, precum și de a reduce efortul fizic sau cantitatea de carburant consumată, depinzând de modul de deplasare ales. Pentru îmbunătățirea acestui modul în viitor se dorește implementarea unor facilități care să permită existența mai multor itinerarii în cadrul aplicației, precum și posibilitatea de parcurgere a acestora în mod de deplasare combinat, pedestru și cu mijloc de transport.

Localizarea punctelor de interes care este în strânsă corelație cu funcțiile concluzionate mai sus reprezintă și ea unul din pilonii aplicației, deoarece fără ea nu ar mai fi valid conceptul de ghid turistic atribuit proiectului. Prin prezența unei game largi de categorii de obiective se oferă diversitate mare utilizatorului fiindu-i permis să aleagă. S-a reușit acoperirea celor mai populare puncte de interes din municipiul Cluj-Napoca, iar gruparea acestora în categorii cu nume sugestive oferă un mod intuitiv și flexibil de căutare. Îmbunătățiri pot fi aduse, desigur și în acest caz. O idee de viitor ar fi localizarea punctelor de interes cu ajutorul unor filtre, de exemplu, pentru situația în care se dorește localizarea bancomatelor dintr-o anumită rețea bancară sau localizarea de restaurante care au un anumit specific culinar. De asemenea, o funcție interesantă ar fi aceea de a permite utilizatorului căutarea doar în interiorul unei arii de interes circulare a cărei rază poate fi definită preferențial. Pentru punctele de interes ar putea fi implementată, prin prisma freneziei curente legată de rețelele sociale, o facilitate gen send to a friend sau posibilitatea de a efectua anumite operațiuni precum rezervări la hotel sau restaurant.

Posibilitatea de adăugare de note și task-uri pentru punctele de interes reprezintă un mare avantaj prin prisma lipsei necesității utilizării unor aplicații separate în acest scop. Mai mult, o aplicație dedicată oricăruia din cele două funcții nu va putea să includă automat în cadrul notei sau a task- ului elemente care sunt de real folos pentru o mai bună gestionare a acestora. Task Manager-ul integrat în această aplicație nu este unul tipic deoarece se bazează pe definirea task-urilor atât prin intermediul coordonatelor temporale, cât și a celor spațiale.

În fine, funcția care permite consultarea condițiilor METEO este un alt element prin a cărui implementare se poate realiza o mai bună gestionare al modului în care se planifică deplasarea. Pentru aplicația de față s-a îndeplinit scopul inițial și anume, furnizarea parametrilor în legătură cu condițiile curente, precum și al modului în care aceștia vor evolua pentru o perioadă de până

la trei zile.

VII. BIBLIOGRAFIE

[1] Reto Meier, 2010, Professional Android 2 Application Development, Editura Wiley

Publishing, ISBN 978-0-470-56552-0, Inc., Indianapolis, Indiana.

[2] Sayed Hashimi, Satya Komatineni, Dave MacLean, 2010, Pro Android 2, Editura

Apres, ISBN 978-1-4302-2660-4, United States of America.

[3] Marcel Cremene, Kuderna-Iulian Bența, 2006, Dezvoltarea de aplicații pentru terminale

mobile, Editura Albastră, ISBN 978-973-650-185-2, Cluj-Napoca, România.

[4] Steve Holzner, 2006, Design Patterns For Dummies, Editura Wiley Publishing, ISBN

978-0-471-79854-5, Inc., Indianapolis, Indiana.

[5] Rick Rogers, John Lombardo, 2009, Android Application Development, 1st Edition,

Editura O'Reilly Media, ISBN 978-0-596-52147-9, Sebastopol, Canada.

[6] Ștefan Tanasă, Ștefan Andrei, Cristian Olaru, 2007, Java de la 0 la expert, Editura

Polirom, ISBN 978-973-46-0317-6, Iași, România.

[7] Chris Haseman, 2008, Android Essentials, Editura Apres, ISBN 978-1-4302-1063-4, United States of America.

[8] Mark L. Murphy, 2010, Beginning Android 2, Editura Apres, ISBN 978-1-4302-2630-7, United States of America.

[9] Jerome (J.F.) DiMarizio, 2008, Android – A Programmer's Guide, Editura McGraw-Hill, ISBN 978-972-610-176-2, United States of America.

[10] Marziah Karch, 2010, Android Productivity For Professionals, Editura Apres, ISBN

978-1-4302-3001-4, United States of America.

[11] Mark L. Murphy, 2009, Beginning Android, Editura Apres, ISBN 978-1-4302-2420-4, United States of America.

[12] Mike Hendrickson, Brian Sawyer, 2010, Best Android Apps: The Guide for Discriminating Downloaders, Editura O'Rilley Media, ISBN 978-1-449-38255-1, Sebastopol, Canada

[13] Ed Burnette, 2008, Hello Android, Copyright Ed Burnette, ISBN 978-1-934356-17-3, United States of America.

[14] Lauren Darcey, Shane Conder, 2010, Teach Yourself Android In 24 Hours, Editura

Sams, ISBN 978-0-321-67335-0, United States Of America.

[15] Jerry Ledford, Bill Zimmerly, Prasanna Amirthalingam, 2010, Editura Pearson

Education, ISBN 978-0-7897-3972-8, United States Of America. [16] Android Developers: http://developer.android.com/index.html

[17] Stack Overflow: http://stackoverflow.com/questions/tagged/android

[18] Android Development Community: http://www.anddev.org/

[19] Iqest Trip Journal: http://www.iquest.ro/pages/portofoliu/studii-de-caz/trip-journal– aplicatia-mobila-pentru-pasionatii-de-calatorii.php

ANEXE

Anexa A: Clasa BaloonItemizedOverlay.java

/* Copyright (c) 2010 readyState Software Ltd

* Licensed under the Apache License, Version 2.0 (the "License"); you may

* not use this file except in compliance with the License. You may obtain

* a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

// Pachetul din care face parte clasa

package com.google.tourguide.gmaps.overlay;

// Pachetele din cadrul carora sunt folosite elemente pentru clasa curenta

import java.lang.reflect.Method;

import java.util.List;

import android.graphics.drawable.Drawable;

import android.util.Log;

import android.view.MotionEvent;

import android.view.View;

import android.view.View.OnTouchListener;

import android.view.ViewGroup.LayoutParams;

import com.google.android.maps.GeoPoint;

import com.google.android.maps.ItemizedOverlay;

import com.google.android.maps.MapController;

import com.google.android.maps.MapView;

import com.google.android.maps.Overlay;

import com.google.android.maps.OverlayItem;

import com.google.tourguide.R;

// Clasa care reprezinta o extensie abstracta a clasei de baza

// ItemizedOverlay permitand afisarea unui balon atunci cand se detecteaza o

// atingere asupra unui marker

public abstract class BalloonItemizedOverlay<Item extends OverlayItem>

extends ItemizedOverlay<Item> {

// Declarare atribute clasa

private MapView mapView;

private BalloonOverlayView<Item> balloonView;

private View clickRegion; private int viewOffset; final MapController mc;

// Constructrul clasei care are ca paramteri balonul ce trebuie desenat

// si obiectul referitor la harta pe care este desenat

public BalloonItemizedOverlay(Drawable defaultMarker, MapView mapView) {

super(defaultMarker); this.mapView = mapView; viewOffset = 0;

mc = mapView.getController();

}

// Metoda care seteaza distanta dintre partea inferioara a balonului si

// marker. Valoarea implicita este 0

public void setBalloonBottomOffset(int pixels) {

viewOffset = pixels;

}

// Metoda getter pentru obtinerea offsetului dintre marker si balon

public int getBalloonBottomOffset() {

return viewOffset;

}

// Suprascrierea acestei metode gestioneaza o atingere asupra unui

// balon. In mod implicit nu executa nimic si returenaza valoarea

// booleana false

protected boolean onBalloonTap(int index) {

return false;

}

// Metoda mostenita din clasa de baza. Gestioneaza atingerile detectate

// asupra obiectelor de pe ecran

@Override

protected final boolean onTap(int index) {

boolean isRecycled;

final int thisIndex; GeoPoint point; thisIndex = index;

point = createItem(index).getPoint();

// Verifica daca exista un obiect de tipul Ballonview pe harta

if (balloonView == null) {

// Vaca nu exista, creaza un nou balon balloonView = createBalloonOverlayView();

// Incarca din directorul de resurse layout-ul folosit in

// interiorul balonului

clickRegion = (View) balloonView

.findViewById(R.id.balloon_inner_layout);

// Seteaza valoarea variabilei boolene la false indicand ca

// balonul nu a existat si a fost creat isRecycled = false;

} else {

// Daca balonul exista pe harta, seteaza valoarea variabilei

// boolene la true isRecycled = true;

}

// Ascunde balonul pana la setarea parametrilor din interiorul

// acestuia

balloonView.setVisibility(View.GONE);

// Obtine lista de overlay-uri care sunt afisate pe harta

List<Overlay> mapOverlays = mapView.getOverlays();

// Verifica daca sunt overlay-uri adaugate in lista

if (mapOverlays.size() > 1) {

// In acest caz, ascunde toate baloanele mai putin cel

// curent

hideOtherBalloons(mapOverlays);

}

// Seteaza datele in interiorul balonului

balloonView.setData(createItem(index));

// Variabila params este cea care defineste modul de amplasare al

// scrisului in interiorul balonului

MapView.LayoutParams params = new MapView.LayoutParams(

LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, point, MapView.LayoutParams.BOTTOM_CENTER);

params.mode = MapView.LayoutParams.MODE_MAP;

// Inregistreaza balonul pentru a detecta si raspunde la

// atingerile de pe suprafata lui setBalloonTouchListener(thisIndex);

// Afiseaza balonul pe harta balloonView.setVisibility(View.VISIBLE);

// Verifica daca balonul a existat anterior

if (isRecycled) {

// Daca a existat, schimba doar informatiile afisate in

// cadrul sau balloonView.setLayoutParams(params);

} else {

// In caz contrar, adauga la harta un nou view reprezentat

// de un obiect de tipul balon

mapView.addView(balloonView, params);

}

// Animeaza harta la markerul al carui balon este afisat mc.animateTo(point);

// Returneaza true daca balonul a fost afisat pe harta

return true;

}

// Functie care creaza view-ul reprezentat de balon. Aceasta metoda

// poate fi suprascrisa pentru a creea view-uri aditionale

protected BalloonOverlayView<Item> createBalloonOverlayView() {

return new BalloonOverlayView<Item>(getMapView().getContext(),

getBalloonBottomOffset());

}

// Metoda care returneaza variabila ce se refera la harta. Este de folos

// pentru a creea view-uri de tip balon

protected MapView getMapView() {

return mapView;

}

// Metoda folosita pentru ascunderea balonului curent

public void hideBalloon() {

// Verifica daca balonul exista in mod curent pe harta

if (balloonView != null) {

// Daca exista, fa invizibil view-ul care il reprezinta pe

// harta

balloonView.setVisibility(View.GONE);

}

}

// Metoda folosita pentru ascunderea tuturor baloanelor de pe harta, mai

// putin al celui curent

private void hideOtherBalloons(List<Overlay> overlays) {

// Parcurge lista de overlay-uri prezente pe harta

for (Overlay overlay : overlays) {

// Pune conditia ca referirea sa se faca doar la instantele

// celorlalte baloane, nu si a celui curent

if (overlay instanceof BalloonItemizedOverlay<?> && overlay

!= this) {

// Ascunde baloanele din lista de overlay-uri pentru

// care conditia este indeplinita

((BalloonItemizedOverlay<?>) overlay).hideBalloon();

}

}

}

// Metoda folosita pentru a inregistra balonul sa raspunda la atingerile

// de pe suprafata sa prin apelarea functiei onBaloonTap

private void setBalloonTouchListener(final int thisIndex) {

try {

@SuppressWarnings("unused")

// Referire la metoda onBaloonTap

Method m = this.getClass().getDeclaredMethod("onBalloonTap",

int.class);

// Creeaza un onClickListener pentru ca balonul sa dtecteze

// si sa reactioneze la atingerile de pe suprafata sa clickRegion.setOnTouchListener(new OnTouchListener() {

public boolean onTouch(View v, MotionEvent event) {

// Creeaza un nou view care contine layoutul ce

// defineste balonul

View l = ((View) v.getParent())

.findViewById(R.id.balloon_main_layout);

Drawable d = l.getBackground();

// Daca se detecteaza faptul ca balonul este

// atins

if (event.getAction()==MotionEvent.ACTION_DOWN){

// Seteaza background-ul acestuia astfel

// incat sa se indice faptul ca balonul

// este apasat int[] states = { android.R.attr.state_pressed }; if (d.setState(states)) {

d.invalidateSelf();

}

return true;

// Daca se detecteaza faptul ca balonul nu

// mai este atins

} else if (event.getAction() ==

MotionEvent.ACTION_UP) {

// Seteaza background-ul corespunzator si

// apeleaza metoda onBaloonTap

int newStates[] = {};

if (d.setState(newStates)) {

d.invalidateSelf();

} onBalloonTap(thisIndex); return true;

} else {

return false;

}

}

});

} catch (SecurityException e) { Log.e("BalloonItemizedOverlay","setBalloonTouchListener reflection SecurityException");

return;

} catch (NoSuchMethodException e) {

return;

}

}

}

Anexa B: Clasa BaloonOverlayView.java

/*

* Copyright (c) 2010 readyState Software Ltd

* Licensed under the Apache License, Version 2.0 (the "License"); you may

* not use this file except in compliance with the License. You may obtain

* a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

// Pachetul din care face parte clasa

package com.google.tourguide.gmaps.overlay;

// Pachetele din cadrul carora sunt folosite elemente ale clasei

import android.content.Context;

import android.view.Gravity;

import android.view.LayoutInflater;

import android.view.View;

import android.widget.FrameLayout;

import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView;

import com.google.android.maps.OverlayItem;

import com.google.tourguide.R;

// Clasa care defineste un obiect de tip View pentru a crea un balon ce va fi

// afisat deasupra unui marker, pe harta

public class BalloonOverlayView<Item extends OverlayItem> extends FrameLayout

{

// Declararea atributelor clasei private LinearLayout layout; private TextView title;

private TextView snippet;

// Constructorul clasei folosit pentru a crea un obiect de tipul

// BaloonOverlayView

public BalloonOverlayView(Context context, int balloonBottomOffset) {

// Apeleaza constructorul clasei de baza

super(context);

// Seteaza parametri pozitiei balonului fata de marker setPadding(10, 0, 10, balloonBottomOffset);

// Creaza un obiect de tipul LinearLayout care va fi folosit

// pentru gazduirea elementelor ce alcatuiesc balonul layout = new LinearLayout(context);

layout.setVisibility(VISIBLE);

// Obtine o instanta a clasei LayoutInflater folosita pentru a

// incarca elementele din directorul de resurse

LayoutInflater inflater = (LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

// Incarca din fisierul XML layout-ul pentru balon si fa legatura

// dintre acesta si ViewGroup-ul din care face parte

View v = inflater.inflate(R.layout.balloon_overlay, layout);

// Incarca obiectul de tip TextView care va contine titlul si

// adauga-l la view group

title = (TextView) v.findViewById(R.id.balloon_item_title);

// Incarca obiectul de tip TextView care va contine descrierea si

// adauga-l la view group

snippet = (TextView) v.findViewById(R.id.balloon_item_snippet);

// Incarca imaginea care va constitui partea grafica a butonului

// pentru inchiderea balonului si adaug-o la view group

ImageView close = (ImageView)

v.findViewById(R.id.close_img_button);

// Inregistreaza butonul de inchidere pentru a raspunde la

// atingeri

close.setOnClickListener(new OnClickListener() {

public void onClick(View v) {

// Daca se detecteaza un click pe butonul de

// inchidere, ascunde layout-ul care defineste balonul

// curent

layout.setVisibility(GONE);

}

});

// Creaza un obiect de tip FrameLayout care constituie suportul

// pentru textul afisat in interiorul balonului

FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(

LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

params.gravity = Gravity.NO_GRAVITY;

// Adauga obiectul nou creat la view group addView(layout, params);

}

// Metoda folosita pentru setarea datelor in interiorul balonului

public void setData(Item item) {

// Seteaza ca layout-ul ce defineste balonul sa fie vizibil layout.setVisibility(VISIBLE);

// Daca titlul ce va aparea in cadrul balonului este valid

if (item.getTitle() != null) {

// Daca este valid, seteaza-l sa fie vizibil title.setVisibility(VISIBLE);

// Seteaza textul propriu-zis

title.setText(item.getTitle());

} else {

// Daca titlul nu este valid, seteaza-l sa fie invizibil title.setVisibility(GONE);

}

// Idem pentru textul care se refera la descriere

if (item.getSnippet() != null) {

snippet.setVisibility(VISIBLE);

snippet.setText(item.getSnippet());

} else {

snippet.setVisibility(GONE);

}

}

}

Anexa C: Clasa RoadProvider.java

// Pachetul din care face parte clasa

package com.google.tourguide.directions;

// Pachetele din cadrul carora au fost folosite elemente pentru constuirea

// clasei

import java.io.IOException;

import java.io.InputStream;

import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;

import javax.xml.parsers.SAXParser;

import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;

import org.xml.sax.SAXException;

import org.xml.sax.helpers.DefaultHandler;

// Clasa cu ajutorul careia se preiau de pe serverul Google datele

// referitoare la trasarea rutei

public class RoadProvider {

// Metoda folosita pentru parsarea codului KML returnat de server

public static Road getRoute(InputStream is) {

// Obtine un obiect de tipul KMLHandler pentru gestionarea

// fisierului KML

KMLHandler handler = new KMLHandler();

try {

// Obtine o instanta a clasei SAXParser folosita pentru

// parsarea codului KML

SAXParser parser = SAXParserFactory.newInstance().newSAXParser();

// Parseaza codul obtinut prin streamul de intrare, folosind

// un handler

parser.parse(is, handler);

} catch (ParserConfigurationException e) {

e.printStackTrace();

} catch (SAXException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

// Returneaza ruta

return handler.mRoad;

}

// Functia folosita pentru construirea URL-ului pe baza caruia se

// stabileste conexiunea la servers

public static String getUrl(double fromLat, double fromLon, double

toLat, double toLon, String mode) {

// Construieste un obiect de tipul StringBuffer

StringBuffer urlString = new StringBuffer();

// Adauga la obiectul construit elementele ce il definesc urlString.append("http://maps.google.com/maps?f=d&hl=en");

// Punctul care defineste pozitia de unde se incepe trasarea rutei

urlString.append("&saddr="); urlString.append(Double.toString(fromLat)); urlString.append(","); urlString.append(Double.toString(fromLon));

// Punctul care defineste pozitia unde se termina trasarea rutei urlString.append("&daddr="); urlString.append(Double.toString(toLat));

urlString.append(",");

urlString.append(Double.toString(toLon));

// Modul in care se doreste parcurgerea rutei urlString.append("&dirflg="); urlString.append(mode);

// Formatul in care se doreste returnarea datelor, in acest caz

// fisier KML

urlString.append("&ie=UTF8&0&om=0&output=kml");

return urlString.toString();

}

}

// Clasa interioara care defineste un handler pentru gestionarea datelor

// obtinute

class KMLHandler extends DefaultHandler {

// Declararea atributelor clasei

Road mRoad;

boolean isPlacemark;

boolean isRoute;

boolean isItemIcon;

private Stack<String> mCurrentElement = new Stack<String>();

private String mString;

// Constructorul clasei

public KMLHandler() {

// Creaza un obiect de tipul Road mRoad = new Road();

}

public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {

mCurrentElement.push(localName);

if (localName.equalsIgnoreCase("Placemark")) {

isPlacemark = true;

mRoad.mPoints = addPoint(mRoad.mPoints);

} else if (localName.equalsIgnoreCase("ItemIcon")) {

if (isPlacemark)

isItemIcon = true;

}

mString = new String();

}

public void characters(char[] ch, int start, int length)

throws SAXException {

String chars = new String(ch, start, length).trim();

mString = mString.concat(chars);

}

public void endElement(String uri, String localName, String name)

throws SAXException {

if (mString.length() > 0) {

if (localName.equalsIgnoreCase("name")) {

if (isPlacemark) {

isRoute = mString.equalsIgnoreCase("Route");

if (!isRoute) {

mRoad.mPoints[mRoad.mPoints.length –

1].mName = mString;

}

} else {

mRoad.mName = mString;

}

} else if (localName.equalsIgnoreCase("color") &&

!isPlacemark) {

mRoad.mColor = Integer.parseInt(mString, 16);

} else if (localName.equalsIgnoreCase("width") &&

!isPlacemark) {

mRoad.mWidth = Integer.parseInt(mString);

} else if (localName.equalsIgnoreCase("description")) {

if (isPlacemark) {

String description = cleanup(mString);

if (!isRoute)

mRoad.mPoints[mRoad.mPoints.length –

1].mDescription = description;

else

}

mRoad.mDescription = description;

} else if (localName.equalsIgnoreCase("href")) {

if (isItemIcon) {

mRoad.mPoints[mRoad.mPoints.length – 1].mIconUrl

= mString;

}

} else if (localName.equalsIgnoreCase("coordinates")) {

if (isPlacemark) {

if (!isRoute) {

String[] xyParsed = split(mString, ","); double lon = Double.parseDouble(xyParsed[0]);

double lat =

Double.parseDouble(xyParsed[1]);

mRoad.mPoints[mRoad.mPoints.length –

1].mLatitude = lat;

mRoad.mPoints[mRoad.mPoints.length –

1].mLongitude = lon;

} else {

String[] coodrinatesParsed = split(mString, " "); mRoad.mRoute = new

double[coodrinatesParsed.length][2]; for (int i = 0; i < coodrinatesParsed.length; i++) {

String[] xyParsed =

split(coodrinatesParsed[i], ",");

for (int j = 0; j < 2 && j <

xyParsed.length; j++)

mRoad.mRoute[i][j] = Double

.parseDouble(xyParsed[j]);

}

}

}

}

}

mCurrentElement.pop();

if (localName.equalsIgnoreCase("Placemark")) {

isPlacemark = false;

if (isRoute)

isRoute = false;

} else if (localName.equalsIgnoreCase("ItemIcon")) {

if (isItemIcon)

isItemIcon = false;

}

}

private String cleanup(String value) { String remove = "<br/>";

int index = value.indexOf(remove);

if (index != -1)

value = value.substring(0, index);

remove = "&#160;";

index = value.indexOf(remove); int len = remove.length(); while (index != -1) {

value = value.substring(0, index).concat(

value.substring(index + len, value.length()));

index = value.indexOf(remove);

}

return value;

}

public Point[] addPoint(Point[] points) {

Point[] result = new Point[points.length + 1];

for (int i = 0; i < points.length; i++)

result[i] = points[i]; result[points.length] = new Point(); return result;

}

private static String[] split(String strString, String strDelimiter) { String[] strArray;

int iOccurrences = 0;

int iIndexOfInnerString = 0;

int iIndexOfDelimiter = 0;

int iCounter = 0;

if (strString == null) {

throw new IllegalArgumentException("Input string cannot be null.");

}

if (strDelimiter.length() <= 0 || strDelimiter == null) {

throw new IllegalArgumentException(

"Delimeter cannot be null or empty.");

}

if (strString.startsWith(strDelimiter)) {

strString = strString.substring(strDelimiter.length());

}

if (!strString.endsWith(strDelimiter)) {

strString += strDelimiter;

}

while ((iIndexOfDelimiter = strString.indexOf(strDelimiter,

iIndexOfInnerString)) != -1) {

iOccurrences += 1;

iIndexOfInnerString = iIndexOfDelimiter +

strDelimiter.length();

}

strArray = new String[iOccurrences];

iIndexOfInnerString = 0;

iIndexOfDelimiter = 0;

while ((iIndexOfDelimiter = strString.indexOf(strDelimiter,

iIndexOfInnerString)) != -1) { strArray[iCounter] = strString.substring(iIndexOfInnerString,iIndexOfDelimiter); iIndexOfInnerString = iIndexOfDelimiter + strDelimiter.length();

iCounter += 1;

}

return strArray;

}

}

Anexa D: Clasa WeatherForecast.java

// Pachetul din care face parte clasa

package com.google.tourguide.weatherforecast;

// Pachetele din cadrul carora s-au folosit elemente pentru construirea clasei

import java.io.ByteArrayInputStream; import java.net.MalformedURLException; import java.net.URL;

import javax.xml.parsers.SAXParser;

import javax.xml.parsers.SAXParserFactory;

import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet;

import org.apache.http.impl.client.BasicResponseHandler;

import org.apache.http.impl.client.DefaultHttpClient;

import org.xml.sax.InputSource;

import org.xml.sax.XMLReader;

import android.app.Activity; import android.os.Bundle; import android.util.Log;

import android.view.MotionEvent;

import android.view.View;

import android.view.View.OnTouchListener;

import android.view.animation.AnimationUtils;

import com.google.tourguide.R;

import com.google.tourguide.manager.WeatherCurrentConditionManager;

import com.google.tourguide.manager.WeatherForecastConditionManager;

import com.google.tourguide.weatherforecast.views.SingleCurentWeatherInfoView import com.google.tourguide.weatherforecast.view.SingleFoecastWeatherInfoView import com.google.tourguide.weatherforecast.weather.GoogleWeatherHandler; import com.google.tourguide.weatherforecast.weather.WeatherSet;

public class WeatherForecast extends Activity implements OnTouchListener {

// Declarare atribute clasa

private final String DEBUG_TAG = "WeatherForcast";

private View weatherView;

@Override

public void onCreate(Bundle icicle) {

super.onCreate(icicle);

setContentView(R.layout.weatherforecast);

weatherView = (View) findViewById(R.id.weather_relative);

weatherView.startAnimation(AnimationUtils.loadAnimation(this,

R.anim.fading_in_slow));

weatherView.setOnTouchListener(this);

try {

// URL-ul pe baza caruia se preiau datele de pe serverul

// Google

String cityParamString = "Cluj-Napoca";

String queryString =

"https://www.google.com/ig/api?weather="

+ cityParamString + "&hl=en-GB";

// Obtine o instanta a clasei SAXParserFactory si un obiect

// de tipul SAXParser

SAXParserFactory spf = SAXParserFactory.newInstance();

SAXParser sp = spf.newSAXParser();

// Obtine un interpretor de cod XML din obiectul de tip

// SAXParser

XMLReader xr = sp.getXMLReader();

// Obtine un gestionar de continut si aplica-l

// interpretorului XML

GoogleWeatherHandler gwh = new GoogleWeatherHandler();

xr.setContentHandler(gwh);

// Foloseste un client HTTP pentru gestionarea URL-ului

HttpClient httpclient = new DefaultHttpClient();

HttpGet httpget = new HttpGet(queryString.replace(" ", "%20"));

Log.d(DEBUG_TAG, "executing request " + httpget.getURI());

// Creaza un handler pentru gestionarea codului returnat de

// server

ResponseHandler<String> responseHandler = new

BasicResponseHandler(); Log.i("Respond Handler", "Step 1");

String responseBody = httpclient.execute(httpget,

responseHandler);

Log.d(DEBUG_TAG, "response from httpclient:\n " +

responseBody);

ByteArrayInputStream is = new ByteArrayInputStream(

responseBody.getBytes());

// Parseaza codul returnat de server xr.parse(new InputSource(is));

Log.d(DEBUG_TAG, "parse complete");

// Handler-ul furnizeaza datele returnate prin parsarea

// codului

WeatherSet ws = gwh.getWeatherSet();

// Updateaza view-ul pentru afisarea conditiilor curente updateWeatherInfoView(R.id.weather_today,

ws.getWeatherCurrentCondition());

updateWeatherInfoView(R.id.weather_1, ws

.getWeatherForecastConditions().get(0));

updateWeatherInfoView(R.id.weather_2, ws

.getWeatherForecastConditions().get(1));

updateWeatherInfoView(R.id.weather_3, ws

.getWeatherForecastConditions().get(2));

updateWeatherInfoView(R.id.weather_4, ws

.getWeatherForecastConditions().get(3));

} catch (Exception e) {

resetWeatherInfoViews();

Log.e(DEBUG_TAG, "WeatherQueryError", e);

}

}

// Metoda folosita pentru preluarea si setarea parametrilor conditiilor

// vremii pentru ziua curenta si urmatoarele trei zile

private void updateWeatherInfoView(int aResourceID,

WeatherForecastConditionManager aWFIS) throws

MalformedURLException {

// Construieste un URL pentru preluarea imaginii care descrie

// conditiile vremii

URL imgURL = new URL("http://www.google.com" +

aWFIS.getIconURL());

((SingleForecastWeatherInfoView) findViewById(aResourceID))

.setRemoteImage(imgURL);

// Obtine temperatura minima in grade celsius

int tempMin = aWFIS.getTempMinCelsius();

// Obtine temeperatura maxima in grade celsius

int tempMax = aWFIS.getTempMaxCelsius();

// Obtine ziua la care se refera conditiile

String day = aWFIS.getDayofWeek();

// Obtine descrierea conditiilor

String condition = aWFIS.getCondition();

// Seteaza parametri obtinuti de la server

((SingleForecastWeatherInfoView) findViewById(aResourceID))

.setDayofWeek(day);

((SingleForecastWeatherInfoView) findViewById(aResourceID))

.setTempCelciusMinMax(tempMin, tempMax);

((SingleForecastWeatherInfoView) findViewById(aResourceID))

.setCondition(condition);

}

// Metoda folosita pentru setarea parametrilor conditiilor curente

private void updateWeatherInfoView(int aResourceID,

WeatherCurrentConditionManager aWCIS) throws

MalformedURLException {

// Construieste URL-ul de unde se preia imaginea ce descrie

// conditiile curente

URL imgURL = new URL("http://www.google.com" +

aWCIS.getIconURL());

((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setRemoteImage(imgURL);

// Seteaza descrierea parametrilor pentru conditiile curente

((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setTempCelcius(aWCIS.getTempCelcius()); ((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setCondition(aWCIS.getCondition()); ((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setWindCondition(aWCIS.getWindCondition());

((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setHumidity(aWCIS.getHumidity());

}

// Metoda folosita pentru resetarea view-urilor ce descriu conditiile

// vremii la valorile implicite

private void resetWeatherInfoViews() {

((SingleCurrentWeatherInfoView) findViewById(R.id.weather_today))

.reset();

((SingleForecastWeatherInfoView)findViewById(R.id.weather_1)).reset();

((SingleForecastWeatherInfoView)findViewById(R.id.weather_2)).reset(); ((SingleForecastWeatherInfoView)findViewById(R.id.weather_3)).reset(); ((SingleForecastWeatherInfoView)findViewById(R.id.weather_4)).reset();

}

@Override

public boolean onTouch(View view, MotionEvent event) {

if (event.getAction() == MotionEvent.ACTION_DOWN) {

finish();

}

return false;

}

}

Anexa E: Clasa GoogleWeatherHandler.java

// Pachetul din care face parte clasa

package com.google.tourguide.weatherforecast.weather;

// Pachetele din cadrul carora au fost folosite elemente pentru constuirea

// clasei

import org.xml.sax.Attributes;

import org.xml.sax.SAXException;

import org.xml.sax.helpers.DefaultHandler;

import com.google.tourguide.manager.WeatherCurrentConditionManager;

import com.google.tourguide.manager.WeatherForecastConditionManager;

// Clasa care implementeaza handler-ul pentru parsarea codului returnat de

// serverul Google

public class GoogleWeatherHandler extends DefaultHandler {

// Declarare atribute clasa

private WeatherSet myWeatherSet = null;

public boolean in_forecast_information = false; private boolean in_current_conditions = false; private boolean in_forecast_conditions = false;

// Metoda pentru setarea conditiilor obtinute prin interpretarea

// codului, cu ajutorul unui obiect de tipul WeatherSet

public WeatherSet getWeatherSet() {

return this.myWeatherSet;

}

@Override

public void startDocument() throws SAXException {

this.myWeatherSet = new WeatherSet();

}

@Override

public void endDocument() throws SAXException {

}

@Override

public void startElement(String namespaceURI, String localName,

String qName, Attributes atts) throws SAXException {

if (localName.equals("forecast_information")) {

this.in_forecast_information = true;

} else if (localName.equals("current_conditions")) {

this.myWeatherSet

.setWeatherCurrentCondition(new

WeatherCurrentConditionManager());

this.in_current_conditions = true;

} else if (localName.equals("forecast_conditions")) {

this.myWeatherSet.getWeatherForecastConditions().add(

new WeatherForecastConditionManager());

this.in_forecast_conditions = true;

} else {

String dataAttribute = atts.getValue("data");

if (localName.equals("city")) {

} else if (localName.equals("postal_code")) {

} else if (localName.equals("latitude_e6")) {

} else if (localName.equals("longitude_e6")) {

} else if (localName.equals("forecast_date")) {

} else if (localName.equals("current_date_time")) {

} else if (localName.equals("day_of_week")) {

if (this.in_current_conditions) {

this.myWeatherSet.getWeatherCurrentCondition()

.setDayofWeek(dataAttribute);

} else if (this.in_forecast_conditions) {

this.myWeatherSet.getLastWeatherForecastCondition()

.setDayofWeek(dataAttribute);

}

} else if (localName.equals("icon")) {

if (this.in_current_conditions) {

this.myWeatherSet.getWeatherCurrentCondition()

.setIconURL(dataAttribute);

} else if (this.in_forecast_conditions) {

this.myWeatherSet.getLastWeatherForecastCondition()

.setIconURL(dataAttribute);

}

} else if (localName.equals("condition")) {

if (this.in_current_conditions) {

this.myWeatherSet.getWeatherCurrentCondition()

.setCondition(dataAttribute);

} else if (this.in_forecast_conditions) {

this.myWeatherSet.getLastWeatherForecastCondition()

.setCondition(dataAttribute);

}

} else if (localName.equals("temp_f")) {

this.myWeatherSet.getWeatherCurrentCondition()

.setTempFahrenheit(Integer.parseInt(dataAttribute));

} else if (localName.equals("temp_c")) {

this.myWeatherSet.getWeatherCurrentCondition()

.setTempCelcius(Integer.parseInt(dataAttribute));

} else if (localName.equals("humidity")) {

this.myWeatherSet.getWeatherCurrentCondition()

.setHumidity(dataAttribute);

} else if (localName.equals("wind_condition")) {

this.myWeatherSet.getWeatherCurrentCondition()

.setWindCondition(dataAttribute);

} else if (localName.equals("low")) {

int temp = Integer.parseInt(dataAttribute);

this.myWeatherSet.getLastWeatherForecastCondition()

.setTempMinCelsius(temp);

} else if (localName.equals("high")) {

int temp = Integer.parseInt(dataAttribute);

this.myWeatherSet.getLastWeatherForecastCondition()

.setTempMaxCelsius(temp);

}

}

}

@Override

public void endElement(String namespaceURI, String localName, String

qName)

throws SAXException {

if (localName.equals("forecast_information")) {

this.in_forecast_information = false;

} else if (localName.equals("current_conditions")) {

this.in_current_conditions = false;

} else if (localName.equals("forecast_conditions")) {

this.in_forecast_conditions = false;

}

}

@Override

public void characters(char ch[], int start, int length) {

}

}

VII. BIBLIOGRAFIE

[1] Reto Meier, 2010, Professional Android 2 Application Development, Editura Wiley

Publishing, ISBN 978-0-470-56552-0, Inc., Indianapolis, Indiana.

[2] Sayed Hashimi, Satya Komatineni, Dave MacLean, 2010, Pro Android 2, Editura

Apres, ISBN 978-1-4302-2660-4, United States of America.

[3] Marcel Cremene, Kuderna-Iulian Bența, 2006, Dezvoltarea de aplicații pentru terminale

mobile, Editura Albastră, ISBN 978-973-650-185-2, Cluj-Napoca, România.

[4] Steve Holzner, 2006, Design Patterns For Dummies, Editura Wiley Publishing, ISBN

978-0-471-79854-5, Inc., Indianapolis, Indiana.

[5] Rick Rogers, John Lombardo, 2009, Android Application Development, 1st Edition,

Editura O'Reilly Media, ISBN 978-0-596-52147-9, Sebastopol, Canada.

[6] Ștefan Tanasă, Ștefan Andrei, Cristian Olaru, 2007, Java de la 0 la expert, Editura

Polirom, ISBN 978-973-46-0317-6, Iași, România.

[7] Chris Haseman, 2008, Android Essentials, Editura Apres, ISBN 978-1-4302-1063-4, United States of America.

[8] Mark L. Murphy, 2010, Beginning Android 2, Editura Apres, ISBN 978-1-4302-2630-7, United States of America.

[9] Jerome (J.F.) DiMarizio, 2008, Android – A Programmer's Guide, Editura McGraw-Hill, ISBN 978-972-610-176-2, United States of America.

[10] Marziah Karch, 2010, Android Productivity For Professionals, Editura Apres, ISBN

978-1-4302-3001-4, United States of America.

[11] Mark L. Murphy, 2009, Beginning Android, Editura Apres, ISBN 978-1-4302-2420-4, United States of America.

[12] Mike Hendrickson, Brian Sawyer, 2010, Best Android Apps: The Guide for Discriminating Downloaders, Editura O'Rilley Media, ISBN 978-1-449-38255-1, Sebastopol, Canada

[13] Ed Burnette, 2008, Hello Android, Copyright Ed Burnette, ISBN 978-1-934356-17-3, United States of America.

[14] Lauren Darcey, Shane Conder, 2010, Teach Yourself Android In 24 Hours, Editura

Sams, ISBN 978-0-321-67335-0, United States Of America.

[15] Jerry Ledford, Bill Zimmerly, Prasanna Amirthalingam, 2010, Editura Pearson

Education, ISBN 978-0-7897-3972-8, United States Of America. [16] Android Developers: http://developer.android.com/index.html

[17] Stack Overflow: http://stackoverflow.com/questions/tagged/android

[18] Android Development Community: http://www.anddev.org/

[19] Iqest Trip Journal: http://www.iquest.ro/pages/portofoliu/studii-de-caz/trip-journal– aplicatia-mobila-pentru-pasionatii-de-calatorii.php

ANEXE

Anexa A: Clasa BaloonItemizedOverlay.java

/* Copyright (c) 2010 readyState Software Ltd

* Licensed under the Apache License, Version 2.0 (the "License"); you may

* not use this file except in compliance with the License. You may obtain

* a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

// Pachetul din care face parte clasa

package com.google.tourguide.gmaps.overlay;

// Pachetele din cadrul carora sunt folosite elemente pentru clasa curenta

import java.lang.reflect.Method;

import java.util.List;

import android.graphics.drawable.Drawable;

import android.util.Log;

import android.view.MotionEvent;

import android.view.View;

import android.view.View.OnTouchListener;

import android.view.ViewGroup.LayoutParams;

import com.google.android.maps.GeoPoint;

import com.google.android.maps.ItemizedOverlay;

import com.google.android.maps.MapController;

import com.google.android.maps.MapView;

import com.google.android.maps.Overlay;

import com.google.android.maps.OverlayItem;

import com.google.tourguide.R;

// Clasa care reprezinta o extensie abstracta a clasei de baza

// ItemizedOverlay permitand afisarea unui balon atunci cand se detecteaza o

// atingere asupra unui marker

public abstract class BalloonItemizedOverlay<Item extends OverlayItem>

extends ItemizedOverlay<Item> {

// Declarare atribute clasa

private MapView mapView;

private BalloonOverlayView<Item> balloonView;

private View clickRegion; private int viewOffset; final MapController mc;

// Constructrul clasei care are ca paramteri balonul ce trebuie desenat

// si obiectul referitor la harta pe care este desenat

public BalloonItemizedOverlay(Drawable defaultMarker, MapView mapView) {

super(defaultMarker); this.mapView = mapView; viewOffset = 0;

mc = mapView.getController();

}

// Metoda care seteaza distanta dintre partea inferioara a balonului si

// marker. Valoarea implicita este 0

public void setBalloonBottomOffset(int pixels) {

viewOffset = pixels;

}

// Metoda getter pentru obtinerea offsetului dintre marker si balon

public int getBalloonBottomOffset() {

return viewOffset;

}

// Suprascrierea acestei metode gestioneaza o atingere asupra unui

// balon. In mod implicit nu executa nimic si returenaza valoarea

// booleana false

protected boolean onBalloonTap(int index) {

return false;

}

// Metoda mostenita din clasa de baza. Gestioneaza atingerile detectate

// asupra obiectelor de pe ecran

@Override

protected final boolean onTap(int index) {

boolean isRecycled;

final int thisIndex; GeoPoint point; thisIndex = index;

point = createItem(index).getPoint();

// Verifica daca exista un obiect de tipul Ballonview pe harta

if (balloonView == null) {

// Vaca nu exista, creaza un nou balon balloonView = createBalloonOverlayView();

// Incarca din directorul de resurse layout-ul folosit in

// interiorul balonului

clickRegion = (View) balloonView

.findViewById(R.id.balloon_inner_layout);

// Seteaza valoarea variabilei boolene la false indicand ca

// balonul nu a existat si a fost creat isRecycled = false;

} else {

// Daca balonul exista pe harta, seteaza valoarea variabilei

// boolene la true isRecycled = true;

}

// Ascunde balonul pana la setarea parametrilor din interiorul

// acestuia

balloonView.setVisibility(View.GONE);

// Obtine lista de overlay-uri care sunt afisate pe harta

List<Overlay> mapOverlays = mapView.getOverlays();

// Verifica daca sunt overlay-uri adaugate in lista

if (mapOverlays.size() > 1) {

// In acest caz, ascunde toate baloanele mai putin cel

// curent

hideOtherBalloons(mapOverlays);

}

// Seteaza datele in interiorul balonului

balloonView.setData(createItem(index));

// Variabila params este cea care defineste modul de amplasare al

// scrisului in interiorul balonului

MapView.LayoutParams params = new MapView.LayoutParams(

LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, point, MapView.LayoutParams.BOTTOM_CENTER);

params.mode = MapView.LayoutParams.MODE_MAP;

// Inregistreaza balonul pentru a detecta si raspunde la

// atingerile de pe suprafata lui setBalloonTouchListener(thisIndex);

// Afiseaza balonul pe harta balloonView.setVisibility(View.VISIBLE);

// Verifica daca balonul a existat anterior

if (isRecycled) {

// Daca a existat, schimba doar informatiile afisate in

// cadrul sau balloonView.setLayoutParams(params);

} else {

// In caz contrar, adauga la harta un nou view reprezentat

// de un obiect de tipul balon

mapView.addView(balloonView, params);

}

// Animeaza harta la markerul al carui balon este afisat mc.animateTo(point);

// Returneaza true daca balonul a fost afisat pe harta

return true;

}

// Functie care creaza view-ul reprezentat de balon. Aceasta metoda

// poate fi suprascrisa pentru a creea view-uri aditionale

protected BalloonOverlayView<Item> createBalloonOverlayView() {

return new BalloonOverlayView<Item>(getMapView().getContext(),

getBalloonBottomOffset());

}

// Metoda care returneaza variabila ce se refera la harta. Este de folos

// pentru a creea view-uri de tip balon

protected MapView getMapView() {

return mapView;

}

// Metoda folosita pentru ascunderea balonului curent

public void hideBalloon() {

// Verifica daca balonul exista in mod curent pe harta

if (balloonView != null) {

// Daca exista, fa invizibil view-ul care il reprezinta pe

// harta

balloonView.setVisibility(View.GONE);

}

}

// Metoda folosita pentru ascunderea tuturor baloanelor de pe harta, mai

// putin al celui curent

private void hideOtherBalloons(List<Overlay> overlays) {

// Parcurge lista de overlay-uri prezente pe harta

for (Overlay overlay : overlays) {

// Pune conditia ca referirea sa se faca doar la instantele

// celorlalte baloane, nu si a celui curent

if (overlay instanceof BalloonItemizedOverlay<?> && overlay

!= this) {

// Ascunde baloanele din lista de overlay-uri pentru

// care conditia este indeplinita

((BalloonItemizedOverlay<?>) overlay).hideBalloon();

}

}

}

// Metoda folosita pentru a inregistra balonul sa raspunda la atingerile

// de pe suprafata sa prin apelarea functiei onBaloonTap

private void setBalloonTouchListener(final int thisIndex) {

try {

@SuppressWarnings("unused")

// Referire la metoda onBaloonTap

Method m = this.getClass().getDeclaredMethod("onBalloonTap",

int.class);

// Creeaza un onClickListener pentru ca balonul sa dtecteze

// si sa reactioneze la atingerile de pe suprafata sa clickRegion.setOnTouchListener(new OnTouchListener() {

public boolean onTouch(View v, MotionEvent event) {

// Creeaza un nou view care contine layoutul ce

// defineste balonul

View l = ((View) v.getParent())

.findViewById(R.id.balloon_main_layout);

Drawable d = l.getBackground();

// Daca se detecteaza faptul ca balonul este

// atins

if (event.getAction()==MotionEvent.ACTION_DOWN){

// Seteaza background-ul acestuia astfel

// incat sa se indice faptul ca balonul

// este apasat int[] states = { android.R.attr.state_pressed }; if (d.setState(states)) {

d.invalidateSelf();

}

return true;

// Daca se detecteaza faptul ca balonul nu

// mai este atins

} else if (event.getAction() ==

MotionEvent.ACTION_UP) {

// Seteaza background-ul corespunzator si

// apeleaza metoda onBaloonTap

int newStates[] = {};

if (d.setState(newStates)) {

d.invalidateSelf();

} onBalloonTap(thisIndex); return true;

} else {

return false;

}

}

});

} catch (SecurityException e) { Log.e("BalloonItemizedOverlay","setBalloonTouchListener reflection SecurityException");

return;

} catch (NoSuchMethodException e) {

return;

}

}

}

Anexa B: Clasa BaloonOverlayView.java

/*

* Copyright (c) 2010 readyState Software Ltd

* Licensed under the Apache License, Version 2.0 (the "License"); you may

* not use this file except in compliance with the License. You may obtain

* a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

// Pachetul din care face parte clasa

package com.google.tourguide.gmaps.overlay;

// Pachetele din cadrul carora sunt folosite elemente ale clasei

import android.content.Context;

import android.view.Gravity;

import android.view.LayoutInflater;

import android.view.View;

import android.widget.FrameLayout;

import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView;

import com.google.android.maps.OverlayItem;

import com.google.tourguide.R;

// Clasa care defineste un obiect de tip View pentru a crea un balon ce va fi

// afisat deasupra unui marker, pe harta

public class BalloonOverlayView<Item extends OverlayItem> extends FrameLayout

{

// Declararea atributelor clasei private LinearLayout layout; private TextView title;

private TextView snippet;

// Constructorul clasei folosit pentru a crea un obiect de tipul

// BaloonOverlayView

public BalloonOverlayView(Context context, int balloonBottomOffset) {

// Apeleaza constructorul clasei de baza

super(context);

// Seteaza parametri pozitiei balonului fata de marker setPadding(10, 0, 10, balloonBottomOffset);

// Creaza un obiect de tipul LinearLayout care va fi folosit

// pentru gazduirea elementelor ce alcatuiesc balonul layout = new LinearLayout(context);

layout.setVisibility(VISIBLE);

// Obtine o instanta a clasei LayoutInflater folosita pentru a

// incarca elementele din directorul de resurse

LayoutInflater inflater = (LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

// Incarca din fisierul XML layout-ul pentru balon si fa legatura

// dintre acesta si ViewGroup-ul din care face parte

View v = inflater.inflate(R.layout.balloon_overlay, layout);

// Incarca obiectul de tip TextView care va contine titlul si

// adauga-l la view group

title = (TextView) v.findViewById(R.id.balloon_item_title);

// Incarca obiectul de tip TextView care va contine descrierea si

// adauga-l la view group

snippet = (TextView) v.findViewById(R.id.balloon_item_snippet);

// Incarca imaginea care va constitui partea grafica a butonului

// pentru inchiderea balonului si adaug-o la view group

ImageView close = (ImageView)

v.findViewById(R.id.close_img_button);

// Inregistreaza butonul de inchidere pentru a raspunde la

// atingeri

close.setOnClickListener(new OnClickListener() {

public void onClick(View v) {

// Daca se detecteaza un click pe butonul de

// inchidere, ascunde layout-ul care defineste balonul

// curent

layout.setVisibility(GONE);

}

});

// Creaza un obiect de tip FrameLayout care constituie suportul

// pentru textul afisat in interiorul balonului

FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(

LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

params.gravity = Gravity.NO_GRAVITY;

// Adauga obiectul nou creat la view group addView(layout, params);

}

// Metoda folosita pentru setarea datelor in interiorul balonului

public void setData(Item item) {

// Seteaza ca layout-ul ce defineste balonul sa fie vizibil layout.setVisibility(VISIBLE);

// Daca titlul ce va aparea in cadrul balonului este valid

if (item.getTitle() != null) {

// Daca este valid, seteaza-l sa fie vizibil title.setVisibility(VISIBLE);

// Seteaza textul propriu-zis

title.setText(item.getTitle());

} else {

// Daca titlul nu este valid, seteaza-l sa fie invizibil title.setVisibility(GONE);

}

// Idem pentru textul care se refera la descriere

if (item.getSnippet() != null) {

snippet.setVisibility(VISIBLE);

snippet.setText(item.getSnippet());

} else {

snippet.setVisibility(GONE);

}

}

}

Anexa C: Clasa RoadProvider.java

// Pachetul din care face parte clasa

package com.google.tourguide.directions;

// Pachetele din cadrul carora au fost folosite elemente pentru constuirea

// clasei

import java.io.IOException;

import java.io.InputStream;

import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;

import javax.xml.parsers.SAXParser;

import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;

import org.xml.sax.SAXException;

import org.xml.sax.helpers.DefaultHandler;

// Clasa cu ajutorul careia se preiau de pe serverul Google datele

// referitoare la trasarea rutei

public class RoadProvider {

// Metoda folosita pentru parsarea codului KML returnat de server

public static Road getRoute(InputStream is) {

// Obtine un obiect de tipul KMLHandler pentru gestionarea

// fisierului KML

KMLHandler handler = new KMLHandler();

try {

// Obtine o instanta a clasei SAXParser folosita pentru

// parsarea codului KML

SAXParser parser = SAXParserFactory.newInstance().newSAXParser();

// Parseaza codul obtinut prin streamul de intrare, folosind

// un handler

parser.parse(is, handler);

} catch (ParserConfigurationException e) {

e.printStackTrace();

} catch (SAXException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

// Returneaza ruta

return handler.mRoad;

}

// Functia folosita pentru construirea URL-ului pe baza caruia se

// stabileste conexiunea la servers

public static String getUrl(double fromLat, double fromLon, double

toLat, double toLon, String mode) {

// Construieste un obiect de tipul StringBuffer

StringBuffer urlString = new StringBuffer();

// Adauga la obiectul construit elementele ce il definesc urlString.append("http://maps.google.com/maps?f=d&hl=en");

// Punctul care defineste pozitia de unde se incepe trasarea rutei

urlString.append("&saddr="); urlString.append(Double.toString(fromLat)); urlString.append(","); urlString.append(Double.toString(fromLon));

// Punctul care defineste pozitia unde se termina trasarea rutei urlString.append("&daddr="); urlString.append(Double.toString(toLat));

urlString.append(",");

urlString.append(Double.toString(toLon));

// Modul in care se doreste parcurgerea rutei urlString.append("&dirflg="); urlString.append(mode);

// Formatul in care se doreste returnarea datelor, in acest caz

// fisier KML

urlString.append("&ie=UTF8&0&om=0&output=kml");

return urlString.toString();

}

}

// Clasa interioara care defineste un handler pentru gestionarea datelor

// obtinute

class KMLHandler extends DefaultHandler {

// Declararea atributelor clasei

Road mRoad;

boolean isPlacemark;

boolean isRoute;

boolean isItemIcon;

private Stack<String> mCurrentElement = new Stack<String>();

private String mString;

// Constructorul clasei

public KMLHandler() {

// Creaza un obiect de tipul Road mRoad = new Road();

}

public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {

mCurrentElement.push(localName);

if (localName.equalsIgnoreCase("Placemark")) {

isPlacemark = true;

mRoad.mPoints = addPoint(mRoad.mPoints);

} else if (localName.equalsIgnoreCase("ItemIcon")) {

if (isPlacemark)

isItemIcon = true;

}

mString = new String();

}

public void characters(char[] ch, int start, int length)

throws SAXException {

String chars = new String(ch, start, length).trim();

mString = mString.concat(chars);

}

public void endElement(String uri, String localName, String name)

throws SAXException {

if (mString.length() > 0) {

if (localName.equalsIgnoreCase("name")) {

if (isPlacemark) {

isRoute = mString.equalsIgnoreCase("Route");

if (!isRoute) {

mRoad.mPoints[mRoad.mPoints.length –

1].mName = mString;

}

} else {

mRoad.mName = mString;

}

} else if (localName.equalsIgnoreCase("color") &&

!isPlacemark) {

mRoad.mColor = Integer.parseInt(mString, 16);

} else if (localName.equalsIgnoreCase("width") &&

!isPlacemark) {

mRoad.mWidth = Integer.parseInt(mString);

} else if (localName.equalsIgnoreCase("description")) {

if (isPlacemark) {

String description = cleanup(mString);

if (!isRoute)

mRoad.mPoints[mRoad.mPoints.length –

1].mDescription = description;

else

}

mRoad.mDescription = description;

} else if (localName.equalsIgnoreCase("href")) {

if (isItemIcon) {

mRoad.mPoints[mRoad.mPoints.length – 1].mIconUrl

= mString;

}

} else if (localName.equalsIgnoreCase("coordinates")) {

if (isPlacemark) {

if (!isRoute) {

String[] xyParsed = split(mString, ","); double lon = Double.parseDouble(xyParsed[0]);

double lat =

Double.parseDouble(xyParsed[1]);

mRoad.mPoints[mRoad.mPoints.length –

1].mLatitude = lat;

mRoad.mPoints[mRoad.mPoints.length –

1].mLongitude = lon;

} else {

String[] coodrinatesParsed = split(mString, " "); mRoad.mRoute = new

double[coodrinatesParsed.length][2]; for (int i = 0; i < coodrinatesParsed.length; i++) {

String[] xyParsed =

split(coodrinatesParsed[i], ",");

for (int j = 0; j < 2 && j <

xyParsed.length; j++)

mRoad.mRoute[i][j] = Double

.parseDouble(xyParsed[j]);

}

}

}

}

}

mCurrentElement.pop();

if (localName.equalsIgnoreCase("Placemark")) {

isPlacemark = false;

if (isRoute)

isRoute = false;

} else if (localName.equalsIgnoreCase("ItemIcon")) {

if (isItemIcon)

isItemIcon = false;

}

}

private String cleanup(String value) { String remove = "<br/>";

int index = value.indexOf(remove);

if (index != -1)

value = value.substring(0, index);

remove = "&#160;";

index = value.indexOf(remove); int len = remove.length(); while (index != -1) {

value = value.substring(0, index).concat(

value.substring(index + len, value.length()));

index = value.indexOf(remove);

}

return value;

}

public Point[] addPoint(Point[] points) {

Point[] result = new Point[points.length + 1];

for (int i = 0; i < points.length; i++)

result[i] = points[i]; result[points.length] = new Point(); return result;

}

private static String[] split(String strString, String strDelimiter) { String[] strArray;

int iOccurrences = 0;

int iIndexOfInnerString = 0;

int iIndexOfDelimiter = 0;

int iCounter = 0;

if (strString == null) {

throw new IllegalArgumentException("Input string cannot be null.");

}

if (strDelimiter.length() <= 0 || strDelimiter == null) {

throw new IllegalArgumentException(

"Delimeter cannot be null or empty.");

}

if (strString.startsWith(strDelimiter)) {

strString = strString.substring(strDelimiter.length());

}

if (!strString.endsWith(strDelimiter)) {

strString += strDelimiter;

}

while ((iIndexOfDelimiter = strString.indexOf(strDelimiter,

iIndexOfInnerString)) != -1) {

iOccurrences += 1;

iIndexOfInnerString = iIndexOfDelimiter +

strDelimiter.length();

}

strArray = new String[iOccurrences];

iIndexOfInnerString = 0;

iIndexOfDelimiter = 0;

while ((iIndexOfDelimiter = strString.indexOf(strDelimiter,

iIndexOfInnerString)) != -1) { strArray[iCounter] = strString.substring(iIndexOfInnerString,iIndexOfDelimiter); iIndexOfInnerString = iIndexOfDelimiter + strDelimiter.length();

iCounter += 1;

}

return strArray;

}

}

Anexa D: Clasa WeatherForecast.java

// Pachetul din care face parte clasa

package com.google.tourguide.weatherforecast;

// Pachetele din cadrul carora s-au folosit elemente pentru construirea clasei

import java.io.ByteArrayInputStream; import java.net.MalformedURLException; import java.net.URL;

import javax.xml.parsers.SAXParser;

import javax.xml.parsers.SAXParserFactory;

import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet;

import org.apache.http.impl.client.BasicResponseHandler;

import org.apache.http.impl.client.DefaultHttpClient;

import org.xml.sax.InputSource;

import org.xml.sax.XMLReader;

import android.app.Activity; import android.os.Bundle; import android.util.Log;

import android.view.MotionEvent;

import android.view.View;

import android.view.View.OnTouchListener;

import android.view.animation.AnimationUtils;

import com.google.tourguide.R;

import com.google.tourguide.manager.WeatherCurrentConditionManager;

import com.google.tourguide.manager.WeatherForecastConditionManager;

import com.google.tourguide.weatherforecast.views.SingleCurentWeatherInfoView import com.google.tourguide.weatherforecast.view.SingleFoecastWeatherInfoView import com.google.tourguide.weatherforecast.weather.GoogleWeatherHandler; import com.google.tourguide.weatherforecast.weather.WeatherSet;

public class WeatherForecast extends Activity implements OnTouchListener {

// Declarare atribute clasa

private final String DEBUG_TAG = "WeatherForcast";

private View weatherView;

@Override

public void onCreate(Bundle icicle) {

super.onCreate(icicle);

setContentView(R.layout.weatherforecast);

weatherView = (View) findViewById(R.id.weather_relative);

weatherView.startAnimation(AnimationUtils.loadAnimation(this,

R.anim.fading_in_slow));

weatherView.setOnTouchListener(this);

try {

// URL-ul pe baza caruia se preiau datele de pe serverul

// Google

String cityParamString = "Cluj-Napoca";

String queryString =

"https://www.google.com/ig/api?weather="

+ cityParamString + "&hl=en-GB";

// Obtine o instanta a clasei SAXParserFactory si un obiect

// de tipul SAXParser

SAXParserFactory spf = SAXParserFactory.newInstance();

SAXParser sp = spf.newSAXParser();

// Obtine un interpretor de cod XML din obiectul de tip

// SAXParser

XMLReader xr = sp.getXMLReader();

// Obtine un gestionar de continut si aplica-l

// interpretorului XML

GoogleWeatherHandler gwh = new GoogleWeatherHandler();

xr.setContentHandler(gwh);

// Foloseste un client HTTP pentru gestionarea URL-ului

HttpClient httpclient = new DefaultHttpClient();

HttpGet httpget = new HttpGet(queryString.replace(" ", "%20"));

Log.d(DEBUG_TAG, "executing request " + httpget.getURI());

// Creaza un handler pentru gestionarea codului returnat de

// server

ResponseHandler<String> responseHandler = new

BasicResponseHandler(); Log.i("Respond Handler", "Step 1");

String responseBody = httpclient.execute(httpget,

responseHandler);

Log.d(DEBUG_TAG, "response from httpclient:\n " +

responseBody);

ByteArrayInputStream is = new ByteArrayInputStream(

responseBody.getBytes());

// Parseaza codul returnat de server xr.parse(new InputSource(is));

Log.d(DEBUG_TAG, "parse complete");

// Handler-ul furnizeaza datele returnate prin parsarea

// codului

WeatherSet ws = gwh.getWeatherSet();

// Updateaza view-ul pentru afisarea conditiilor curente updateWeatherInfoView(R.id.weather_today,

ws.getWeatherCurrentCondition());

updateWeatherInfoView(R.id.weather_1, ws

.getWeatherForecastConditions().get(0));

updateWeatherInfoView(R.id.weather_2, ws

.getWeatherForecastConditions().get(1));

updateWeatherInfoView(R.id.weather_3, ws

.getWeatherForecastConditions().get(2));

updateWeatherInfoView(R.id.weather_4, ws

.getWeatherForecastConditions().get(3));

} catch (Exception e) {

resetWeatherInfoViews();

Log.e(DEBUG_TAG, "WeatherQueryError", e);

}

}

// Metoda folosita pentru preluarea si setarea parametrilor conditiilor

// vremii pentru ziua curenta si urmatoarele trei zile

private void updateWeatherInfoView(int aResourceID,

WeatherForecastConditionManager aWFIS) throws

MalformedURLException {

// Construieste un URL pentru preluarea imaginii care descrie

// conditiile vremii

URL imgURL = new URL("http://www.google.com" +

aWFIS.getIconURL());

((SingleForecastWeatherInfoView) findViewById(aResourceID))

.setRemoteImage(imgURL);

// Obtine temperatura minima in grade celsius

int tempMin = aWFIS.getTempMinCelsius();

// Obtine temeperatura maxima in grade celsius

int tempMax = aWFIS.getTempMaxCelsius();

// Obtine ziua la care se refera conditiile

String day = aWFIS.getDayofWeek();

// Obtine descrierea conditiilor

String condition = aWFIS.getCondition();

// Seteaza parametri obtinuti de la server

((SingleForecastWeatherInfoView) findViewById(aResourceID))

.setDayofWeek(day);

((SingleForecastWeatherInfoView) findViewById(aResourceID))

.setTempCelciusMinMax(tempMin, tempMax);

((SingleForecastWeatherInfoView) findViewById(aResourceID))

.setCondition(condition);

}

// Metoda folosita pentru setarea parametrilor conditiilor curente

private void updateWeatherInfoView(int aResourceID,

WeatherCurrentConditionManager aWCIS) throws

MalformedURLException {

// Construieste URL-ul de unde se preia imaginea ce descrie

// conditiile curente

URL imgURL = new URL("http://www.google.com" +

aWCIS.getIconURL());

((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setRemoteImage(imgURL);

// Seteaza descrierea parametrilor pentru conditiile curente

((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setTempCelcius(aWCIS.getTempCelcius()); ((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setCondition(aWCIS.getCondition()); ((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setWindCondition(aWCIS.getWindCondition());

((SingleCurrentWeatherInfoView) findViewById(aResourceID))

.setHumidity(aWCIS.getHumidity());

}

// Metoda folosita pentru resetarea view-urilor ce descriu conditiile

// vremii la valorile implicite

private void resetWeatherInfoViews() {

((SingleCurrentWeatherInfoView) findViewById(R.id.weather_today))

.reset();

((SingleForecastWeatherInfoView)findViewById(R.id.weather_1)).reset();

((SingleForecastWeatherInfoView)findViewById(R.id.weather_2)).reset(); ((SingleForecastWeatherInfoView)findViewById(R.id.weather_3)).reset(); ((SingleForecastWeatherInfoView)findViewById(R.id.weather_4)).reset();

}

@Override

public boolean onTouch(View view, MotionEvent event) {

if (event.getAction() == MotionEvent.ACTION_DOWN) {

finish();

}

return false;

}

}

Anexa E: Clasa GoogleWeatherHandler.java

// Pachetul din care face parte clasa

package com.google.tourguide.weatherforecast.weather;

// Pachetele din cadrul carora au fost folosite elemente pentru constuirea

// clasei

import org.xml.sax.Attributes;

import org.xml.sax.SAXException;

import org.xml.sax.helpers.DefaultHandler;

import com.google.tourguide.manager.WeatherCurrentConditionManager;

import com.google.tourguide.manager.WeatherForecastConditionManager;

// Clasa care implementeaza handler-ul pentru parsarea codului returnat de

// serverul Google

public class GoogleWeatherHandler extends DefaultHandler {

// Declarare atribute clasa

private WeatherSet myWeatherSet = null;

public boolean in_forecast_information = false; private boolean in_current_conditions = false; private boolean in_forecast_conditions = false;

// Metoda pentru setarea conditiilor obtinute prin interpretarea

// codului, cu ajutorul unui obiect de tipul WeatherSet

public WeatherSet getWeatherSet() {

return this.myWeatherSet;

}

@Override

public void startDocument() throws SAXException {

this.myWeatherSet = new WeatherSet();

}

@Override

public void endDocument() throws SAXException {

}

@Override

public void startElement(String namespaceURI, String localName,

String qName, Attributes atts) throws SAXException {

if (localName.equals("forecast_information")) {

this.in_forecast_information = true;

} else if (localName.equals("current_conditions")) {

this.myWeatherSet

.setWeatherCurrentCondition(new

WeatherCurrentConditionManager());

this.in_current_conditions = true;

} else if (localName.equals("forecast_conditions")) {

this.myWeatherSet.getWeatherForecastConditions().add(

new WeatherForecastConditionManager());

this.in_forecast_conditions = true;

} else {

String dataAttribute = atts.getValue("data");

if (localName.equals("city")) {

} else if (localName.equals("postal_code")) {

} else if (localName.equals("latitude_e6")) {

} else if (localName.equals("longitude_e6")) {

} else if (localName.equals("forecast_date")) {

} else if (localName.equals("current_date_time")) {

} else if (localName.equals("day_of_week")) {

if (this.in_current_conditions) {

this.myWeatherSet.getWeatherCurrentCondition()

.setDayofWeek(dataAttribute);

} else if (this.in_forecast_conditions) {

this.myWeatherSet.getLastWeatherForecastCondition()

.setDayofWeek(dataAttribute);

}

} else if (localName.equals("icon")) {

if (this.in_current_conditions) {

this.myWeatherSet.getWeatherCurrentCondition()

.setIconURL(dataAttribute);

} else if (this.in_forecast_conditions) {

this.myWeatherSet.getLastWeatherForecastCondition()

.setIconURL(dataAttribute);

}

} else if (localName.equals("condition")) {

if (this.in_current_conditions) {

this.myWeatherSet.getWeatherCurrentCondition()

.setCondition(dataAttribute);

} else if (this.in_forecast_conditions) {

this.myWeatherSet.getLastWeatherForecastCondition()

.setCondition(dataAttribute);

}

} else if (localName.equals("temp_f")) {

this.myWeatherSet.getWeatherCurrentCondition()

.setTempFahrenheit(Integer.parseInt(dataAttribute));

} else if (localName.equals("temp_c")) {

this.myWeatherSet.getWeatherCurrentCondition()

.setTempCelcius(Integer.parseInt(dataAttribute));

} else if (localName.equals("humidity")) {

this.myWeatherSet.getWeatherCurrentCondition()

.setHumidity(dataAttribute);

} else if (localName.equals("wind_condition")) {

this.myWeatherSet.getWeatherCurrentCondition()

.setWindCondition(dataAttribute);

} else if (localName.equals("low")) {

int temp = Integer.parseInt(dataAttribute);

this.myWeatherSet.getLastWeatherForecastCondition()

.setTempMinCelsius(temp);

} else if (localName.equals("high")) {

int temp = Integer.parseInt(dataAttribute);

this.myWeatherSet.getLastWeatherForecastCondition()

.setTempMaxCelsius(temp);

}

}

}

@Override

public void endElement(String namespaceURI, String localName, String

qName)

throws SAXException {

if (localName.equals("forecast_information")) {

this.in_forecast_information = false;

} else if (localName.equals("current_conditions")) {

this.in_current_conditions = false;

} else if (localName.equals("forecast_conditions")) {

this.in_forecast_conditions = false;

}

}

@Override

public void characters(char ch[], int start, int length) {

}

}

Similar Posts