This is an old revision of the document!
Pentru rezolvarea subiectelor propuse în cadrul colocviului 2, sunt necesare:
Accesul la servicii web prin intermediul protocolului HTTP poate fi realizată prin intermediul bibliotecii Apache HTTP Components.
Integrarea sa în mediul integrat pentru dezvoltare Android Studio 2.2.3 se face prin referirea acestei biblioteci ca modul al aplicației Android.
Atașarea unui modul nou la proiectul Android Studio se face accesând File → New → New Module.
În fereastra Create New Module se indică tipul de modul Import .JAR/.AAR Package.
Se precizează:
După ce biblioteca a fost referită în cadrul mediului integrat pentru dezvoltare Android Studio 2.2.3, aceasta va putea fi vizualizată în vizualizarea Android a proiectului.
De asemenea, în fișierul build.gradle
trebuie specificată explicit dependența de această bibliotecă:
... dependencies { ... compile group: 'cz.msebera.android', name: 'httpclient', version: '4.4.1.2' }
Alternativ, în situația în care se folosește un SDK corespunzător unui nivel de API mai mare decât 23, se poate preciza folosirea bibliotecii org.apache.http.legacy
:
android { compileSdkVersion 25 buildToolsVersion "25.0.2" ... } dependencies { ... useLibrary 'org.apache.http.legacy' }
În situația în care este necesară procesarea codului sursă (în format HTML) al paginii Internet furnizat ca răspuns la cererea transmisă către un serviciu web, se va folosi biblioteca Jsoup. Referirea acestei biblioteci se face tot sub formă de modul al proiectului Android Studio.
Pentru a putea folosi funcționalitatea pusă la dispoziție de această bibliotecă, este necesar să se definească dependența de aceasta în fișierul build.gradle
:
... dependencies { ... compile project (':jsoup-1.10.2') }
În situația în care este necesară procesarea unui document JSON furnizat ca răspuns la cererea transmisă către un serviciu web, se vor utiliza clasele JSONObject, respectiv JSONArray, din cadrul SDK-ului Android.
Pentru vizualizare, se poate folosi utilitarul JSON Formatter & Validator, prin care structura documentului JSON poate fi inspectată cu ușurință.
Proiectul Android Studio corespunzător aplicației Android ce conține rezolvările complete ale cerințelor colocviului sunt disponibile pe contul de Github al disciplinei.
1. a) Se accesează Github și se realizează autentificarea în contul personal, prin intermediul butonului Sign in.
Se creează o zonă de lucru corespunzătoare unui proiect prin intermediului butonului New Repository.
Configurarea depozitului la distanță presupune specificarea:
git init
;git clone
- depozitul la distanță nu trebuie să fie vid, în această situațoe README
(opțional);.gitignore
;
b) Prin intermediul comenzii git clone
se poate descărca întregul conținut în directorul curent (de pe discul local), inclusiv istoricul complet al versiunilor anterioare (care poate fi ulterior reconstituit după această copie, în cazul coruperii informațiilor stocate pe serverul la distanță).
student@eim2017:~$ git clone https://www.github.com/perfectstudent/PracticalTest02
c) Se urmăresc indicațiile disponibile în secțiunea Crearea unei aplicații Android în Android Studio.
2. Pentru implementarea interfeței grafice, se vor defini controalele care asigură interacțiunea cu utilizatorul pentru fiecare dintre componentele aplicației Android:
localhost
sau 127.0.0.1
);
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="ro.pub.cs.systems.eim.practicaltest02.view.PracticalTest02MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:textSize="25sp" android:textStyle="bold" android:text="@string/server"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:baselineAligned="false"> <ScrollView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1"> <EditText android:id="@+id/server_port_edit_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/server_port"/> </ScrollView> <ScrollView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1"> <Button android:id="@+id/connect_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/connect"/> </ScrollView> </LinearLayout> <Space android:layout_width="wrap_content" android:layout_height="10dp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:textSize="25sp" android:textStyle="bold" android:text="@string/client"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:baselineAligned="false"> <ScrollView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1"> <EditText android:id="@+id/client_address_edit_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/client_address"/> </ScrollView> <ScrollView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1"> <EditText android:id="@+id/client_port_edit_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/client_port"/> </ScrollView> </LinearLayout> <GridLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:rowCount="2" android:columnCount="2"> <EditText android:id="@+id/city_edit_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="5" android:hint="@string/city" android:layout_row="0" android:layout_column="0"/> <Spinner android:id="@+id/information_type_spinner" android:layout_width="wrap_content" android:layout_height="wrap_content" android:entries="@array/information_types" android:layout_row="1" android:layout_column="0"/> <Button android:id="@+id/get_weather_forecast_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="@string/get_weather_forecast" android:layout_row="0" android:layout_rowSpan="2" android:layout_column="1"/> </GridLayout> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/weather_forecast_text_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:maxLines="10"/> </ScrollView> </LinearLayout>
android.permission.INTERNET
), specificată explicit în fișierul AndroidManifest.xml
:
<manifest ...> <!-- other elements --> <uses-permission android:name="android.permission.INTERNET" /> </manifest>
post()
a elementului respectiv pentru interacțiunea cu utilizatorul sau un obiect de tip Handler
);
3. Implementarea serverului presupune:
a) un fir de execuție care gestionează solicitările de conexiune de la clienți:
ServerSocket
care va primi ca parametru portul indicat de utilizator: try { serverSocket = new ServerSocket(port); } catch (IOException ioException) { Log.e(Constants.TAG, "An exception has occurred: " + ioException.getMessage()); if (Constants.DEBUG) { ioException.printStackTrace(); } }
Hashmap
în care vor fi reținute informațiile meteorologice cu privire la diferite orașe care au fost interogate anterior, astfel încât acestea să fie furnizate de la nivel local, fără a mai fi necesară conexiunea la distanță (în acest sens trebuie definită și clasa model WeatherForecastInformation
care să conțină datele meteorologice disponibile - temperatură, viteza vântului, stare generală, presiune, umiditate - implementând pentru fiecare dintre acestea metode de tip getter și setter): data = new HashMap<String, WeatherForecastInformation>();
accept()
apelată pe obiectul de tip ServerSocket
, aceasta întorcând un obiect de tip Socket
), comunicația dintre acestea fiind tratată pe un fir de execuție dedicat: @Override public void run() { try { while (!Thread.currentThread().isInterrupted()) { Log.i(Constants.TAG, "[SERVER THREAD] Waiting for a client invocation..."); Socket socket = serverSocket.accept(); Log.i(Constants.TAG, "[SERVER THREAD] A connection request was received from " + socket.getInetAddress() + ":" + socket.getLocalPort()); CommunicationThread communicationThread = new CommunicationThread(this, socket); communicationThread.start(); } } catch (ClientProtocolException clientProtocolException) { Log.e(Constants.TAG, "[SERVER THREAD] An exception has occurred: " + clientProtocolException.getMessage()); if (Constants.DEBUG) { clientProtocolException.printStackTrace(); } } catch (IOException ioException) { Log.e(Constants.TAG, "[SERVER THREAD] An exception has occurred: " + ioException.getMessage()); if (Constants.DEBUG) { ioException.printStackTrace(); } } }
De remarcat faptul că pentru un obiect de tip Socket
se poate determina:
getInetAddress()
;getLocalPort()
.Pornirea firului de execuție corespunzător serverului va fi realizată pe metoda de callback a obiectului ascultător pentru evenimentul de tip apăsare a butonului aferent din interfața grafică:
În prealabil, trebuie să se verifice completarea câmpului text care conține portul pe care vor fi acceptate conexiuni de la clienți.
private class ConnectButtonClickListener implements Button.OnClickListener { @Override public void onClick(View view) { String serverPort = serverPortEditText.getText().toString(); if (serverPort == null || serverPort.isEmpty()) { Toast.makeText(getApplicationContext(), "[MAIN ACTIVITY] Server port should be filled!", Toast.LENGTH_SHORT).show(); return; } serverThread = new ServerThread(Integer.parseInt(serverPort)); if (serverThread.getServerSocket() == null) { Log.e(Constants.TAG, "[MAIN ACTIVITY] Could not create server thread!"); return; } serverThread.start(); } }
@Override protected void onDestroy() { Log.i(Constants.TAG, "[MAIN ACTIVITY] onDestroy() callback method has been invoked"); if (serverThread != null) { serverThread.stopThread(); } super.onDestroy(); }
public void stopThread() { interrupt(); if (serverSocket != null) { try { serverSocket.close(); } catch (IOException ioException) { Log.e(Constants.TAG, "[SERVER THREAD] An exception has occurred: " + ioException.getMessage()); if (Constants.DEBUG) { ioException.printStackTrace(); } } } }
b) un fir de execuție care gestionează comunicația dintre client și server:
BufferedReader
și PrintWriter
(prin apelul metodelor statice din clasa Utilities
: getReader()
și getWriter()
), prin care se vor realiza operațiile de citire și scriere pe canalul de comunicație;POST
la adresa Internet http://www.wunderground.com/cgi-bin/findweather/getForecast; parametrul care trebuie precizat este query
și are valoarea orașului pentru care se dorește să se obțină informațiile; acesta se atașează cererii de tip POST
, folosind codificarea UTF-8
;GET
în care valoarea parametrului este inclusă în cadrul adresei Internet: https://www.wunderground.com/cgi-bin/findweather/getForecast?query=...);<script> wui.asyncCityPage = true; wui.bootstrapped.API = ""; wui.api_data = { "response": { "version": "2.0", "units": "metric", "location": { "name": "Bucuresti", "neighborhood":null, "city": "Bucuresti", "state": null, "state_name":"Romania", "country": "RO", "country_iso3166":null, "country_name":"Romania", "zip":"00000", "magic":"11", "wmo":"15420", "latitude":44.50000000, "longitude":26.12999916, "elevation":91.00000000, "l": "/q/zmw:00000.11.15420" }, "date": { ... }, "current_observation": { "source": "PWS", "station": { ... }, "estimated": null, "date": { ... }, "metar": "AAXX 16161 15420 22997 80502 10201 20109 30072 40180 55008 8807/ 333 55300 0//// 20454", "condition":"Overcast", "temperature": 21.5, "humidity":48, "wind_speed":6.9, ..., "pressure": 1018, ..., } } }; </script>
Se observă faptul că informațiile necesare se regăsesc în cadrul unei etichete de tip <script> … </script>
care conține un obiect denumit wui.api_dat
exprimat în format JSON. În acest sens, se obține lista tuturor etichetelor de tip script
(se folosește metoda getElementsByTag()
), se preia conținutul acestora (prin intermediul metodei data()
din cadrul clasei Element
) și se verifică dacă se regăsește șirul de caractere wui.api_dat
;
response
→ current_observation
și ulterior datele meteorologice, regăsite ca valori sub cheile temperature
, wind_speed
, condition
, pressure
, humidity
;WeatherForecastInformation
folosind informațiile furnizate și se transmite către server pentru ca acesta să fie utilizat ulterior pentru cereri provenite de la alți clienți, vizând același oraș.
public synchronized void setData(String city, WeatherForecastInformation weatherForecastInformation) { this.data.put(city, weatherForecastInformation); } public synchronized HashMap<String, WeatherForecastInformation> getData() { return data; }
public void run() { if (socket == null) { Log.e(Constants.TAG, "[COMMUNICATION THREAD] Socket is null!"); return; } try { BufferedReader bufferedReader = Utilities.getReader(socket); PrintWriter printWriter = Utilities.getWriter(socket); if (bufferedReader == null || printWriter == null) { Log.e(Constants.TAG, "[COMMUNICATION THREAD] Buffered Reader / Print Writer are null!"); return; } Log.i(Constants.TAG, "[COMMUNICATION THREAD] Waiting for parameters from client (city / information type!"); String city = bufferedReader.readLine(); String informationType = bufferedReader.readLine(); if (city == null || city.isEmpty() || informationType == null || informationType.isEmpty()) { Log.e(Constants.TAG, "[COMMUNICATION THREAD] Error receiving parameters from client (city / information type!"); return; } HashMap<String, WeatherForecastInformation> data = serverThread.getData(); WeatherForecastInformation weatherForecastInformation = null; if (data.containsKey(city)) { Log.i(Constants.TAG, "[COMMUNICATION THREAD] Getting the information from the cache..."); weatherForecastInformation = data.get(city); } else { Log.i(Constants.TAG, "[COMMUNICATION THREAD] Getting the information from the webservice..."); HttpClient httpClient = new DefaultHttpClient(); HttpPost httpPost = new HttpPost(Constants.WEB_SERVICE_ADDRESS); List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair(Constants.QUERY_ATTRIBUTE, city)); UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(params, HTTP.UTF_8); httpPost.setEntity(urlEncodedFormEntity); ResponseHandler<String> responseHandler = new BasicResponseHandler(); String pageSourceCode = httpClient.execute(httpPost, responseHandler); if (pageSourceCode == null) { Log.e(Constants.TAG, "[COMMUNICATION THREAD] Error getting the information from the webservice!"); return; } Document document = Jsoup.parse(pageSourceCode); Element element = document.child(0); Elements elements = element.getElementsByTag(Constants.SCRIPT_TAG); for (Element script: elements) { String scriptData = script.data(); if (scriptData.contains(Constants.SEARCH_KEY)) { int position = scriptData.indexOf(Constants.SEARCH_KEY) + Constants.SEARCH_KEY.length(); scriptData = scriptData.substring(position); JSONObject content = new JSONObject(scriptData); JSONObject currentObservation = content.getJSONObject(Constants.CURRENT_OBSERVATION); String temperature = currentObservation.getString(Constants.TEMPERATURE); String windSpeed = currentObservation.getString(Constants.WIND_SPEED); String condition = currentObservation.getString(Constants.CONDITION); String pressure = currentObservation.getString(Constants.PRESSURE); String humidity = currentObservation.getString(Constants.HUMIDITY); weatherForecastInformation = new WeatherForecastInformation( temperature, windSpeed, condition, pressure, humidity ); serverThread.setData(city, weatherForecastInformation); break; } } } if (weatherForecastInformation == null) { Log.e(Constants.TAG, "[COMMUNICATION THREAD] Weather Forecast Information is null!"); return; } String result = null; switch(informationType) { case Constants.ALL: result = weatherForecastInformation.toString(); break; case Constants.TEMPERATURE: result = weatherForecastInformation.getTemperature(); break; case Constants.WIND_SPEED: result = weatherForecastInformation.getWindSpeed(); break; case Constants.CONDITION: result = weatherForecastInformation.getCondition(); break; case Constants.HUMIDITY: result = weatherForecastInformation.getHumidity(); break; case Constants.PRESSURE: result = weatherForecastInformation.getPressure(); break; default: result = "[COMMUNICATION THREAD] Wrong information type (all / temperature / wind_speed / condition / humidity / pressure)!"; } printWriter.println(result); printWriter.flush(); } catch (IOException ioException) { Log.e(Constants.TAG, "[COMMUNICATION THREAD] An exception has occurred: " + ioException.getMessage()); if (Constants.DEBUG) { ioException.printStackTrace(); } } catch (JSONException jsonException) { Log.e(Constants.TAG, "[COMMUNICATION THREAD] An exception has occurred: " + jsonException.getMessage()); if (Constants.DEBUG) { jsonException.printStackTrace(); } } finally { if (socket != null) { try { socket.close(); } catch (IOException ioException) { Log.e(Constants.TAG, "[COMMUNICATION THREAD] An exception has occurred: " + ioException.getMessage()); if (Constants.DEBUG) { ioException.printStackTrace(); } } } } }
4. Implementarea clientului presupune un fir de execuție pe care sunt realizate următoarele operații:
BufferedReader
și PrintWriter
(prin apelul metodelor statice din clasa Utilities
: getReader()
și getWriter()
), prin care se vor realiza operațiile de citire și scriere pe canalul de comunicație;null
a șirului de caractere furnizat);TextView
, din cadrul interfeței grafice; întucât modificarea se face din contextul unui fir de execuție care gestionează comunicația prin rețea, accesul la controlul grafic se face prin intermediul metodei post()
care primește ca parametru un obiect anonim de tip Runnable
al cărui conținut va fi rulat în contextul firului de execuție principal al aplicației Android;public void run() { try { socket = new Socket(address, port); if (socket == null) { Log.e(Constants.TAG, "[CLIENT THREAD] Could not create socket!"); return; } BufferedReader bufferedReader = Utilities.getReader(socket); PrintWriter printWriter = Utilities.getWriter(socket); if (bufferedReader == null || printWriter == null) { Log.e(Constants.TAG, "[CLIENT THREAD] Buffered Reader / Print Writer are null!"); return; } printWriter.println(city); printWriter.flush(); printWriter.println(informationType); printWriter.flush(); String weatherInformation; while ((weatherInformation = bufferedReader.readLine()) != null) { final String finalizedWeateherInformation = weatherInformation; weatherForecastTextView.post(new Runnable() { @Override public void run() { weatherForecastTextView.setText(finalizedWeateherInformation); } }); } } catch (IOException ioException) { Log.e(Constants.TAG, "[CLIENT THREAD] An exception has occurred: " + ioException.getMessage()); if (Constants.DEBUG) { ioException.printStackTrace(); } } finally { if (socket != null) { try { socket.close(); } catch (IOException ioException) { Log.e(Constants.TAG, "[CLIENT THREAD] An exception has occurred: " + ioException.getMessage()); if (Constants.DEBUG) { ioException.printStackTrace(); } } } } }
Pornirea firului de execuție corespunzător clientului va fi realizată pe metoda de callback a obiectului ascultător pentru evenimentul de tip apăsare a butonului aferent din interfața grafică.
Verificările care trebuie realizate sunt:
private class GetWeatherForecastButtonClickListener implements Button.OnClickListener { @Override public void onClick(View view) { String clientAddress = clientAddressEditText.getText().toString(); String clientPort = clientPortEditText.getText().toString(); if (clientAddress == null || clientAddress.isEmpty() || clientPort == null || clientPort.isEmpty()) { Toast.makeText(getApplicationContext(), "[MAIN ACTIVITY] Client connection parameters should be filled!", Toast.LENGTH_SHORT).show(); return; } if (serverThread == null || !serverThread.isAlive()) { Toast.makeText(getApplicationContext(), "[MAIN ACTIVITY] There is no server to connect to!", Toast.LENGTH_SHORT).show(); return; } String city = cityEditText.getText().toString(); String informationType = informationTypeSpinner.getSelectedItem().toString(); if (city == null || city.isEmpty() || informationType == null || informationType.isEmpty()) { Toast.makeText(getApplicationContext(), "[MAIN ACTIVITY] Parameters from client (city / information type) should be filled", Toast.LENGTH_SHORT).show(); return; } weatherForecastTextView.setText(Constants.EMPTY_STRING); clientThread = new ClientThread( clientAddress, Integer.parseInt(clientPort), city, informationType, weatherForecastTextView ); clientThread.start(); } }
Conexiunea la serverul implementat în cadrul aplicației Android se poate realiza și prin intermediul unei console de pe mașina fizică, folosind utilitarele nc
(Linux), respectiv telnet
(Windows).
Trebuie determinată adresa Internet la care poate fi accesat dispozitivul mobil:
usb0
/ rndis0
- Linux, respectiv Ethernet pe Windows (se rulează comenzile ifconfig
pe Linux, ipconfig
pe Windows);192.168.56.101
→ 192.168.56.254
;
student@eim2017:~$ nc 192.168.56.101 5000 Bucuresti all temperature: 17.0 wind_speed: 5.0 condition: Clear pressure: 1015 humidity: 68 |
C:\Users\Eim2017> telnet 192.168.56.101 5000 Bucuresti all temperature: 17.0 wind_speed: 5.0 condition: Clear pressure: 1015 humidity: 68 Connection to host lost. |
5. Pentru încărcarea codului în contextul depozitului din cadrul contului Github personal:
git add
, indicându-se și fișierele respective (pot fi folosite șabloane pentru a desemna mai multe fișiere);git commit -m
, precizându-se și un mesaj sugestiv:git push
, care primește ca parametrii:origin
se indică depozitul de unde au fost descărcat conținutul);master
).student@eim2017:~/PracticalTest02$ git add * student@eim2017:~/PracticalTest02$ git commit -m "finished tasks for PracticalTest02" student@eim2017:~/PracticalTest02$ git push origin master
În cazul în care este necesar, vor fi configurați parametrii necesari operației de consemnare (numele de utilizator și adresa de poștă electronică):
student@eim2017:~/PracticalTest02$ git config --global user.name "Perfect Student" student@eim2017:~/PracticalTest02$ git config --global user.email perfectstudent@cs.pub.ro
6. Protocolul SIP pornește de la o presupunere optimistă conform căreia atât sursa cât și destinația se găsesc în cadrul aceluiași sistem autonom, astfel încât nu sunt necesare credențiale pentru autentificare. Din acest model, o cerere de tip REGISTER
se transmite inițial de către user agent fără aceste informații. În condițiile în care răspunsul furnizat de registration server este Status: 401 Unauthorized, mesajul este retransmis împreună cu informațiile necesare identificării (SIP Authorization ID, Password). Se poate observa că dimensiunile celor două pachete sunt diferite (mesajul fără credențiale 1095 octeți, mesajul cu credențiale 1249 octeți). În cazul în care informațiile de autentificare sunt valide, răspunsul va fi Status: 200 OK.
7. Harta Google este implementată în SDK-ul Android:
Referințele către aceste obiecte se obțin în mod obișnuit, prin intermediul metodelor findViewById()
, respectiv findFragmentById()
.
Ambele componente grafice încapsulează un obiect de tipul GoogleMap
.
În cazul folosirii unui control grafic de tipul MapView
, evenimentele legate de ciclul de viață al aplicației Android vor trebui să fie gestionate manual de câtre programator (componenta va trebui să fie notificată atunci când un astfel de eveniment s-a produs).
În cazul folosirii unui control grafic de tipul MapFragment
, metodele de callback corespunzătoare evenimentelor care guvernează ciclul de viață al aplicației Android sunt deja implementate (ca în cazul oricărui fragment, de altfel), motiv pentru care programatorul nu are altceva de făcut decât să specifice comportamentul pentru fiecare situație în parte, după caz.
Pe baza controalelor grafice MapView
sau MapFragment
, se poate obține o instanță a unui obiect GoogleMap, prin intermediul metodei getMapAsync()
. Metoda de callback onMapReady()
a clasei ascultător OnMapReadyCallback
nu va fi apelată în situația în care serviciul Google Play Services nu este disponibil pe dispozitivul mobil sau obiectul este distrus imediat după ce a fost creat.
if (googleMap == null) { ((MapFragment)getFragmentManager().findFragmentById(R.id.google_map)).getMapAsync(new OnMapReadyCallback() { @Override public void onMapReady(GoogleMap readyGoogleMap) { googleMap = readyGoogleMap; } }); }