====== Colocviu 2 ====== ===== Model de Subiect =====
student@eim2016:~$ git clone https://www.github.com/perfectstudent/PracticalTest02
**3.** Se urmăresc indicațiile disponibile în secțiunea [[laboratoare:laborator01#crearea_unei_aplicatii_android_in_eclipse_mars_1_451_-_obligatoriu|Crearea unei aplicații Android în Eclipse Mars 1 (4.5.1)]], respecti [[laboratoare:laborator01#crearea_unei_aplicatii_android_in_android_studio_151_-_obligatoriu|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:
* 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.
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();
* 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] Waiting for a connection...");
Socket socket = serverSocket.accept();
Log.i(Constants.TAG, "[SERVER] 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, "An exception has occurred: " + clientProtocolException.getMessage());
if (Constants.DEBUG) {
clientProtocolException.printStackTrace();
}
} catch (IOException ioException) {
Log.e(Constants.TAG, "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(),
"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!");
}
}
}
@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();
}
}
}
}
Se observă faptul că informațiile necesare se regăsesc în cadrul unei etichete de tip '''' 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 ''response'' → ''current_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ș.
public synchronized void setData(String city, WeatherForecastInformation weatherForecastInformation) {
this.data.put(city, weatherForecastInformation);
}
public synchronized HashMap 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 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 params = new ArrayList();
params.add(new BasicNameValuePair(Constants.QUERY_ATTRIBUTE, city));
UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(params, HTTP.UTF_8);
httpPost.setEntity(urlEncodedFormEntity);
ResponseHandler 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!");
}
}
@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();
}
}
}
{{ :colocvii:colocviu02:graphic_interface02.png?nolink&400 }}
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(),
"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:
* 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.101'' → ''192.168.56.254'';
* AVD: se utilizează redirectarea porturilor.
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.
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:
* ''NsdServiceInfo'' - adresaInternet (metoda ''setHost()'') și port (metoda ''setPort()'');
* ''ServiceInfo'' - portul, configurat prin intermediul constructorului.
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:
* adresa Internet - ''getInetAddress()'';
* port - ''getLocalPort()''.
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//.
{{ :laboratoare:laborator09:wireshark01.png?nolink&800 }}
**10.** Harta Google este implementată în SDK-ul Android:
* prin intermediul unei componente grafice de tipul [[https://developer.android.com/reference/com/google/android/gms/maps/MapView.html|MapView]];
* în cadrul unui fragment, de tipul [[http://developer.android.com/reference/com/google/android/gms/maps/MapFragment.html|MapFragment]].
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 [[https://developer.android.com/reference/com/google/android/gms/maps/GoogleMap.html|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;
}
});
}