Table of Contents

Colocviu 2

Model de Subiect

eim2016-tp02-varsim.pdf

Observații Generale

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

<hidden>

În situația în care lucrați pe calculatoarele din laboratorul Ixia (EG106) este recomandat să folosiți:
  • fluxbox ca utilitar pentru gestiunea ferestrelor (se alege din interfața de autentificare);
  • epiphany pentru navigare Internet.

Este de preferat să nu se depășească memoria disponibilă, întrucât comportamentul acestor mașini este imprevizibil în momentul în care se folosește swap-ul (se blochează, fără posibilitatea de recuperare din eroare).

</hidden>

Î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 poate folosi biblioteca Jsoup.

Integrarea sa în mediul integrat pentru dezvoltare Android Studio 1.5.1 se face prin plasarea fișierului corespunzător în directorul libs al proiectului. Ulterior, din meniul contextual al aplicației Android (accesibil prin click dreapta) se accesează opțiunea Open Module Settings sau se apasă tasta F4.

În fereastra Project Structure se apasă tasta + pentru a se specifica un modul nou.

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

Se precizează:

După ce biblioteca a fost integrată în cadrul mediului integrat pentru dezvoltare Android Studio 1.5.1, aceasta va putea fi vizualizată în fereastra Project Structure.

Integrarea sa în mediul integrat pentru dezvoltare Eclipse Mars 1 (4.5.1) se face prin plasarea fișierului corespunzător în directorul libs al proiectului. Acesta devine vizibil în momentul în care se accesează operația Refresh din meniul contextual, disponibilă și prin tasta F5. Ulterior, pentru a fi inclus în classpath, este necesar ca din meniul contextual asociat fișierului jsoup-1.9.1.jar (disponibil pe butonul drept al mouse-ului) să se acceseze Build PathAdd to Build Path.

După ce biblioteca a fost integrată în cadrul mediului integrat pentru dezvoltare Eclipse Mars 1 (4.5.1), aceasta va putea fi vizualizată în secțiunea Referenced Libraries din proiectul corespunzător.

Î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

Proiectele Android Studio și Eclipse corespunzătoare aplicației Android ce conține rezolvările complete ale cerințelor colocviului sunt disponibile pe contul de Github al disciplinei.

1. 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:

2. 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@eim2016:~$ git clone https://www.github.com/perfectstudent/PracticalTest02

3. Se urmăresc indicațiile disponibile în secțiunea Crearea unei aplicații Android în Eclipse Mars 1 (4.5.1), respecti Crearea unei aplicații Android în Android Studio 1.5.1.

4. Pentru implementarea interfeței grafice, se vor defini controalele care asigură interacțiunea cu utilizatorul pentru fiecare dintre componentele aplicației Android:

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
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  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.graphicuserinterface.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="100dp"
      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:singleLine="false"
      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.

5. Implementarea serverului presupune:

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

De remarcat faptul că pentru un obiect de tip Socket se poate determina:

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(),
        "Server port should be filled!",
        Toast.LENGTH_SHORT
      ).show();
      return;
    }
    serverThread = new ServerThread(Integer.parseInt(serverPort));
    if (serverThread.getServerSocket() != null) {
      serverThread.start();
    } else {
      Log.e(Constants.TAG, "[MAIN ACTIVITY] Could not creat server thread!");
    }
  }
}
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() {
  if (serverThread != null) {
    serverThread.stopThread();
  }
  super.onDestroy();
}
public void stopThread() {
  if (serverSocket != null) {
    interrupt();
    try {
      if (serverSocket != null) {
        serverSocket.close();
      }
    } catch (IOException ioException) {
      Log.e(Constants.TAG, "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:

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;
}
@Override
public void run() {
  if (socket != null) {
    try {
      BufferedReader bufferedReader = Utilities.getReader(socket);
      PrintWriter    printWriter    = Utilities.getWriter(socket);
      if (bufferedReader != null && printWriter != null) {
        Log.i(Constants.TAG, "[COMMUNICATION THREAD] Waiting for parameters from client (city / information type)!");
        String city            = bufferedReader.readLine();
        String informationType = bufferedReader.readLine();
        HashMap<String, WeatherForecastInformation> data = serverThread.getData();
        WeatherForecastInformation weatherForecastInformation = null;
        if (city != null && !city.isEmpty() && informationType != null && !informationType.isEmpty()) {
          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<NameValuePair>();
            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) {
              Document document = Jsoup.parse(pageSourceCode);
              Element element = document.child(0);
              Elements scripts = element.getElementsByTag(Constants.SCRIPT_TAG);
              for (Element script: scripts) {
                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;
                }
              }
            } else {
              Log.e(Constants.TAG, "[COMMUNICATION THREAD] Error getting the information from the webservice!");
            }
          }
          if (weatherForecastInformation != null) {
            String result = null;
            if (Constants.ALL.equals(informationType)) {
              result = weatherForecastInformation.toString();
            } else if (Constants.TEMPERATURE.equals(informationType)) {
              result = weatherForecastInformation.getTemperature();
            } else if (Constants.WIND_SPEED.equals(informationType)) {
              result = weatherForecastInformation.getWindSpeed();
            } else if (Constants.CONDITION.equals(informationType)) {
              result = weatherForecastInformation.getCondition();
            } else if (Constants.HUMIDITY.equals(informationType)) {
              result = weatherForecastInformation.getHumidity();
            } else if (Constants.PRESSURE.equals(informationType)) {
              result = weatherForecastInformation.getPressure();
            } else {
              result = "Wrong information type (all / temperature / wind_speed / condition / humidity / pressure)!";
            }
            printWriter.println(result);
            printWriter.flush();
          } else {
            Log.e(Constants.TAG, "[COMMUNICATION THREAD] Weather Forecast information is null!");
          }
        } else {
          Log.e(Constants.TAG, "[COMMUNICATION THREAD] Error receiving parameters from client (city / information type)!");
        }
      } else {
        Log.e(Constants.TAG, "[COMMUNICATION THREAD] BufferedReader / PrintWriter are null!");
      }
      socket.close();
    } 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();
      }
    }
  } else {
    Log.e(Constants.TAG, "[COMMUNICATION THREAD] Socket is null!");
  }
}
În cazul în care se folosește Android Studio și este instalat un SDK corespunzător nivelului de API 23 în care a fost eliminat suportul pentru biblioteca Apache HTTP Components, este necesar ca în fișierul build.gradle din directorul app al aplicației Android să se precizeze explicit folosirea acesteia (denumirea sa este org.apache.http.legacy).
build.gradle
...
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.2"
 
  defaultConfig {
    applicationId "ro.pub.cs.systems.eim.practicaltest02"
    minSdkVersion 16
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
 
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
 
  useLibrary 'org.apache.http.legacy'
}
...

6. Implementarea clientului presupune un fir de execuție pe care sunt realizate următoarele operații:

@Override
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) {
      printWriter.println(city);
      printWriter.flush();
      printWriter.println(informationType);
      printWriter.flush();
      String weatherInformation;
      while ((weatherInformation = bufferedReader.readLine()) != null) {
        final String finalizedWeatherInformation = weatherInformation;
        weatherForecastTextView.post(new Runnable() {
          @Override
          public void run() {
            weatherForecastTextView.append(finalizedWeatherInformation + "\n");
          }
        });
      }
    } else {
      Log.e(Constants.TAG, "[CLIENT THREAD] BufferedReader / PrintWriter are null!");
    }
    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(),
        "Client connection parameters should be filled!",
        Toast.LENGTH_SHORT
      ).show();
      return;
    }
    if (serverThread == null || !serverThread.isAlive()) {
      Log.e(Constants.TAG, "[MAIN ACTIVITY] There is no server to connect to!");
      return;
    }
    String city = cityEditText.getText().toString();
    String informationType = informationTypeSpinner.getSelectedItem().toString();
    if (city == null || city.isEmpty() ||
      informationType == null || informationType.isEmpty()) {
      Toast.makeText(
        getApplicationContext(),
        "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:

student@eim2016:~$ nc 192.168.56.101 5000
Bucuresti
all
temperature: 17.0
wind_speed: 5.0
condition: Clear
pressure: 1015
humidity: 68
C:\Users\Eim2016> 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.

7. 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@eim2016:~/PracticalTest02$ git add *
student@eim2016:~/PracticalTest02$ git commit -m "finished tasks for PracticalTest02"
student@eim2016:~/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@eim2016:~/PracticalTest02$ git config --global user.name "Perfect Student"
student@eim2016:~/PracticalTest02$ git config --global user.email perfectstudent@cs.pub.ro

8. Înregistrarea unui serviciu în cadrul rețelei locale se face prin intermediul unor obiecte NsdServiceInfo (Android NSD) respectiv ServiceInfo (JmDNS). Acestea sunt transmise ca parametrii ai metodelor registerService() din clasele NsdManager, respectiv JmDNS.

Aceste obiecte sunt caracterizate prin parametrii de conectare la serviciu:

Se recomandă ca portul să fie furnizat de sistemul de operare, pentru a evita situațiile în care utilizatorul poate specifica un port care este ocupat. În acest sens, se instanțiază un obiect de tip ServerSocket care primește parametrul 0, indicându-se astfel faptul că se dorește utilizarea unui port aleator, care poate fi folosit la momentul respectiv. Ulterior, parametrii de conectare pot fi obținuți folosind metodele specifice ale unui astfel de obiect:

ServerSocket serverSocket = new ServerSocket(0);
if (serverSocket != null) {
  InetAddress inetAddress = serverSocket.getInetAddress();
  int port = serverSocket.getLocalPort();
}

9. 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.

10. 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().

Pe baza controalelor grafice MapView sau MapFragment, se poate obține o instanță a unui obiect GoogleMap, prin intermediul metodelor getMap(), respectiv getMapAsync(). Se recomandă să se folosească metoda asincronă care garantează faptul că obiectul furnizat este nenul. 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;
    }
  });
}