Colocviu 2

Model de Subiect

Observații Generale

Pentru rezolvarea subiectelor propuse în cadrul colocviului 2, sunt necesare:

  • un cont Github personal, pe care să existe drepturi de citire și de scriere;
  • SDK-ul de Android (cu o imagine pentru nivelul de API 16 - Jelly Bean 4.1);
  • mediul de dezvoltare integrat Android Studio sau Eclipse, împreună cu plugin-ul Android Developer Tools;
  • un dispozitiv mobil:
    • fizic (bring your own device);
    • virtual: Genymotion, AVD.

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 FileNewNew Module.

În fereastra Create New Module se indică tipul de modul Import .JAR/.AAR Package.

Se precizează:

  • locația la care se găsește biblioteca ce va fi importată sub formă de modul;
  • denumirea subproiectului din care va face parte modulul;

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ă:

build.gradle
...
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:

build.gradle
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:

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ță.

Rezolvări

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:

  • unei denumiri;
  • unei descrieri (opțional);
  • tipului de director (public sau privat - doar contra cost!!!);
  • modului de inițializare:
    • local, prin git init;
    • la distanță, prin git clone - depozitul la distanță nu trebuie să fie vid, în această situațoe
  • unui fișier README (opțional);
  • extensiilor ignorate (corespunzătoare limbajului de programare folosit, în cazul de față - Android) - incluse în fișierul .gitignore;
  • tipului de licență sub care este publicat codul sursă.

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ță).

În situația în care se dorește clonarea conținutului din depozitul la distanță în alt director decât cel curent, acesta poate fi transmis ca parametru al comenzii, după URL-ul la care poate fi accesat proiectul în Github.

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:

  • serverul
    • un câmp text pentru specificarea portului pe care va accepta conexiuni de la clienți;
    • un buton pentru lansarea în execuție;
  • clientul
    • câmpuri text pentru precizarea parametrilor de conexiune la server:
      • adresa Internet (se va folosi localhost sau 127.0.0.1);
      • port;
    • elemente grafice pentru indicarea informațiilor solicitate:
      • un câmp text pentru oraș;
      • o listă populată cu datele meteorologice care se doresc a fi furnizate (temperature, wind_speed, condition, pressure, humidity, all); ca alternativă, se poate folosi tot un câmp text, cu restricția ca valorile introduse să facă parte din mulțimea acceptată;
    • un buton pentru trimiterea cererii și primirea răspunsului, condiționată de completarea anterioară a tuturor celorlalte informații.

Nu este necesar ca în dezvoltarea interfeței grafice să se utilizeze elemente grafice complexe (de tipul fragmentelor sau listelor). Nu este punctat nici aspectul estetic al acesteia. Tot ce contează este ca interfața grafică să poată fi utilizată pentru implementarea cerințelor funcționale.

activity_practical_test02_main.xml
<?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>

O aplicație Android care accesează rețeaua trebuie să dețină permisiunea necesară (android.permission.INTERNET), specificată explicit în fișierul AndroidManifest.xml:

AndroidManifest.xml
<manifest ...>
  <!-- other elements -->
  <uses-permission
    android:name="android.permission.INTERNET" />
</manifest>

O aplicație Android care accesează rețeaua va gestiona (cel puțin) două fire de execuție dedicate:

  • firul de execuție al interfeței grafice; un control grafic nu va putea fi modificat decât din contextul acestui fir de execuție (în acest sens, se va utiliza metoda post() a elementului respectiv pentru interacțiunea cu utilizatorul sau un obiect de tip Handler);
  • firul de execuție pentru comunicație, necesar astfel încât operațiile care necesită acces la rețea să nu blocheze interacțiunea cu utilizatorul, afectând experiența acestuia; în practică, se vor defini:
    • un fir de execuție pentru server (pe care sunt acceptate conexiunile de la clienți);
    • câte un fir de execuție pentru fiecare comunicație dintre client și server, astfel încât interacțiunea dintre acestea să nu influențeze reponsivitatea serverului și nici comunicația cu ceilalți clienți.

3. Implementarea serverului presupune:

a) un fir de execuție care gestionează solicitările de conexiune de la clienți:

  • se va instanția un obiect de tip 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();
      }
    }
  • se va gestiona un obiect de tip 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>();
  • atâta vreme cât firul de execuție nu este întrerupt (aplicația Android nu a fost distrusă), sunt acceptate conexiuni de la clienți (prin invocarea metodei 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:

  • adresa Internet, furnizată de metoda getInetAddress();
  • portul, furnizat de metoda 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();
  }
 
}

Este important ca atunci când aplicația Android este distrusă să se oprească firul de execuție corespunzător serverului, eliberându-se resursele alocate:

@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:

  • se obțin obiectele de tip 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;
  • se obține o referință către obiectul gestionat de server ce reține informațiile meteorologice pentru orașele pentru care au fost realizate interogări anterior;
  • se primesc de la client (prin citire de pe canalul de comunicație) parametrii necesari pentru furnizarea informațiilor meteorologice (oraș și tip de date: temperatură, viteza vântului, starea generală, presiune, umiditate, toate);
  • în situația în care informațiile meteorologice se găsesc în obiectul gestionat de server, sunt preluate local;
  • în situația în care informațiile meteorologice nu se găsesc în obiectul gestionat de server, sunt preluate prin interogarea serviciului Internet, la distanță:
    • se realizează o cerere de tip 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;
    • alternativ, se poate realiza o cerere de tip GET în care valoarea parametrului este inclusă în cadrul adresei Internet: https://www.wunderground.com/cgi-bin/findweather/getForecast?query=...);
    • se obține răspunsul sub forma unui șir de caractere, reprezentând codul sursă al paginii Internet;
    • folosind biblioteca Jsoup, se inspectează documentul în format HTML furnizat pentru a determina locația la care sunt disponibile informațiile necesare
      <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;

    • se inspectează documentul în format JSON pentru a obține informațiile necesare: se obțin, succesiv, obiectele atașate ca valori pentru cheile responsecurrent_observation și ulterior datele meteorologice, regăsite ca valori sub cheile temperature, wind_speed, condition, pressure, humidity;
    • se construiește un obiect de tipul 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ș.

Pe server, metodele care gestionează obiectul ce conține informațiile meteorologice legate de orașse trebuie să fie sincronizate, astfel încât să nu apară inconsistențe în situația în care acestea sunt accesate concomitent de mai multe fire de execuție:

public synchronized void setData(String city, WeatherForecastInformation weatherForecastInformation) {
  this.data.put(city, weatherForecastInformation);
}
 
public synchronized HashMap<String, WeatherForecastInformation> getData() {
  return data;
}

  • se trimit la client (prin scriere pe canalul de comunicație) informațiile meteorologice solicitate;
  • se eliberează resursele corespunzătoare canalului de comunicație.
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:

  • deschiderea unui canal de comunicație folosind parametrii de conexiune la server (adresa Internet, port);
  • obținerea unor obiecte de tip 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;
  • trimiterea la server (prin scriere pe canalul de comunicație) parametrii necesari pentru furnizarea informațiilor meteorologice (oraș și tip de date: temperatură, viteza vântului, starea generală, presiune, umiditate, toate);
  • primirea de la server (prin citire de pa canalul de comunicație) a liniilor distincte conținând datele meteorologice (cât timp nu se primește EOF, reprezentat printr-o valoare null a șirului de caractere furnizat);
  • vizualizarea datelor meterologice într-un obiect de tip 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;
  • închiderea canalului de comunicație.
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:

  • completarea câmpurilor text ce conțin parametrii de conexiune la server (adresă Internet, port);
  • existența unui fir de execuție corespunzător serverului care să ruleze la momentul respectiv;
  • completarea controalelor grafice ce conțin denumirea orașului și tipul de informație meteorologică ce se dorește a fi solicitată.
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:

  • dispozitiv fizic: se folosește USB Tethering, iar adresa Internet este indicată de valoarea Default Gateway a rețelei usb0 / rndis0 - Linux, respectiv Ethernet pe Windows (se rulează comenzile ifconfig pe Linux, ipconfig pe Windows);
  • dispozitiv virtual:
    • Genymotion: adresele Internet sunt alocate de serverul DHCP din cadrul VirtualBox în intervalul 192.168.56.101192.168.56.254;
    • AVD: se utilizează redirectarea porturilor.
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:

  1. se transferă modificările din zona de lucru în zona de lucru în zona de așteptare prin intermediul comenzii git add, indicându-se și fișierele respective (pot fi folosite șabloane pentru a desemna mai multe fișiere);
  2. se consemnează modificările din zona de așteptare în directorul Git prin intermediul comenzii git commit -m, precizându-se și un mesaj sugestiv:
  3. se încarcă modificările la distanță, prin intermediul comenzii git push, care primește ca parametrii:
    1. sursa (prin eticheta origin se indică depozitul de unde au fost descărcat conținutul);
    2. destinația: ramificația curentă (implicit, aceasta poartă denumirea 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. Se configurează avahi-daemon (fișierul /etc/avahi/services/chat.service) astfel:

<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
  <name>My desktop</name>
  <service>
    <type>_myservice._tcp</type>
    <port>5555</port>
  </service>
</service-group>
  • se restarteaza demonul
root@eg106:~# /etc/init.d/avahi-daemon restart
[ ok ] Restarting avahi-daemon (via systemctl): avahi-daemon.service.
root@eg106:~# 

7a. Folosind utilitarul din linie de comandă avahi-browse cu parametrii -rca se capturează mesajele advertisment și se identifică interfețele raportate din output.

Exemplu de output:

+ vboxnet4 IPv6 My desktop                                    _myservice._tcp      local
+ vboxnet4 IPv4 My desktop                                    _myservice._tcp      local
+ vmnet8 IPv6 My desktop                                    _myservice._tcp      local
+ vmnet8 IPv4 My desktop                                    _myservice._tcp      local
+ vmnet1 IPv6 My desktop                                    _myservice._tcp      local
+ vmnet1 IPv4 My desktop                                    _myservice._tcp      local
+ docker0 IPv4 My desktop                                    _myservice._tcp      local
+   eno1 IPv6 My desktop                                    _myservice._tcp      local
+   eno1 IPv4 My desktop                                    _myservice._tcp      local
= vboxnet4 IPv6 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [fe80::800:27ff:fe00:4]
   port = [5555]
   txt = []
= vboxnet4 IPv4 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [192.168.60.1]
   port = [5555]
   txt = []
= vmnet8 IPv6 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [fe80::250:56ff:fec0:8]
   port = [5555]
   txt = []
= vmnet8 IPv4 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [192.168.207.1]
   port = [5555]
   txt = []
= vmnet1 IPv6 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [fe80::250:56ff:fec0:1]
   port = [5555]
   txt = []
= vmnet1 IPv4 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [172.16.238.1]
   port = [5555]
   txt = []
= docker0 IPv4 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [172.17.0.1]
   port = [5555]
   txt = []
=   eno1 IPv6 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [fe80::ed2d:c447:8f31:8128]
   port = [5555]
   txt = []
=   eno1 IPv4 My desktop                                    _myservice._tcp      local
   hostname = [eg106-2.local]
   address = [172.16.4.53]
   port = [5555]
   txt = []

Numele de masina publicat este eg106-2.local, pe interfețele vmnet1, vmnet8, docker0, eno1, etc. IP-urile sunt respectiv: 192.168.207.1, 172.16.238.1, 172.17.0.1, 172.16.4.53, etc.

7b. Să se captureze în emulator publicarea serviciului. Ce adresă IPv4 este folosită în advertisement? Răspuns:

root@eg106:~# adb shell 
root@vbox86p:/ # tcpdump -ni eth0 'udp port 5353' 
# atenție eth1 în configurația default este NAT-ul către internet, acolo nu este nimeni (din DNS-SD)!
... 
06:39:44.024608 IP6 fe80::800:27ff:fe00:4.5353 > ff02::fb.5353: 0*- [0q] 2/0/0[|domain]
06:39:44.207351 IP6 fe80::800:27ff:fe00:4.5353 > ff02::fb.5353: 0 [2n][|domain]
06:39:44.207385 IP 192.168.60.1.5353 > 224.0.0.251.5353: 0 [2n] ANY (QM)? My desktop._myservice._tcp.local. (91)
06:39:44.458079 IP6 fe80::800:27ff:fe00:4.5353 > ff02::fb.5353: 0 [2n][|domain]
06:39:44.458108 IP 192.168.60.1.5353 > 224.0.0.251.5353: 0 [2n] ANY (QM)? My desktop._myservice._tcp.local. (91)
06:39:44.708406 IP6 fe80::800:27ff:fe00:4.5353 > ff02::fb.5353: 0 [2n][|domain]
06:39:44.708439 IP 192.168.60.1.5353 > 224.0.0.251.5353: 0 [2n] ANY (QM)? My desktop._myservice._tcp.local. (91)
06:39:44.909356 IP 192.168.60.1.5353 > 224.0.0.251.5353: 0*- [0q] 6/0/0[|domain]
06:39:44.909381 IP6 fe80::800:27ff:fe00:4.5353 > ff02::fb.5353: 0*- [0q] 5/0/0[|domain]
06:39:45.156505 IP 192.168.60.1.5353 > 224.0.0.251.5353: 0*- [0q] 2/0/0[|domain]
06:39:45.156532 IP6 fe80::800:27ff:fe00:4.5353 > ff02::fb.5353: 0*- [0q] 1/0/0 (111)
06:39:45.240427 IP6 fe80::800:27ff:fe00:4.5353 > ff02::fb.5353: 0 [2q] PTR (QM)? _ipp._tcp.local.[|domain]
06:39:45.240458 IP 192.168.60.1.5353 > 224.0.0.251.5353: 0 [2q] PTR (QM)? _ipp._tcp.local. PTR (QM)? _ipps._tcp.local. (45)
06:39:46.041712 IP 192.168.60.1.5353 > 224.0.0.251.5353: 0*- [0q] 6/0/0[|domain]
06:39:46.041740 IP6 fe80::800:27ff:fe00:4.5353 > ff02::fb.5353: 0*- [0q] 5/0/0[|domain]

Se foloseste adresa 192.168.60.1

eim/colocvii/colocviu02.txt · Last modified: 2018/05/17 21:07 by dragos.niculescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0