Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
|
laboratoare:laborator08 [2015/04/27 13:30] Andrei Roșu-Cojocaru |
laboratoare:laborator08 [2017/02/20 20:55] (current) |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Laborator 08. Transmiterea de Notificări folosind Serviciul Google Cloud Messaging ====== | + | ====== Laborator 08. Implementarea Serviciilor de Rețea ====== |
| - | **Google Cloud Messaging for Android (GCM)** este un serviciu gratuit prin intermediul căruia programatorii pot transmite informații între un server de aplicații și un dispozitiv mobil (pe care rulează sistemul de operare Android), implementând toate aspectele legate de gestiunea mesajelor, asigurând livrarea acestora. Nu există nici un fel de limite în privința resurselor implicate pentru transmiterea acestor informații. | + | În Android, prin intermediul serviciilor, sunt puse la dispoziție funcționalități care pot fi accesate de către dispozitivele care se găsesc în aceeași rețea locală. De obicei, o astfel de abordare se folosește pentru stabilirea de conexiuni punct la punct peste care pot fi dezvoltare jocuri la care pot participa mai mulți utilizatori, aplicații de partajare a unor resurse sau pentru transmiterea de mesaje. Totodată, pot fi expuse și servicii ale unor alte dispozitive din cadrul rețelei locale cum ar fi: calculatoare, imprimante, televizoare, ceasuri inteligente, acestea putând fi astfel accesate la distanță. |
| - | Necesitatea unui astfel de serviciu este impusă de optimizările specifice aplicațiilor pentru dispozitive mobile: | + | Serviciile pot fi implementate folosind două variante: |
| - | * pentru serverul de aplicații: detecția disponibilității dispozitivului mobil și a momentului în care se poate realiza transmisia de date (în funcție de tipul de conexiune), gestiunea mesajelor (implementarea unor cozi de așteptare), implementarea mesajelor cu mai mulți destinatari (reutilizarea unei conexiuni), transmiterea mai multor mesaje către un singur dispozitiv mobil, aspecte legate de compresie și detecția erorilor; | + | - [[http://developer.android.com/training/connect-devices-wirelessly/nsd.html|Android Network Service Discovery (NSD)]], un protocol integrat în Android începând cu nivelul de API 16 (Jelly Bean), pentru implementarea de servicii disponibile în rețeaua locală; |
| - | * pentru dispozitivul mobil: limitări în privința consumului de energie și al transferului de date (în cazul unor anumite tipuri de conexiuni costurile sau viteza implicate fiind semnificative). | + | - [[http://jmdns.sourceforge.net/|JmDNS]], un proiect open-source care își propune implementarea în Java a unor funcționalități legate de proiectarea și dezvoltarea de servicii disponibile în rețeaua locală, fără a realiza nici un fel de configurări legate de infrastructura de comunicație. |
| - | Serviciile precum Google Cloud Messaging for Android implementează cele mai bune practici în domeniu pentru tratarea acestor aspecte, astfel încât programatorul se poate concentra exclusiv asupra funcționalității aplicației. | + | |
| - | Comunicația dintre cele două entități se poate realiza în ambele sensuri: | + | <note>Atât Android NSD cât și JmDNS folosesc [[http://www.multicastdns.org/|multi-cast DNS]] (utilizarea de operații DNS în rețele de dimensiuni mici, în care nu există un server propriu-zis pentru un astfel de serviciu) pentru accesul la servicii în rețeaua locală.</note> |
| - | * server de aplicații → dispozitiv mobil prin intermediul Google Cloud Messaging, nefiind necesar ca aplicația Android să ruleze la momentul în care este transmis mesajul, ea fiind pornită prin intermediul unei intenții (cu condiția ca aplicația să poată primi mesajele cu difuzare și să aibă permisiunile necesare specificate); | + | |
| - | * dispozitiv mobil → server de aplicații prin intermediul Cloud Connection Server. | + | |
| - | Google Cloud Messaging for Android nu pune la dispoziție un API pentru procesarea mesajelor, acestea fiind trimise exact în forma în care au fost primite. | + | Operațiile utilizate în implementarea serviciilor de rețea sunt: |
| + | - **configurarea** diferiților parametri (pregătirea mediului de lucru); | ||
| + | - **înregistrarea** unui serviciu, prin care celelalte dispozitive din rețeaua locală pot afla detalii cu privire la funcționalitatea oferită (tip de serviciu, adresă, port, descriere); | ||
| + | - **descoperirea** unui serviciu, prin care un dispozitiv este informat cu privire la serviciile care pot fi accesate în rețeaua locală, filtrându-le în funcție de denumire și tip; | ||
| + | - **rezolvarea** unui serviciu, prin care sunt identificate adresa și portul la care trebuie realizată o conexiune în vederea exploatării funcționalității pe care acesta o pune la dispoziție. | ||
| - | Un mesaj transmis prin intermediul Google Cloud Messaging poate: | + | <note tip>Operațiile de înregistrare respectiv descoperire / rezolvare sunt independente, astfel încât o aplicație poate alege doar să înregistreze un serviciu, în timp ce altă aplicație poate doar să descopere / rezolve serviciile din rețeua locală. Totodată, operațiile pot fi realizate și împreună, fără a exista o ordine prestabilită.</note> |
| - | * indica aplicației Android faptul că trebuie să se sincronizeze cu serverul de aplicații (util mai ales pentru aplicațiile de poștă electronică); | + | |
| - | * conține informații utile (de până la 4kb), care sunt interpretate de aplicația Android (folosite în special pentru aplicațiile de mesagerie instantanee). | + | |
| - | Pentru dispozitivele mobile cu o versiune a sistemului de operare Android mai mică decât 3.0 este necesară crearea unui cont de utilizator Google. Această cerință nu este obligatorie pentru versiuni mai mari decât 4.0.4. | + | ===== Tipuri de Servicii ===== |
| - | <note warning>Google Cloud Messaging for Android nu poate fi utilizat pe platforme care au instalată o versiune a sistemului de operare Android anterioară 2.2.</note> | + | Cele mai multe servicii accesibile prin rețeaua locală sunt descrise prin intermediul unui tip care are de obicei forma ''_<protocol_nivel_aplicatie>._<protocol_nivel_transport>.'', unde: |
| + | * ''<protocol_nivel_aplicatie>'' poate fi standard sau definit de utilizator; | ||
| + | * ''<protocol_nivel_transport>'' este de regulă ''tcp'' sau ''udp'' (sau variații ale acestora). | ||
| - | ===== Arhitectura Google Cloud Messaging for Android ===== | + | Se poate consulta [[http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml|lista cu tipurile de servicii rezervate]], gestionată de IANA (Autoritatea Internațională pentru Numere Alocate). Unele tipuri de servicii au definite și porturile pe care pot fi accesate.Pentru rezervarea unui astfel de tip de serviciu, este necesară o solicitare prealabilă, care poate fi aprobată sau respinsă, după caz. |
| - | Un sistem informatic folosind infrastructura de comunicații Google Cloud Messaging for Android implică dezvoltarea următoarelor componente: | + | Pentru Android NSD, pentru cele mai multe servicii nespecifice, se poate folosi tipul ''_http._tcp.'' (se folosește transferul de date prin HTTP folosind protocolul de transport TCP). |
| - | * **un server de aplicații** responsabil cu transmiterea de informații către dispozitivul mobil prin intermediul serviciului Google Cloud Messaging for Android; de asemenea, serverul va putea procesa anumite mesaje transmise de către client (de exemplu, identificatorul prin care dispozitivul mobil este înregistrat în mod unic în cadrul serverului Google Cloud Messaging); | + | |
| - | * **o aplicație Android** rulând pe un dispozitiv mobil cu un sistem de operare Android având o versiune mai mare de 2.2, pe care este instalat Google Play Services. | + | |
| - | {{ :laboratoare:laborator08:arhitectura_google_cloud_messaging.png?nolink&700 }} | + | În cazul JmDNS, trebuie să se precizeze faptul că serviciul rulează local, definindu-se un tip corespunzător aplicației care implementează funcționalitatea respectivă (''_<denumire_aplicatie>._tcp.local.''). |
| - | + | ||
| - | De regulă, procesul de proiectare și dezvoltare a sistemului informatic implică următorul flux de lucru: | + | |
| - | + | ||
| - | **1.** se obține un număr de proiect prin intermediul Google Developers Console, prin care un dezvoltator este identificat în mod unic; de asemenea, se activează serviciul Google Cloud Messaging;\\ | + | |
| - | **2.** se implementează aplicația Android care va rula pe client: | + | |
| - | * se realizează înregistrarea dispozitivului mobil (printr-un apel al metodei ''register'' a clasei ''GoogleCloudMessaging'', care primește ca parametru numărul de proiect) în cadrul serverului Google Cloud Messaging (**etapa 1**), generându-se un identificator care va fi utilizat pentru a desemna mesajele care trebuie transmise către acesta (**etapa 2**); valoarea sa este generată astfel încât să identifice în mod unic: | + | |
| - | * numărul de proiect al dezvoltatorului de la al cărui server de aplicații vor fi transmise mesajele; | + | |
| - | * dispozitivul mobil către care se transmite mesajul (prin intermediul contului Google asociat acestuia); | + | |
| - | * aplicația care va procesa mesajul respectiv (pe baza informațiilor din ''AndroidManifest.xml'' - denumirea pachetului care identifică în mod unic o aplicație de pe un dispozitiv mobil și versiunea). | + | |
| - | + | ||
| - | <note important>Generarea unui identificator de înregistrare se realizează o singură dată, urmând ca această valoare să fie stocată pentru a fi utilizată ulteror. În situația în care nu se mai dorește ca mesajele să fie primite, se poate realiza și procesul invers, de anulare a înregistrării realizate anterior.</note> | + | |
| - | + | ||
| - | * se transmite identificatorul de înregistrare către serverul de aplicații (**etapa 3**) astfel încât acesta să îl poată utiliza pentru a indica Google Cloud Messaging locația către care va livra mesajele; | + | |
| - | * se tratează mecanismul de primire a unui mesaj (**etapa 6**); acesta va fi livrat aplicației corespunzătoare prin intermediul unei intenții de difuzare (de tip ''com.google.android.c2dm.intent.RECEIVE''), după ce perechile (atribut, valoare) au fost extrase din conținutul mesajului și atașate acesteia; nu este necesar ca aplicația Android să ruleze la momentul primirii mesajului, aceasta fiind lansată în execuție în mod automat, dacă nu se găsește în această stare. | + | |
| - | + | ||
| - | <note important>În situația în care prelucrarea unui mesaj implică un volum de procesare semnificativ, se recomandă ca acesta să fie realizat prin intermediul unui serviciu, după ce s-a obținut un mutex (de tip ''PowerManager.WakeLock'') care să prevină trecerea dispozitivului mobil într-o stare latentă.</note> | + | |
| - | + | ||
| - | **3.** se obține o cheie publică pentru serverul de aplicații ce va fi utilizată în procesul de autentificare pentru a obține drepturi de accesare a serverului Google Cloud Messaging prin intermediul căruia vor fi transmise mesajele;\\ | + | |
| - | **4.** se implementează serverul de aplicații: | + | |
| - | * se vor procesa cererile venite din partea clientului de transmitere a identificatorului de înregistrare prin stocarea lor într-o bază de date locală (**etapa 4**); | + | |
| - | * se va transmite un mesaj către serverul Google Cloud Messaging (**etapa 5**) care va include: | + | |
| - | * cheia publică a serverului de aplicații, utilizată în procesul de autentificare, inclusă în antetul mesajului; | + | |
| - | * identificatorul de înregistrare a dispozitivului mobil, astfel încât serverul Google Cloud Messaging să cunoască locația către care se transmite mesajul. | + | |
| - | * datele propriu-zise (dacă este cazul). | + | |
| - | + | ||
| - | <note>Un mesaj transmis de pe serverul de aplicații va fi stocat într-o coadă de așteptare pe serverul Google Cloud Messaging și livrat în momentul în care dispozitivul mobil este disponibil.</note> | + | |
| ===== Configurare ===== | ===== Configurare ===== | ||
| - | **1.** Prin intermediul [[https://code.google.com/apis/console/|Consolei Google API]], se obțin **numărul de proiect** și **cheia publică** ce vor fi utilizate de dispozitivul mobil, respectiv de serverul de aplicații pentru a se autentifica în cadrul Google Cloud Messaging for Android. | + | Inițializarea mediului de lucru presupune instanțierea unor obiecte care gestionează operațiile ce pot fi realizate la nivelul serviciilor de rețea, respectiv a unor obiecte ascultător pentru evenimentele care pot surveni în cadrul acestora. |
| - | <note important>Trebuie să fiți autentificați folosind numele de utilizator și parola contului Google, altfel va trebui să vă creați un astfel de cont.</note> | + | ==== Android NSD ==== |
| - | * În situația în care nu a fost creat un proiect Google API anterior, se va afișa o fereastră prin care se poate realiza acest lucru, accesându-se opțiunea //Create Project...//: | + | Configurarea Android NSD presupune obținerea unei referințe către un obiect de tipul [[http://developer.android.com/reference/android/net/nsd/NsdManager.html|NsdManager]], care oferă funcționalități referitoare la gestiunea serviciilor existente doar în cadrul rețelei locale. |
| - | {{ :laboratoare:laborator08:start_using_google_apis_console.png?nolink }} | + | <code java> |
| + | NsdManager nsdManager = (NsdManager)context.getSystemService(Context.NSD_SERVICE); | ||
| + | </code> | ||
| - | * Ulterior, vor trebui acceptate [[https://console.developers.google.com/tos?id=UDS|termenii și condițiile utilizării Google API]]: | + | API-ul pe care îl pune la dispoziție un astfel de obiect este asincron, astfel încât metodele apelate în cadrul claselor ascultător pentru diferite evenimente (înregistrare, descoperire, rezolvare) sunt executate în contextul unor fire de execuție dedicate, așa cum trebuie procedat în condițiile unor operații ce implică comunicația prin rețea. |
| - | {{ :laboratoare:laborator08:terms_of_service.png?nolink }} | + | Metodele pe care le implementează un obiect de tipul ''NsdManager'' sunt legate de operațiile ce pot fi realizate cu serviciile accesibile în cadrul rețelei locale: |
| + | * înregistrare | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#registerService%28android.net.nsd.NsdServiceInfo,%20int,%20android.net.nsd.NsdManager.RegistrationListener%29|registerService(NsdServiceInfo, int, NsdManager.RegistrationListener)]] - folosită pentru înregistrarea unui serviciu, care să poată fi disponibil ulterior în cadrul rețelei locale; | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#unregisterService%28android.net.nsd.NsdManager.RegistrationListener%29|unregisterService(NsdManager.RegistrationListener)]] - folosită pentru deînregistrarea unui serviciu, astfel încât acesta să nu mai poată fi accesat, atunci când nu mai este necesar sau aplicația Android este distrusă. | ||
| + | * descoperire | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#discoverServices%28java.lang.String,%20int,%20android.net.nsd.NsdManager.DiscoveryListener%29|discoverServices(String, int, NsdManager.DiscoveryListener)]] - folosită pentru pornirea descoperirii de servicii accesibile în rețeaua locală, operație care va fi realizată permanent, până când se va specifica altfel (explicit), afectând resurse precum transferul de informații prin rețeaua locală (lățimea de bandă) și bateria; | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#stopServiceDiscovery%28android.net.nsd.NsdManager.DiscoveryListener%29|stopServiceDiscovery(NsdManager.DiscoveryListener)]] - folosită pentru oprirea descoperirii de servicii, atunci când acesta nu mai este necesară sau aplicația Android este întreruptă temporar. | ||
| + | * rezolvare - [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#resolveService%28android.net.nsd.NsdServiceInfo,%20android.net.nsd.NsdManager.ResolveListener%29|resolveService(NsdServiceInfo, NsdManager.ResolveListener)]], pentru a identifica parametrii de conexiune ai unui serviciu (adresă și port). | ||
| - | * Se identifică **numărul de proiect** care va fi utilizat de dispozitivul mobil pentru a se înregistra la serverul Google Cloud Messaging for Android (''SENDER_ID''), acesta putând fi preluat din: | + | Se observă că de regulă aceste metode primesc parametrii de tip: |
| - | * bara de adrese a aplicației de navigare {{ :laboratoare:laborator08:project_number01.png?nolink }} | + | * [[http://developer.android.com/reference/android/net/nsd/NsdServiceInfo.html|NsdServiceInfo]] - în care sunt stocate perechi de tipul (atribut, valoare) cu privire la serviciul disponibil în rețea: |
| - | * secțiunea //Overview// a consolei Google API {{ :laboratoare:laborator08:project_number02.png?nolink&1200 }} | + | * denumire; |
| - | * Se activează API-ul //Cloud Messaging for Android// din secțiunea //Mobile APIs//, ce poate fi accesată din //APIs & auth// → //APIs// a consolei Google API. | + | * tip; |
| + | * adresă; | ||
| + | * port. | ||
| + | * ascultător pentru diferite evenimente legate de operațiile cu serviciile accesibile în rețea (înregistrare, descoperire, rezolvare), fiecare definind metode care descriu comportamentul pentru fiecare rezultat posibil al acestora. | ||
| - | {{ :laboratoare:laborator08:enable_google_cloud_messaging_for_android01.png?nolink&400 }} | + | În fișierul ''AndroidManifest.xml'', singurele permisiuni care trebuie oferite aplicației sunt cele legate de accesul la rețeaua locală: |
| - | * Ulterior, vor putea fi vizualizate statistici cu privire la gradul de utilizare al acestui serviciu (număr de cereri realizate, distribuția codurilor de răspuns), precum și informații legate de monetizare. | + | <file xml AndroidManifest.xml> |
| + | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
| + | package="ro.pub.cs.systems.eim.lab08.chatservice" | ||
| + | android:versionCode="1" | ||
| + | android:versionName="1.0"> | ||
| + | |||
| + | <uses-permission | ||
| + | android:name="android.permission.INTERNET" /> | ||
| + | |||
| + | <!-- ... --> | ||
| - | {{ :laboratoare:laborator08:enable_google_cloud_messaging_for_android02.png?nolink&400 }} | + | </manifest> |
| + | </file> | ||
| - | * În secțiunea //APIs & auth// → //Credentials// se creează o **cheie publică** ce va fi utilizată pentru a transmite notificări de la serverul de aplicații la serverul Google Cloud Messaging for Android. | + | ==== JmDNS ==== |
| - | * Se selectează butonul //Create New Key// din secțiunea //Public API access//. {{ :laboratoare:laborator08:create_new_key.png?nolink }} | + | |
| - | * Se specifică **proprietățile cheii publice** ce se dorește a fi generată în funcție de aplicația prin intermediul căreia se vor transmite notificări către dispozitivul Android prin intermediul Google Cloud Messaging. | + | |
| - | * **tipul cheii publice** (variante disponibile: //Server Key//, //Browser Key//, //Android Key//, //iOS Key//) - se va selecta cel mai general tip, //Server Key//. {{ :laboratoare:laborator08:key_types.png?nolink }} | + | |
| - | * **adresele Internet de la care pot fi acceptate solicitări** (câte o adresă Internet pe un rând): acest câmp poate fi lăsat vid, dacă vor fi acceptate cereri de la mai multe aplicații web, disponibile la diferite locații, sau se poate folosi adresa IP ''0.0.0.0/0'', în scop de testare. {{ :laboratoare:laborator08:server_key_allowed_IPs.png?nolink }} | + | |
| - | * Se identifică valoarea cheii publice care va fi utilizată {{ :laboratoare:laborator08:key_for_server_applications.png?nolink }} | + | |
| - | ==== Configurare client ==== | + | Întrucât [[http://sourceforge.net/projects/jmdns/|versiunea curentă a JmDNS (3.4.1)]] oferă o bibliotecă sub forma unei arhive .jar care nu poate fi procesată în momentul în care este transformată în formatul ''.dex'', este necesar ca aceasta să fie prelucrată, în sensul menținerii acelor clase care sunt strict necesare pentru gestiunea serviciilor de rețea, și anume cele din pachetul ''javax.jmdns''. |
| - | **2.** Se instalează SDK-ul Google Play Services, necesară autentificării / deautentificării la Google Cloud Messaging for Android din cadrul aplicației instalată pe dispozitivul mobil. | + | <code> |
| - | * În Eclipse Luna, se accesează //Android SDK Manager// | + | student@eim2016:~$ jar xf jmdns.jar |
| - | * din bara de unelte\\ {{ :laboratoare:laborator08:eclipse_toolbar.png?nolink }} | + | student@eim2016:~$ jar cfm jmdns.jar META-INF/MANIFEST.MF javax/ |
| - | * din meniul principal, //Window// → //Android SDK Manager// | + | </code> |
| - | * Se selectează următoarele pachete:\\ {{ :laboratoare:laborator08:android_sdk_manager.png?nolink }} | + | |
| - | * din secțiunea ''Android 4.4.2 (API 19)'', pachetele ''Google APIs'' | + | |
| - | * din sețiunea //Extras//, pachetul //Google Play Services// | + | |
| - | * Biblioteca pentru accesarea Google Cloud Messaging va fi instalată la ''<android-sdk>/extras/google/google_play_services/libproject/google-play-services_lib''. | + | |
| - | **3.** Se configurează emulatorul astfel încât să poată fi rulată aplicația Android ce folosește Google Cloud Messaging. | + | JmDNS folosește pachete de tip multicast pentru a gestiona serviciile disponibile în rețea. Politica Android este de a dezactiva implicit astfel de transferuri, pentru a optimiza bateria, motiv pentru care această funcționalitate trebuie activată temporar, doar pe parcursul aplicației Android. În acest sens, trebuie obținut mutext-ul corespunzător operațiilor de acest tip, care va fi eliberat ulterior: |
| - | <note tip>Pentru aplicațiile ce rulează pe dispozitivul fizic (având o versiune a sistemului de operare Android ulterioară 2.2) nu este necesară nici o configurare suplimentară (cu excepția asocierii unui cont Google, prin specificarea parametrilor acestuia - nume de utilizator și parolă).</note> | + | <file java ChatActivity.java> |
| + | public class ChatActivity extends Activity { | ||
| - | * **AVD** | + | // ... |
| - | * se creează un dispozitiv virtual pentru care va specifica la secțiunea //Target'' valoarea ''Google APIs (Google Inc.)''\\ {{ :laboratoare:lab09-configurareAVD.jpg?nolink }} | + | |
| - | * se asociază datele unui cont Google (nume de utilizator, parola) din ''Settings'' → ''Accounts'', selectându-se opțiunea ''Add account''\\ {{ :laboratoare:lab09-avd.jpg?nolink }} | + | protected WifiManager wifiManager = null; |
| - | * **Genymotion** - vor trebui instalate manual ''ARM Transalation'' și ''Google Apps for Android'' întrucât suportul pentru acestea nu mai este asigurat începând cu versiunea 2.0 | + | |
| - | * în ''Settings'' → ''ADB'' → ''ADB Connection Settings'' se indică locația unde se găsește instalat SDK-ul de Android\\ {{ :laboratoare:lab09-genymotionsettings.jpg?nolink }} | + | |
| - | * se instalează [[http://pdsd2014.andreirosucojocaru.ro/Genymotion-ARM-Translation_v1.1.zip|ARM Translation 1.1]] printr-o operație de tip drag-and-drop în ecranul principal al emulatorului | + | |
| - | * se afișează o fereastră de dialog ce va conține mesajul ''File transfer in progress...''\\ {{ :laboratoare:lab09-genymotionfiletransfer.jpg?nolink }} | + | |
| - | * odată realizat transferul, se afișează o fereastră de dialog prin care utilizatorul este atenționat de faptul că arhiva ar putea corupe dispozitivul virtual, solicitându-i-se acordul pentru a continua instalarea\\ {{ :laboratoare:lab09-genymotioninstall.jpg?nolink }} | + | |
| - | * odată realizată instalarea, se afișează o fereastră de dialog ce conține rezultatul operației\\ {{ :laboratoare:lab09-genymotionresult.jpg?nolink }} | + | |
| - | * se repornește emulatorul prin comanda\\ <code>adb -s 192.168.56.101:5555 reboot</code> | + | |
| - | * se instalează ''Google Apps for Android'' corespunzătoare versiunii sistemului de operare existentă pe dispozitivul virtual, printr-o operație de tip drag-and-drop în ecranul principal al emulatorului (sunt parcurse aceleași etape ca anterior) | + | |
| - | * [[http://pdsd2014.andreirosucojocaru.ro/gapps-kk-20140105-signed.zip|Google Apps for Android 4.4]] | + | |
| - | * [[http://pdsd2014.andreirosucojocaru.ro/gapps-jb-20130813-signed.zip|Google Apps for Android 4.3]] | + | |
| - | * [[http://pdsd2014.andreirosucojocaru.ro/gapps-jb-20130812-signed.zip|Google Apps for Android 4.2]] | + | |
| - | * [[http://pdsd2014.andreirosucojocaru.ro/gapps-jb-20121011-signed.zip|Google Apps for Android 4.1]] | + | |
| - | * se repornește emulatorul prin comanda\\ <code>adb -s 192.168.56.101:5555 reboot</code> | + | |
| - | * se asociază datele unui cont Google (nume de utilizator, parola) din ''Settings'' → ''Accounts'', selectându-se opțiunea ''Add account''\\ {{ :laboratoare:lab09-genymotiongoogleaccount.jpg?nolink }} | + | |
| - | * în ''Play Store'' se actualizează toate aplicațiile care necesită acest lucru\\ {{ :laboratoare:lab09-genymotionsoftwareupdate.jpg?nolink }} | + | |
| - | **4.** În situația în care se va folosi un server CCS (XMPP), este necesară o înregistrare suplimentară pentru a se obține acceptul Google de a utiliza serviciul de transmitere a mesajelor de pe dispozitivul mobil, prin furnizarea informațiilor solicitate la [[https://services.google.com/fb/forms/gcm/|Upstream Google Cloud Messaging and User Notifications Sign-up]]. | + | @Override |
| + | protected void onCreate(Bundle state) { | ||
| + | super.onCreate(state); | ||
| + | |||
| + | // ... | ||
| - | <note warning>Dacă nu se urmează acest pas, va fi transmis un mesaj prin care se refuză conexiunea la server, de tipul ''Project not whitelisted''.</note> | + | WifiManager wifiManager = (WifiManager)getSystemService(Context.WIFI_SERVICE); |
| + | multicastLock = wifiManager.createMulticastLock(Constants.TAG); | ||
| + | multicastLock.setReferenceCounted(true); | ||
| + | multicastLock.acquire(); | ||
| + | } | ||
| - | <note tip>Procesul de aprobare a accesului la acest serviciu poate dura o perioadă de timp mai îndelungată.</note> | + | @Override |
| + | protected void onDestroy() { | ||
| + | super.onDestroy(); | ||
| + | |||
| + | // ... | ||
| + | |||
| + | if (multicastLock != null) { | ||
| + | multicastLock.relase(); | ||
| + | multicastLock = null; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // ... | ||
| + | |||
| + | } | ||
| + | </file> | ||
| - | ==== Configurare server ==== | + | Configurarea JmDNS presupune crearea unei instanțe a unui obiect de tipul [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html|JmDNS]], care oferă funcționalități referitoare la gestiunea serviciilor existente doar în cadrul rețelei locale. Se utilizează metoda statică [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#create%28java.net.InetAddress,%20java.lang.String%29|create(InetAddress, String)]], care primește ca parametri: |
| + | * adresa mașinii, obținută prin intermediul metodei ''getIpAddress()'' apelată pe obiectul ''ConnectionInfo'' asociat obiectului care gestoniează interfața pentru comunicația prin intermediul rețelei fără fir; | ||
| + | * denumirea mașinii (determinată pe baza adresei, prin rezoluție inversă). | ||
| - | <note>Pot exista mai multe variante de implementare a unui server de aplicații, în acest caz fiind aleasă o soluție care implică un efort redus de configurare și de implementare propriu-zisă.\\ Ca atare, se va proiecta un server de aplicații care va trimite mesaje către serverul Google Cloud Messaging, respectiv va primi mesaje de la un client Android folosind protocolul HTTP. Pentru dezvoltarea acestei funcționalități, soluția aleasă se bazează pe pagini Internet generate cu **Java Servlets** și utilizând un motor pentru gestiunea bazelor de date **MySQL**, serverul web folosit fiind **Apache Tomcat**.</note> | + | <code java> |
| + | try { | ||
| + | WifiManager wifiManager = ((ChatActivity)context).getWifiManager(); | ||
| + | InetAddress address = InetAddress.getByAddress( | ||
| + | ByteBuffer.allocate(4).putInt( | ||
| + | Integer.reverseBytes(wifiManager.getConnectionInfo().getIpAddress()) | ||
| + | ).array() | ||
| + | ); | ||
| + | String name = address.getHostName(); | ||
| + | Log.i(Constants.TAG, "address = " + address + " name = " + name); | ||
| + | jmDns = JmDNS.create(address, name); | ||
| + | } catch (IOException ioException) { | ||
| + | Log.e(Constants.TAG, "An exception has occurred: " + ioException.getMessage()); | ||
| + | if (Constants.DEBUG) { | ||
| + | ioException.printStackTrace(); | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| - | **5.** Se descarcă cea mai recentă versiune stabilă a serverului web Apache Tomcat ([[http://mirrors.hostingromania.ro/apache.org/tomcat/tomcat-7/v7.0.53/bin/apache-tomcat-7.0.53.zip|zip]]|[[http://mirrors.hostingromania.ro/apache.org/tomcat/tomcat-7/v7.0.53/bin/apache-tomcat-7.0.53.tar.gz|tar.gz]]) și se dezarhivează. | + | <note important>Instanțierea unui obiect de tip ''JmDNS'' trebuie să se realizeze pe firul de execuție al comunicației prin rețea, în caz contrar generându-se o excepție de tipul ''android.os.NetworkOnMainThreadException''.</note> |
| - | **6**. Se descarcă cea mai recentă versiune stabilă a mediului de dezvoltare Eclipse IDE for Java EE Developers (Windows: [[http://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/kepler/SR2/eclipse-jee-kepler-SR2-win32.zip|32-bit]]|[[http://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/kepler/SR2/eclipse-jee-kepler-SR2-win32-x86_64.zip|64-bit]], Linux: [[http://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/kepler/SR2/eclipse-jee-kepler-SR2-linux-gtk.tar.gz|32-bit]]|[[http://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/kepler/SR2/eclipse-jee-kepler-SR2-linux-gtk-x86_64.tar.gz|64-bit]]) și se integrează cu serverul web Apache Tomcat. | + | API-ul pe care îl pune la dispoziție un astfel de obiect este asincron, astfel încât metodele apelate în cadrul claselor ascultător pentru diferite evenimente (înregistrare, descoperire, rezolvare) sunt executate în contextul unor fire de execuție dedicate, așa cum trebuie procedat în condițiile unor operații ce implică comunicația prin rețea. |
| - | * ''Window'' → ''Preferences'' → ''Server'' → ''Runtime Environments'' și se accesează butonul ''Add...''\\ {{ :laboratoare:lab09-eclipseEEserverruntimeenvironments.jpg?nolink }} | + | * înregistrare |
| - | * se specifică tipul serverului web (''Apache Tomcat v7.0'')\\ {{ :laboratoare:lab09-eclipseEENewServerRuntimeEnvironment.jpg?nolink }} | + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#registerService%28javax.jmdns.ServiceInfo%29|registerService(ServiceInfo)]] - folosită pentru înregistrarea unui serviciu, care să poată fi disponibil ulterior în cadrul rețelei locale; |
| - | * se precizează locația la care se găsește serverul web (directorul în care a fost dezarhivat, fără alte configurări) și versiunea de Java Runtime Environment\\ {{ :laboratoare:lab09-eclipseEETomcatServer.jpg?nolink }} | + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#unregisterService%28javax.jmdns.ServiceInfo%29|unregisterService(ServiceInfo)]] sau [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#unregisterAllServices%28%29|unregisterAllServices()]]- folosită pentru deînregistrarea unui serviciu, astfel încât acesta să nu mai poată fi accesat, atunci când nu mai este necesar sau aplicația Android este distrusă. |
| - | <note tip>Se oferă și posibilitatea descărcării și instalării unei instanțe a serverului web Apache Tomcat, însă de regulă aceasta nu referă cea mai recentă versiune.</note> | + | * descoperire |
| - | * ''Window'' → ''Show View'' → ''Other...'' → ''Servers'' → ''Server'' și se accesează legătura ''No servers are available. Click this link to create a new server...'' sau click pe butonul drept al mouse-ului în spațiul de lucru ''Server'' → ''New'' → ''Server''\\ {{ :laboratoare:lab09-eclipseEEShowView.jpg?nolink }}\\ {{ :laboratoare:lab09-eclipseEEcreateanewserver.jpg?nolink }} | + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#addServiceListener%28java.lang.String,%20javax.jmdns.ServiceListener%29|addServiceListener(String, ServiceListener)]] - folosită pentru pornirea descoperirii de servicii accesibile în rețeaua locală, operație care va fi realizată permanent, până când se va specifica altfel (explicit), afectând resurse precum transferul de informații prin rețeaua locală (lățimea de bandă) și bateria; vor fi monitorizate doar serviciile de un anumit tip; |
| - | * se selectează serverul web definit anterior și se accesează butonul ''Finish''\\ {{ :laboratoare:lab09-eclipseEEdefineanewserver.jpg?nolink }} | + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#removeServiceListener%28java.lang.String,%20javax.jmdns.ServiceListener%29|removeServiceListener(ServiceListener)]] - folosită pentru oprirea descoperirii de servicii de un anumit tip, atunci când acesta nu mai este necesară sau aplicația Android este întreruptă temporar. |
| - | * se asociază proiectului un server web pe care va rula: click pe butonul drept al mouse-ului pe denumirea proiectului → ''Properties'' → ''Server'' și se selectează serverul web definit anterior\\ {{ :laboratoare:lab09-eclipseEEprojectproperties.jpg?nolink }} | + | * rezolvare - [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#requestServiceInfo%28java.lang.String,%20java.lang.String%29|requestServiceInfo(String, String)]], pentru a identifica parametrii de conexiune (adresă și port) ai unui serviciu pentru care se cunoaște tipul și denumirea.â |
| - | * se realizează o referință către biblioteca serverului web, aceasta fiind trecută în classpath: click pe butonul drept al mouse-ului pe denumirea proiectului → ''Build Path'' → ''Add Library'' | + | |
| - | * se alege ''Server Runtime''\\ {{ :laboratoare:lab09-eclipseEEAddLibrary.jpg?nolink }} | + | |
| - | * se indică serverul web definit anterior și se accesează butonul ''Finish''\\ {{ :laboratoare:lab09-eclipseEEServerLibrary.jpg?nolink }} | + | |
| - | **7.** Se descarcă și se instalează cea mai recentă versiune stabilă a motorului pentru baze de date MySQL. | + | Se observă că de regulă aceste metode primesc parametrii de tip: |
| - | * Windows: se poate folosi [[http://dev.mysql.com/get/Downloads/MySQLInstaller/mysql-installer-community-5.6.17.0.msi|MySQL Community Server]]. | + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/ServiceInfo.html|ServiceInfo]] - în care sunt stocate perechi de tipul (atribut, valoare) cu privire la serviciul disponibil în rețea: |
| - | * Linux: se poate utiliza comanda <code>sudo apt-get install mysql-server</code> | + | * denumire; |
| + | * tip / subtip; | ||
| + | * adresă / denumire dispozitiv care găzduiește serviciul; | ||
| + | * port pe care poate fi accesat serviciul; | ||
| + | * prioritate; | ||
| + | * protocol | ||
| + | * date | ||
| + | * URL. | ||
| + | * ascultător pentru diferite evenimente legate de operațiile cu serviciile accesibile în rețea (înregistrare, descoperire, rezolvare), fiecare definind metode care descriu comportamentul pentru fiecare rezultat posibil al acestora. | ||
| - | <note>Configurarea propriu-zisă nu presupune decât specificarea unei parole de administrare.</note> | + | Resusele asociate obiectului de tip ''JmDNS'' trebuie eliberate în momentul în care acesta nu mai este necesar (aplicația Android este distrusă): |
| - | Pentru gestiunea propriu-zisă a bazelor de date, poate fi utiliza MySQL Workbench [[http://dev.mysql.com/get/Downloads/MySQLGUITools/mysql-workbench-community-6.1.4-win32.msi|Windows]]|[[http://dev.mysql.com/get/Downloads/MySQLGUITools/mysql-workbench-community-6.1.4-1ubu1204-i386.deb|Ubuntu]]|[[http://dev.mysql.com/get/Downloads/MySQLGUITools/mysql-workbench-community-6.1.4-1.fc19.i686.rpm|Fedora Core]]|[[http://dev.mysql.com/get/Downloads/MySQLGUITools/mysql-workbench-community-6.1.4-1.el6.i686.rpm|Red Hat]].\\ De asemenea, se poate accesa consola MySQL folosind comanda <code>mysql -u root -p</code> | + | <code java> |
| - | Va trebui indicată parola, atunci când aceasta este solicitată. | + | try { |
| - | + | if (jmDns != null) { | |
| - | === MySQL pe Debian Wheezy === | + | jmDns.close(); |
| - | + | jmDns = null; | |
| - | * in cazul in care nu exista, se instaleaza pachetul ''libaio1'' <code> | + | } |
| - | sudo apt-get install libaio1 | + | } catch (IOException ioException) { |
| + | Log.e(Constants.TAG, "An exception has occurred: " + ioException.getMessage()); | ||
| + | if (Constants.DEBUG) { | ||
| + | ioException.printStackTrace(); | ||
| + | } | ||
| + | } | ||
| </code> | </code> | ||
| - | * se descarca versiunea corespunzatoare arhitecturii pe care se doreste sa se instaleze serverul MySQL pentru gestiunea bazei de date ([[http://dev.mysql.com/downloads/file.php?id=451433|32-bit]]|[[http://dev.mysql.com/downloads/file.php?id=451434|64-bit]]). | + | În fișierul ''AndroidManifest.xml'', permisiunile care trebuie oferite aplicației vizează: |
| + | * accesul la Internet; | ||
| + | * schimbarea politicii cu privire la procesarea pachetelor de tip multi-cast (implicit dezactivate, pentru a optimiza consumul de energie); | ||
| + | * accesul la starea rețelei fără fir; | ||
| + | * accesul la starea rețelei cu fir. | ||
| - | * se instaleaza pachetul in ''/opt/mysql/server-5.6'' <code> | + | <file xml AndroidManifest.xml> |
| - | sudo dpkg -i mysql-5.6.17-debian6.0-<arhitectura>.deb | + | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
| - | </code> | + | package="ro.pub.cs.systems.eim.lab08.chatservice" |
| + | android:versionCode="1" | ||
| + | android:versionName="1.0"> | ||
| + | |||
| + | <uses-permission | ||
| + | android:name="android.permission.INTERNET"/> | ||
| + | <uses-permission | ||
| + | android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/> | ||
| + | <uses-permission | ||
| + | android:name="android.permission.ACCESS_WIFI_STATE" /> | ||
| + | <uses-permission | ||
| + | android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||
| + | |||
| + | <!-- ... --> | ||
| - | * se copiaza fisierul de configurare ''my-default.cnf'' in ''/etc'', astfel incat acesta sa fie utilizat atunci cand se vor rula script-urile MySQL <code> | + | </manifest> |
| - | sudo cp /opt/mysql/server-5.6/support-files/my-default.cnf /etc/my.cnf | + | </file> |
| - | </code> | + | |
| - | * se editeaza fisierul de configurare ''/etc/my.cnf'' <code> | + | ===== Înregistrarea / Deînregistrarea unui Serviciu ===== |
| - | gedit /etc/my.cnf | + | |
| - | basedir = /opt/mysql/server-5.6 | + | ==== Android NSD ==== |
| - | #datadir = ..... | + | |
| - | port = 3306 | + | |
| - | #server_id = ..... | + | |
| - | #socket = ..... | + | |
| - | </code> | + | |
| - | * se specifica calea catre binare <code> | + | Pentru **înregistrarea unui serviciu**, se apelează metoda [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#registerService%28android.net.nsd.NsdServiceInfo,%20int,%20android.net.nsd.NsdManager.RegistrationListener%29|registerService(NsdServiceInfo, int, NsdManager.RegistrationListener)]] din clasa ''NsdManager'', ulterior serviciul putând fi accesat din cadrul altor mașini fizice / dispozitive mobile. Aceasta primește ca parametri: |
| - | sudo gedit /etc/profile | + | * un obiect ''NsdServiceInfo'', ce reține informații precum denumirea și tipul serviciului, adresa și portul mașinii / dispozitivului pe care va fi disponibil; |
| - | PATH=/opt/mysql/server-5.6/bin:$PATH | + | <note tip> |
| - | </code> | + | Întrucât, de regulă, odată cu înregistrarea serviciului este pornit și un server care va aștepta invocări de la clienți, adresa și portul pot fi preluate de la obiectul de tip ''ServerSocket'' astfel instanțiat sau pot fi folosite valori obținute de la utilizator: |
| - | + | <code java> | |
| - | <note important>Incarcarea acestor configurari va fi realizata in momentul in care se realizeaza autentificarea unui utilizator in cadrul sistemului de operare sau se ruleaza in terminal ''. /etc/profile''.</note> | + | ServerSocket serverSocket = new ServerSocket(0); |
| - | + | InetAddress address = serverSocket.getInetAddress(); | |
| - | * se creeaza grupul si utilizatorul ''mysql'', care va primi drepturi asupra directorului ''/opt/mysql'' <code> | + | int port = serverSocket.getLocalPort(); |
| - | sudo groupadd mysql | + | |
| - | sudo useradd -r -g mysql mysql | + | |
| - | cd /opt/mysql | + | |
| - | sudo chown -R mysql.mysql /opt/mysql | + | |
| - | sudo chgrp -R mysql /opt/mysql | + | |
| - | </code> | + | |
| - | + | ||
| - | * se ruleaza script-urile pentru configurarea propriu-zisa a serverului MySQL pentru gestiunea bazei de date <code> | + | |
| - | cd /opt/mysql/server-5.6/scripts | + | |
| - | sudo ./mysql_install_db --user=mysql | + | |
| - | mysqld_safe --user=mysql | + | |
| - | mysql_secure_installation | + | |
| - | </code> | + | |
| - | + | ||
| - | * se realizeaza configurarile necesare pentru ca serverul pentru gestiunea bazei de date sa porneasca / sa se opreasca odata cu sistemul de operare <code> | + | |
| - | sudo cp /opt/mysql/server-5.6/support-files/mysql.server /etc/init.d/mysql.server | + | |
| - | cd /etc/init.d | + | |
| - | sudo chmod 755 mysql.server | + | |
| - | sudo inssev mysql.server | + | |
| - | </code> | + | |
| - | ===== Proiectarea și dezvoltarea clientului ===== | + | |
| - | + | ||
| - | ==== Proiectul Eclipse ==== | + | |
| - | + | ||
| - | <note warning>Există posibilitatea ca rularea proiectului Eclipse să necesite mai multă memorie ce trebuie alocată mașinii virtuale Java, aceasta realizându-se prin intermediul valorilor furnizate în cadrul fișierului de configurare ''eclipse.ini'':\\ <code> | + | |
| - | --launcher.XXMaxPermSize | + | |
| - | 512M | + | |
| - | --launcher.XXMaxPermSize | + | |
| - | 512m | + | |
| - | --launcher.appendVmargs | + | |
| - | -vmargs | + | |
| - | -Dosgi.requiredJavaVersion=1.6 | + | |
| - | -Xms512m | + | |
| - | -Xmx1024m | + | |
| </code> | </code> | ||
| + | Valoarea 0 a parametrului din constructorul obiectului ''ServerSocket'' indică faptul că se va folosi un port libere, aleator. O astfel de abordare este de preferat, întrucât se evită eventualele conflicte cu privire la porturile ocupate. | ||
| </note> | </note> | ||
| - | Se realizează o referință către biblioteca pentru accesarea Google Cloud Messaging care este localizată în ''<android-sdk>/extras/google/google_play_services/libproject/google-play-services_lib'' | + | * protocolul, în acest caz folosindu-se ''NsdManager.PROTOCOL_DNS_SD''; |
| - | * se accesează ''File'' → ''Import'' → ''Android'' → ''Existing Android Code Into Workspace''\\ {{ :laboratoare:lab09-eclipseimportandroidcode.jpg?nolink }} | + | * un obiect ascultător de tipul ''NsdManager.RegistrationListener'', care trebuie să implementeze metodele: |
| - | * se indică locația unde se găsește instalată biblioteca pentru accesarea Google Cloud Messaging, creându-se o copie a acestuia în spațiul de lucru (se bifează opțiunea ''Copy projects into workspace'')\\ {{ :laboratoare:lab09-eclipseimportandroidproject.jpg?nolink }} | + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.RegistrationListener.html#onServiceRegistered%28android.net.nsd.NsdServiceInfo%29|onServiceRegistered(NsdServiceInfo)]], apelată în mod automat în momentul în care înregistrarea serviciului a fost realizată cu succes; |
| - | * în fereastra corespunzătoare proiectului (click dreapta pe denumirea proiectului în ''Package Explorer'' → ''Properties''), se accesează opțiunea ''Android'' și se specifică referința către biblioteca respectivă în secțiunea ''Library'' (accesând butonul ''Add'')\\ {{ :laboratoare:lab09-eclipseprojectproperties.jpg?nolink }} | + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.RegistrationListener.html#onRegistrationFailed%28android.net.nsd.NsdServiceInfo,%20int%29|onRegistrationFailed(NsdServiceInfo, int)]], apelată în mod automat în momentul în care înregistrarea serviciului a eșuat, oferindu-se și un cod de eroare pentru depanarea cauzei. |
| - | ==== Structura Proiectului ==== | + | Pentru **deînregistrarea unui serviciu**, se apelează metoda [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#unregisterService%28android.net.nsd.NsdManager.RegistrationListener%29|unregisterService(NsdManager.RegistrationListener)]] din clasa ''NsdManager''. Aceasta primește ca parametru un obiect ascultător de tipul ''NsdManager.RegistrationListener'', care trebuie să implementeze metodele: |
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.RegistrationListener.html#onServiceUnregistered%28android.net.nsd.NsdServiceInfo%29|onServiceUnegistered(NsdServiceInfo)]], apelată în mod automat în momentul în care deînregistrarea serviciului a fost realizată cu succes; | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.RegistrationListener.html#onUnregistrationFailed%28android.net.nsd.NsdServiceInfo,%20int%29|onUnregistrationFailed(NsdServiceInfo, int)]], apelată în mod automat în momentul în care deînregistrarea serviciului a eșuat, oferindu-se și un cod de eroare pentru depanarea cauzei. | ||
| - | Aplicația Android care va rula pe dispozitivul mobil va consta din: | + | <note tip>Odată cu deînregistrarea serviciului, se va opri și serverul care așteaptă invocări de la clienți.</note> |
| - | - o **activitate** (''GoogleCloudMessagingActivity'') care: | + | |
| - | - verifică dacă pe dispozitivul mobil este instalat Google Play Services și dacă această aplicație este actualizată; | + | |
| - | - permite înregistrarea dispozitivului mobil la serverul Google Cloud Messaging dacă această operație nu a fost realizată în prealabil (o astfel de informație ar trebui stocată în SharedPreferences astfel încât operația să fie realizată o singură dată); | + | |
| - | - transmite identificatorul de înregistrare obținut către serverul de aplicații (împreună cu alte informații prin care să poată fi recunoscut dispozitivul mobil - de exemplu, un nume de utilizator sau o adresă de posță electronică) | + | |
| - | - un **ascultător** pentru mesajele cu difuzare (''GoogleCloudMessagingBroadcastReceiver'') către care este livrat un mesaj transmis de serverul Google Cloud Messaging, prin intermediul unei intenții | + | |
| - | - (opțional) un **serviciu** (''GoogleCloudMessagingIntentService'') lansat în execuție în momentul în care este livrat mesajul (de către ascultător) în cadrul căruia acesta este procesat propriu-zis; utilizarea unui serviciu este necesară doar în situația în care prelucrarea mesajului se realizează într-un interval de timp mai mare, fiind necesar să se asigure faptul că dispozitivul mobil nu devine inactiv între timp. | + | |
| - | + | ||
| - | {{ :laboratoare:lab09-structuraproiectului.jpg?nolink }} | + | |
| - | + | ||
| - | === Activitatea GoogleCloudMessagingActivity === | + | |
| - | + | ||
| - | Pe lângă implementarea interfeței grafice prin care se asigură interacțiunea cu utilizatorul, activitatea ''GoogleCloudMessagingActivity'' este responsabilă pentru: | + | |
| - | + | ||
| - | **1.** verificarea compatibilității dispozitivului mobil pe care rulează aplicația cu Google Play Services, SDK pe care se bazează Google Cloud Messaging; această operație trebuie să se realizeze la crearea activității (metoda ''onCreate()'') dar și în cazul revenirii la aceasta prin alte mijloace (metoda ''onResume()'') | + | |
| <code java> | <code java> | ||
| - | private boolean checkPlayServices() { | + | public void registerNetworkService() throws Exception { |
| - | int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); | + | chatServer = new ChatServer(this); |
| - | if (resultCode != ConnectionResult.SUCCESS) { | + | ServerSocket serverSocket = chatServer.getServerSocket(); |
| - | if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) { | + | if (serverSocket == null) { |
| - | GooglePlayServicesUtil.getErrorDialog(resultCode, this, Configuration.PLAY_SERVICES_RESOLUTION_REQUEST).show(); | + | throw new Exception("Could not get server socket"); |
| - | } else { | + | } |
| - | Log.i(TAG, "This mobile device does not support Google Play Services."); | + | chatServer.start(); |
| - | finish(); | + | |
| - | } | + | NsdServiceInfo nsdServiceInfo = new NsdServiceInfo(); |
| - | return false; | + | nsdServiceInfo.setServiceName(Constants.SERVICE_NAME); |
| - | } | + | nsdServiceInfo.setServiceType(Constants.SERVICE_TYPE); |
| - | return true; | + | nsdServiceInfo.setHost(serverSocket.getInetAddress()); |
| + | nsdServiceInfo.setPort(serverSocket.getLocalPort()); | ||
| + | |||
| + | nsdManager.registerService( | ||
| + | nsdServiceInfo, | ||
| + | NsdManager.PROTOCOL_DNS_SD, | ||
| + | registrationListener | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | public void unregisterNetworkService() { | ||
| + | nsdManager.unregisterService(registrationListener); | ||
| + | |||
| + | chatServer.stopThread(); | ||
| + | |||
| + | // ... | ||
| } | } | ||
| </code> | </code> | ||
| - | În situația în care dispozitivul mobil este compatibil cu Google Play Services (rulează un sistem de operare Android cu o versiune mai mare decât 2.2), se lasă utilizatorului posibilitatea de a-l instala (sau actualiza, după caz) prin afișarea unei ferestre de dialog, altfel aplicația este terminată. | + | De regulă, metodele obiectului de tip ''NsdManager.RegistrationListener'' nu vor avea alt rol cu excepția jurnalizării evenimentelor respective, mai ales atunci când operațiile de înregistrare / deînregistrare eșuează. |
| - | **2.** înregistrarea dispozitivului mobil la Google Cloud Messaging, în situația în care această operație nu s-a realizat în prealabil (nu există o astfel de valoare stocată ca preferință a utilizatorului): | + | În momentul în care înregistrarea s-a realizat cu succes, se va reține denumirea serviciului, fapt important întrucât în situația în care în rețeaua locală există un alt serviciu cu aceeași denumire, sistemul de operare Android va trata o astfel de situație prin atașarea unui indice '' (x)'', indicând al câtelea serviciu duplicat a fost identificat. |
| + | |||
| + | În momentul în care deînregistrarea s-a realizat cu succes, nu este necesar să se realizeze nici o operație. | ||
| <code java> | <code java> | ||
| - | private void registerInBackground() { | + | NsdManager.RegistrationListener registrationListener = new RegistrationListener() { |
| - | new AsyncTask<Object, Integer, String>() { | + | @Override |
| - | @Override | + | public void onServiceRegistered(NsdServiceInfo nsdServiceInfo) { |
| - | protected String doInBackground(Object... params) { | + | serviceName = nsdServiceInfo.getServiceName(); |
| - | try { | + | } |
| - | if (googleCloudMessaging == null) { | + | |
| - | googleCloudMessaging = GoogleCloudMessaging.getInstance(context); | + | @Override |
| - | } | + | public void onRegistrationFailed(NsdServiceInfo nsdServiceInfo, int errorCode) { |
| - | registrationId = googleCloudMessaging.register(Configuration.PROJECT_NUMBER); | + | Log.e(Constants.TAG, "An exception occured while registering the service: " + errorCode); |
| - | // store the registration ID into the SharedPreferences | + | } |
| - | // the ID is not yet registered with the application server | + | |
| - | setSharedPreferences(context, registrationId, Configuration.FAILURE); | + | @Override |
| - | } catch (IOException exception) { | + | public void onServiceUnregistered(NsdServiceInfo nsdServiceInfo) { |
| - | registrationId = "an exception has occurred: " + exception.getMessage(); | + | } |
| - | exception.printStackTrace(); | + | |
| - | } | + | @Override |
| - | return registrationId; | + | public void onUnregistrationFailed(NsdServiceInfo nsdServiceInfo, int errorCode) { |
| - | } | + | Log.e(Constants.TAG, "An exception occured while unregistering the service: " + errorCode); |
| - | + | } | |
| - | @Override | + | }; |
| - | protected void onPostExecute(String registrationId) { | + | |
| - | registrationIdTextViewValue.append(registrationId + "\n"); | + | |
| - | Log.i(TAG, "registration ID supplied by the Google Cloud Messaging Server is: "+registrationId); | + | |
| - | } | + | |
| - | }.execute(null, null, null); | + | |
| - | } | + | |
| </code> | </code> | ||
| - | <note warning>Întrucât procesul de înregistrare la Google Cloud Messaging implică comunicație prin rețea, este necesar ca acesta să se realizeze asincron, pe un fir de execuție separat, motiv pentru care procesarea a fost realizată în cadrul metodei ''doInBackground'' a unui obiect de tip ''AsyncTask''.</note> | + | ==== JmDNS ==== |
| - | Testarea existenței unei valori corespunzătoare identificatorului de înregistrare în preferințele utilizatorului se face foarte ușor, acesta având o structură de tip asociere între o cheie și o valoare. | + | Pentru **înregistrarea unui serviciu**, se apelează metoda [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#registerService%28javax.jmdns.ServiceInfo%29|registerService(ServiceInfo)]] din clasa ''JmDNS'', ulterior serviciul putând fi accesat din cadrul altor mașini fizice / dispozitive mobile. |
| - | <code java> | + | Pentru **deînregistrarea unui serviciu**, se poate apela una din metodele [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#unregisterService%28javax.jmdns.ServiceInfo%29|unregisterService(ServiceInfo)]], respectiv [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#unregisterAllServices()|unregisterAllServices()]] din clasa ''JmDNS''. |
| - | private String getRegistrationId(Context context) { | + | |
| - | final SharedPreferences sharedPreferences = getSharedPreferences(context); | + | |
| - | String registrationId = sharedPreferences.getString(Configuration.REGISTRATION_ID_PROPERTY, ""); | + | |
| - | if (registrationId.isEmpty()) { | + | |
| - | Log.i(TAG, "Registration ID not found into the SharedPreferences."); | + | |
| - | return ""; | + | |
| - | } | + | |
| - | // check whether the application was updated | + | |
| - | // if it is the case, the registration ID previously supplied must be cleared | + | |
| - | // as it is not guaranteed to work with the current version | + | |
| - | int registeredVersion = sharedPreferences.getInt(Configuration.APPLICATION_VERSION_PROPERTY, Integer.MIN_VALUE); | + | |
| - | int currentVersion = getApplicationVersion(context); | + | |
| - | if (registeredVersion != currentVersion) { | + | |
| - | Log.i(TAG, "The application version was changed."); | + | |
| - | return ""; | + | |
| - | } | + | |
| - | return registrationId; | + | |
| - | } | + | |
| - | </code> | + | |
| - | De remarcat este faptul că în preferințele utilizatorului va fi stocată și versiunea aplicației Android pentru care a fost generat identificatorul de înregistrare, iar în situația în care aceasta este actualizată, se va realiza o nouă înregistrare la serverul Google Cloud Messaging, de vreme ce o astfel de valoare identifică în mod unic o aplicație caracterizată prin denumirea pachetului și versiunea acesteia. | + | <note>Aceste metode primesc ca parametru un obiect de tip ''ServiceInfo'', ce reține informații precum denumirea și tipul serviciului, adresa și portul mașinii / dispozitivului pe care va fi disponibil (ce pot fi preluate, din cadrul unui obiect de tip ''ServerSocket'').</note> |
| - | **3.** transmiterea identificatorului de înregistrare către serverul de aplicații printr-o cerere HTTP de tip POST care va conține și alte informații prin care să poată fi referit utilizatorul respectiv (de exemplu, numele de utilizator sau adresa de poștă electronică): | + | <note warning>JmDNS nu oferă informații cu privire la rezultatul operațiilor de înregistrare / deînregistrare.</note> |
| <code java> | <code java> | ||
| - | public static class RegisterButtonListener implements OnClickListener { | + | public void registerNetworkService() throws Exception { |
| - | @Override | + | chatServer = new ChatServer(this); |
| - | public void onClick(View view) { | + | ServerSocket serverSocket = chatServer.getServerSocket(); |
| - | new AsyncTask<Object, Integer, Integer>() { | + | if (serverSocket == null) { |
| - | @Override | + | throw new Exception("Could not get server socket"); |
| - | protected Integer doInBackground(Object... params) { | + | } |
| - | try { | + | chatServer.start(); |
| - | HttpClient httpClient = new DefaultHttpClient(); | + | |
| + | ServiceInfo serviceInfo = ServiceInfo.create( | ||
| + | Constants.SERVICE_TYPE, | ||
| + | Constants.SERVICE_NAME, | ||
| + | port, | ||
| + | Constants.SERVICE_DESCRIPTION | ||
| + | ); | ||
| + | |||
| + | if (jmDns != null && serviceInfo != null) { | ||
| + | serviceName = serviceInfo.getName(); | ||
| + | jmDns.registerService(serviceInfo); | ||
| + | } | ||
| + | } | ||
| - | HttpPost httpPost = new HttpPost(Configuration.GOOGLECLOUDMESSAGING_WEBSERVER_URL); | + | public void unregisterNetworkService() { |
| - | + | if (jmDns != null) { | |
| - | // send the username, email and registration ID as fields of the HTTP body | + | jmDns.unregisterAllServices(); |
| - | // to the application server | + | } |
| - | List<NameValuePair> requestParameters = new ArrayList<NameValuePair>(); | + | |
| - | requestParameters.add(new BasicNameValuePair(Configuration.USER_NAME, usernameEditText.getText().toString())); | + | chatServer.stopThread(); |
| - | requestParameters.add(new BasicNameValuePair(Configuration.EMAIL, emailEditText.getText().toString())); | + | |
| - | requestParameters.add(new BasicNameValuePair(Configuration.REGISTRATION_ID, registrationIdTextViewValue.getText().toString())); | + | // ... |
| - | UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(requestParameters, HTTP.UTF_8); | + | |
| - | httpPost.setEntity(urlEncodedFormEntity); | + | |
| - | + | ||
| - | HttpResponse httpPostResponse = httpClient.execute(httpPost); | + | |
| - | + | ||
| - | // transmission of information to the application server was successful | + | |
| - | if (httpPostResponse.getStatusLine().getStatusCode() == 200) | + | |
| - | return Configuration.SUCCESS; | + | |
| - | } catch (Exception exception) { | + | |
| - | Log.e(TAG, exception.getMessage()); | + | |
| - | exception.printStackTrace(); | + | |
| - | } | + | |
| - | // transmission of information to the application server was not successful | + | |
| - | return Configuration.FAILURE; | + | |
| - | } | + | |
| - | + | ||
| - | @Override | + | |
| - | protected void onPostExecute(Integer result) { | + | |
| - | // ... | + | |
| - | } | + | |
| - | }.execute(null, null, null); | + | |
| - | } | + | |
| } | } | ||
| </code> | </code> | ||
| - | În cadrul serverului de aplicații va trebui procesată o astfel de cerere prin stocarea informațiilor transmise în cadrul unei baze de date și transmiterea unui răspuns corespunzător prin care se indică rezultatul operației, pentru a fi marcat și la nivelul clientului (în preferințele utilizatorului). | + | ===== Descoperirea Serviciilor Accesibile ===== |
| - | <note important>Transmiterea identificatorului de înregistrare către serverul de aplicații se realizează o singură dată, astfel încât să nu se înregistreze duplicate în baza de date.</note> | + | Operațiile de **pornire** / **oprire** a descoperirii serviciilor disponibile trebuie să aibă în vedere resursele afectate precum și impactul asupra altor funcționalități cum ar fi viteza de transfer prin rețea, respectiv autonomia. |
| - | === Ascultătorul GoogleCloudMessagingBroadcastReceiver === | + | De regulă, pornirea operației de descoperire a serviciilor disponibile este realizată pe metoda ''onResume()'', adică din momentul în care activitatea este vizibilă. |
| - | Implementarea unui ascultător pentru mesaje cu difuzare poate fi realizată în două variante: | + | Similar, oprirea operației de descoperire a serviciilor disponibile este realizată pe metoda ''onPause()'', adică din momentul în care activitatea nu este vizibilă. |
| - | - în cazul în care procesarea mesajului implică operații triviale (ce pot fi realizate într-un interval de timp rezonabil), se poate defini un ascultător ''BroadcastReceiver'', prelucrarea propriu-zisă realizându-se în cadrul metodei ''onReceive'' (ce primește ca parametri contextul și intenția care a apelat-o, cu informațiile din corpul mesajului atașate într-un obiect de tip ''Bundle''), apelată în mod automat în momentul în care mesajul este livrat. | + | |
| - | - în situația în care procesarea mesajului presupune mai multe operații realizate într-un interval de timp semnificativ, prelucrarea se va realiza în cadrul unui serviciu dedicat (de tip ''IntentService''), ce va rula în fundal, în timp ce se obține accesul la un tip de mutex ce împiedică dispozitivul mobil să intre într-o stare de inactivitate care ar suspenda tratarea evenimentului de livrare a unui mesaj, dacă aceasta nu este încă completată: <code java> | + | |
| - | public class GoogleCloudMessagingBroadcastReceiver extends WakefulBroadcastReceiver { | + | |
| - | @Override | + | |
| - | public void onReceive(Context context, Intent intent) { | + | |
| - | // create a component associated to the service that will handle the intent | + | |
| - | ComponentName component = new ComponentName(context.getPackageName(), GoogleCloudMessagingIntentService.class.getName()); | + | |
| - | // start the service | + | |
| - | // maintain the mobile device awake | + | |
| - | startWakefulService(context, (intent.setComponent(component))); | + | |
| - | setResultCode(Activity.RESULT_OK); | + | |
| - | } | + | |
| - | } | + | |
| - | </code>Așa cum se poate observa, a fost utilizat un ascultător de tip ''WakefulBroadcastReceiver'', delegarea procesării mesajului către serviciul dedicat făcându-se prin metoda ''startWakefulService'' ce asigură și obținerea accesului la mutexul care împiedică intrarea dispozitivului mobil într-o stare de inactivitate. | + | |
| - | === Serviciul GoogleCloudMessagingIntentService === | + | Totodată, este recomandat să se pună la dispoziția utilizatorilor elemente în cadrul interfeței grafice prin intermediul cărora aceste operații să poată fi controlate prin interacțiunea cu cei care folosesc aplicația Android. |
| - | În cadrul serviciului pentru tratarea evenimentului (de tip ''IntentService'') are loc procesarea propriu-zisă a mesajului, aceasta fiind realizată în cadrul metodei ''onHandleIntent()'' care primește ca parametru aceeași intenție care a fost transmisă ascultătorului, și pentru care a fost obținut mutextul care împiedică intrarea dispozitivului mobil într-o stare de inactivitate. | + | ==== Android NSD ==== |
| - | Prelucrarea unui mesaj se face în funcție de tipul acestuia, furnizat de metoda ''getMessageType'' a obiectului de tip ''GoogleCloudMessaging'', pe baza informațiilor din intenție. Odată cunoscut tipul mesajului și datele asociate prin ''Bundle'', utilizatorul poate realiza orice acțiune, de la transmiterea unei notificări la modificarea conținutului unor controale grafice din cadrul activității. | + | **Pornirea ** operației de descoperire a serviciilor disponibile se face prin intermediul metodei [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#discoverServices%28java.lang.String,%20int,%20android.net.nsd.NsdManager.DiscoveryListener%29|discoverServices(String, int, NsdManager.DiscoveryListener)]] din clasa ''NsdManager'' care primește ca parametri: |
| + | * tipul de serviciu; | ||
| + | * protocolul, în acest caz folosindu-se ''NsdManager.PROTOCOL_DNS_SD''; | ||
| + | * un obiect ascultător de tipul ''NsdManager.DiscoveryListener'', care trebuie să implementeze metodele: | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.DiscoveryListener.html#onDiscoveryStarted%28java.lang.String%29|onDiscoveryStarted(String)]] - apelată în mod automat dacă pornirea operației de căutare a serviciilor a fost realizată cu succes; | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.DiscoveryListener.html#onStartDiscoveryFailed%28java.lang.String,%20int%29|onStartDiscoveryFailed(String, int)]] - apelată în mod automat dacă pornirea operației de căutare a serviciilor a eșuat, oferindu-se și un cod de eroare care poate fi folosit în scopuri de depanare. | ||
| - | <note important>Ulterior analizei mesajului, trebuie să se asigure și eliberarea mutexului ce împiedică intrarea dispozitivului mobil într-o stare de inactivitate, prin apelul metodei ''completeWakefulIntent()'' căreia i se transmite ca parametru aceeași intenție pentru care a fost obținută această resursă.</note> | + | **Oprirea** operației de descoperire a serviciilor disponibile se face prin intermediul metodei [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#stopServiceDiscovery%28android.net.nsd.NsdManager.DiscoveryListener%29|stopServiceDiscovery(NsdManager.DiscoveryListener)]] din clasa ''NsdManager'' care primește ca parametru un obiect ascultător de tipul ''NsdManager.DiscoveryListener'', care trebuie să implementeze metodele: |
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.DiscoveryListener.html#onDiscoveryStopped%28java.lang.String%29|onDiscoveryStopped(String)]] - apelată în mod automat dacă oprirea operației de căutare a serviciilor a fost realizată cu succes; | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.DiscoveryListener.html#onStopDiscoveryFailed%28java.lang.String,%20int%29|onStopDiscoveryFailed(String, int)]] - apelată în mod automat dacă oprirea operației de căutare a serviciilor a eșuat, oferindu-se și un cod de eroare care poate fi folosit în scopuri de depanare. | ||
| <code java> | <code java> | ||
| - | public class GoogleCloudMessagingIntentService extends IntentService { | + | public void startNetworkServiceDiscovery() { |
| + | nsdManager.discoverServices(Constants.SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener); | ||
| + | } | ||
| + | |||
| + | public void stopNetworkServiceDiscovery() { | ||
| + | nsdManager.stopServiceDiscovery(discoveryListener); | ||
| + | |||
| + | // ... | ||
| + | } | ||
| + | </code> | ||
| - | private final static String TAG = "GoogleCloudMessagingIntentService"; | + | Pentru obiectul de tip ''NsdManager.DiscoveryListener'' trebuie implementate și metodele apelate în mod automat în momentul în care un serviciu este găsit, respectiv este pierdut: |
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.DiscoveryListener.html#onServiceFound%28android.net.nsd.NsdServiceInfo%29|onServiceFound(NsdServiceInfo)]] - se verifică parametrii serviciului descoperit (tip și denumire), putându-se întâlni următoarele situații: | ||
| + | - tip de serviciu necunoscut (cu toate că descoperirea implică filtrarea după un tip de servicii specific); | ||
| + | - descoperirea serviciului oferit de mașina curentă / dispozitivul curent - se realizează comparația dintre denumirea serviciului găsit și denumirea serviciului curent; | ||
| + | - descoperirea unui serviciu oferit de o altă mașină / un alt dispozitiv (se poate folosi un șablon pe denumirea serviciului) - se trece la rezolvarea serviciului respectiv (prin invocarea metodei ''resolveService(NsdManager, NsdManager.ResolveListener)'' din cadrul obiectului de tip ''NsdManager''. | ||
| + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.DiscoveryListener.html#onServiceLost%28android.net.nsd.NsdServiceInfo%29|onServiceLost(NsdServiceInfo)]] - se gestionează corespunzător lista de servicii descoperite. | ||
| - | public GoogleCloudMessagingIntentService() { | + | <note tip>În cazul în care se produce o eroare (indiferent de momentul la care are loc - pornirea sau oprirea procesului de descoperire a serviciilor), se termină orice activitate legată de descoperirea serviciilor.</note> |
| - | super(TAG); | + | |
| - | } | + | |
| - | @Override | + | <code java> |
| - | protected void onHandleIntent(Intent intent) { | + | DiscoveryListener discoveryListener = new DiscoveryListener() { |
| - | // the intent is supplied via the BroadcastReceiver | + | |
| - | GoogleCloudMessaging googleCloudMessaging = GoogleCloudMessaging.getInstance(this); | + | @Override |
| - | + | public void onDiscoveryStarted(String serviceType) { | |
| - | Bundle extras = intent.getExtras(); | + | Log.i(Constants.TAG, "Service discovery started: " + serviceType); |
| - | if (!extras.isEmpty()) { | + | } |
| - | String messageType = googleCloudMessaging.getMessageType(intent); | + | |
| - | // filter the message according to its type | + | @Override |
| - | // send a notification containing the data attached to the message (as extras) | + | public void onServiceFound(NsdServiceInfo nsdServiceInfo) { |
| - | if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR.equals(messageType)) { | + | Log.i(Constants.TAG, "Service found: " + nsdServiceInfo); |
| - | sendNotification("Send error: " + extras.toString()); | + | if (!nsdServiceInfo.getServiceType().equals(Constants.SERVICE_TYPE)) { |
| - | } else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED.equals(messageType)) { | + | Log.i(Constants.TAG, "Unknown Service Type: " + nsdServiceInfo.getServiceType()); |
| - | sendNotification("Deleted messages on server: " + extras.toString()); | + | } else if (nsdServiceInfo.getServiceName().equals(serviceName)) { |
| - | } else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) { | + | Log.i(Constants.TAG, "The service running on the same machine has been discovered: " + serviceName); |
| - | sendNotification("Received: " + extras.toString()); | + | } else if (nsdServiceInfo.getServiceName().contains(Constants.SERVICE_NAME_SEARCH_KEY)) { |
| - | } | + | nsdManager.resolveService(nsdServiceInfo, resolveListener); |
| - | } | + | |
| - | // release the wake lock provided by the WakefulBroadcastReceiver. | + | |
| - | GoogleCloudMessagingBroadcastReceiver.completeWakefulIntent(intent); | + | |
| } | } | ||
| - | + | } | |
| - | // create a notification containing the message and post it | + | |
| - | private void sendNotification(String message) { | + | @Override |
| - | // ... | + | public void onServiceLost(final NsdServiceInfo nsdServiceInfo) { |
| + | Log.i(Constants.TAG, "Service lost: " + nsdServiceInfo); | ||
| + | |||
| + | ArrayList<NetworkService> discoveredServices = chatNetworkServiceFragment.getDiscoveredServices(); | ||
| + | NetworkService networkService = new NetworkService( | ||
| + | nsdServiceInfo.getServiceName(), | ||
| + | (nsdServiceInfo.getHost() != null) ? nsdServiceInfo.getHost().toString() : null, | ||
| + | nsdServiceInfo.getPort(), | ||
| + | -1 | ||
| + | ); | ||
| + | |||
| + | if (discoveredServices.contains(networkService)) { | ||
| + | int index = discoveredServices.indexOf(networkService); | ||
| + | discoveredServices.remove(index); | ||
| } | } | ||
| - | } | + | chatNetworkServiceFragment.setDiscoveredServices(discoveredServices); |
| + | |||
| + | // ... | ||
| + | } | ||
| + | |||
| + | @Override | ||
| + | public void onDiscoveryStopped(String serviceType) { | ||
| + | Log.i(Constants.TAG, "Service discovery stopped: " + serviceType); | ||
| + | } | ||
| + | |||
| + | @Override | ||
| + | public void onStartDiscoveryFailed(String serviceType, int errorCode) { | ||
| + | Log.e(Constants.TAG, "Service discovery start failed - Error code:" + errorCode); | ||
| + | nsdManager.stopServiceDiscovery(this); | ||
| + | } | ||
| + | |||
| + | @Override | ||
| + | public void onStopDiscoveryFailed(String serviceType, int errorCode) { | ||
| + | Log.e(Constants.TAG, "Service discovery stop failed - Error code:" + errorCode); | ||
| + | nsdManager.stopServiceDiscovery(this); | ||
| + | } | ||
| + | }; | ||
| </code> | </code> | ||
| - | ==== Permisiuni AndroidManifest.xml ==== | ||
| - | * în fișierul ''AndroidManifest.xml'' se indică versiunea Google Cloud Messaging în cadrul elementului ''<application>'':\\ <code xml> | + | ==== JmDNS ==== |
| - | <meta-data android:name="com.google.android.gms.version" | + | |
| - | android:value="@integer/google_play_services_version" /> | + | |
| - | </code> | + | |
| - | * în fișierul ''AndroidManifest.xml'' se precizează permisiunile necesare pentru ca aplicația să poată utiliza funcționalitățile oferite de Google Cloud Messaging\\ <code xml> | + | |
| - | <manifest package="ro.pub.systems.pdsd.lab09.GoogleCloudMessagingClient" ...> | + | |
| - | <uses-permission android:name="android.permission.INTERNET" /> | + | **Pornirea ** operației de descoperire a serviciilor disponibile se face prin intermediul metodei [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#addServiceListener%28java.lang.String,%20javax.jmdns.ServiceListener%29|addServiceListener(String, ServiceListener)]] din clasa ''JmDNS'' care primește ca parametri: |
| - | <uses-permission android:name="android.permission.GET_ACCOUNTS" /> | + | * tipul de serviciu; |
| - | <uses-permission android:name="android.permission.WAKE_LOCK" /> | + | * un obiect ascultător de tipul ''ServiceListener'', care reacționează la evenimentele legate de serviciile găsite / pierdute. |
| - | <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" /> | + | |
| - | <permission android:name="ro.pub.systems.pdsd.lab09.GoogleCloudMessagingClient.permission.C2D_MESSAGE" | + | **Oprirea** operației de descoperire a serviciilor disponibile se face prin intermediul metodei [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#removeServiceListener%28java.lang.String,%20javax.jmdns.ServiceListener%29|removeServiceListener(String, ServiceListener)]] din clasa ''JmDNS'' care primește ca parametru un obiect ascultător de tipul ''ServiceListener'', care reacționează la evenimentele legate de serviciile găsite / pierdute. |
| - | android:protectionLevel="signature" /> | + | |
| - | <uses-permission android:name="ro.pub.systems.pdsd.lab09.GoogleCloudMessagingClient.permission.C2D_MESSAGE" /> | + | |
| - | <application ...> | + | Ca atare, informațiile cu privire la găsirea / pierderea serviciilor în rețeaua locală vor fi furnizate numai între apelurile metodelor ''addServiceListener()'', respectiv ''removeServiceListener()''. |
| - | <receiver | + | |
| - | android:name=".GoogleCloudMessagingBroadcastReceiver" | + | |
| - | android:permission="com.google.android.c2dm.permission.SEND" > | + | |
| - | <intent-filter> | + | |
| - | <action android:name="com.google.android.c2dm.intent.RECEIVE" /> | + | |
| - | <category android:name="ro.pub.systems.pdsd.lab09.GoogleCloudMessagingClient" /> | + | |
| - | </intent-filter> | + | |
| - | </receiver> | + | |
| - | <service android:name=".GoogleCloudMessagingIntentService" /> | + | |
| - | </application> | + | |
| - | </manifest> | + | <code java> |
| - | </code> | + | public void startNetworkServiceDiscovery() { |
| - | <note important>Pentru a se asigura faptul că aplicația Android va rula corespunzător, ținând cont de faptul că Google Cloud Messaging nu poate fi folosit pe dispozitive mobile rulând un sistem de operare Android anterior versiunii 2.3, proprietatea ''android:minSdkVersion'' va avea o valoare egală sau mai mare cu 8.</note> | + | if (jmDns != null && serviceListener != null) { |
| - | * permisiunea ''android.permission.INTERNET'' este necesară pentru ca aplicația să poate transmite identificatorul său serverului de aplicații, prin intermediul căruia se realizează înregistrarea sa | + | jmDns.addServiceListener(Constants.SERVICE_TYPE, serviceListener); |
| - | * permisiunea ''android.permission.GET_ACCOUNTS'' este necesară întrucât Google Cloud Messaging solicită (pentru sisteme de operare Android anterioare versiunii 4.0.4) un cont Google | + | } |
| - | * permisiunea ''android.permission.WAKE_LOCK'' (opțională) este necesară pentru a se preveni întreruperea activității în momentul în care este primit un mesaj | + | |
| - | * permisiunea ''android.permission.RECEIVE'' este necesară pentru ca aplicația să se poată înregistra (în cadrul serverului de aplicații) și pentru a putea primi mesaje | + | |
| - | * permisiunea de tip ''permission.C2D_MESSAGE'' (prefixată de denumirea pachetului care identifică în mod unic aplicația) pentru a preveni altă aplicație Android să se înregistreze (în cadrul serverului de aplicații) și să primească mesajele care îi sunt destinate | + | |
| - | <note warning>Este necesar ca acest tip de permisiune să utilizeze această sintaxă, în caz contrar mesajele nefiind transmise aplicației Android.</note> | + | |
| - | * un **ascultător** (de tipul ''com.google.android.c2dm.intent.RECEIVE'') având permisiunea ''com.google.android.c2dm.SEND'' astfel încât doar motorul Google Cloud Messaging să îi poată transmite mesaje | + | |
| - | <note>Proprietatea ''category'' a ascultătorului trebuie să aibă ca valoare denumirea pachetului aplicației Android.</note> | + | |
| - | <note tip>În cazul în care este asociat și un serviciu (șablon de proiectare frecvent utilizat, însă nu obligatoriu), ascultătorul va fi un obiect de tipul ''WakefulBroadcastReceiver'', creând un mutex parțial pentru a se asigura faptul că dispozitivul mobil rulează.</note> | + | |
| - | * un **serviciu** (de obicei, un obiect de tip ''IntentService'') - opțional - căruia i se transmite mesajul Google Cloud Messaging (de către obiectul de tip ''WakefulBroadcastReceiver'') având sarcina procesării sale și asigurând faptul că dispozitivul nu își întrerupe activitatea în timpul acestui proces | + | |
| - | <note tip>În situația în care nu se dorește să se utilizeze un serviciu, procesarea mesajului poate fi realizată în ascultător (în acest caz, un obiect de tipul ''BroadcastReceiver'').</note> | + | |
| - | + | ||
| - | ==== Reguli Proguard ==== | + | |
| - | + | ||
| - | Se precizează regulile pentru obfuscatorul Proguard (în fișierul ''proguard-project.txt'' din rădăcina proiectului), astfel încât acesta să nu elimine clasele necesare:\\ <code java> | + | |
| - | -keep class * extends java.util.ListResourceBundle { | + | |
| - | protected Object[][] getContents(); | + | |
| } | } | ||
| - | + | ||
| - | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { | + | public void stopNetworkServiceDiscovery() { |
| - | public static final *** NULL; | + | if (jmDns != null && serviceListener != null) { |
| - | } | + | jmDns.removeServiceListener(Constants.SERVICE_TYPE, serviceListener); |
| - | + | } | |
| - | -keepnames @com.google.android.gms.common.annotation.KeepName class * | + | |
| - | -keepclassmembernames class * { | + | // ... |
| - | @com.google.android.gms.common.annotation.KeepName *; | + | |
| - | } | + | |
| - | + | ||
| - | -keepnames class * implements android.os.Parcelable { | + | |
| - | public static final ** CREATOR; | + | |
| } | } | ||
| </code> | </code> | ||
| - | ===== Proiectarea și dezvoltarea serverului ===== | ||
| - | Arhitectura Google Cloud Messaging implică existența a două servere: | + | Pentru obiectul de tip ''ServiceListener'' trebuie implementate metodele apelate în mod automat în momentul în care un serviciu este găsit, respectiv este pierdut: |
| + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/ServiceListener.html#serviceAdded%28javax.jmdns.ServiceEvent%29|serviceAdded(ServiceEvent)]] - se verifică parametrii serviciului descoperit (tip și denumire), putându-se întâlni următoarele situații: | ||
| + | - tip de serviciu necunoscut (cu toate că descoperirea implică filtrarea după un tip de servicii specific); | ||
| + | - descoperirea serviciului oferit de mașina curentă / dispozitivul curent - se realizează comparația dintre denumirea serviciului găsit și denumirea serviciului curent; | ||
| + | - descoperirea unui serviciu oferit de o altă mașină / un alt dispozitiv (se poate folosi un șablon pe denumirea serviciului) - se trece la rezolvarea serviciului respectiv (prin invocarea metodei ''requestServiceInfo(String, String)'' din cadrul obiectului de tip ''JmDNS''. | ||
| - | **1.** un **server de conexiune** furnizat de serviciul Google Cloud Messaging, prin intermediul căruia pot fi livrate mesaje către un client Android, transmise anterior de la un server de aplicații; la rândul său, acesta este disponibil în două variante: | + | <note tip> |
| - | * [[http://developer.android.com/google/gcm/http.html|HTTP (Hyper Text Transfer Protocol)]] | + | Alternativ, rezolvarea serviciului descoperit poate fi realizată ad-hoc (fără a se apela metoda ''serviceResolved()''), prin apelul metodei [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#getServiceInfo%28java.lang.String,%20java.lang.String%29|getServiceInfo(String, String)]], cu precizarea că timpul său de execuție poate fi considerabil: |
| - | * [[http://developer.android.com/google/gcm/ccs.html|CCS (Cloud Connection Server) / XMPP (eXtensible Messaging and Presence Protocol)]] | + | <code java> |
| + | ServiceInfo serviceInfo = serviceEvent.getDNS().getServiceInfo( | ||
| + | serviceEvent.getType(), | ||
| + | serviceEvent.getName() | ||
| + | ); | ||
| + | </code> | ||
| + | </note> | ||
| - | În alegerea uneia dintre cele două variante ale serverului de conexiune, trebuie să se țină cont de funcționalitățile puse la dispoziție de acestea: | + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/ServiceListener.html#serviceRemoved%28javax.jmdns.ServiceEvent%29|serviceRemoved(ServiceEvent)]] - se gestionează corespunzător lista de servicii descoperite. |
| - | ^ **FUNCȚIONALITATE** ^ **HTTP** ^ **CCS/XMPP** ^ | + | Un obiect de tipul [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/ServiceInfo.html|ServiceInfo]] conține, printre altele, și informații cu privire la: |
| - | | sens conexiune | server GCM → client Android (unidirecțional) | server GCM ↔ client Android (bidirecțional) | | + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/ServiceInfo.html#getInetAddresses%28%29|getInetAddresses()]] - adresele la care poate fi accesat serviciul; |
| - | | tip comunicație | sincron\\ expeditorul se conectează la destinatar printr-o conexine HTTP nepersistentă, transmite o cerere POST și se blochează în așteptarea unui răspuns | asincron\\ expeditorul se conectează la destinatar printr-o conexiune XMPP persistentă, putând trimite/primi mesaje la viteză maximă (rezultatul fiind transmis sub forma unor notificări codificate utilizând același protocol) | | + | * [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/ServiceInfo.html#getPort%28%29|getPort()]] - portul pe care poate fi accesat serviciul. |
| - | | corp mesaj | text sau JSON transmise prin intermediul unei cereri HTTP POST | JSON încapsulat într-un mesaj XMPP | | + | |
| - | | destinatar multiplu | NU (pentru mesaje text)\\ DA (pentru mesaje JSON) | NU | | + | |
| - | | înregistrare anterioară | NU | DA | | + | |
| - | + | ||
| - | <note tip>Cele două soluții pentru serverul de conexiune la serviciul Google Cloud Messaging pot fi utilizate concomitent, în funcție de specificațiile aplicației respective.</note> | + | |
| - | + | ||
| - | **2.** un **server de aplicații**, implementat de programator, pentru a transmite mesaje către serverul de conexiune Google Cloud Messaging ce vor fi livrate ulterior clientului Android (atunci când acesta devine disponibil); acesta trebuie să îndeplinească mai multe condiții: | + | |
| - | * să poată comunica cu clientul Android (să fie disponibil la o adresă publică în Internet sau să facă parte din cadrul aceleiași rețele, în caz contrar); | + | |
| - | * să construiască mesaje către serverul de conexiune Google Cloud Messaging într-un format acceptat de acesta; | + | |
| - | * să poată stoca o cheie publică prin intermediul căreia să se autentifice în cadrul serverului de conexiune Google Cloud Messaging precum și identificatorii de identificare a clienților Android pentru a specifica locația la care trebuie livrate mesajele; | + | |
| - | * să poată primi mesaje și să își adapteze rata de transmisie în funcție de informațiile furnizate cu privire la rezultatul acestora; | + | |
| - | * să genereze identificatori unici pentru mesajele pe care le trimite. | + | |
| - | + | ||
| - | <note>În continuare, va fi prezentată structura unui mesaj folosind un server de conexiune HTTP.</note> | + | |
| - | + | ||
| - | ==== Structura unui mesaj ==== | + | |
| - | + | ||
| - | Mesajele interschimbate între serverul de aplicații și serverul de conexiune Google Cloud Messaging de tip HTTP pot fi: | + | |
| - | * o cerere de tip POST transmisă către [[https://android.googleapis.com/gcm/send|]] și formată din: | + | |
| - | * un **antet** care specifică informațiile necesare autentificării în cadrul serviciului Google Cloud Messaging, precum și formatul mesajului; | + | |
| - | * un **corp** (în format text sau JSON) în care informațiile propriu-zise sunt conținute în perechi de tip (atribut, valoare). | + | |
| - | * un răspuns care conține rezultatul operației precum și alte informații suplimentare. | + | |
| - | + | ||
| - | === Cerere: server de aplicații → server de conexiune GCM === | + | |
| - | + | ||
| - | Un mesaj transmis de serverul de aplicații către serverul de conexiune Google Cloud Messaging de tip HTTP este o cerere de tip POST ce conține informațiile ce trebuie livrate către unul sau mai multe dispozitive mobile (în momentul în care acestea vor deveni disponibile). | + | |
| <code java> | <code java> | ||
| + | ServiceListener serviceListener = new ServiceListener() { | ||
| - | // the address of the Google Cloud Messaging HTTP connection server | + | // ... |
| - | // to which the application server sends a POST request in order to be further transmitted to the mobile device | + | |
| - | + | @Override | |
| - | final public static String REQUEST_SERVER_URL = "https://android.googleapis.com/gcm/send"; | + | public void serviceAdded(ServiceEvent serviceEvent) { |
| + | if (!serviceEvent.getType().equals(Constants.SERVICE_TYPE)) { | ||
| + | Log.i(Constants.TAG, "Unknown Service Type: " + serviceEvent.getType()); | ||
| + | } else if (serviceEvent.getName().equals(serviceName)) { | ||
| + | Log.i(Constants.TAG, "The service running on the same machine has been discovered: " + serviceName); | ||
| + | } else if (serviceEvent.getName().contains(Constants.SERVICE_NAME_SEARCH_KEY)) { | ||
| + | Log.i(Constants.TAG, "The service should be resolved now: " + serviceEvent); | ||
| + | jmDns.requestServiceInfo(serviceEvent.getType(), serviceEvent.getName()); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | @Override | ||
| + | public void serviceRemoved(final ServiceEvent serviceEvent) { | ||
| + | ServiceInfo serviceInfo = serviceEvent.getInfo(); | ||
| + | if (serviceInfo == null) { | ||
| + | Log.e(Constants.TAG, "Service Info for Service is null!"); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | String[] hosts = serviceInfo.getHostAddresses(); | ||
| + | String host = null; | ||
| + | if (hosts.length != 0) { | ||
| + | host = hosts[0]; | ||
| + | if(host.startsWith("/")) { | ||
| + | host = host.substring(1); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | int port = serviceInfo.getPort(); | ||
| + | |||
| + | // ... | ||
| + | |||
| + | ArrayList<NetworkService> discoveredServices = chatNetworkServiceFragment.getDiscoveredServices(); | ||
| + | NetworkService networkService = new NetworkService(serviceEvent.getName(), host, port, -1); | ||
| + | if (discoveredServices.contains(networkService)) { | ||
| + | int index = discoveredServices.indexOf(networkService); | ||
| + | discoveredServices.remove(index); | ||
| + | } | ||
| + | chatNetworkServiceFragment.setDiscoveredServices(discoveredServices); | ||
| + | } | ||
| + | }; | ||
| + | </code> | ||
| - | // ... | + | ===== Rezolvarea Serviciilor Descoperite Anterior ===== |
| - | HttpPost httpRequest = new HttpPost(REQUEST_SERVER_URL); | + | Rezolvarea unui serviciu descoperit anterior implică obținerea de informații suplimentare cu privire la acesta. |
| - | </code> | + | |
| - | Un astfel de mesaj va trebui să conțină: | + | Astfel, pentru un serviciu pentru care se cunoaște doar tipul și denumirea, se poate determina, prin intermediul multi-cast DNS, adresa și portul la care acesta este disponibil, putând fi accesat. |
| - | * în antet: | + | |
| - | * informațiile de autentificare în cadrul serverului Google Cloud Messaging - cheia publică obținută prin intermediul Google Developer's Console, certificându-se faptul că există drepturile necesare pentru realizarea unei conexiuni de acest tip; | + | |
| - | * informații referitoare la tipul de mesaj (text sau JSON); | + | |
| - | * în corp: datele propriu-zise. | + | |
| - | == Antet == | + | ==== Android NSD ==== |
| - | În cadrul antetului cererii de tip POST trebuie să fie incluse câmpurile: | + | În cadrul metodei ''onServiceFound()'' din ascultătorul de tip ''DiscoveryListener'', în situația în care denumirea serviciului corespunde unui anumit șablon (deși o astfel de condiție nu este întotdeauna necesară), se invocă metoda [[http://developer.android.com/reference/android/net/nsd/NsdManager.html#resolveService%28android.net.nsd.NsdServiceInfo,%20android.net.nsd.NsdManager.ResolveListener%29|resolveService(NsdServiceInfo, NsdManager.ResolveListener]] care primește ca parametri: |
| - | * ''Authorization'' - conține informațiile necesare procesului de autentificare în cadrul serverului Google Cloud Messaging, având forma ''key='', urmată de cheia publică furnizată prin intermediul Google Developer's Console. | + | * un obiect de tip ''NsdServiceInfo'', serviciul care se dorește a fi rezolvat; |
| - | * ''Content-Type'' - indică tipul pe care îl are corpul mesajului, acesta putând fi text (''application/x-www-form-urlencoded;charset=UTF-8'') sau JSON (''application/json''). | + | * un obiect ascultător de tipul ''NsdManager.ResolveListener'', care trebuie să implementeze metodele ce vor fi invocate în funcție de rezultatul operației de rezolvare: |
| - | + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.ResolveListener.html#onServiceResolved%28android.net.nsd.NsdServiceInfo%29|onServiceResolved(NsdServiceInfo)]] - apelată în mod automat în situația în care rezolvarea serviciului a fost realizată cu succes; | |
| - | <note tip>În situația în care nu este inclus câmpul ''Content-Type'', se așteaptă ca mesajul să aibă un corp de tip text.</note> | + | * [[http://developer.android.com/reference/android/net/nsd/NsdManager.ResolveListener.html#onResolveFailed%28android.net.nsd.NsdServiceInfo,%20int%29|onResolvedFailed(NsdServiceInfo,int)]] - apelată în mod automat în situația în care rezolvarea serviciului a eșuat. |
| <code java> | <code java> | ||
| - | /** | + | DiscoveryListener discoveryListener = new DiscoveryListener() { |
| - | * HTTP headers | + | |
| - | * | + | // ... |
| - | * Authorization: key=API key provided from Google Developer Console | + | |
| - | * Content-Type: the format of the HTTP body (may be omitted for plain text) | + | @Override |
| - | * - application/x-www-form-urlencoded;charset=UTF-8 for plain text | + | public void onServiceFound(NsdServiceInfo nsdServiceInfo) { |
| - | * - application/json for JSON | + | |
| - | * | + | // ... |
| - | */ | + | if (nsdServiceInfo.getServiceName().contains(Constants.SERVICE_NAME_SEARCH_KEY)) { |
| - | httpRequest.setHeader("Authorization", "key=" + Constants.API_KEY); | + | nsdManager.resolveService(nsdServiceInfo, resolveListener); |
| - | httpRequest.setHeader("Content-Type", "application/json"); | + | } |
| + | } | ||
| + | } | ||
| </code> | </code> | ||
| - | == Corp == | + | Metoda ''onResolveFailed()'' indică faptul că nu au putut fi obținute informații suplimentare (adresă și port) cu privire la serviciu. Frecvent, este furnizat codul de eroare ''NsdManager.FAILURE_INTERNAL_ERROR'', având valoarea 0, ceea ce indică faptul că este destul de probabil ca problema să nu fi fost cauzată de o greșeală de proiectare / dezvoltare. |
| - | Informațiile conținute de corpul unei cereri de tip POST au forma unor perechi (cheie, valoare), fiind definită o serie de parametrii standard, în funcție de formatul în care este transmis mesajul. | + | Metoda ''onServiceResolved()'' indică faptul că au putut fi obținute informații suplimentare (adresă și port) cu privire la serviciu. În acest caz, se va gestiona corespunzător lista de servicii descoperite în rețeaua locală, obținându-se și un canal de comunicație către parametrii determinați. |
| - | + | ||
| - | ^ **PARAMETRU** ^ **DESCRIERE** ^ **text** ^ **JSON** ^ **obligativitate** ^ | + | |
| - | | ''registration_id'' | un identificator de înregistrare descriind dispozitivul mobil către care se dorește să se livreze mesajul respectiv | ✔ | ✘ | ✔ | | + | |
| - | | ''registration_ids'' | o listă conținând 1-1000 identificatori de înregistrare indicând dispozitivele mobile către care se dorește să se livreze mesajul respectiv | ✘ | ✔ | ✔ | | + | |
| - | | ''notification_key'' | șir de caractere asociat mai multor dispozitive mobile (1-10) ce aparțin aceluiași utilizator | | | ✔ | | + | |
| - | | ''collapse_key'' | șir de caractere (aleator, definit de utilizator) folosit pentru a identifica mesajele de același tip, astfel încât, pe serverul Google Cloud Messaging să se stocheze doar cea mai recentă valoare asociată, în scopul optimizării traficului de mesaje către dispozitivul mobil (dacă acesta nu este disponibil la un moment dat și au fost transmise mai multe mesaje, va fi stocat doar cel mai recent); se permite stocarea a maximum 4 chei de acest tip pe serverul Google Cloud Messaging | ✔ | ✔ | | | + | |
| - | | ''data'' | obiect JSON format din perechi (atribut, valoare) reprezentând conținutul propriu-zis al mesajului, acestea fiind incluse în cadrul intenției sub forma unui obiect ''Bundle'' (ca ''extras''), având aceeași structură;\\ atributul poate avea orice valoare, cu excepția unor cuvinte rezervate (''from'', ''google.*''), evitându-se pe cât posibil folosirea denumirilor de parametri, întrucât aceștia vor fi suprascriși;\\ pentru valoare poate fi utilizat orice tip de obiect JSON, însă la nivelul serverului Google Cloud Messaging se face conversia la șiruri de caractere;\\ \\ dimensiunea obiectului este limitată la 4kb;\\ nu există limitări în privința numărului de perechi (atribut, valoare); | ✔ ''data.<atribut>'' | ✔ | | | + | |
| - | | ''delay_while_idle'' | indică faptul că mesajul nu trebuie transmis imediat dacă dispozitivul se află într-o stare de inactivitate, urmând a fi livrat în momentul în care acesta devine activ; valoarea implicită este ''false'' | ✔ | ✔ | | | + | |
| - | | ''time_to_live'' | durata de stocare a mesajului pe serverul Google Cloud Messaging (exprimată în secunde) în situația în care dispozitivul mobil nu este disponibil; valoarea implicită este de 4 săptămâni | ✔ | ✔ | | | + | |
| - | | ''restricted_package_name'' | șir de caractere referind denumirea pachetului corespunzător unei aplicații Android, astfel încât mesajul să fie livrat doar acelor identificatori de înregistrare care au fost generați pentru acesta | ✔ | ✔ | | | + | |
| - | | ''dry_run'' | utilizat în scop de testare, pentru a verifica funcționalitatea cererii de tip POST fără a transmite propriu-zis un mesaj către dispozitivul mobil; valoarea implicită este ''false'' | ✔ | ✔ | | | + | |
| - | + | ||
| - | <note tip>Locația (dispozitivul mobil) către care va fi livrat un mesaj trebuie să fie specificată fie prin parametrul ''registration_id'' / ''registration_ids'', fie prin parametrul ''notification_key''.</note> | + | |
| - | + | ||
| - | <note important>Pentru mesajele de tip text, se va defini câte un parametru ''data.<atribut>'' pentru fiecare informație, asociindu-i-se valoarea respectivă. În această situație, se mențin restricțiile din cazul mesajelor de tip JSON.</note> | + | |
| <code java> | <code java> | ||
| - | /** | + | ResolveListener resolveListener = new ResolveListener() { |
| - | * HTTP body | + | |
| - | * | + | |
| - | * -> registration_id/registration_ids indicates the recipient/recipients | + | |
| - | * (for each mobile device to which the message is to be transmitted the message must specify | + | |
| - | * the id provided by the GCM server during the registration process) | + | |
| - | * | + | |
| - | * -> data contains the payload to be transmitted; the developer may define its own keys | + | |
| - | * | + | |
| - | * -> other fields: collapse_key, delay_while_idle, time_to_live, restricted_package_name, dry_run | + | |
| - | * | + | |
| - | * The complete list of the message parameters can be found out at: | + | |
| - | * http://developer.android.com/google/gcm/server.html#params | + | |
| - | */ | + | |
| - | JSONObject requestBody = new JSONObject(); | + | @Override |
| - | JSONArray registrationIds = new JSONArray(); | + | public void onResolveFailed(NsdServiceInfo nsdServiceInfo, int errorCode) { |
| - | registrationIds.add(registrationId); | + | Log.e(Constants.TAG, "Resolve failed: " + errorCode); |
| - | requestBody.put("registration_ids", registrationIds); | + | } |
| - | JSONObject payload = new JSONObject(); | + | |
| - | payload.put("message", message); | + | @Override |
| - | requestBody.put("data", payload); | + | public void onServiceResolved(NsdServiceInfo nsdServiceInfo) { |
| + | Log.i(Constants.TAG, "Resolve succeeded: " + nsdServiceInfo); | ||
| + | |||
| + | if (nsdServiceInfo.getServiceName().equals(serviceName)) { | ||
| + | Log.i(Constants.TAG, "The service running on the same machine has been discovered."); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | String host = nsdServiceInfo.getHost().toString(); | ||
| + | if(host.startsWith("/")) { | ||
| + | host = host.substring(1); | ||
| + | } | ||
| + | int port = nsdServiceInfo.getPort(); | ||
| + | |||
| + | ArrayList<NetworkService> discoveredServices = ((ChatActivity)context).getChatNetworkServiceFragment().getDiscoveredServices(); | ||
| + | NetworkService networkService = new NetworkService(nsdServiceInfo.getServiceName(), host, port, Constants.CONVERSATION_TO_SERVER); | ||
| + | if (!discoveredServices.contains(networkService)) { | ||
| + | discoveredServices.add(networkService); | ||
| + | } | ||
| + | ((ChatActivity)context).getChatNetworkServiceFragment().setDiscoveredServices(discoveredServices); | ||
| + | Log.i(Constants.TAG, "A service has been discovered on " + host + ":" + port); | ||
| + | } | ||
| + | }; | ||
| </code> | </code> | ||
| - | === === | + | ==== JmDNS ==== |
| - | O astfel de cerere va avea forma: | + | În cadrul metodei ''serviceAdded()'' din ascultătorul de tip ''ServiceListener'', în situația în care denumirea serviciului corespunde unui anumit șablon (deși o astfel de condiție nu este întotdeauna necesară), se invocă metoda [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/JmDNS.html#requestServiceInfo%28java.lang.String,%20java.lang.String%29|requestServiceInfo(String, String)]] care primește ca parametri: |
| + | * tipul serviciului; | ||
| + | * denumirea serviciului. | ||
| - | <code> | + | <code java> |
| - | Authorization:key=... | + | Service serviceListener = new ServiceListener() { |
| - | Content-Type:application/json | + | // ... |
| - | { | + | |
| - | "registration_ids": ["..."], | + | @Override |
| - | "data": { | + | public void serviceAdded(ServiceEvent serviceEvent) { |
| - | "message": "..." | + | |
| - | } | + | // ... |
| - | } | + | if (serviceEvent.getName().contains(Constants.SERVICE_NAME_SEARCH_KEY)) { |
| + | Log.i(Constants.TAG, "The service should be resolved now: " + serviceEvent); | ||
| + | jmDns.requestServiceInfo(serviceEvent.getType(), serviceEvent.getName()); | ||
| + | } | ||
| + | } | ||
| + | }; | ||
| </code> | </code> | ||
| - | Ulterior construirii cererii, aceasta va trebui transmisă, operația fiind realizată sincron, producând blocarea metodei în așteptarea răspunsului de la server: | + | În situația în care operația de rezolvare a serviciului a fost realizată cu succes, se apelează metoda [[http://jmdns.sourceforge.net/apidocs/javax/jmdns/ServiceListener.html#serviceResolved%28javax.jmdns.ServiceEvent%29|serviceResolved(ServiceEvent)]] în cadrul aceluiași obiect ascultător (de tip ''ServiceListener''). Aceasta primește ca parametru un obiect ''ServiceEvent'' care încapsulează informații cu privire la serviciul rezolvat, sub forma unui obiect de tip ''ServiceInfo''. Și în această situație se va gestiona corespunzător lista de servicii descoperite în rețeaua locală, obținându-se și un canal de comunicație către parametrii determinați. |
| <code java> | <code java> | ||
| - | HttpClient client = HttpClientBuilder.create().build(); | + | ServiceListener serviceListener = new ServiceListener() { |
| + | @Override | ||
| + | public void serviceResolved(ServiceEvent serviceEvent) { | ||
| + | if (serviceEvent.getName().equals(serviceName)) { | ||
| + | Log.i(Constants.TAG, "The service running on the same machine has been discovered."); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | ServiceInfo serviceInfo = serviceEvent.getInfo(); | ||
| + | if (serviceInfo == null) { | ||
| + | Log.e(Constants.TAG, "Service Info for Service is null!"); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | String[] hosts = serviceInfo.getHostAddresses(); | ||
| + | String host = null; | ||
| + | if (hosts.length == 0) { | ||
| + | Log.e(Constants.TAG, "No host addresses returned for the service!"); | ||
| + | return; | ||
| + | } | ||
| + | host = hosts[0]; | ||
| + | if(host.startsWith("/")) { | ||
| + | host = host.substring(1); | ||
| + | } | ||
| + | |||
| + | int port = serviceInfo.getPort(); | ||
| + | |||
| + | ArrayList<NetworkService> discoveredServices = ((ChatActivity)context).getChatNetworkServiceFragment().getDiscoveredServices(); | ||
| + | NetworkService networkService = new NetworkService(serviceEvent.getName(), host, port, Constants.CONVERSATION_TO_SERVER); | ||
| + | if (!discoveredServices.contains(networkService)) { | ||
| + | discoveredServices.add(networkService); | ||
| + | communicationToServers.add(new ChatClient(null, host, port)); | ||
| + | } | ||
| + | ((ChatActivity)context).getChatNetworkServiceFragment().setDiscoveredServices(discoveredServices); | ||
| + | |||
| + | Log.i(Constants.TAG, "A service has been discovered on " + host + ":" + port); | ||
| + | } | ||
| + | |||
| + | // ... | ||
| + | }; | ||
| + | </code> | ||
| - | // ... | + | ===== Gestiunea unei Conexiuni către Servicii Disponibile ===== |
| - | httpRequest.setEntity(new StringEntity(JSONValue.toJSONString(requestBody))); | + | Fiecare dispozitiv poate fi avea în același timp rolul de: |
| + | * server pentru serviciile pe care le-a înregistrat, putând primi solicitări din partea clienților; | ||
| + | * client pentru serviciile pe care le-a descoperit, putând trimite solicitări către servere. | ||
| - | // send the request | + | În acest sens, va trebui menținut un obiect server, care va gestiona comunicația cu clienții. |
| - | HttpResponse httpResponse = client.execute(httpRequest); | + | |
| - | </code> | + | |
| - | <note warning>În situația în care comunicația dintre serverul de aplicații și serverul Google Cloud Messaging nu poate fi realizată, ar trebui investigată posibilitatea blocării porturilor pe care se realizează transmisia de mesaje (de obicei 5228, dar și 5529, respectiv 5530).</note> | + | Totodată, vor trebui menținuți mai mulți clienți, reprezentând canalele de comunicație de tip punct-la-punct care pot fi de două tipuri: |
| - | + | * canale de comunicație pentru solicitări primite (clienți); | |
| - | === Răspuns: server de conexiune GCM → server de aplicații === | + | * canale de comunicație pentru servicii descoperite (servere). |
| - | + | ||
| - | Răspunsul transmis de serverul de conexiune la Google Cloud Messaging către serverul de aplicații conține rezultatul operației solicitate, acesta putând fi investigat prin intermediul codului de stare: | + | |
| <code java> | <code java> | ||
| - | int responseCode = httpResponse.getStatusLine().getStatusCode(); | + | public class NetworkServiceDiscoveryOperations { |
| + | |||
| + | private ChatServer chatServer = null; | ||
| + | |||
| + | private List<ChatClient> communicationToServers = null; | ||
| + | private List<ChatClient> communicationFromClients = null; | ||
| + | |||
| + | // ... | ||
| + | |||
| + | } | ||
| </code> | </code> | ||
| - | Aceasta poate avea valorile: | + | Actualizarea acestor obiecte va fi realizată mai ales în situația în care un serviciu este rezolvat, respectiv un serviciu este pierdut. |
| - | * ''200'' - dacă mesajul a fost procesat cu succes; | + | |
| - | * o valoare diferită de ''200'' - în situația în care mesajul a fost respins de serverul Google Cloud Messaging. | + | |
| - | == Mesaj procesat cu succes == | + | Obiectul de tip **''ChatServer''** este de fapt un fir de execuție pe care se instanțiază un obiect de tip ''ServerSocket'', așteptându-se, în bucla principală, solicitări de la clienți, actualizându-se corespunzător lista conținând canalele de comunicație pentru solicitările primite. |
| - | În situația în care mesajul a fost procesat cu succes, codul de stare returnat este 200, iar corpul răspunsului conține informații suplimentare, reprezentate sub forma unor perechi de tip (atribut, valoare) în care există un set de parametri predefiniți. | + | De remarcat faptul că a fost definită și o metodă pentru oprirea firului de execuție și închiderea obiectului ''ServerSocket'', aceasta urmând a fi apelată la deînregistrarea serviciului respectiv. |
| - | ** text ** | + | <file java ChatServer.java> |
| + | public class ChatServer extends Thread { | ||
| - | Există două variante de răspunsuri: | + | private NetworkServiceDiscoveryOperations networkServiceDiscoveryOperations = null; |
| - | - mesaj care a fost procesat cu succes <code> | + | |
| - | 1: id=identificatorul mesajului care a fost transmis | + | private ServerSocket serverSocket = null; |
| - | 2(opțional): registration_id=identificator de înregistrare canonic al dispozitivului mobil stocat pe serverul Google Cloud Messaging | + | |
| - | </code> | + | public ChatServer(NetworkServiceDiscoveryOperations networkServiceDiscoveryOperations, int port) { |
| - | - mesaj care nu a fost procesat cu succes <code> | + | this.networkServiceDiscoveryOperations = networkServiceDiscoveryOperations; |
| - | 1: Error=cod de eroare furnizat de serverul Google Cloud Messaging | + | try { |
| - | </code> | + | serverSocket = new ServerSocket(port); |
| - | + | } catch (IOException ioException) { | |
| - | Algoritmul de procesare a unui răspuns este următorul: | + | Log.e(Constants.TAG, "An error has occurred during server run: " + ioException.getMessage()); |
| - | + | if (Constants.DEBUG) { | |
| - | <code> | + | ioException.printStackTrace(); |
| - | if (firstLine.startsWith("id")) { | + | } |
| - | if (secondLine.equals("registration_id")) { | + | |
| - | old_registration_id = get registration_id from the request field | + | |
| - | replace old_registration_id with new registration_id in the request / application server database | + | |
| } | } | ||
| - | } else { | + | } |
| - | switch(Error) { | + | |
| - | case "NotRegistered": | + | public void run() { |
| - | // the application was uninstalled from the device | + | try { |
| - | // or it does not have a broadcast receiver in order to process com.google.android.c2dm.intent.RECEIVE intents | + | while (!Thread.currentThread().isInterrupted()) { |
| - | remove the registration ID from the application server database | + | Socket socket = serverSocket.accept(); |
| - | break; | + | List<ChatClient> communicationFromClients = networkServiceDiscoveryOperations.getCommunicationFromClients(); |
| - | default: | + | communicationFromClients.add(new ChatClient(null, socket)); |
| - | // non-recoverable error regarding the registration ID | + | networkServiceDiscoveryOperations.setCommunicationFromClients(communicationFromClients); |
| - | remove the registration ID from the application server database | + | } |
| - | break; | + | } catch (IOException ioException) { |
| + | Log.e(Constants.TAG, "An error has occurred during server run: " + ioException.getMessage()); | ||
| + | if (Constants.DEBUG) { | ||
| + | ioException.printStackTrace(); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | public void stopThread() { | ||
| + | interrupt(); | ||
| + | try { | ||
| + | if (serverSocket != null) { | ||
| + | serverSocket.close(); | ||
| + | } | ||
| + | } catch (IOException ioException) { | ||
| + | Log.e(Constants.TAG, "An error has occurred while closing server socket: " + ioException.getMessage()); | ||
| + | if (Constants.DEBUG) { | ||
| + | ioException.printStackTrace(); | ||
| + | } | ||
| } | } | ||
| + | } | ||
| } | } | ||
| - | </code> | + | </file> |
| - | ** JSON ** | + | Obiectul de tip **''ChatClient''** definește un canal de comunicație bidirecțional între două mașini / dispozitive, care poate fi creat pe baza unuei adrese și a unui port (conexiune către server) sau pe baza altui canal de comunicație (conexiune de la client). |
| - | ^ **PARAMETRU** ^ **DESCRIERE** ^ | + | Acesta încapsulează două fire de execuție: |
| - | | ''multicast_id'' | identificator unic ce reprezintă un mesaj transmis către mai multe dispozitive mobile | | + | - ''SendThread'' - pentru trimiterea de mesaje; |
| - | | ''success'' | numărul de mesaje care au fost procesate cu succes | | + | - ''ReceiveThread'' - pentru primirea de mesaje. |
| - | | ''failure'' | numărul de mesaje care nu au fost procesate cu succes | | + | Atât mesajele trimise cât și mesajele trimise sunt plasate și în interfața grafică (dacă aceasta este vizibilă) dar și într-un obiect membru al clasei. |
| - | | ''canonical_ids'' | număr de rezultate care conțin un identificator de înregistrare canonic | | + | |
| - | | ''results'' | listă de obiecte conținând starea mesajelor procesate, în ordinea în care au fost incluse în cadrul solicitării (pe baza identificatorului de înregistrare):\\ ♦ ''message_id'': șir de caractere reprezentând mesajul respectiv, în situația în care a fost procesat cu succes\\ ♦ ''registration_id'': în situația în care este inclus, specifică faptul că mesajul a fost procesat, dar serverul Google Cloud Messaging deține un alt identificator de înregistrare canonic pentru dispozitivul mobil, astfel încât solicitările ulterioare ar trebui să îl utilizeze pentru a nu fi respinse; nu este inclus dacă s-a produs o eroare\\ ♦ ''error'': șir de caractere indicând eroarea ce s-a produs în timpul procesării mesajului; valoarea ''Unavailable'' indică faptul că serverul Google Cloud Messaging nu era disponibil la momentul transmiterii solicitării, astfel încât aceasta trebuie retransmisă | | + | |
| - | Algoritmul de procesare a unui răspuns este următorul: | + | Au fost definite metode atât pentru pornirea cât și pentru oprirea firelor de execuție. Astfel, firele de execuție pentru trimiterea / primirea de mesaje rulează cât timp nu sunt întrerupte. |
| - | <code> | + | <file java ChatClient.java> |
| - | if (failure != 0 && canonical_ids != 0) { | + | public class ChatClient { |
| - | for-each (result ∈ results) { | + | private Socket socket = null; |
| - | if (result.registration_id) { | + | |
| - | // the old registration ID is not included within the response | + | private SendThread sendThread = null; |
| - | old_registration_id = get registration_id from the request field registration_ids, having the same index | + | private ReceiveThread receiveThread = null; |
| - | + | ||
| - | replace old_registration_id with new result.registration_id in the request | + | private BlockingQueue<String> messageQueue = new ArrayBlockingQueue<String>(Constants.MESSAGE_QUEUE_CAPACITY); |
| - | } else { | + | private List<Message> conversationHistory = new ArrayList<Message>(); |
| - | switch(error) { | + | |
| - | case "Unavailable": | + | public ChatClient(final String host, final int port) { |
| - | retry sending the request | + | new Thread(new Runnable() { |
| - | break; | + | @Override |
| - | case "NotRegistered": | + | public void run() { |
| - | // the application was uninstalled from the device | + | try { |
| - | // or it does not have a broadcast receiver in order to process com.google.android.c2dm.intent.RECEIVE intents | + | socket = new Socket(host, port); |
| - | remove the registration ID from the application server database | + | } catch (IOException ioException) { |
| - | break; | + | Log.e(Constants.TAG, "An exception has occurred while creating the socket: " + ioException.getMessage()); |
| - | default: | + | if (Constants.DEBUG) { |
| - | // non-recoverable error regarding the registration ID | + | ioException.printStackTrace(); |
| - | remove the registration ID from the application server database | + | } |
| - | break; | + | } |
| + | startThreads(); | ||
| + | } | ||
| + | }).start(); | ||
| + | } | ||
| + | |||
| + | public ChatClient(Context context, Socket socket) { | ||
| + | this.socket = socket; | ||
| + | |||
| + | startThreads(); | ||
| + | } | ||
| + | |||
| + | public void sendMessage(String message) { | ||
| + | try { | ||
| + | messageQueue.put(message); | ||
| + | } catch (InterruptedException interruptedException) { | ||
| + | Log.e(Constants.TAG, "An exception has occurred: " + interruptedException.getMessage()); | ||
| + | if (Constants.DEBUG) { | ||
| + | interruptedException.printStackTrace(); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private class SendThread extends Thread { | ||
| + | @Override | ||
| + | public void run() { | ||
| + | PrintWriter printWriter = Utilities.getWriter(socket); | ||
| + | if (printWriter != null) { | ||
| + | try { | ||
| + | while (!Thread.currentThread().isInterrupted()) { | ||
| + | String content = messageQueue.take(); | ||
| + | if (content != null) { | ||
| + | printWriter.println(content); | ||
| + | printWriter.flush(); | ||
| + | Message message = new Message(content, Constants.MESSAGE_TYPE_SENT); | ||
| + | conversationHistory.add(message); | ||
| + | // display the message in the graphic user interface | ||
| } | } | ||
| + | } | ||
| + | } catch (InterruptedException interruptedException) { | ||
| + | Log.e(Constants.TAG, "An exception has occurred: " + interruptedException.getMessage()); | ||
| + | if (Constants.DEBUG) { | ||
| + | interruptedException.printStackTrace(); | ||
| + | } | ||
| } | } | ||
| + | } | ||
| } | } | ||
| + | | ||
| + | public void stopThread() { | ||
| + | interrupt(); | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | private class ReceiveThread extends Thread { | ||
| + | @Override | ||
| + | public void run() { | ||
| + | BufferedReader bufferedReader = Utilities.getReader(socket); | ||
| + | if (bufferedReader != null) { | ||
| + | try { | ||
| + | while (!Thread.currentThread().isInterrupted()) { | ||
| + | String content = bufferedReader.readLine(); | ||
| + | if (content != null) { | ||
| + | Message message = new Message(content, Constants.MESSAGE_TYPE_RECEIVED); | ||
| + | conversationHistory.add(message); | ||
| + | // display the message in the graphic user interface | ||
| + | } | ||
| + | } | ||
| + | } catch (IOException ioException) { | ||
| + | Log.e(Constants.TAG, "An exception has occurred: " + ioException.getMessage()); | ||
| + | if (Constants.DEBUG) { | ||
| + | ioException.printStackTrace(); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | public void stopThread() { | ||
| + | interrupt(); | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | public void setConversationHistory(List<Message> conversationHistory) { | ||
| + | this.conversationHistory = conversationHistory; | ||
| + | } | ||
| + | | ||
| + | public List<Message> getConversationHistory() { | ||
| + | return conversationHistory; | ||
| + | } | ||
| + | | ||
| + | public void startThreads() { | ||
| + | sendThread = new SendThread(); | ||
| + | sendThread.start(); | ||
| + | | ||
| + | receiveThread = new ReceiveThread(); | ||
| + | receiveThread.start(); | ||
| + | } | ||
| + | | ||
| + | public void stopThreads() { | ||
| + | sendThread.stopThread(); | ||
| + | receiveThread.stopThread(); | ||
| + | try { | ||
| + | if (socket != null) { | ||
| + | socket.close(); | ||
| + | } | ||
| + | } catch (IOException ioException) { | ||
| + | Log.e(Constants.TAG, "An exception has occurred while closing the socket: " + ioException.getMessage()); | ||
| + | if (Constants.DEBUG) { | ||
| + | ioException.printStackTrace(); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| } | } | ||
| - | </code> | + | </file> |
| - | == Mesaj respins de serverul Google Cloud Messaging == | + | ===== Activitate de Laborator ===== |
| - | În situația în care mesajul a fost respins de serverul Google Cloud Messaging, pot fi returnate mai multe coduri de stare: | + | Se dorește implementarea unei aplicații Android de tip mesagerie instantanee bazată pe legături de tip punct-la-punct stabilite între dispozitive mobile care oferă un serviciu de acest tip și dispozitive mobile care l-au descoperit, putându-l accesa în cadrul rețelei locale. |
| - | ^ **COD DE STARE** ^ **DESCRIERE** ^ | + | Funcționalitățile pe care le implementează această aplicație Android sunt: |
| - | | 400 | (doar pentru mesaje JSON) eroare ce indică faptul că mesajul nu a putut fi parsat ca JSON sau conține câmpuri invalide (fie atribute - denumire, fie valoare), detalii suplimentare fiind incluse în corpul mesajului pentru a remedia problema înainte de retransmisie | | + | - înregistrarea / deînregistrarea unui serviciu de mesagerie instantanee, pe un anumit port pe care îl specifică; |
| - | | 401 | eroare privind procesul de autentificare a serverului de aplicații în cadrul serverului Google Cloud Messaging | | + | - pornirea / oprirea operației de descoperire a serviciilor în rețeaua locală; |
| - | | 5xx | eroare în cadrul serverului Google Cloud Messaging (în timpul procesării mesajului) sau indisponibilitate temporară a acestuia; mesajul trebuie transmis potrivit informațiilor furnizate de antetul ''Retry-After'' | | + | - conectarea / deconectarea la un serviciu descoperit sau la un dispozitiv mobil care a accesat serviciul înregistrat; |
| + | - transmiterea de mesaje în cadrul legăturii de tip punct-la-punct. | ||
| - | Cele mai frecvente tipuri de erori sunt: | + | Va fi utilizat următorul cod de culori pentru butoanele care au atașată funcționalitatea de înregistrare / deînregistrare, respectiv pornire / oprire: |
| - | * ''MissingRegistration'' - cauzată de faptul că nu se include câmpul ''registration_id'' (text) / ''registration_ids'' (JSON) în corpul mesajului; | + | * roșu - serviciul este oprit; |
| - | * ''InvalidRegistration'' - valoarea identificatorului de înregistrare trimisă prin mesaj este diferită de valoarea primită de telefon prin intenția ''com.google.android.c2dm.intent.REGISTRATION''; | + | * verde - serviciul este pornit. |
| - | * ''MismatchSenderId'' - utilizatorul care a transmis mesajul nu are permisiunea de a o face (în cadrul procesului de înregistrare a dispozitivului mobil se precizează care sunt utilizatorii care pot transmite mesaje); | + | |
| - | * ''NotRegistered'' - identificatorul de înregistrare nu mai este valid, în următoarele cazuri: | + | |
| - | * aplicația Android anulează înregistrarea la serverul Google Cloud Messaging folosind o intenție de tip ''com.google.android.c2dm.intent.UNREGISTER''; | + | |
| - | * aplicația Android a fost actualizată astfel încât să nu mai prelucreze intenții de tip ''com.google.android.c2dm.intent.RECEIVE''. | + | |
| - | * aplicația Android a fost dezinstalată de pe dispozitivul mobil (în acest caz, nu se garantează că procesul de anulare a înregistrării la Google Cloud Messaging va fi realizată automat); | + | |
| - | * identificatorul de înregistrare a expirat; | + | |
| - | * ''MessageTooBig'' - dimensiunea mesajului propriu-zis (atât a valorilor, cât și a atributelor) depășește 4096 octeți | + | |
| - | * ''InvalidDataKey'' - mesajul propriu-zis include parametrii care sunt rezervați (''from'', ''google.*'') spre a fi utilizați de intenția ''com.google.android.c2dm.intent.RECEIVE''; | + | |
| - | * ''InvalidTtl'' - valoarea parametrului ''time_to_live'' nu este o valoare întreagă (exprimând o durată de timp, în secunde) cuprinsă între 0 și 2419200 (4 săptămâni); | + | |
| - | * cod de stare 401 - utilizatorul care a transmis un mesaj către serverul Google Cloud Messaging nu a putut fi autentificat datorită uneia dintre următoarele cauze: | + | |
| - | * lipsește câmpul ''Authorization'' din antetul mesajului sau a fost folosită o sintaxă incorectă; | + | |
| - | * este folosit un număr de proiect invalid drept cheie publică; | + | |
| - | * cheia publică este validă, însă serviciul Google Cloud Messaging este dezactivat pentru contul care a transmis mesajul respectiv; | + | |
| - | * mesajul a fost transmis de pe un server de aplicații de la o adresă Internet care nu a fost precizată la momentul în care a fost generată cheia publică. | + | |
| - | * ''InternalServerError'' / cod de stare 500 - s-a produs o eroare pe serverul Google Cloud Messaging la procesarea mesajului, acesta trebuind retransmis; în cazul în care problema persistă, aceasta trebuie raportată la [[https://groups.google.com/forum/?fromgroups#!forum/android-gcm|]]; | + | |
| - | * ''Unavailable'' / cod de stare 501-599 - mesajul nu a putut fi procesat de serverul Google Cloud Messaging în timp util și trebuie retransmis ținând cont de următoarele recomandări: | + | |
| - | * dacă este inclus antetul ''Retry-After'', trebuie respectată valoarea indicată; | + | |
| - | * trebuie implementat un mecanism de adaptare a transmisiei (//eng.// exponential back-off), astfel încât perioada după care se retransmite un mesaj să fie modificată în mod exponențial după o încercare eșuată; în cazul în care se transmit mai multe mesaje, acestea trebuie tratate independent; utilizatorilor care transmit prea multe mesaje către serverul Google Cloud Messaging li se refuză accesul ulterior; | + | |
| - | * ''InvalidPackageName'' - mesajul a fost transmis folosind un identificator de înregistrare | + | |
| - | ==== Tratarea cererilor de înregistrare ==== | + | {{ :laboratoare:laborator08:chatservice01.png?nolink&400 }} |
| - | Un server de aplicații trebuie să trateze și mesajele provenite de la clientul Android (cereri de înregistrare), stocând informațiile referitoare la identificatorii de înregistrare în baza de date locală, pentru a fi utilizate ulterior, când se dorește precizarea unei locații către care să fie livrat un mesaj. | + | În cazul în care descoperirea serviciilor este pornită, vor fi întreținute două liste: |
| + | * lista cu serviciile descoperite, disponibile în cadrul altor dispozitive mobile din rețeaua locală; | ||
| + | * lista conținând conversațiile cu alte dispozitive mobile care au accesat serviciul înregistrat. | ||
| - | Comunicația dintre clientul Android și serverul de aplicații poate folosi un protocol de comunicație definit de utilizator, folosind propriile reguli pentru definirea mesajelor schimbate între aceste entități. | + | {{ :laboratoare:laborator08:chatservice02.png?nolink&400 }} |
| - | <code java> | + | În situația în care descoperirea serviciilor este oprită, lista cu serviciile descoperite va fi vidă. |
| - | @Override | + | |
| - | public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { | + | {{ :laboratoare:laborator08:chatservice03.png?nolink&400 }} |
| - | Enumeration<String> parameters = request.getParameterNames(); | + | |
| - | boolean found = false; | + | Comunicația se va realiza în cadrul unei ferestre dedicate, în care pot fi vizualizate diferit mesajele trimise și mesajele primite. Din cadrul acestei interfețe grafice se poate reveni în orice moment la panoul de control. |
| - | while(parameters.hasMoreElements()) { | + | |
| - | String parameter = (String)parameters.nextElement(); | + | {{ :laboratoare:laborator08:chatservice04.png?nolink&400 }} |
| - | if (parameter.equals(Constants.REGISTRATION_ID)) { | + | |
| - | found = true; | + | **1.** În contul Github personal, să se creeze un depozit denumit 'Laborator08'. Inițial, acesta trebuie să fie gol (nu trebuie să bifați nici adăugarea unui fișier ''README.md'', nici a fișierului ''.gitignore'' sau a a fișierului ''LICENSE''). |
| - | registrationId = request.getParameter(parameter); | + | |
| - | } | + | **2.** Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la [[https://www.github.com/eim2016/Laborator08|]]. |
| - | if (parameter.equals(Constants.USER_NAME)) { | + | |
| - | userName = request.getParameter(parameter); | + | În urma acestei operații, directorul Laborator08 va trebui să se conțină directoarele ''labtasks'' și ''solutions''. |
| - | } | + | |
| - | if (parameter.equals(Constants.EMAIL)) { | + | <code> |
| - | email = request.getParameter(parameter); | + | student@eim2016:~$ git clone https://www.github.com/eim2016/Laborator08 |
| - | } | + | |
| - | } | + | |
| - | if (found) { | + | |
| - | ArrayList<String> values = new ArrayList<String>(); | + | |
| - | values.add(registrationId); | + | |
| - | values.add(userName); | + | |
| - | values.add(email); | + | |
| - | values.add(general.Constants.SYSTEM_FUNCTION[0]); | + | |
| - | try { | + | |
| - | DataBaseConnection.insertValuesIntoTable(Constants.TABLE_NAME, null, values, true); | + | |
| - | } catch (Exception exception) { | + | |
| - | System.out.println("exceptie: "+exception.getMessage()); | + | |
| - | if (Constants.DEBUG) | + | |
| - | exception.printStackTrace(); | + | |
| - | } | + | |
| - | } | + | |
| - | response.setContentType("text/html"); | + | |
| - | try { | + | |
| - | PrintWriter printWriter = new PrintWriter(response.getWriter()); | + | |
| - | ArrayList<ArrayList<Object>> records = DataBaseConnection.getTableContent(Constants.TABLE_NAME, null, null, null, null); | + | |
| - | GoogleCloudMessagingGraphicUserInterface.displayGoogleCloudMessagingGraphicUserInterface(records, printWriter); | + | |
| - | } catch (Exception exception) { | + | |
| - | System.out.println("exceptie: "+exception.getMessage()); | + | |
| - | if (Constants.DEBUG) | + | |
| - | exception.printStackTrace(); | + | |
| - | response.setStatus(HttpServletResponse.SC_BAD_REQUEST); | + | |
| - | } | + | |
| - | response.setStatus(HttpServletResponse.SC_OK); | + | |
| - | } | + | |
| </code> | </code> | ||
| - | Se observă faptul că se prelucrează cererea de tip POST, iterându-se pe toți parametrii incluși în scopul identificării valorilor asociate atributelor denumite nume de utilizator, adresă de poștă electronică și identificator de înregistrare (conform protocolului de comunicație definit de utilizator), aceștia fiind stocați în baza de date locală pentru a fi utilizați în transmiterea ulterioară a unui mesaj. | + | **3.** Să se încarce conținutul descărcat în cadrul depozitului 'Laborator08' de pe contul Github personal. <code> |
| + | student@eim2016:~$ cd Laborator08 | ||
| + | student@eim2016:~/Laborator08$ git remote add Laborator08_perfectstudent https://github.com/perfectstudent/Laborator08 | ||
| + | student@eim2016:~/Laborator08$ git push Laborator08_perfectstudent master | ||
| + | </code> | ||
| - | <note important>Este foarte important să fie specificată o stare în cadrul răspunsului (prin apelul metodei ''setStatus'' a obiectului ''HttpServletResponse'') desemnând rezultatul procesării mesajului, în caz contrar clientul Android blocându-se până la expirarea perioadei de timp pentru așteptare (//eng.// timeout).</note> | + | **4.** Să se importe în mediul integrat de dezvoltare preferat (Android Studio sau Eclipse) proiectul ''ChatServiceAndroidNSD'' sau ''ChatServiceJmDNS'' din directorul ''labtasks''. |
| - | ===== Activitate de Laborator ===== | + | |
| - | **1.** Să se acceseze [[https://code.google.com/apis/console/|Google Developer's Console]], după ce a fost realizată autentificarea cu datele contului Google (nume de utilizator, parolă): | + | **5.** Să se ruleze aplicația Android pe două dispozitive mobile care să se găsească în aceeași rețea. |
| - | * se generează un număr de proiect; | + | |
| - | * se activează serviciul Google Cloud Messaging; | + | |
| - | * se generează o cheie publică pentru transmiterea de mesaje provenind de la un server, indiferent de adresa Internet la care se găsește acesta. | + | |
| - | Mai multe detalii pot fi obținute în secțiunea [[:laboratoare:laborator09#configurare|Configurare]]. | + | <note tip> |
| + | În cazul Genymotion, se pot crea două imagini pentru două dispozitive mobile (virtuale) de același tip, care vor putea fi rulate separat, fiecare dintre acestea primind adrese Internet diferite din intervalul ''192.168.56.101'' → ''192.168.56.254''. | ||
| - | <note>În cazul în care nu dispuneți de un cont Google, acesta va trebui creat înainte de a implementa aceste cerințe.</note> | + | {{ :laboratoare:laborator08:genymotion01.png?nolink&600 }} |
| - | ==== Clientul Android ==== | + | În VirtualBox (> versiunea 4.3.26), se verifică faptul că dispozitivele virtuale comunică între ele prin intermediul unei interfețe de rețea, configurată să folosească NAT (Network Address Translation). |
| - | **2.** Să se configureze clientul pentru a putea rula o aplicație Android folosind Google Cloud Messaging: | + | În acest sens, trebuie realizate următoarele operații: |
| - | * se instalează SDK-ul Google Cloud Messaging | + | |
| - | * se configurează mașina pe care va rula aplicația | + | |
| - | * în cazul în care se utilizează un emulator, se configurează astfel încât acesta să aibă instalat Google Play Services | + | |
| - | * în cazul în care se utilizează un dispozitiv fizic, acesta trebuie să ruleze un sistem de operare Android cu o versiune ulterioară 2.2, având asociat un cont Google | + | |
| - | Mai multe detalii pot fi obținute în secțiunea [[:laboratoare:laborator09#configurare_client|Configurare client]] | + | * se va crea o rețea NAT în cadrul VirtualBox (//File// → //Preferences// sau Ctrl + G) |
| - | **3.** În mediul de dezvoltare să se deschidă proiectele ''google-play-services_lib'' (acesta ar trebui să se găsească și la ''<android-sdk>/extras/google/google_play_services/libproject/google-play-services_lib'' după ce a fost instalat SDK-ul Google Cloud Messaging) și ''GoogleCloudMessagingClient''. | + | {{ :laboratoare:laborator08:virtualbox01.png?nolink&400 }} |
| - | **4.** În interfața ''ro.pub.systems.pdsd.lab09.googlecloudmessagingclient.Configuration'', trebuie modificate următoarele valori: | + | {{ :laboratoare:laborator08:virtualbox02.png?nolink&400 }} |
| - | * ''PROJECT_NUMBER'' - cu numărul de proiect generat de consola Google API; | + | |
| - | * ''GOOGLECLOUDMESSAGING_WEBSERVER_URL'' - cu adresa Internet la care rulează serverul de aplicații | + | |
| - | * dispozitiv mobil - adresa Internet indicată ca default gateway a rețelei de calculatoare din care fac parte dispozitivul mobil și mașina pe care se găsește serverul de aplicații | + | |
| - | * AVD - 10.0.2.1 | + | |
| - | * Genymotion - 192.168.56.1 | + | |
| - | <note tip>Portul pe care rulează serverul web Apache Tomcat este de regulă 8080 (dacă nu s-au realizat alte modificări în fișierul de configurare ''server.xml'').</note> | + | * în configurația aferentă fiecărui dispozitiv virtual (//Machine// → //Settings// sau Ctrl + S), se va selecta //NAT Network// folosind rețeaua astfel definită pentru interfața //Adapter 2// |
| - | **5.** Să se ruleze aplicația Android și să se genereze identificatorul de înregistrare, care va fi transmis prin accesarea butonului ''Send'' de îndată ce serverul de aplicații devine disponibil. | + | {{ :laboratoare:laborator08:virtualbox03.png?nolink&400 }} |
| + | </note> | ||
| - | ==== Serverul de Aplicații ==== | + | Acestea vor putea rula instanțe diferite ale aplicației Android, fiecare folosind __**o denumire proprie pentru serviciu**__ (la valoarea generică ''Constants.SERVICE_NAME'' definită în pachetul ''ro.pub.cs.systems.eim.lab08.chatservice.general'' se sufixează în mod automat un șir de caractere generat aleator, astfel încât aceasta să fie unică în rețeaua locală). |
| - | <note tip>Informații referitoare la configurarea serverului de aplicații pot fi obținute la [[:laboratoare:laborator09#configurare_server|Configurare Server]].</note> | + | Se verifică faptul că fiecare aplicație Android rulează pe un dispozitiv diferit: |
| - | **6.** Să se descarce cea mai recentă versiune stabilă a serverului web Apache Tomcat și să se dezarhiveze. | + | {{ :laboratoare:laborator08:genymotion02.png?nolink&600 }} |
| - | **7.** Să se descarce cea mai recentă versiune stabilă a mediului de dezvoltare Eclipse IDE for Java EE Developers, integrându-se cu serverul web Apache Tomcat. | + | În Logcat, se pot utiliza filtre diferite pentru fiecare dintre dintre instanțele aplicației Android, astfel încât să se faciliteze procesul de depanare. |
| - | **8.** Să se instaleze motorul pentru gestiunea bazelor de date MySQL și să se ruleze script-ul ''Laborator09.sql'' din cadrul proiectului ''GoogleCloudMessagingWebServer'' care creează o bază de date cu o tabelă în care vor fi stocate informațiile referitoare la identificatorii de înregistrare transmiși de clienții Android. | + | **6.** Să se implementeze rutina pentru trimiterea mesajelor, pe metoda ''run()'' a firului de execuție ''SendThread'' din clasa ''ChatClient'' a pachetului ''ro.pub.cs.systems.eim.lab08.chatservice.networkservicediscoveryoperations''. |
| - | <code sql> | + | Metoda va rula cât timp firul de execuție nu este întrerupt, informație oferită de metoda ''isInterrupted()''. |
| - | CREATE DATABASE googlecloudmessaging; | + | |
| - | USE googlecloudmessaging; | + | <code java> |
| - | + | while (!Thread.currentThread().isInterrupted()) { | |
| - | CREATE TABLE registered_devices ( | + | // ... |
| - | id INT(10) UNSIGNED AUTO_INCREMENT PRIMARY KEY NOT NULL, | + | } |
| - | registration_id TEXT NOT NULL, | + | |
| - | username VARCHAR(255), | + | |
| - | email VARCHAR(255), | + | |
| - | creation_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP | + | |
| - | ); | + | |
| </code> | </code> | ||
| - | Rularea acestuia se poate realiza folosind una dintre următoarele variante, presupunând că vă aflați în directorul în care se găsește acest fișier: | + | Pe fiecare iterație, se vor realiza următoarele operații: |
| - | - <code> | + | - se va prelua un mesaj din coada ''messageQueue'' (de tip ''BlockingQueue<String>''); metoda ''take()'' este blocantă, în așteptarea unei valori care să poată fi furnizată - astfel, în momentul în care firul de execuție este întrerupt, se va genera o excepție de tipul ''InterruptedException'' care trebuie tratată; |
| - | mysql -u root -p <Laborator09.sql | + | - în cazul unui mesaj nenul, sunt realizate următoarele operații: |
| - | </code> | + | - se trimite mesajul pe canalul de comunicație corespunzător (se apelează metoda ''println()'' a obiectului de tip ''PrintWriter''); |
| - | - <code> | + | - se construiește un obiect de tip ''Message'', format din: |
| - | mysql -u root -p | + | - conținut: valoarea propriu-zisă a mesajului (șirul de caractere); |
| - | Enter password: | + | - tip: mesaj transmis (''Constants.MESSAGE_TYPE_SENT''); |
| - | </code> | + | - se atașează mesajul istoricului conversației (obiectul ''conversationHistory'' este de tipul ''List<Message>'') - în acest fel, chiar dacă interfața grafică nu este vizibilă la momentul în care este trimis mesajul, conversația poate fi încărcată atunci când se întâmplă acest lucru; |
| - | - <code> | + | - se afișează mesajul în fragmentul de tip ''ChatConversationFragment'' (prin intermediul metodei ''appendMessage()''), dacă acesta este asociat activității la momentul respectiv <code java> |
| - | mysql> source Laborator09.sql; | + | if (context != null) { |
| - | </code> sau | + | ChatActivity chatActivity = (ChatActivity)context; |
| - | - <code> | + | FragmentManager fragmentManager = chatActivity.getFragmentManager(); |
| - | mysql> \. Laborator09.sql; | + | Fragment fragment = fragmentManager.findFragmentByTag(Constants.FRAGMENT_TAG); |
| + | if (fragment instanceof ChatConversationFragment && fragment.isVisible()) { | ||
| + | ChatConversationFragment chatConversationFragment = (ChatConversationFragment)fragment; | ||
| + | chatConversationFragment.appendMessage(message); | ||
| + | } | ||
| + | } | ||
| </code> | </code> | ||
| - | **9.** În mediul de dezvoltare Eclipse IDE for Java EE Developers să se deschidă proiectul ''GoogleCloudMessaging''. | + | **7.** Să se implementeze rutina pentru primirea mesajelor, pe metoda ''run()'' a firului de execuție ''ReceiveThread'' din clasa ''ChatClient'' a pachetului ''ro.pub.cs.systems.eim.lab08.chatservice.networkservicediscoveryoperations''. |
| - | **10.** În interfața ''general.Constants'', trebuie modificate următoarele valori: | + | Metoda va rula cât timp firul de execuție nu este întrerupt, informație oferită de metoda ''isInterrupted()''. |
| - | * ''DATABASE_PASSWORD'' - parola de administrare a motorului pentru gestiunea bazei de date MySQL, care a fost introdusă la instalarea acestui produs; | + | |
| - | * ''API_KEY'' - cheia publică de acces a serverului Google Cloud Messaging de către serverul de aplicații, așa cum a fost generată de Consola Google API. | + | |
| - | ===== ===== | + | <code java> |
| + | while (!Thread.currentThread().isInterrupted()) { | ||
| + | // ... | ||
| + | } | ||
| + | </code> | ||
| - | **11.** În acest moment, se poate realiza comunicația client Android ↔ server de aplicații prin intermediul serviciului Google Cloud Messaging: | + | Pe fiecare iterație, se vor realiza următoarele operații: |
| - | * clientul Android trimite identificatorul de înregistrare către serverul de aplicații;\\ {{ :laboratoare:lab09-mobiledeviceunregistered.jpg?nolink }}\\ {{ :laboratoare:lab09-mobiledeviceregistered.jpg?nolink }} | + | - se primește mesajul pe canalul de comunicație corespunzător (se apelează metoda ''readLine()'' a obiectului de tip ''BufferedReader''); |
| - | * se accesează servletul ''GoogleCloudMessagingRegisterServlet'' prin intermediul unei aplicații de navigare (//eng.// browser) pentru a se transmite un mesaj către dispozitivul mobil, acesta fiind afișat sub forma unei notificări.\\ {{ :laboratoare:lab09-GoogleCloudMessagingWebServer.jpg?nolink }} | + | - în cazul unui mesaj nenul, sunt realizate următoarele operații: |
| + | - se construiește un obiect de tip ''Message'', format din: | ||
| + | - conținut: valoarea propriu-zisă a mesajului (șirul de caractere); | ||
| + | - tip: mesaj transmis (''Constants.MESSAGE_TYPE_RECEIVED''); | ||
| + | - se atașează mesajul istoricului conversației (obiectul ''conversationHistory'' este de tipul ''List<Message>''); | ||
| + | - se afișează mesajul în fragmentul de tip ''ChatConversationFragment'' (prin intermediul metodei ''appendMessage()''), dacă acesta este asociat activității la momentul respectiv. | ||
| + | |||
| + | **8.** Să se încarce modificările realizate în cadrul depozitului 'Laborator08' de pe contul Github personal, folosind un mesaj sugestiv. <code> | ||
| + | student@eim2016:~/Laborator08$ git add * | ||
| + | student@eim2016:~/Laborator08$ git commit -m "implemented taks for laboratory 08" | ||
| + | student@eim2016:~/Laborator08$ git push Laborator08_perfectstudent master | ||
| + | </code> | ||
| ===== Resurse Utile ===== | ===== Resurse Utile ===== | ||
| - | {{:laboratoare:client.zip|Surse Client}}\\ | ||
| - | {{:laboratoare:server.zip|Surse Server}} | ||
| - | [[http://tomcat.apache.org/download-70.cgi|Apache Tomcat 7 Downloads]]\\ | + | [[http://www.multicastdns.org/|Multicast DNS]]\\ |
| - | [[http://www.eclipse.org/downloads/packages/eclipse-ide-java-ee-developers/keplersr2|Eclipse IDE for Java EE Developers]]\\ | + | [[http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml|Service Name and Transport Protocol Port Number Registry]]\\ |
| - | [[http://dev.mysql.com/downloads/|MySQL Downloads]] | + | |
| + | [[http://developer.android.com/training/connect-devices-wirelessly/index.html|Android Tutorial on Connecting Devices Wirelessly]]\\ | ||
| - | [[http://androidexample.com/Android_Push_Notifications_using_Google_Cloud_Messaging_%28GCM%29_-_PART_1/index.php?view=article_discription&aid=119&aaid=139|Android Push Notifications using Google Cloud Messaging]]\\ | + | [[http://jmdns.sourceforge.net/|JmDNS - Project Official Page]]\\ |
| - | [[http://developer.android.com/google/gcm/index.html|Google Cloud Messaging for Android]]\\ | + | [[http://jmdns.sourceforge.net/apidocs/index.html|JmDNS - API Documentation]]\\ |
| - | [[http://developer.android.com/reference/com/google/android/gms/gcm/GoogleCloudMessaging.html|Google Cloud Messaging API]]\\ | + | [[http://sourceforge.net/projects/jmdns/|JmDNS - Library Download]]\\ |
| - | [[http://stackoverflow.com/questions/20121883/how-to-install-google-play-services-in-a-genymotion-vm-with-no-drag-and-drop-su|How to install Google Play Services in a Genymotion VM (with no drag and drop support)?]]\\ | + | [[http://home.heeere.com/tech-androidjmdns.html|Tutorial on Using JmDNS on Android]]\\ |
| - | [[http://wiki.cyanogenmod.org/w/Google_Apps|Google Apps]]\\ | + | [[https://developer.appcelerator.com/question/130715/how-to-discovery-deviceor-service-on-andorid|How to discover device (or service) on Android?]]\\ |
| - | [[http://forum.xda-developers.com/showthread.php?t=2528952|Installing ARM Translation and GApps]] | + | [[http://stackoverflow.com/questions/10289100/service-discovery-using-jmdns-in-eclipse-wifistatemachine-errors|Service discovery using JmDNS in Eclipse: Wifi State Machine errors]]\\ |
